├── .gitignore ├── .prettierrc ├── app ├── scenes │ ├── 0-start.js │ ├── home.js │ ├── 10-complete.js │ ├── 1-desktop.js │ ├── 2-toggleable.js │ ├── 3-responsive.js │ ├── 4-transition.js │ ├── 5-fixed.js │ ├── 6-backdrop.js │ ├── 9-component.js │ ├── 7-focus.js │ └── 8-focustrap.js ├── index.css ├── index.js ├── index.html ├── hooks │ └── useBreakpoint.js └── components │ ├── FocusTrap.js │ ├── Transition.js │ └── Menu.js ├── tailwind.config.js ├── netlify.toml ├── README.md ├── .eslintrc ├── .vscode └── tasks.json ├── babel.config.js ├── functions └── sample.js ├── package.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "arrowParens": "always", 4 | "singleQuote": true, 5 | "endOfLine": "auto" 6 | } 7 | -------------------------------------------------------------------------------- /app/scenes/0-start.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export default function HomeScene() { 4 | return ( 5 |
6 |

Home

7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [], 3 | theme: { 4 | extend: { 5 | margin: { 6 | '-full': '-100%', 7 | full: '100%', 8 | '-cat': '-100%', 9 | }, 10 | }, 11 | }, 12 | variants: {}, 13 | plugins: [], 14 | } 15 | -------------------------------------------------------------------------------- /app/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; 4 | 5 | html { 6 | font-family: 'Noto Sans TC', sans-serif; 7 | } 8 | 9 | body { 10 | margin: 0; 11 | } 12 | 13 | a { 14 | text-decoration: none; 15 | } 16 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | 3 | import 'core-js/stable' 4 | import 'regenerator-runtime/runtime' 5 | 6 | import * as React from 'react' 7 | import ReactDOM from 'react-dom' 8 | 9 | import HomeScene from './scenes/home' 10 | 11 | function App() { 12 | return 13 | } 14 | 15 | ReactDOM.render(, document.getElementById('root')) 16 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sliding Sidebar 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /app/hooks/useBreakpoint.js: -------------------------------------------------------------------------------- 1 | import { useMediaQuery } from 'react-responsive' 2 | 3 | import resolveConfig from 'tailwindcss/resolveConfig' 4 | import tailwindConfig from '../../tailwind.config' 5 | const Tailwind = resolveConfig(tailwindConfig) 6 | 7 | export default function useBreakpoint(breakpoint) { 8 | return useMediaQuery({ 9 | query: `(min-width: ${Tailwind.theme.screens[breakpoint]})`, 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/home" 3 | to = "/index.html" 4 | status = 200 5 | force = true 6 | 7 | [build] 8 | command = "npm run build" 9 | publish = "dist/" 10 | function = "functions" # keep this committed in the repo 11 | # functions = "dist/api" # keep this active in dev 12 | 13 | [[headers]] 14 | for = "/*" 15 | [headers.values] 16 | Access-Control-Allow-Origin = "*" 17 | Access-Control-Allow-Headers = "Content-Type" 18 | 19 | 20 | [dev] 21 | framework = "#custom" 22 | command = "npm run serve:webpack" 23 | targetPort = 8080 24 | autoLaunch = true -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-boilerplate 2 | A simple boilerplate for bootstrapping a new React application 3 | 4 | # Usage 5 | 6 | Create a new respository based on this as a template 7 | 8 | Clone that repository to your local environment 9 | 10 | ```sh 11 | git clone https://github.com/jacobparis/react-boilerplate my-project 12 | ``` 13 | 14 | Install node modules 15 | 16 | ```sh 17 | npm install 18 | ``` 19 | 20 | Register the repository with Netlify 21 | 22 | Link 23 | # Commands 24 | 25 | * `npm run build` to compile and drop the bundle in the `dist` directory 26 | 27 | * `npm run serve` to start a local webserver 28 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "parser": "babel-eslint", 4 | "ecmaVersion": 2018, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "jsx": true 8 | } 9 | }, 10 | "env": { 11 | "es6": true, 12 | "node": true, 13 | "browser": true 14 | }, 15 | "extends": [ 16 | "plugin:prettier/recommended", 17 | "eslint:recommended", 18 | "plugin:react/recommended", 19 | "plugin:react-hooks/recommended" 20 | ], 21 | "plugins": ["react"], 22 | "rules": { 23 | "semi": [ 24 | 2, 25 | "never" 26 | ], 27 | "react/prop-types": "off" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/scenes/home.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import Menu from '../components/Menu' 4 | 5 | import useBreakpoint from '../hooks/useBreakpoint' 6 | 7 | export default function HomeScene() { 8 | const [isClosed, setClosed] = React.useState(true) 9 | 10 | const isStatic = useBreakpoint('sm') 11 | 12 | return ( 13 | 14 |
15 |

Home

16 | 17 |
18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /app/scenes/10-complete.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import Menu from '../components/Menu' 4 | 5 | import useBreakpoint from '../hooks/useBreakpoint' 6 | 7 | export default function HomeScene() { 8 | const [isClosed, setClosed] = React.useState(true) 9 | 10 | const isStatic = useBreakpoint('sm') 11 | 12 | return ( 13 | 14 |
15 |

Home

16 | 17 |
18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "test", 9 | "group": "test", 10 | "problemMatcher": [], 11 | "presentation": { 12 | "group": "groupA" 13 | } 14 | }, 15 | { 16 | "type": "npm", 17 | "script": "serve", 18 | "problemMatcher": [], 19 | "presentation": { 20 | "group": "groupA" 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "build", 26 | "group": "build", 27 | "problemMatcher": [], 28 | "presentation": { 29 | "group": "groupA" 30 | } 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | api.cache.using(() => process.env.NODE_ENV) 3 | 4 | return { 5 | presets: [ 6 | [ 7 | '@babel/preset-env', 8 | { 9 | useBuiltIns: 'entry', 10 | corejs: '3', 11 | targets: '> 1%, not dead', 12 | }, 13 | ], 14 | ['@babel/preset-react'], 15 | ], 16 | plugins: [ 17 | '@babel/plugin-proposal-optional-chaining', 18 | api.env('development') && 'react-refresh/babel', 19 | ].filter(Boolean), 20 | env: { 21 | test: { 22 | presets: [ 23 | [ 24 | '@babel/preset-env', 25 | { 26 | targets: { 27 | node: 10, 28 | }, 29 | }, 30 | ], 31 | '@babel/preset-react', 32 | ], 33 | }, 34 | }, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/scenes/1-desktop.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export default function HomeScene() { 4 | return ( 5 |
6 | 33 | 34 |
35 |
36 |
37 |

Home

38 | 39 |
40 |
41 |
42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /functions/sample.js: -------------------------------------------------------------------------------- 1 | // const { MongoClient } = require('mongodb') 2 | // const promiseRetry = require('promise-retry') 3 | 4 | // const querystring = require('querystring') 5 | 6 | exports.handler = async function sample(req) { 7 | /** 8 | * Handle the authorization header here 9 | */ 10 | 11 | // const authorization = req.headers.authorization || '' 12 | 13 | // const [protocol, token] = authorization.split(' ') 14 | // if (protocol !== 'Bearer' || !validTokens.includes(token)) return { 15 | // headers: { 16 | // 'WWW-Authenticate': 'Bearer' 17 | // }, 18 | // statusCode: 401 19 | // } 20 | 21 | /** 22 | * Connect to mongo here 23 | * 24 | * Make a new user on Atlas 25 | */ 26 | 27 | // const mongo = new MongoClient('mongodb+srv://user:pass@cluster0-shifn.mongodb.net/test?retryWrites=true&w=majority', { 28 | // useNewUrlParser: true, 29 | // useUnifiedTopology: true, 30 | // }) 31 | 32 | // await promiseRetry((retry, number) => { 33 | // console.info(`MongoClient connecting - attempt ${number}`) 34 | 35 | // return mongo.connect() 36 | // .catch(error => { 37 | // console.error(error) 38 | 39 | // retry() 40 | // }) 41 | // .then(() => { 42 | // console.log('MongoClient connected successfully') 43 | // }) 44 | // }, { 45 | // retries: 3, 46 | // minTimeout: 2000, 47 | // maxTimeout: 3000, 48 | // }) 49 | 50 | return { 51 | statusCode: 200, 52 | headers: { 53 | 'content-type': 'application/json; charset=utf8', 54 | 'cache-control': 'max-age=36000, immutable' 55 | }, 56 | body: JSON.stringify({ 57 | name: 'Sample Function', 58 | healthy: true 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/components/FocusTrap.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export default function FocusTrap({ children, isActive }) { 4 | const topTabTrap = React.useRef() 5 | const bottomTabTrap = React.useRef() 6 | const container = React.useRef() 7 | 8 | React.useEffect(() => { 9 | document.addEventListener('focusin', trapFocus) 10 | 11 | return () => document.removeEventListener('focusin', trapFocus) 12 | 13 | function trapFocus(event) { 14 | // Only trap focus in modal form 15 | if (!isActive) return 16 | 17 | let elements 18 | if (event.target === topTabTrap.current) { 19 | elements = getFocusableElements() 20 | 21 | if (elements.length > 0) { 22 | const lastElement = elements[elements.length - 1] 23 | lastElement.focus() 24 | } 25 | } 26 | 27 | if (event.target === bottomTabTrap.current) { 28 | elements = getFocusableElements() 29 | 30 | if (elements.length > 0) { 31 | const firstElement = elements[0] 32 | firstElement.focus() 33 | } 34 | } 35 | } 36 | 37 | function getFocusableElements() { 38 | if (!container.current) return [] 39 | 40 | const FOCUSABLE_SELECTOR = [ 41 | 'button', 42 | 'a[href]', 43 | 'input', 44 | 'select', 45 | 'textarea', 46 | '[tabindex]', 47 | '[contenteditable]', 48 | ] 49 | .map((selector) => `${selector}:not(:disabled):not([disabled])`) 50 | .join(', ') 51 | 52 | return Array.from(container.current.querySelectorAll(FOCUSABLE_SELECTOR)) 53 | .filter((element) => element !== topTabTrap.current) 54 | .filter((element) => element !== bottomTabTrap.current) 55 | } 56 | }, [isActive, topTabTrap, bottomTabTrap, container]) 57 | 58 | return ( 59 |
60 | {isActive && } 61 | {children} 62 | {isActive && } 63 |
64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-boilerplate", 3 | "version": "1.0.0", 4 | "description": "A simple boilerplate for bootstrapping a new React application", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "npx webpack", 9 | "serve": "netlify dev --functions functions", 10 | "serve:webpack": "npx webpack-dev-server" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/jacobparis/react-boilerplate.git" 15 | }, 16 | "author": "Jacob Paris", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/jacobparis/react-boilerplate/issues" 20 | }, 21 | "homepage": "https://github.com/jacobparis/react-boilerplate#readme", 22 | "devDependencies": { 23 | "@babel/core": "^7.9.6", 24 | "@babel/plugin-proposal-optional-chaining": "^7.10.1", 25 | "@babel/preset-env": "^7.10.1", 26 | "@babel/preset-react": "^7.10.1", 27 | "@babel/runtime-corejs3": "^7.9.6", 28 | "@pmmmwh/react-refresh-webpack-plugin": "^0.3.3", 29 | "@tailwindcss/custom-forms": "^0.2.1", 30 | "autoprefixer": "^9.8.4", 31 | "babel-eslint": "^10.1.0", 32 | "babel-loader": "^8.1.0", 33 | "command-line-args": "^5.1.1", 34 | "core-js": "^3.6.5", 35 | "css-loader": "^3.5.3", 36 | "eslint": "^6.8.0", 37 | "eslint-config-prettier": "^6.11.0", 38 | "eslint-plugin-prettier": "^3.1.4", 39 | "eslint-plugin-react": "^7.20.0", 40 | "eslint-plugin-react-hooks": "^4.0.5", 41 | "html-webpack-plugin": "^4.3.0", 42 | "mini-css-extract-plugin": "^0.9.0", 43 | "netlify-cli": "^2.54.0", 44 | "node-sass": "^4.14.1", 45 | "prettier": "^2.0.5", 46 | "react-refresh": "^0.8.3", 47 | "sass-loader": "^8.0.2", 48 | "webpack": "^4.43.0", 49 | "webpack-cli": "^3.3.11", 50 | "webpack-dev-server": "^3.11.0" 51 | }, 52 | "dependencies": { 53 | "@xstate/react": "^0.8.1", 54 | "postcss-loader": "^3.0.0", 55 | "prop-types": "^15.7.2", 56 | "react": "^16.13.1", 57 | "react-dom": "^16.13.1", 58 | "react-responsive": "^8.1.0", 59 | "react-transition-group": "^4.4.1", 60 | "tailwindcss": "^1.4.6", 61 | "xstate": "^4.10.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const webpack = require('webpack') 4 | const resolve = require('path').resolve 5 | // const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') 6 | const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin') 7 | 8 | const isDevelopment = process.env.NODE_ENV !== 'production' 9 | module.exports = { 10 | mode: isDevelopment ? 'development' : 'production', 11 | entry: resolve(__dirname, 'app/index.js'), 12 | output: { 13 | path: resolve(__dirname, 'dist'), 14 | filename: '[name].[hash].js', 15 | publicPath: '/', 16 | }, 17 | devServer: { 18 | contentBase: resolve(__dirname, 'dist'), 19 | hot: true, 20 | historyApiFallback: { 21 | index: 'index.html', 22 | }, 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.(js|jsx)$/, 28 | exclude: /node_modules/, 29 | use: 'babel-loader', 30 | }, 31 | { 32 | test: /\.(png|jpg|gif)$/, 33 | use: [ 34 | { 35 | loader: 'url-loader', 36 | options: { 37 | limit: 8192, 38 | }, 39 | }, 40 | ], 41 | }, 42 | { 43 | test: /\.(s?css)$/, 44 | use: [ 45 | MiniCssExtractPlugin.loader, 46 | 'css-loader', 47 | { 48 | loader: 'postcss-loader', 49 | options: { 50 | ident: 'postcss', 51 | plugins: [require('tailwindcss'), require('autoprefixer')], 52 | }, 53 | }, 54 | ], 55 | }, 56 | ], 57 | }, 58 | plugins: [ 59 | new webpack.HashedModuleIdsPlugin(), 60 | new HtmlWebpackPlugin({ 61 | template: './app/index.html', 62 | }), 63 | new MiniCssExtractPlugin({ filename: '[contenthash].css' }), 64 | // new BundleAnalyzerPlugin() 65 | isDevelopment && new ReactRefreshWebpackPlugin(), 66 | ].filter(Boolean), 67 | optimization: { 68 | runtimeChunk: 'single', 69 | splitChunks: { 70 | chunks: 'all', 71 | maxInitialRequests: Infinity, 72 | minSize: 0, 73 | cacheGroups: { 74 | vendor: { 75 | test: /[\\/]node_modules[\\/]/, 76 | name(module) { 77 | // get the name. E.g. node_modules/packageName/not/this/part.js 78 | // or node_modules/packageName 79 | const packageName = module.context.match( 80 | /[\\/]node_modules[\\/](.*?)([\\/]|$)/ 81 | )[1] 82 | 83 | // npm package names are URL-safe, but some servers don't like @ symbols 84 | return `npm.${packageName.replace('@', '')}` 85 | }, 86 | }, 87 | }, 88 | }, 89 | }, 90 | } 91 | -------------------------------------------------------------------------------- /app/scenes/2-toggleable.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export default function HomeScene() { 4 | const [isClosed, setClosed] = React.useState(false) 5 | 6 | return ( 7 |
8 | {!isClosed && ( 9 | 39 | )} 40 | 41 |
42 |
43 | {isClosed ? ( 44 | 63 | ) : ( 64 | 82 | )} 83 | 84 |
85 |

Home

86 | 87 |
88 |
89 |
90 |
91 | ) 92 | } 93 | -------------------------------------------------------------------------------- /app/scenes/3-responsive.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import useBreakpoint from '../hooks/useBreakpoint' 4 | 5 | export default function HomeScene() { 6 | const [isClosed, setClosed] = React.useState(false) 7 | const isStatic = useBreakpoint('sm') 8 | 9 | return ( 10 |
11 | {(isStatic || !isClosed) && ( 12 | 42 | )} 43 | 44 |
45 |
46 | {!isStatic && 47 | (isClosed ? ( 48 | 67 | ) : ( 68 | 86 | ))} 87 | 88 |
89 |

Home

90 | 91 |
92 |
93 |
94 |
95 | ) 96 | } 97 | -------------------------------------------------------------------------------- /app/components/Transition.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Original source: https://gist.github.com/adamwathan/3b9f3ad1a285a2d1b482769aeb862467 3 | * Author: Adam Wathan 4 | * 5 | * I modified the script to not remove the enterTo and leaveTo classes upon completing the transition 6 | * Instead they're removed when the opposite transition begins 7 | */ 8 | import { CSSTransition as ReactCSSTransition } from 'react-transition-group' 9 | import * as React from 'react' 10 | 11 | const TransitionContext = React.createContext({ 12 | parent: {}, 13 | }) 14 | 15 | function useIsInitialRender() { 16 | const isInitialRender = React.useRef(true) 17 | React.useEffect(() => { 18 | isInitialRender.current = false 19 | }, []) 20 | return isInitialRender.current 21 | } 22 | 23 | function CSSTransition({ 24 | show, 25 | enter = '', 26 | enterFrom = '', 27 | enterTo = '', 28 | leave = '', 29 | leaveFrom = '', 30 | leaveTo = '', 31 | appear, 32 | children, 33 | }) { 34 | const enterClasses = enter.split(' ').filter((s) => s.length) 35 | const enterFromClasses = enterFrom.split(' ').filter((s) => s.length) 36 | const enterToClasses = enterTo.split(' ').filter((s) => s.length) 37 | const leaveClasses = leave.split(' ').filter((s) => s.length) 38 | const leaveFromClasses = leaveFrom.split(' ').filter((s) => s.length) 39 | const leaveToClasses = leaveTo.split(' ').filter((s) => s.length) 40 | 41 | function addClasses(node, classes) { 42 | classes.length && node.classList.add(...classes) 43 | } 44 | 45 | function removeClasses(node, classes) { 46 | classes.length && node.classList.remove(...classes) 47 | } 48 | 49 | return ( 50 | { 55 | node.addEventListener('transitionend', done, false) 56 | }} 57 | onEnter={(node) => { 58 | removeClasses(node, [...leaveToClasses]) 59 | addClasses(node, [...enterClasses, ...enterFromClasses]) 60 | }} 61 | onEntering={(node) => { 62 | removeClasses(node, [...enterFromClasses]) 63 | addClasses(node, [...enterToClasses]) 64 | }} 65 | onEntered={(node) => { 66 | removeClasses(node, [...enterClasses]) 67 | }} 68 | onExit={(node) => { 69 | removeClasses(node, [...enterToClasses]) 70 | addClasses(node, [...leaveClasses, ...leaveFromClasses]) 71 | }} 72 | onExiting={(node) => { 73 | removeClasses(node, [...leaveFromClasses]) 74 | addClasses(node, [...leaveToClasses]) 75 | }} 76 | onExited={(node) => { 77 | removeClasses(node, [...leaveClasses]) 78 | }} 79 | > 80 | {children} 81 | 82 | ) 83 | } 84 | 85 | function Transition({ show, appear, ...rest }) { 86 | const { parent } = React.useContext(TransitionContext) 87 | const isInitialRender = useIsInitialRender() 88 | const isChild = show === undefined 89 | 90 | if (isChild) { 91 | return ( 92 | 97 | ) 98 | } 99 | 100 | return ( 101 | 110 | 111 | 112 | ) 113 | } 114 | 115 | export default Transition 116 | -------------------------------------------------------------------------------- /app/scenes/4-transition.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import Transition from '../components/Transition' 4 | 5 | import useBreakpoint from '../hooks/useBreakpoint' 6 | 7 | export default function HomeScene() { 8 | const [isClosed, setClosed] = React.useState(false) 9 | const isStatic = useBreakpoint('sm') 10 | 11 | return ( 12 |
13 | 21 | 48 | 49 | 50 |
51 |
52 | {!isStatic && 53 | (isClosed ? ( 54 | 73 | ) : ( 74 | 92 | ))} 93 | 94 |
95 |

Home

96 | 97 |
98 |
99 |
100 |
101 | ) 102 | } 103 | -------------------------------------------------------------------------------- /app/scenes/5-fixed.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import Transition from '../components/Transition' 4 | 5 | import useBreakpoint from '../hooks/useBreakpoint' 6 | 7 | export default function HomeScene() { 8 | const [isClosed, setClosed] = React.useState(false) 9 | const isStatic = useBreakpoint('sm') 10 | 11 | return ( 12 |
13 | 21 | 73 | 74 | 75 |
76 |
77 | {!isStatic && ( 78 | 98 | )} 99 | 100 |
101 |

Home

102 | 103 |
104 |
105 |
106 |
107 | ) 108 | } 109 | -------------------------------------------------------------------------------- /app/components/Menu.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import Transition from './Transition' 4 | import FocusTrap from './FocusTrap' 5 | 6 | export default function Menu({ children, isStatic, isClosed, setClosed }) { 7 | return ( 8 |
9 | 17 | 71 | 72 | 73 | 83 |
84 | 85 | 86 |
87 |
88 | {!isStatic && ( 89 | 109 | )} 110 | 111 | {children} 112 |
113 |
114 |
115 | ) 116 | } 117 | -------------------------------------------------------------------------------- /app/scenes/6-backdrop.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import Transition from '../components/Transition' 4 | 5 | import useBreakpoint from '../hooks/useBreakpoint' 6 | 7 | export default function HomeScene() { 8 | const [isClosed, setClosed] = React.useState(false) 9 | const isStatic = useBreakpoint('sm') 10 | 11 | return ( 12 |
13 | 21 | 74 | 75 | 76 | 86 |
87 | 88 | 89 |
90 |
91 | {!isStatic && ( 92 | 112 | )} 113 | 114 |
115 |

Home

116 | 117 |
118 |
119 |
120 |
121 | ) 122 | } 123 | -------------------------------------------------------------------------------- /app/scenes/9-component.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import Transition from '../components/Transition' 4 | import FocusTrap from '../components/FocusTrap' 5 | 6 | import useBreakpoint from '../hooks/useBreakpoint' 7 | 8 | function Menu({ children, isStatic, isClosed, setClosed }) { 9 | return ( 10 |
11 | 19 | 73 | 74 | 75 | 85 |
86 | 87 | 88 |
89 |
90 | {!isStatic && ( 91 | 111 | )} 112 | 113 | {children} 114 |
115 |
116 |
117 | ) 118 | } 119 | 120 | export default function HomeScene() { 121 | const [isClosed, setClosed] = React.useState(true) 122 | 123 | const isStatic = useBreakpoint('sm') 124 | 125 | return ( 126 | 127 |
128 |

Home

129 | 130 |
131 |
132 | ) 133 | } 134 | -------------------------------------------------------------------------------- /app/scenes/7-focus.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import Transition from '../components/Transition' 4 | 5 | import useBreakpoint from '../hooks/useBreakpoint' 6 | 7 | export default function HomeScene() { 8 | const [isClosed, setClosed] = React.useState(true) 9 | 10 | const isStatic = useBreakpoint('sm') 11 | 12 | const topTabTrap = React.useRef() 13 | const bottomTabTrap = React.useRef() 14 | 15 | const firstFocusableElement = React.useRef() 16 | const lastFocusableElement = React.useRef() 17 | 18 | React.useEffect(() => { 19 | document.addEventListener('focusin', trapFocus) 20 | 21 | return () => document.removeEventListener('focusin', trapFocus) 22 | 23 | function trapFocus(event) { 24 | // Only trap focus in modal form 25 | if (isStatic) return 26 | 27 | if (event.target === topTabTrap.current) { 28 | lastFocusableElement.current.focus() 29 | } 30 | 31 | if (event.target === bottomTabTrap.current) { 32 | firstFocusableElement.current.focus() 33 | } 34 | } 35 | }, [isStatic, firstFocusableElement, lastFocusableElement]) 36 | 37 | return ( 38 |
39 | 47 | 106 | 107 | 108 | 118 |
119 | 120 | 121 |
122 |
123 | {!isStatic && ( 124 | 144 | )} 145 | 146 |
147 |

Home

148 | 149 |
150 |
151 |
152 |
153 | ) 154 | } 155 | -------------------------------------------------------------------------------- /app/scenes/8-focustrap.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import Transition from '../components/Transition' 4 | 5 | import useBreakpoint from '../hooks/useBreakpoint' 6 | 7 | function FocusTrap({ children, isActive }) { 8 | const topTabTrap = React.useRef() 9 | const bottomTabTrap = React.useRef() 10 | const container = React.useRef() 11 | 12 | React.useEffect(() => { 13 | document.addEventListener('focusin', trapFocus) 14 | 15 | return () => document.removeEventListener('focusin', trapFocus) 16 | 17 | function trapFocus(event) { 18 | // Only trap focus in modal form 19 | if (!isActive) return 20 | 21 | let elements 22 | if (event.target === topTabTrap.current) { 23 | elements = getFocusableElements() 24 | 25 | if (elements.length > 0) { 26 | const lastElement = elements[elements.length - 1] 27 | lastElement.focus() 28 | } 29 | } 30 | 31 | if (event.target === bottomTabTrap.current) { 32 | elements = getFocusableElements() 33 | 34 | if (elements.length > 0) { 35 | const firstElement = elements[0] 36 | firstElement.focus() 37 | } 38 | } 39 | } 40 | 41 | function getFocusableElements() { 42 | if (!container.current) return [] 43 | 44 | const FOCUSABLE_SELECTOR = [ 45 | 'button', 46 | 'a[href]', 47 | 'input', 48 | 'select', 49 | 'textarea', 50 | '[tabindex]', 51 | '[contenteditable]', 52 | ] 53 | .map((selector) => `${selector}:not(:disabled):not([disabled])`) 54 | .join(', ') 55 | 56 | return Array.from(container.current.querySelectorAll(FOCUSABLE_SELECTOR)) 57 | .filter((element) => element !== topTabTrap.current) 58 | .filter((element) => element !== bottomTabTrap.current) 59 | } 60 | }, [isActive, topTabTrap, bottomTabTrap, container]) 61 | 62 | return ( 63 |
64 | {isActive && } 65 | {children} 66 | {isActive && } 67 |
68 | ) 69 | } 70 | 71 | export default function HomeScene() { 72 | const [isClosed, setClosed] = React.useState(true) 73 | 74 | const isStatic = useBreakpoint('sm') 75 | 76 | return ( 77 |
78 | 86 | 140 | 141 | 142 | 152 |
153 | 154 | 155 |
156 |
157 | {!isStatic && ( 158 | 178 | )} 179 | 180 |
181 |

Home

182 | 183 |
184 |
185 |
186 |
187 | ) 188 | } 189 | --------------------------------------------------------------------------------