Repository: yusinto/react-site-nav
Branch: master
Commit: d93cc53f02f7
Files: 59
Total size: 87.4 KB
Directory structure:
gitextract_u844912z/
├── .babelrc
├── .eslintrc
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── examples/
│ ├── advanced/
│ │ ├── .babelrc
│ │ ├── .eslintrc
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── now/
│ │ │ ├── index.html
│ │ │ └── now.json
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── client/
│ │ │ │ └── index.js
│ │ │ ├── server/
│ │ │ │ ├── index.js
│ │ │ │ └── server.js
│ │ │ └── universal/
│ │ │ ├── app/
│ │ │ │ ├── app.js
│ │ │ │ ├── company.js
│ │ │ │ ├── developers.js
│ │ │ │ ├── pricing.js
│ │ │ │ └── products.js
│ │ │ ├── contact.js
│ │ │ └── home.js
│ │ ├── webpack.config.client.js
│ │ ├── webpack.config.server.js
│ │ └── webpack.prod.config.js
│ ├── basic/
│ │ ├── .babelrc
│ │ ├── .eslintrc
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── client/
│ │ │ │ └── index.js
│ │ │ ├── server/
│ │ │ │ ├── index.js
│ │ │ │ └── server.js
│ │ │ └── universal/
│ │ │ ├── app.css
│ │ │ ├── app.js
│ │ │ ├── home.js
│ │ │ ├── spaLink1.js
│ │ │ └── spaLink2.js
│ │ ├── webpack.config.client.js
│ │ └── webpack.config.server.js
│ └── cra-with-nav/
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── public/
│ │ ├── index.html
│ │ └── manifest.json
│ └── src/
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── index.css
│ ├── index.js
│ └── registerServiceWorker.js
├── jest.config.js
├── lib/
│ └── index.js
├── package.json
├── sketch/
│ ├── logo-sketch.sketch
│ └── react-site-nav-logo.sketch
├── src/
│ └── index.js
└── test/
└── setup.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-async-to-generator",
["babel-plugin-styled-components", {
"ssr": true,
"displayName": true
}]
]
}
================================================
FILE: .eslintrc
================================================
{
"parser": "babel-eslint",
"parserOptions": {
"allowImportExportEverywhere": true
},
"extends": [
"airbnb",
"eslint:recommended"
],
"plugins": [
"babel"
],
"rules": {
"arrow-parens": 0,
"eol-last": 0,
"global-require": 0,
"arrow-body-style": 0,
"consistent-return": 0,
"no-unneeded-ternary": 0,
"max-len": 0,
"no-param-reassign": 2,
"new-cap": 0,
"no-console": 0,
"object-curly-spacing": 0,
"spaced-comment": 0,
"import/first": 0,
"import/no-extraneous-dependencies": 0,
"import/prefer-default-export": 0,
"import/no-mutable-exports": 0,
"import/no-named-as-default": 0,
"no-trailing-spaces": 0,
"no-underscore-dangle": 0,
"no-use-before-define": 0,
"no-duplicate-imports": 0,
"import/no-duplicates": 1,
"no-useless-escape": 0,
"no-unused-expressions": [1 , {"allowTernary": true}],
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
"react/sort-comp": 0,
"react/prop-types": 0,
"react/destructuring-assignment": 0
},
"env": {
"jest": true,
"node": true
},
"globals": {
"jest": true,
"td": true
}
}
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
.eslintcache
.idea
================================================
FILE: .npmignore
================================================
*
!lib/*
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Yusinto Ngadiman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
[](https://www.npmjs.com/package/react-site-nav) [](https://www.npmjs.com/package/react-site-nav) [](https://www.npmjs.com/package/react-site-nav) [](https://www.npmjs.com/package/react-site-nav)
> **A beautiful site navigation bar to be proud of. Powered by styled components inspired by stripe.com** :tada:
Check out the live preview here (powered by now ).

Your search for the perfect site navigation bar ends here. Finally a world class navigation bar
you can use straight out of the box. Why use this package?
* Beautiful animations
* Automatic viewport detection and correction so flyouts never get rendered off screen
* Completely customisable
* Powered by css grid, css animations and styled components
No more compromises. Welcome to react-site-nav.
## Installation
yarn add react-site-nav
## Quickstart
```js
import React from 'react';
import {Switch, Link, Route} from 'react-router-dom';
import SiteNav, {ContentGroup} from 'react-site-nav'; // 1. Do this
import Home from './home';
import MyStory from './myStory';
export default () =>
(
{/* 2. Add SiteNav with ContentGroup as children */}
{/* 3. You can add anything in a ContentGroup */}
{/* react router link! */}
My Story
Another list item
Free text followed by some links.
Email
Github
);
```
Check the two [examples](https://github.com/yusinto/react-site-nav/tree/master/examples) for a fully working spa with
react router, server side rendering and hot reload.
## Api
### SiteNav
The main react component that represents the site nav. The root container is a css grid so
most of the props below maps directly to this grid and should be self-explanatory. Place
ContentGroup components as children of SiteNav to render the "flyouts".
```jsx
{ /* These will render as flyouts */}
...
...
```
### ContentGroup
Each SiteNav contains ContentGroup children components. Each ContentGroup will be rendered
as a "flyout" on hover of the root items. It accepts the following props which are self-explanatory.
```jsx
{
/* You can render anything here! */
}
```
To render a root item as a link without a ContentGroup, you can do this:
```jsx
```
By not specifying width and height, SiteNav assumes you just want to render the root item
without a ContentGroup. Of course you can have a linked root item plus a ContentGroup. Just specify either
a width or a height so SiteNav knows you want to render the group.
```jsx
{
/* You can render anything here! */
}
```
Check the demo in my [blog](https://reactjunkie.com/).
================================================
FILE: examples/advanced/.babelrc
================================================
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-async-to-generator",
["babel-plugin-styled-components", {
"ssr": true,
"displayName": true
}]
]
}
================================================
FILE: examples/advanced/.eslintrc
================================================
{
"parser": "babel-eslint",
"parserOptions": {
"allowImportExportEverywhere": true
},
"extends": [
"airbnb",
"eslint:recommended",
"plugin:react/recommended"
],
"plugins": [
"babel"
],
"rules": {
"arrow-parens": 0,
"eol-last": 0,
"global-require": 0,
"arrow-body-style": 0,
"consistent-return": 0,
"no-unneeded-ternary": 0,
"max-len": 0,
"no-param-reassign": 2,
"new-cap": 0,
"no-console": 0,
"object-curly-spacing": 0,
"spaced-comment": 0,
"import/no-extraneous-dependencies": 0,
"import/first": 0,
"import/prefer-default-export": 0,
"import/no-mutable-exports": 0,
"import/no-named-as-default": 0,
"react/jsx-filename-extension": 0,
"react/jsx-indent": 0,
"react/jsx-indent-props": 0,
"react/jsx-space-before-closing": 0,
"react/jsx-first-prop-new-line": 0,
"react/prefer-stateless-function": 0,
"react/jsx-closing-bracket-location": 0,
"react/require-extension": 0,
"react/sort-comp": 0,
"react/jsx-wrap-multilines": 0,
"react/jsx-no-bind": 0,
"react/jsx-users-react": 0,
"react/jsx-tag-spacing": 0,
"jsx-a11y/anchor-is-valid": 0,
"jsx-a11y/img-has-alt": 0,
"no-trailing-spaces": 0,
"no-underscore-dangle": 0,
"no-use-before-define": 0,
"no-duplicate-imports": 0,
"import/no-duplicates": 1,
"no-useless-escape": 0,
"no-unused-expressions": [1 , {"allowTernary": true}]
},
"env": {
"browser": true,
"jest": true,
"node": true
},
"globals": {
"React": true,
"fetch": true,
"jest": true
}
}
================================================
FILE: examples/advanced/.gitignore
================================================
node_modules
.idea
npm-debug.log
dist
.eslintcache
now/build
================================================
FILE: examples/advanced/README.md
================================================
# react-site-nav advanced example
Live demo [here](https://react-site-nav.now.sh).
yarn && yarn start
================================================
FILE: examples/advanced/now/index.html
================================================
React Site Nav - Advanced
================================================
FILE: examples/advanced/now/now.json
================================================
{
"name": "react-site-nav",
"alias": "react-site-nav",
"public": true
}
================================================
FILE: examples/advanced/package.json
================================================
{
"name": "react-site-menu-example",
"version": "1.1.0",
"description": "Demo of react-site-menu a kickass navigation menu inspired by stripe.com",
"main": "src/server/index.js",
"scripts": {
"start": "node src/server/index.js",
"lint": "eslint ./src",
"serve": "webpack-serve webpack.config.server",
"build": "rimraf now/build/* && webpack --config webpack.prod.config.js",
"now": "npm run build && cd ./now && now && now alias"
},
"repository": {
"type": "git",
"url": "https://github.com/yusinto/react-site-menu.git"
},
"keywords": [
"react",
"site",
"navigation",
"menu",
"bar",
"animated",
"stripe"
],
"author": "Yus Ng",
"license": "MIT",
"bugs": {
"url": "https://github.com/yusinto/react-site-menu"
},
"homepage": "https://github.com/yusinto/react-site-menu",
"dependencies": {
"@babel/plugin-transform-async-to-generator": "^7.0.0-beta.54",
"@babel/polyfill": "^7.0.0-beta.54",
"babel-plugin-styled-components": "^1.5.1",
"express": "^4.16.3",
"lodash": "^4.17.11",
"lodash.kebabcase": "^4.1.1",
"memoize-one": "^4.0.2",
"prop-types": "^15.6.2",
"react": "^16.5.0",
"react-dom": "^16.5.0",
"react-router-dom": "^4.3.1",
"react-site-nav": "^0.2.1",
"styled-components": "^3.4.6"
},
"devDependencies": {
"@babel/cli": "^7.0.0-beta.54",
"@babel/core": "^7.0.0-beta.54",
"@babel/plugin-proposal-class-properties": "^7.0.0-beta.54",
"@babel/preset-env": "^7.0.0-beta.54",
"@babel/preset-react": "^7.0.0-beta.54",
"babel-eslint": "^8.2.6",
"babel-loader": "^8.0.0-beta",
"eslint": "^5.5.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jsx-a11y": "^6.1.1",
"eslint-plugin-react": "^7.11.1",
"file-loader": "^2.0.0",
"rimraf": "^2.6.2",
"universal-hot-reload": "^1.0.6",
"webpack": "^4.18.0",
"webpack-cli": "^3.1.0",
"webpack-node-externals": "^1.7.2",
"webpack-serve": "^2.0.2"
}
}
================================================
FILE: examples/advanced/src/client/index.js
================================================
import React from 'react';
import {hydrate} from 'react-dom';
import {BrowserRouter} from 'react-router-dom';
import App from '../universal/app/app';
hydrate(
,
document.getElementById('reactDiv'),
);
================================================
FILE: examples/advanced/src/server/index.js
================================================
const UniversalHotReload = require('universal-hot-reload').default;
UniversalHotReload(require('../../webpack.config.server.js'), require('../../webpack.config.client.js'));
================================================
FILE: examples/advanced/src/server/server.js
================================================
import Express from 'express';
import React from 'react';
import {renderToString} from 'react-dom/server';
import {ServerStyleSheet, StyleSheetManager} from 'styled-components';
import {StaticRouter} from 'react-router-dom';
import App from '../universal/app/app';
const PORT = 3000;
const app = Express();
app.use('/dist', Express.static('dist', {maxAge: '1d'}));
app.use((req, res) => {
const sheet = new ServerStyleSheet();
const StyledApp =
;
const styleTags = sheet.getStyleTags();
const html = `
React Site Nav - Advanced
${styleTags}
`;
res.end(html);
});
const httpServer = app.listen(PORT, () => {
console.log(`Example app listening at ${PORT}...`);
});
// export httpServer object so universal-hot-reload can access it
module.exports = httpServer;
================================================
FILE: examples/advanced/src/universal/app/app.js
================================================
import React, {Component} from 'react';
import {Switch, Link, Route, Redirect} from 'react-router-dom';
import styled, {injectGlobal} from 'styled-components';
import SiteNav, {ContentGroup} from 'react-site-nav';
import Home from '../home';
import Contact from '../contact';
import logo from '../../../assets/logo-transparent.png';
import ProductsContentGroup from './products';
import Developers from './developers';
import Company from './company';
import Pricing from './pricing';
injectGlobal`
// auto-generated from https://www.svgbackgrounds.com/#abstract-envelope
body {
background-color: #77aa77;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25' viewBox='0 0 2 1'%3E%3Cdefs%3E%3ClinearGradient id='a' gradientUnits='userSpaceOnUse' x1='0' x2='0' y1='0' y2='1' gradientTransform='rotate(0,0.5,0.5)'%3E%3Cstop offset='0' stop-color='%2377aa77'/%3E%3Cstop offset='1' stop-color='%234fd'/%3E%3C/linearGradient%3E%3ClinearGradient id='b' gradientUnits='userSpaceOnUse' x1='0' y1='0' x2='0' y2='1' gradientTransform='rotate(360,0.5,0.5)'%3E%3Cstop offset='0' stop-color='%23cf8' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='%23cf8' stop-opacity='1'/%3E%3C/linearGradient%3E%3ClinearGradient id='c' gradientUnits='userSpaceOnUse' x1='0' y1='0' x2='2' y2='2' gradientTransform='rotate(105,0.5,0.5)'%3E%3Cstop offset='0' stop-color='%23cf8' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='%23cf8' stop-opacity='1'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect x='0' y='0' fill='url(%23a)' width='2' height='1'/%3E%3Cg fill-opacity='0.85'%3E%3Cpolygon fill='url(%23b)' points='0 1 0 0 2 0'/%3E%3Cpolygon fill='url(%23c)' points='2 1 2 0 0 0'/%3E%3C/g%3E%3C/svg%3E");
background-attachment: fixed;
background-size: cover;
background-position: center;
}
a {
text-decoration: none;
}
a:visited {
color: lightslategray;
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
}
`;
const Header = styled.div`
display: grid;
grid-template-columns: 400px auto;
grid-template-rows: 80px;
background: transparent;
`;
export default class App extends Component {
render() {
return (
);
}
}
================================================
FILE: examples/advanced/src/universal/app/company.js
================================================
import React from 'react';
import styled from 'styled-components';
import aboutMeIcon from '../../../assets/about-me.png';
import customersIcon from '../../../assets/customers.png';
import jobsIcon from '../../../assets/jobs.png';
import environmentIcon from '../../../assets/environment.png';
const List = styled.ul`
display: flex;
flex-direction: column;
margin-top: 10px;
margin-left: 30px;
`;
const ListItem = styled.li`
display: flex;
flex-direction: row;
margin-top: 20px;
align-items: center;
`;
const Heading = styled.div`
margin: 0;
color: #6772e5;
font-size: 16px;
line-height: 22px;
font-weight: 600;
letter-spacing: .025em;
margin-left: 10px;
`;
const StyledLink = styled.a`
display: flex;
align-items: center;
&:hover {
opacity: 0.7;
}
`;
export default () => {
return (
ABOUT ME
CUSTOMERS
JOBS
ENVIRONMENT
);
};
================================================
FILE: examples/advanced/src/universal/app/developers.js
================================================
import React from 'react';
import styled from 'styled-components';
import documentIcon from '../../../assets/documentation.png';
const RootGrid = styled.div`
display: grid;
grid-template-columns: 60px auto;
grid-template-rows: [top-space] 30px [doco-row] 60px [row-space] 20px [list-row] 60px;
font-size: 15px;
color: lightslategray;
`;
const DocoLogo = styled.div`
grid-row: doco-row / span 1;
grid-column: 1 / span 1;
display: flex;
justify-content: flex-end;
padding-right: 10px;
`;
const HeadingText = styled.div`
grid-row: doco-row / span 1;
grid-column: 2 / span 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
font-size: 15px;
`;
const DocumentationHeading = styled.div`
margin: 0;
padding-bottom: 10px;
color: #6772e5;
font-size: 16px;
line-height: 22px;
font-weight: 600;
letter-spacing: .025em;
`;
const ListGroupGrid = styled.div`
grid-row: list-row / span 1;
grid-column: 2 / span 1;
display: grid;
grid-template-columns: [get-started] auto [popular-topics] auto;
grid-template-rows: 120px;
`;
const GetStartedGridItem = styled.div`
grid-row: 1 / span 1;
grid-column: get-started / span 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
`;
const PopularGridItem = styled.div`
grid-row: 1 / span 1;
grid-column: popular-topics / span 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
`;
const ColumnHeading = styled.div`
color: #8898aa;
font-size: 14px;
`;
const List = styled.ul`
color: #6772e5;
margin-top: 5px;
margin-bottom: 5px;
font-size: 14px
`;
const ListItem = styled.li`
margin-top: 8px;
margin-bottom: 8px;
`;
export default () => {
return (
DOCUMENTATION
Start integrating products and tools.
GET STARTED
Elements
Checkout
Mobile apps
Libraries
POPULAR TOPICS
Apple Pay
Testing
Launch checklist
Plug-ins
);
};
================================================
FILE: examples/advanced/src/universal/app/pricing.js
================================================
import React from 'react';
import styled from 'styled-components';
import smiley from '../../../assets/smiley.png';
const List = styled.ul`
display: flex;
flex-direction: column;
margin-top: 10px;
margin-left: 30px;
color: lightslategray;
`;
const ListItem = styled.li`
display: flex;
flex-direction: row;
margin-top: 20px;
align-items: center;
a:hover {
opacity: 0.7;
}
`;
const Heading = styled.div`
margin: 0;
color: #6772e5;
font-size: 16px;
line-height: 22px;
font-weight: 600;
letter-spacing: .025em;
`;
const HeadingText = styled.div`
display: flex;
flex-direction: column;
margin-left: 10px;
`;
const Text = styled.div`
font-size: 13px;
`;
const StyledLink = styled.a`
display: flex;
align-items: center;
&:hover {
opacity: 0.7;
}
`;
export default () => {
return (
STAR IT!
github.com/yusinto/react-site-nav
);
};
================================================
FILE: examples/advanced/src/universal/app/products.js
================================================
import React from 'react';
import styled from 'styled-components';
import {Link} from 'react-router-dom';
import paymentIcon from '../../../assets/payment.png';
import billingIcon from '../../../assets/billing.png';
import connectIcon from '../../../assets/connect.png';
const ListContainer = styled.div`
display: flex;
height: 100%;
width: 100%;
justify-content: center;
`;
const List = styled.ul`
color: lightslategray;
display: flex;
flex-direction: column;
justify-content: space-evenly;
`;
const ListItemContent = styled.div`
display: flex;
flex-direction: row;
&:hover {
opacity: 0.7;
}
`;
const LisItemHeadingText = styled.div`
display: flex;
flex-direction: column;
justify-content: space-between;
font-size: 15px;
margin-left: 10px;
`;
const ListItemHeading = styled.div`
margin: 0;
color: #6772e5;
font-size: 16px;
line-height: 22px;
font-weight: 600;
letter-spacing: .025em;
`;
export default () => {
return (
PAYMENTS
A complete payments platform engineered.
BILLING
Build and scale your recurring business model.
CONNECT
Everything platforms need to get sellers paid.
);
};
================================================
FILE: examples/advanced/src/universal/contact.js
================================================
import React, {Component, Timeout} from 'react';
import styled from 'styled-components';
const RootDiv = styled.div`
margin-top: 30px;
margin-left: 30px;
color: #fff;
`;
const Heading = styled.h1`
font-weight: 400;
color: #fff;
`;
export default props =>
Thanks for checking react-site-nav!
Check out my blog at reactjunkie.com
You can reach me via:
;
================================================
FILE: examples/advanced/src/universal/home.js
================================================
import React, {Component} from 'react';
import styled from 'styled-components';
const RootDiv = styled.div`
margin-top: 30px;
margin-left: 30px;
color: #fff;
`;
const Heading = styled.h1`
font-weight: 400;
color: #fff;
`;
export default class Home extends Component {
render() {
return (
React Site Nav
The new standard in site navigation.
);
}
}
================================================
FILE: examples/advanced/webpack.config.client.js
================================================
const path = require('path');
const WebpackServeUrl = 'http://localhost:3002';
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: ['@babel/polyfill', './src/client/index'],
output: {
path: path.resolve('dist'),
publicPath: `${WebpackServeUrl}/dist/`, // MUST BE FULL PATH!
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.jsx?$/,
include: path.resolve('src'),
exclude: /node_modules/,
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
publicPath: 'dist/',
}
}
]
}
],
},
};
================================================
FILE: examples/advanced/webpack.config.server.js
================================================
const path = require('path');
const nodeExternals = require('webpack-node-externals');
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: ['@babel/polyfill', './src/server/server.js'], // set this to your server entry point. This should be where you start your express server with .listen()
target: 'node', // tell webpack this bundle will be used in nodejs environment.
externals: [nodeExternals()], // Omit node_modules code from the bundle. You don't want and don't need them in the bundle.
output: {
path: path.resolve('dist'),
filename: 'serverBundle.js',
libraryTarget: 'commonjs2', // IMPORTANT! Add module.exports to the beginning of the bundle, so universal-hot-reload can access your app.
},
// The rest of the config is pretty standard and can contain other webpack stuff you need.
module: {
rules: [
{
test: /\.jsx?$/,
include: path.resolve('src'),
exclude: /node_modules/,
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
publicPath: 'dist/',
}
}
]
}
],
},
};
================================================
FILE: examples/advanced/webpack.prod.config.js
================================================
const path = require('path');
module.exports = {
mode: 'production',
devtool: 'source-map',
entry: ['@babel/polyfill', './src/client/index'],
output: {
path: path.resolve('now/build'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.jsx?$/,
include: path.resolve('src'),
exclude: /node_modules/,
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
publicPath: 'build/',
}
}
]
}
],
},
};
================================================
FILE: examples/basic/.babelrc
================================================
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-async-to-generator",
["babel-plugin-styled-components", {
"ssr": true,
"displayName": true
}]
]
}
================================================
FILE: examples/basic/.eslintrc
================================================
{
"parser": "babel-eslint",
"parserOptions": {
"allowImportExportEverywhere": true
},
"extends": [
"airbnb",
"eslint:recommended",
"plugin:react/recommended"
],
"plugins": [
"babel"
],
"rules": {
"arrow-parens": 0,
"eol-last": 0,
"global-require": 0,
"arrow-body-style": 0,
"consistent-return": 0,
"no-unneeded-ternary": 0,
"max-len": 0,
"no-param-reassign": 2,
"new-cap": 0,
"no-console": 0,
"object-curly-spacing": 0,
"spaced-comment": 0,
"import/no-extraneous-dependencies": 0,
"import/first": 0,
"import/prefer-default-export": 0,
"import/no-mutable-exports": 0,
"import/no-named-as-default": 0,
"react/jsx-filename-extension": 0,
"react/jsx-indent": 0,
"react/jsx-indent-props": 0,
"react/jsx-space-before-closing": 0,
"react/jsx-first-prop-new-line": 0,
"react/prefer-stateless-function": 0,
"react/jsx-closing-bracket-location": 0,
"react/require-extension": 0,
"react/sort-comp": 0,
"react/jsx-wrap-multilines": 0,
"react/jsx-no-bind": 0,
"react/jsx-users-react": 0,
"react/jsx-tag-spacing": 0,
"jsx-a11y/anchor-is-valid": 0,
"jsx-a11y/img-has-alt": 0,
"no-trailing-spaces": 0,
"no-underscore-dangle": 0,
"no-use-before-define": 0,
"no-duplicate-imports": 0,
"import/no-duplicates": 1,
"no-useless-escape": 0,
"no-unused-expressions": [1 , {"allowTernary": true}]
},
"env": {
"browser": true,
"jest": true,
"node": true
},
"globals": {
"React": true,
"fetch": true,
"jest": true
}
}
================================================
FILE: examples/basic/.gitignore
================================================
node_modules
.idea
npm-debug.log
dist
.eslintcache
================================================
FILE: examples/basic/README.md
================================================
# react-site-nav basic example
A simple spa demonstrating react-site-nav.
yarn && yarn start
================================================
FILE: examples/basic/package.json
================================================
{
"name": "react-site-menu-example",
"version": "1.1.0",
"description": "Demo of react-site-menu a kickass navigation menu inspired by stripe.com",
"main": "src/server/index.js",
"scripts": {
"start": "node src/server/index.js",
"lint": "eslint ./src",
"serve": "webpack-serve webpack.config.server"
},
"repository": {
"type": "git",
"url": "https://github.com/yusinto/react-site-menu.git"
},
"keywords": [
"react",
"site",
"navigation",
"menu",
"bar",
"animated",
"stripe"
],
"author": "Yus Ng",
"license": "MIT",
"bugs": {
"url": "https://github.com/yusinto/react-site-menu"
},
"homepage": "https://github.com/yusinto/react-site-menu",
"dependencies": {
"@babel/plugin-transform-async-to-generator": "^7.0.0-beta.54",
"@babel/polyfill": "^7.0.0-beta.54",
"babel-plugin-styled-components": "^1.5.1",
"css-loader": "^1.0.0",
"express": "^4.16.3",
"lodash": "^4.17.11",
"lodash.kebabcase": "^4.1.1",
"memoize-one": "^4.0.2",
"prop-types": "^15.6.2",
"react": "^16.5.0",
"react-dom": "^16.5.0",
"react-router-dom": "^4.3.1",
"react-site-nav": "^0.2.6",
"style-loader": "^0.23.0",
"styled-components": "^3.4.6"
},
"devDependencies": {
"@babel/cli": "^7.0.0-beta.54",
"@babel/core": "^7.0.0-beta.54",
"@babel/plugin-proposal-class-properties": "^7.0.0-beta.54",
"@babel/preset-env": "^7.0.0-beta.54",
"@babel/preset-react": "^7.0.0-beta.54",
"babel-eslint": "^8.2.6",
"babel-loader": "^8.0.0-beta",
"eslint": "^5.5.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jsx-a11y": "^6.1.1",
"eslint-plugin-react": "^7.11.1",
"file-loader": "^2.0.0",
"universal-hot-reload": "^1.0.6",
"webpack": "^4.18.0",
"webpack-cli": "^3.1.0",
"webpack-node-externals": "^1.7.2",
"webpack-serve": "^2.0.2"
}
}
================================================
FILE: examples/basic/src/client/index.js
================================================
import React from 'react';
import {hydrate} from 'react-dom';
import {BrowserRouter} from 'react-router-dom';
import App from '../universal/app';
hydrate(
,
document.getElementById('reactDiv'),
);
================================================
FILE: examples/basic/src/server/index.js
================================================
const UniversalHotReload = require('universal-hot-reload').default;
UniversalHotReload(require('../../webpack.config.server.js'), require('../../webpack.config.client.js'));
================================================
FILE: examples/basic/src/server/server.js
================================================
import Express from 'express';
import React from 'react';
import {renderToString} from 'react-dom/server';
import {ServerStyleSheet, StyleSheetManager} from 'styled-components';
import {StaticRouter} from 'react-router-dom';
import App from '../universal/app';
const PORT = 3000;
const app = Express();
app.use('/dist', Express.static('dist', {maxAge: '1d'}));
app.use((req, res) => {
const html = `
React Site Nav - Basic
`;
res.end(html);
});
const httpServer = app.listen(PORT, () => {
console.log(`Example app listening at ${PORT}...`);
});
// export httpServer object so universal-hot-reload can access it
module.exports = httpServer;
================================================
FILE: examples/basic/src/universal/app.css
================================================
h1 {
}
ul {
list-style-type: none;
}
li {
margin-top: 10px
}
a {
text-decoration: none;
color: #24b47e;
}
a:hover {
opacity: 0.5;
}
a:visited {
color: #6b7c93;
}
.header {
display: grid;
grid-template-columns: 100px auto 100px;
grid-template-rows: 80px;
background: #fff;
}
.logo {
height: 70px;
margin-left: 20px;
margin-top: 5px
}
================================================
FILE: examples/basic/src/universal/app.js
================================================
import React, {Component} from 'react';
import {Switch, Link, Route, Redirect} from 'react-router-dom';
import Home from './home';
import SpaLink1 from './spaLink1';
import SpaLink2 from './spaLink2';
import SiteNav, {ContentGroup} from 'react-site-nav';
import './app.css';
import logo from '../../assets/logo.jpg';
export default () =>
(
Spa Link
Another Spa Link
);
================================================
FILE: examples/basic/src/universal/home.js
================================================
import React, {Component} from 'react';
export default class Home extends Component {
render() {
return (
react-site-nav
Welcome to react-site-nav! This is a basic demo of its usage.
Prs and comments welcome!
);
}
}
================================================
FILE: examples/basic/src/universal/spaLink1.js
================================================
import React, {Component, Timeout} from 'react';
export default props =>
This is spa link #1
Thanks for checking react-site-nav!
Check out my blog at reactjunkie.com
You can reach me via:
;
================================================
FILE: examples/basic/src/universal/spaLink2.js
================================================
import React, {Component, Timeout} from 'react';
export default props =>
;
================================================
FILE: examples/basic/webpack.config.client.js
================================================
const path = require('path');
const WebpackServeUrl = 'http://localhost:3002';
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: ['@babel/polyfill', './src/client/index'],
output: {
path: path.resolve('dist'),
publicPath: `${WebpackServeUrl}/dist/`, // MUST BE FULL PATH!
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.jsx?$/,
include: path.resolve('src'),
exclude: /node_modules/,
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
publicPath: 'dist/',
}
}
]
},
{
test: /\.css$/,
use: [
{loader: "style-loader"},
{
loader: "css-loader",
options: {
importLoaders: 1,
modules: true,
localIdentName: '[folder]--[name]--[local]--[hash:base64:2]',
},
}
]
},
],
},
};
================================================
FILE: examples/basic/webpack.config.server.js
================================================
const path = require('path');
const nodeExternals = require('webpack-node-externals');
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: ['@babel/polyfill', './src/server/server.js'], // set this to your server entry point. This should be where you start your express server with .listen()
target: 'node', // tell webpack this bundle will be used in nodejs environment.
externals: [nodeExternals()], // Omit node_modules code from the bundle. You don't want and don't need them in the bundle.
output: {
path: path.resolve('dist'),
filename: 'serverBundle.js',
libraryTarget: 'commonjs2', // IMPORTANT! Add module.exports to the beginning of the bundle, so universal-hot-reload can access your app.
},
// The rest of the config is pretty standard and can contain other webpack stuff you need.
module: {
rules: [
{
test: /\.jsx?$/,
include: path.resolve('src'),
exclude: /node_modules/,
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
publicPath: 'dist/',
}
}
]
},
{
test: /\.css$/,
loader: "css-loader",
options: {
importLoaders: 1,
modules: true,
localIdentName: '[folder]--[name]--[local]--[hash:base64:2]',
},
},
],
},
};
================================================
FILE: examples/cra-with-nav/.gitignore
================================================
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: examples/cra-with-nav/README.md
================================================
## Create react app with react-site-nav
Live demo [here](https://build-licattzisr.now.sh/).
================================================
FILE: examples/cra-with-nav/package.json
================================================
{
"name": "cra-with-nav",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^16.5.0",
"react-dom": "^16.5.0",
"react-site-nav": "^0.2.6"
},
"devDependencies": {
"react-scripts": "1.1.5"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"now": "npm run build && cd ./build && now --public"
}
}
================================================
FILE: examples/cra-with-nav/public/index.html
================================================
React App
You need to enable JavaScript to run this app.
================================================
FILE: examples/cra-with-nav/public/manifest.json
================================================
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: examples/cra-with-nav/src/App.css
================================================
ul {
list-style-type: none;
margin: 0;
padding: 0;
}
li {
display: flex;
justify-content: center;
align-items: center;
height: 45px;
font-size: 15px;
}
li:hover {
opacity: 0.7;
}
li > a {
flex: 0 0 110px;
text-align: left;
margin-left: 10px;
text-decoration: none;
color: dodgerblue;
}
.App {
text-align: center;
}
.App-logo {
/* this causes an issue in firefox which breaks the flyout animation */
/*animation: App-logo-spin infinite 20s linear;*/
height: 80px;
}
.App-header {
background-color: #222;
height: 150px;
padding: 20px;
color: white;
}
.App-title {
font-size: 1.5em;
}
.App-intro {
font-size: large;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
================================================
FILE: examples/cra-with-nav/src/App.js
================================================
import React from 'react';
import logo from './logo.svg';
import reactMenuImage from './react-logo.png';
import aboutMeImage from './about-me.png';
import docsImage from './docs.png';
import communityImage from './community.png';
import reactSiteNavImage from './react-site-nav-logo.png';
import tutorialImage from './tutorial.png';
import './App.css';
import SiteNav, {ContentGroup} from 'react-site-nav';
export default () => (
Welcome to React
To get started, edit src/App.js and save to reload.
);
================================================
FILE: examples/cra-with-nav/src/App.test.js
================================================
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render( , div);
ReactDOM.unmountComponentAtNode(div);
});
================================================
FILE: examples/cra-with-nav/src/index.css
================================================
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
================================================
FILE: examples/cra-with-nav/src/index.js
================================================
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
ReactDOM.render( , document.getElementById('root'));
registerServiceWorker();
================================================
FILE: examples/cra-with-nav/src/registerServiceWorker.js
================================================
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://goo.gl/SC7cgQ'
);
});
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
================================================
FILE: jest.config.js
================================================
module.exports = {
setupFiles: ['./test/setup.js'],
};
================================================
FILE: lib/index.js
================================================
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.ContentGroup = void 0;
var _react = _interopRequireWildcard(require("react"));
var _styledComponents = _interopRequireWildcard(require("styled-components"));
var _memoizeOne = _interopRequireDefault(require("memoize-one"));
var _lodash = _interopRequireDefault(require("lodash.kebabcase"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } }
function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var defaultRootAlign = 'center';
var defaultColor = '#fff';
var defaultColumnWidth = 150;
var defaultRowHeight = 45;
var defaultBackground = '#323232';
var defaultBreakpoint = 768;
var defaultContentBackground = '#fff';
var defaultContentColor = '#323232';
var defaultContentWidth = 320;
var defaultContentHeight = 200;
var defaultContentTop = 0;
var arrowHeight = 8;
var perspective = 850;
var fadeOutSeconds = 0.34;
var fadeInSeconds = 0.25;
var moveSeconds = 0.25;
var moveArrowSeconds = 0.28;
var fadeOutContentSeconds = 0.29;
var fadeInContentSeconds = 0.1;
var OffScreenPadding = 10;
var setFromProps = function setFromProps(camelCaseKey) {
return (0, _styledComponents.css)(["", ""], function (props) {
return props[camelCaseKey] ? "".concat((0, _lodash.default)(camelCaseKey), ": ").concat(props[camelCaseKey]) : null;
});
};
var GridContainer = _styledComponents.default.div.withConfig({
displayName: "src__GridContainer",
componentId: "sc-178mgjs-0"
})(["@media(max-width:", "px){position:absolute;visibility:hidden;}@media(min-width:", "px){display:grid;", ";justify-items:stretch;grid-template-columns:repeat(", ",", "px);grid-template-rows:", "px;position:relative;", ";", ";", ";", "px;}"], function (_ref) {
var breakpoint = _ref.breakpoint;
return breakpoint - 1;
}, function (_ref2) {
var breakpoint = _ref2.breakpoint;
return breakpoint;
}, setFromProps('justifyContent'), function (_ref3) {
var columns = _ref3.columns;
return columns;
}, function (_ref4) {
var columnWidth = _ref4.columnWidth;
return columnWidth;
}, function (_ref5) {
var rowHeight = _ref5.rowHeight;
return rowHeight;
}, setFromProps('background'), setFromProps('color'), setFromProps('fontFamily'), setFromProps('fontSize'));
var GridItemLink = _styledComponents.default.a.withConfig({
displayName: "src__GridItemLink",
componentId: "sc-178mgjs-1"
})(["grid-column:", " / span 1;display:flex;justify-content:center;align-items:center;&:hover{opacity:0.5;}", ";&:visited{", ";}"], function (_ref6) {
var index = _ref6.index;
return index + 1;
}, setFromProps('color'), setFromProps('color'));
var GridItem = _styledComponents.default.div.withConfig({
displayName: "src__GridItem",
componentId: "sc-178mgjs-2"
})(["grid-column:", " / span 1;display:flex;justify-content:center;align-items:center;&:hover{opacity:0.5;cursor:default;}"], function (_ref7) {
var index = _ref7.index;
return index + 1;
});
var ContentRow = _styledComponents.default.div.withConfig({
displayName: "src__ContentRow",
componentId: "sc-178mgjs-3"
})(["grid-column:1 / span ", ";grid-row:2 / span 1;position:relative;height:0;"], function (_ref8) {
var columns = _ref8.columns;
return columns;
});
var Move = function Move(fromData, toData) {
return (0, _styledComponents.keyframes)(["from{left:", "px;width:", "px;height:", "px;}to{left:", "px;width:", "px;height:", "px;}"], fromData.left, fromData.width, fromData.height, toData.left, toData.width, toData.height);
};
var FadeIn = (0, _styledComponents.keyframes)(["from{opacity:0;transform:perspective(", "px) rotateX(-60deg);transform-origin:top center;}to{opacity:1;transform:perspective(", "px) rotateX(0deg);transform-origin:top center;}"], perspective, perspective);
var FadeOut = (0, _styledComponents.keyframes)(["from{opacity:1;transform:perspective(", "px) rotateX(0deg);transform-origin:top center;}to{opacity:0;transform:perspective(", "px) rotateX(-60deg);transform-origin:top center;visibility:hidden;}"], perspective, perspective);
var MovingDiv = _styledComponents.default.div.withConfig({
displayName: "src__MovingDiv",
componentId: "sc-178mgjs-4"
})(["opacity:1;", ";", ";position:absolute;top:", "px;left:", "px;width:", "px;height:", "px;display:", ";border-radius:4px;box-shadow:0 8px 28px 1px rgba(138,126,138,0.67);animation:", " ", " forwards ease;"], setFromProps('color'), setFromProps('background'), function (_ref9) {
var top = _ref9.top;
return top;
}, function (_ref10) {
var fromData = _ref10.fromData;
return fromData ? fromData.left : 0;
}, function (_ref11) {
var fromData = _ref11.fromData;
return fromData ? fromData.width : 0;
}, function (_ref12) {
var fromData = _ref12.fromData;
return fromData ? fromData.height : 0;
}, function (_ref13) {
var display = _ref13.display;
return display;
}, function (_ref14) {
var fadeOut = _ref14.fadeOut,
display = _ref14.display,
fromData = _ref14.fromData,
toData = _ref14.toData;
if (fadeOut) return FadeOut;
if (display === 'block') {
if (fromData.left === toData.left) return FadeIn;
if (fromData) return Move(fromData, toData);
}
return ''; // display: none; don't animate
}, function (_ref15) {
var fadeOut = _ref15.fadeOut,
display = _ref15.display,
fromData = _ref15.fromData,
toData = _ref15.toData;
if (fadeOut) return "".concat(fadeOutSeconds, "s");
if (display === 'block') {
if (fromData.left === toData.left) return "".concat(fadeInSeconds, "s"); // fade in
if (fromData) return "".concat(moveSeconds, "s"); // move
}
return '0s'; // display: none; don't animate
});
var FadeInArrow = (0, _styledComponents.keyframes)(["from{opacity:0;}to{opacity:1;}"]);
var FadeOutArrow = (0, _styledComponents.keyframes)(["from{opacity:1;}to{opacity:0;}"]);
var calculateArrowMarginLeft = function calculateArrowMarginLeft(data, leftOffset, rightOffset) {
return (0, _styledComponents.css)(["margin-left:", "px;"], data ? data.left + data.width / 2 - leftOffset + rightOffset - arrowHeight - (leftOffset > 0 || rightOffset > 0 ? OffScreenPadding : 0) : 0);
};
var MoveArrow = function MoveArrow(fromData, toData, leftOffset, rightOffset) {
return (0, _styledComponents.keyframes)(["from{", "}to{", "}"], calculateArrowMarginLeft(fromData, leftOffset, rightOffset), calculateArrowMarginLeft(toData, leftOffset, rightOffset));
};
var Arrow = _styledComponents.default.div.withConfig({
displayName: "src__Arrow",
componentId: "sc-178mgjs-5"
})(["top:-", "px;z-index:1;position:absolute;", " display:", ";width:0;height:0;border-left:", "px solid transparent;border-right:", "px solid transparent;border-bottom:", "px solid ", ";animation:", " ", " forwards ease;"], function (_ref16) {
var top = _ref16.top;
return arrowHeight - top;
}, function (_ref17) {
var toData = _ref17.toData,
leftOffset = _ref17.leftOffset,
rightOffset = _ref17.rightOffset;
return calculateArrowMarginLeft(toData, leftOffset, rightOffset);
}, function (_ref18) {
var display = _ref18.display,
toData = _ref18.toData;
if (toData && toData.width === 0 && toData.height === 0) {
return 'none';
}
return display;
}, arrowHeight, arrowHeight, arrowHeight, function (_ref19) {
var background = _ref19.background;
return background;
}, function (_ref20) {
var fadeOut = _ref20.fadeOut,
display = _ref20.display,
fromData = _ref20.fromData,
toData = _ref20.toData,
leftOffset = _ref20.leftOffset,
rightOffset = _ref20.rightOffset;
if (fadeOut) return FadeOutArrow;
if (display === 'block') {
if (fromData.left === toData.left) return FadeInArrow;
if (fromData) return MoveArrow(fromData, toData, leftOffset, rightOffset);
}
return ''; // display: none; don't animate
}, function (_ref21) {
var fadeOut = _ref21.fadeOut,
display = _ref21.display,
fromData = _ref21.fromData,
toData = _ref21.toData;
if (fadeOut) return "".concat(fadeOutSeconds, "s");
if (display === 'block') {
if (fromData.left === toData.left) return "".concat(fadeInSeconds, "s"); // fade in
if (fromData) return "".concat(moveArrowSeconds, "s"); // move
}
return '0s'; // display: none; don't animate
});
var FadeInContent = (0, _styledComponents.keyframes)(["from{opacity:0;}to{opacity:1;}"]);
var FadeOutContent = (0, _styledComponents.keyframes)(["from{opacity:1;}to{opacity:0;visibility:hidden;}"]);
var ContentGroupContainer = _styledComponents.default.div.withConfig({
displayName: "src__ContentGroupContainer",
componentId: "sc-178mgjs-6"
})(["position:absolute;margin-top:0;margin-bottom:0;width:100%;height:100%;opacity:", ";z-index:", ";pointer-events:", ";animation:", " ", "s forwards;"], function (_ref22) {
var show = _ref22.show;
return show ? 1 : 0;
}, function (_ref23) {
var show = _ref23.show;
return show ? 1 : 0;
}, function (_ref24) {
var show = _ref24.show;
return show ? 'auto' : 'none';
}, function (_ref25) {
var show = _ref25.show,
fadeOut = _ref25.fadeOut;
if (show) return FadeInContent;
if (fadeOut) return FadeOutContent;
return ''; // cold start and everything else just show without animation
}, function (_ref26) {
var show = _ref26.show;
return show ? "".concat(fadeInContentSeconds) : "".concat(fadeOutContentSeconds);
});
var ContentGroup = function ContentGroup(_ref27) {
var title = _ref27.title,
width = _ref27.width,
height = _ref27.height,
background = _ref27.background;
return _react.default.createElement(_react.default.Fragment, null, title, width, "x", height, background);
};
exports.ContentGroup = ContentGroup;
var SiteNav =
/*#__PURE__*/
function (_Component) {
_inherits(SiteNav, _Component);
function SiteNav() {
var _getPrototypeOf2;
var _this;
_classCallCheck(this, SiteNav);
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
_this = _possibleConstructorReturn(this, (_getPrototypeOf2 = _getPrototypeOf(SiteNav)).call.apply(_getPrototypeOf2, [this].concat(args)));
_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "state", {
display: 'none',
fadeOut: false,
fromData: null,
toData: null,
leftOffset: 0,
rightOffset: 0
});
_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "memoizeMenuData", (0, _memoizeOne.default)(function (columnWidth, children) {
return _react.default.Children.map(children, function (child, i) {
// if width and height are not specified, that means we don't want to render the content group i.e. we only
// want to render root item
var _child$props = child.props,
width = _child$props.width,
height = _child$props.height;
var sanitisedWidth, sanitisedHeight;
if (!width && !height) {
sanitisedWidth = 0;
sanitisedHeight = 0;
} else {
// if width or height is not specified, add defaults
sanitisedWidth = width || defaultContentWidth;
sanitisedHeight = height || defaultContentHeight;
}
return _objectSpread({}, child.props, {
// order is important here! spread child.props after height, followed by width.
height: sanitisedHeight,
width: sanitisedWidth,
index: i,
left: (i + 1) * columnWidth - columnWidth / 2 - sanitisedWidth / 2
});
});
}));
_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "memoizeGridItems", (0, _memoizeOne.default)(function (children, color) {
return _react.default.Children.map(children, function (child, i) {
var _child$props2 = child.props,
title = _child$props2.title,
rootUrl = _child$props2.rootUrl;
if (rootUrl) {
return _react.default.createElement(GridItemLink, {
href: rootUrl,
key: "menu-title-".concat(i),
index: i,
onMouseEnter: function onMouseEnter(e) {
return _this.onMouseEnter(e.target, i);
},
color: color
}, title);
}
return _react.default.createElement(GridItem, {
key: "menu-title-".concat(i),
index: i,
onMouseEnter: function onMouseEnter(e) {
return _this.onMouseEnter(e.target, i);
},
color: color
}, title);
});
}));
_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "memoizeContent", (0, _memoizeOne.default)(function (children, fromData, toData) {
return _react.default.Children.map(children, function (child, i) {
return _react.default.createElement(ContentGroupContainer, {
key: "content-group-".concat(i),
show: toData && toData.index === i,
fadeOut: fromData && fromData.index === i
}, child.props.children);
});
}));
_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "memoizeColumns", (0, _memoizeOne.default)(function (children) {
return _react.default.Children.count(children);
}));
_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "memoizeAlign", (0, _memoizeOne.default)(function (align) {
switch (align) {
case 'left':
return 'start';
case 'right':
return 'end';
default:
return 'center';
}
}));
_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "close", function () {
if (_this.props.debug) return;
_this.setState(function (prevState) {
return {
fadeOut: true,
fromData: prevState.toData
};
});
});
_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "onMouseEnter", function (target, menuDataIndex) {
_this.setState(function (prevState) {
var fadeOut = false;
var display = 'block';
var toDataOriginal = _this.memoizeMenuData(_this.props.columnWidth, _this.props.children)[menuDataIndex];
var toData = _objectSpread({}, toDataOriginal);
var leftOffset = 0;
var rightOffset = 0;
if (target) {
// off screen detection
// target is rootGridItem
var _target$getBoundingCl = target.getBoundingClientRect(),
left = _target$getBoundingCl.left,
width = _target$getBoundingCl.width;
var siteNavWidth = target.parentNode.clientWidth;
leftOffset = toData.width / 2 - (left + width / 2);
rightOffset = toData.width / 2 - (siteNavWidth - (left + width / 2));
if (leftOffset > 0) {
// if off screen, toData.left needs to be moved to be on-screen!
toData.left += leftOffset + OffScreenPadding;
} else {
leftOffset = 0;
}
if (rightOffset > 0) {
toData.left -= rightOffset - OffScreenPadding;
} else {
rightOffset = 0;
}
var fromData;
if (prevState.fadeOut || !prevState.toData) {
// on cold start, pop up right from the current item
fromData = toData;
} else {
// on warm start, start animation from the previous item
fromData = prevState.toData;
}
return {
display: display,
fadeOut: fadeOut,
fromData: fromData,
toData: toData,
leftOffset: leftOffset,
rightOffset: rightOffset
};
}
});
});
_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "onMouseLeave", function () {
return _this.close();
});
_defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "onClickMovingDiv", function () {
return _this.close();
});
return _this;
}
_createClass(SiteNav, [{
key: "render",
value: function render() {
var _this$props = this.props,
columnWidth = _this$props.columnWidth,
rowHeight = _this$props.rowHeight,
background = _this$props.background,
contentBackground = _this$props.contentBackground,
contentColor = _this$props.contentColor,
contentTop = _this$props.contentTop,
children = _this$props.children,
align = _this$props.align,
fontSize = _this$props.fontSize,
fontFamily = _this$props.fontFamily,
color = _this$props.color,
breakpoint = _this$props.breakpoint;
var _this$state = this.state,
fromData = _this$state.fromData,
toData = _this$state.toData,
display = _this$state.display,
fadeOut = _this$state.fadeOut,
leftOffset = _this$state.leftOffset,
rightOffset = _this$state.rightOffset;
var columns = this.memoizeColumns(children);
var rootGridItems = this.memoizeGridItems(children, color);
var content = this.memoizeContent(children, fromData, toData);
var justifyContent = this.memoizeAlign(align);
var contentBackgroundSanitised = toData && toData.background || contentBackground;
return _react.default.createElement("nav", null, _react.default.createElement(GridContainer, {
background: background,
columnWidth: columnWidth,
rowHeight: rowHeight,
justifyContent: justifyContent,
fontSize: fontSize,
fontFamily: fontFamily,
color: color,
breakpoint: breakpoint
/* Below are not configurable */
,
onMouseLeave: this.onMouseLeave,
columns: columns
}, rootGridItems, _react.default.createElement(ContentRow, {
columns: columns
}, _react.default.createElement(Arrow, {
display: display,
fadeOut: fadeOut,
fromData: fromData,
toData: toData,
top: contentTop,
onClick: this.onClickMovingDiv,
background: contentBackgroundSanitised,
leftOffset: leftOffset,
rightOffset: rightOffset
}), _react.default.createElement(MovingDiv, {
display: display,
fadeOut: fadeOut,
fromData: fromData,
toData: toData,
color: contentColor,
top: contentTop,
onClick: this.onClickMovingDiv,
background: contentBackgroundSanitised
}, content))));
}
}]);
return SiteNav;
}(_react.Component);
exports.default = SiteNav;
_defineProperty(SiteNav, "defaultProps", {
align: defaultRootAlign,
columnWidth: defaultColumnWidth,
rowHeight: defaultRowHeight,
background: defaultBackground,
contentBackground: defaultContentBackground,
contentColor: defaultContentColor,
contentTop: defaultContentTop,
breakpoint: defaultBreakpoint,
color: defaultColor,
debug: false
});
================================================
FILE: package.json
================================================
{
"name": "react-site-nav",
"version": "0.2.9",
"description": "A kick ass site menu powered by styled components inspired by Stripe.",
"main": "lib/index.js",
"scripts": {
"test": "jest",
"build": "rimraf lib/* && babel src -d lib --ignore *.test.js",
"lint": "eslint --cache --format 'node_modules/eslint-friendly-formatter' ./src",
"prep-publish": "npm run build && npm version patch -m 'Upgrade to %s' && npm publish && git push"
},
"repository": {
"type": "git",
"url": "git+https://github.com/yusinto/react-site-nav.git"
},
"keywords": [
"react",
"site",
"navigation",
"menu",
"bar",
"animated",
"stripe"
],
"author": "Yusinto Ngadiman",
"license": "MIT",
"bugs": {
"url": "https://github.com/yusinto/react-site-nav/issues"
},
"homepage": "https://github.com/yusinto/react-site-nav#readme",
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.1",
"@babel/plugin-proposal-class-properties": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"babel-eslint": "^9.0.0",
"babel-plugin-styled-components": "^1.7.1",
"babel-preset-minify": "^0.4.3",
"eslint": "^5.5.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-plugin-babel": "^5.2.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jsx-a11y": "^6.1.1",
"eslint-plugin-react": "^7.11.1",
"rimraf": "^2.6.2"
},
"dependencies": {
"lodash.kebabcase": "^4.1.1",
"memoize-one": "^4.0.2",
"react": "^16.5.0",
"styled-components": "^3.4.6"
}
}
================================================
FILE: src/index.js
================================================
import React, {Component} from 'react';
import styled, {keyframes, css} from 'styled-components';
import memoize from 'memoize-one';
import kebabCase from 'lodash.kebabcase';
const defaultRootAlign = 'center';
const defaultColor = '#fff';
const defaultColumnWidth = 150;
const defaultRowHeight = 45;
const defaultBackground = '#323232';
const defaultBreakpoint = 768;
const defaultContentBackground = '#fff';
const defaultContentColor = '#323232';
const defaultContentWidth = 320;
const defaultContentHeight = 200;
const defaultContentTop = 0;
const arrowHeight = 8;
const perspective = 850;
const fadeOutSeconds = 0.34;
const fadeInSeconds = 0.25;
const moveSeconds = 0.25;
const moveArrowSeconds = 0.28;
const fadeOutContentSeconds = 0.29;
const fadeInContentSeconds = 0.1;
const OffScreenPadding = 10;
const setFromProps = camelCaseKey => css`
${props => props[camelCaseKey] ? `${kebabCase(camelCaseKey)}: ${props[camelCaseKey]}` : null}`;
const GridContainer = styled.div`
// use visibility hidden instead of display none because menu flashes when breakpoint changes for some reason!
@media(max-width: ${({breakpoint}) => (breakpoint - 1)}px) {
position: absolute;
visibility: hidden;
}
@media(min-width: ${({breakpoint}) => breakpoint}px) {
display: grid;
${setFromProps('justifyContent')};
justify-items: stretch;
grid-template-columns: repeat(${({columns}) => columns}, ${({columnWidth}) => columnWidth}px);
grid-template-rows: ${({rowHeight}) => rowHeight}px;
position: relative;
${setFromProps('background')};
${setFromProps('color')};
${setFromProps('fontFamily')};
${setFromProps('fontSize')}px;
}
`;
const GridItemLink = styled.a`
grid-column: ${({index}) => index + 1} / span 1;
display: flex;
justify-content: center;
align-items: center;
&:hover {
opacity: 0.5;
}
${setFromProps('color')};
&:visited {
${setFromProps('color')};
}
`;
const GridItem = styled.div`
grid-column: ${({index}) => index + 1} / span 1;
display: flex;
justify-content: center;
align-items: center;
&:hover {
opacity: 0.5;
cursor: default;
}
`;
const ContentRow = styled.div`
grid-column: 1 / span ${({columns}) => columns};
grid-row: 2 / span 1;
position: relative;
height: 0;
`;
const Move = (fromData, toData) => keyframes`
from {
left: ${fromData.left}px;
width: ${fromData.width}px;
height: ${fromData.height}px;
}
to {
left: ${toData.left}px;
width: ${toData.width}px;
height: ${toData.height}px;
}
`;
const FadeIn = keyframes`
from {
opacity: 0;
transform: perspective(${perspective}px) rotateX(-60deg);
transform-origin: top center;
}
to {
opacity: 1;
transform: perspective(${perspective}px) rotateX(0deg);
transform-origin: top center;
}
`;
const FadeOut = keyframes`
from {
opacity: 1;
transform: perspective(${perspective}px) rotateX(0deg);
transform-origin: top center;
}
to {
opacity: 0;
transform: perspective(${perspective}px) rotateX(-60deg);
transform-origin: top center;
visibility: hidden;
}
`;
const MovingDiv = styled.div`
opacity: 1;
${setFromProps('color')};
${setFromProps('background')};
position: absolute;
top: ${({top}) => top}px;
left: ${({fromData}) => fromData ? fromData.left : 0}px;
width: ${({fromData}) => fromData ? fromData.width : 0}px;
height: ${({fromData}) => fromData ? fromData.height : 0}px;
display: ${({display}) => display};
border-radius: 4px;
box-shadow: 0 8px 28px 1px rgba(138,126,138,0.67); // Ripped from: https://www.cssmatic.com/box-shadow
animation: ${({fadeOut, display, fromData, toData}) => {
if (fadeOut) return FadeOut;
if (display === 'block') {
if (fromData.left === toData.left) return FadeIn;
if (fromData) return Move(fromData, toData);
}
return ''; // display: none; don't animate
}}
// fade out and in slower than moving sideways
${({fadeOut, display, fromData, toData}) => {
if (fadeOut) return `${fadeOutSeconds}s`;
if (display === 'block') {
if (fromData.left === toData.left) return `${fadeInSeconds}s`; // fade in
if (fromData) return `${moveSeconds}s`; // move
}
return '0s'; // display: none; don't animate
}}
forwards ease;
`;
const FadeInArrow = keyframes`
from {
opacity: 0;
}
to {
opacity: 1;
}
`;
const FadeOutArrow = keyframes`
from {
opacity: 1;
}
to {
opacity: 0;
}
`;
const calculateArrowMarginLeft = (data, leftOffset, rightOffset) => css`
margin-left: ${
data ? data.left + (data.width / 2) - leftOffset + rightOffset - arrowHeight
- (leftOffset > 0 || rightOffset > 0 ? OffScreenPadding : 0)
: 0
}px;
`;
const MoveArrow = (fromData, toData, leftOffset, rightOffset) => keyframes`
from {
${calculateArrowMarginLeft(fromData, leftOffset, rightOffset)}
}
to {
${calculateArrowMarginLeft(toData, leftOffset, rightOffset)}
}
`;
const Arrow = styled.div`
top: -${({top}) => (arrowHeight - top)}px;
z-index: 1;
position: absolute;
${({toData, leftOffset, rightOffset}) => calculateArrowMarginLeft(toData, leftOffset, rightOffset)}
display: ${({display, toData}) => {
if (toData && toData.width === 0 && toData.height === 0) {
return 'none';
}
return display;
}};
width: 0;
height: 0;
border-left: ${arrowHeight}px solid transparent;
border-right: ${arrowHeight}px solid transparent;
border-bottom: ${arrowHeight}px solid ${({background}) => background};
animation: ${({fadeOut, display, fromData, toData, leftOffset, rightOffset}) => {
if (fadeOut) return FadeOutArrow;
if (display === 'block') {
if (fromData.left === toData.left) return FadeInArrow;
if (fromData) return MoveArrow(fromData, toData, leftOffset, rightOffset);
}
return ''; // display: none; don't animate
}}
// fade out and in slower than moving sideways
${({fadeOut, display, fromData, toData}) => {
if (fadeOut) return `${fadeOutSeconds}s`;
if (display === 'block') {
if (fromData.left === toData.left) return `${fadeInSeconds}s`; // fade in
if (fromData) return `${moveArrowSeconds}s`; // move
}
return '0s'; // display: none; don't animate
}}
forwards ease;
`;
const FadeInContent = keyframes`
from {
opacity: 0;
}
to {
opacity: 1;
}
`;
const FadeOutContent = keyframes`
from {
opacity: 1;
}
to {
opacity: 0;
visibility: hidden;
}
`;
const ContentGroupContainer = styled.div`
position: absolute;
margin-top: 0;
margin-bottom: 0;
width: 100%;
height: 100%;
opacity: ${({show}) => show ? 1 : 0};
z-index: ${({show}) => show ? 1 : 0};
pointer-events: ${({show}) => show ? 'auto' : 'none'}; // disregard mouse event if content group is inactive
animation: ${({show, fadeOut}) => {
if (show) return FadeInContent;
if (fadeOut) return FadeOutContent;
return ''; // cold start and everything else just show without animation
}}
${({show}) => show ? `${fadeInContentSeconds}` : `${fadeOutContentSeconds}`}s
forwards;
`;
export const ContentGroup = ({title, width, height, background}) => {
return (
<>
{title}
{width}x{height}
{background}
>
);
};
export default class SiteNav extends Component {
state = {display: 'none', fadeOut: false, fromData: null, toData: null, leftOffset: 0, rightOffset: 0};
static defaultProps = {
align: defaultRootAlign,
columnWidth: defaultColumnWidth,
rowHeight: defaultRowHeight,
background: defaultBackground,
contentBackground: defaultContentBackground,
contentColor: defaultContentColor,
contentTop: defaultContentTop,
breakpoint: defaultBreakpoint,
color: defaultColor,
debug: false,
};
/**
* Injects index and left properties into MenuData
*/
memoizeMenuData = memoize((columnWidth, children) => React.Children.map(children, (child, i) => {
// if width and height are not specified, that means we don't want to render the content group i.e. we only
// want to render root item
const {width, height} = child.props;
let sanitisedWidth, sanitisedHeight;
if (!width && !height) {
sanitisedWidth = 0;
sanitisedHeight = 0;
} else {
// if width or height is not specified, add defaults
sanitisedWidth = width || defaultContentWidth;
sanitisedHeight = height || defaultContentHeight;
}
return {
...child.props, // order is important here! spread child.props after height, followed by width.
height: sanitisedHeight,
width: sanitisedWidth,
index: i,
left: (((i + 1) * columnWidth) - (columnWidth / 2)) - (sanitisedWidth / 2),
};
}));
memoizeGridItems = memoize((children, color) => React.Children.map(children, (child, i) => {
const {title, rootUrl} = child.props;
if (rootUrl) {
return (
this.onMouseEnter(e.target, i)}
color={color}
>
{title}
);
}
return (
this.onMouseEnter(e.target, i)}
color={color}
>
{title}
);
}
));
memoizeContent = memoize((children, fromData, toData) => React.Children.map(children, (child, i) => (
{child.props.children}
)));
memoizeColumns = memoize(children => React.Children.count(children));
memoizeAlign = memoize(align => {
switch (align) {
case 'left':
return 'start';
case 'right':
return 'end';
default:
return 'center';
}
});
close = () => {
if (this.props.debug) return;
this.setState((prevState) => ({fadeOut: true, fromData: prevState.toData}));
};
onMouseEnter = (target, menuDataIndex) => {
this.setState((prevState) => {
const fadeOut = false;
const display = 'block';
const toDataOriginal = this.memoizeMenuData(this.props.columnWidth, this.props.children)[menuDataIndex];
const toData = {...toDataOriginal};
let leftOffset = 0;
let rightOffset = 0;
if (target) { // off screen detection
// target is rootGridItem
const {left, width} = target.getBoundingClientRect();
const siteNavWidth = target.parentNode.clientWidth;
leftOffset = (toData.width / 2) - (left + (width / 2));
rightOffset = (toData.width / 2) - (siteNavWidth - (left + (width / 2)));
if (leftOffset > 0) {
// if off screen, toData.left needs to be moved to be on-screen!
toData.left += leftOffset + OffScreenPadding;
} else {
leftOffset = 0;
}
if (rightOffset > 0) {
toData.left -= rightOffset - OffScreenPadding;
} else {
rightOffset = 0;
}
let fromData;
if (prevState.fadeOut || !prevState.toData) {
// on cold start, pop up right from the current item
fromData = toData;
} else {
// on warm start, start animation from the previous item
fromData = prevState.toData;
}
return {
display,
fadeOut,
fromData,
toData,
leftOffset,
rightOffset,
};
}
});
};
onMouseLeave = () => this.close();
onClickMovingDiv = () => this.close();
render() {
const {
columnWidth, rowHeight, background, contentBackground, contentColor, contentTop,
children, align, fontSize, fontFamily, color, breakpoint
} = this.props;
const {fromData, toData, display, fadeOut, leftOffset, rightOffset} = this.state;
const columns = this.memoizeColumns(children);
const rootGridItems = this.memoizeGridItems(children, color);
const content = this.memoizeContent(children, fromData, toData);
const justifyContent = this.memoizeAlign(align);
const contentBackgroundSanitised = (toData && toData.background) || contentBackground;
return (
{rootGridItems}
{content}
);
}
}
================================================
FILE: test/setup.js
================================================
// TODO:
// import td from 'testdouble';
//
// global.td = td;