├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .prettierignore ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── __mocks__ └── styles.js ├── commitlint.config.js ├── examples └── cra │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ ├── src │ ├── App.js │ ├── Basic │ │ ├── Basic.css │ │ └── index.js │ ├── ThreePanels │ │ ├── ThreePanels.module.css │ │ └── index.js │ ├── index.js │ └── serviceWorker.js │ └── yarn.lock ├── package-lock.json ├── package.json ├── rollup.config.js └── src ├── components ├── AnimatedPanel │ ├── AnimatedPanel.module.css │ └── index.js ├── CloseButton │ ├── CloseButton.module.css │ └── index.js ├── Menu │ ├── DropdownMenu.js │ ├── DropdownMenu.module.css │ ├── Menu.js │ ├── MenuItem.js │ ├── MenuItem.module.css │ └── index.js ├── Overlay │ ├── Overlay.module.css │ └── index.js ├── ReactOffcanvasComponent │ ├── ReactOffcanvasComponent.js │ └── ReactOffcanvasComponent.module.css └── index.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "modules": false, 7 | "targets": { 8 | "browsers": "> 0.25%, IE 11, not op_mini all, not dead", 9 | "node": 10 10 | } 11 | } 12 | ], 13 | "@babel/react" 14 | ], 15 | "env": { 16 | "test": { 17 | "presets": ["@babel/env", "@babel/react"] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | indent_style = space 10 | indent_size = 2 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "plugin:prettier/recommended", 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:jest/recommended" 11 | ], 12 | "globals": { 13 | "Atomics": "readonly", 14 | "SharedArrayBuffer": "readonly" 15 | }, 16 | "parserOptions": { 17 | "ecmaFeatures": { 18 | "jsx": true 19 | }, 20 | "ecmaVersion": 2018, 21 | "sourceType": "module" 22 | }, 23 | "plugins": ["react"], 24 | "rules": { 25 | "indent": ["error", 2], 26 | "linebreak-style": ["error", "unix"], 27 | "quotes": ["error", "double"], 28 | "semi": ["error", "always"] 29 | }, 30 | "settings": { 31 | "react": { 32 | "version": "detect" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /dist 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | coverage 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /examples 2 | /__mocks__ 3 | /.vscode 4 | /src 5 | .babelrc 6 | .editorconfig 7 | .eslintignore 8 | .eslintrc.json 9 | .gitignore 10 | .prettierignore 11 | .travis.yml 12 | commitlint.config.js 13 | rollup.config.js -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | 4 | branches: 5 | only: 6 | - master 7 | 8 | node_js: 9 | - node 10 | 11 | after_success: 12 | - npm run semantic-release 13 | 14 | script: 15 | - npm run lint 16 | - npm run build 17 | 18 | notifications: 19 | email: false 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.formatOnSave": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sameera Chathuranga Abeywickrama 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/samAbeywickrama/react-offcanvas-component.svg?branch=master)](https://travis-ci.org/samAbeywickrama/react-offcanvas-component) 2 | ![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg) 3 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 4 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 5 | 6 | # React Offcanvas Component 7 | 8 | Create Off-canvas Sidebars with React and Popmotion's pose 9 | 10 | ## [**Demo**](https://samabeywickrama.github.io/roc-examples/) 11 | 12 | ## Installation 13 | 14 | This npm module utilizes `react-pose` for animations. 15 | 16 | #### yarn 17 | 18 | `yarn add react-pose react-offcanvas-component` 19 | 20 | #### npm 21 | 22 | `npm i -S react-pose react-offcanvas-component` 23 | 24 | ## Developments 25 | 26 | This repo uses `Commitizen` for git commit conventions. 27 | 28 | Run `yarn commit` or `npm run commit` 29 | 30 | You'll be prompted to fill in any required fields and your commit messages will be formatted according to the standards 31 | 32 | ## Basic usage 33 | 34 | Please visit this [Repo](https://github.com/samAbeywickrama/roc-examples) for examples. 35 | 36 | ```js 37 | import ReactOffcanvasComponent from "react-offcanvas-component"; 38 | import "./Basic.css"; 39 | 40 | const { Menu, DropdownMenu, CloseButton } = ReactOffcanvasComponent; 41 | 42 | const openAnimation = { 43 | x: 0, 44 | transition: { 45 | duration: 200 46 | } 47 | }; 48 | 49 | 54 | 55 | x 56 | 57 |
ROC
58 | 59 | Home 60 | Another Menu Item 61 | 62 | Dropdown 63 | 64 | Abc 65 | Abc 66 | Abc 67 | 68 | 69 | Is n't it awesome 70 | 71 |
; 72 | ``` 73 | 74 | ```css 75 | .wrapper { 76 | width: 400px; 77 | background: #fff; 78 | padding: 20px; 79 | } 80 | .menu-item { 81 | margin-bottom: 20px; 82 | } 83 | .dropdown { 84 | cursor: pointer; 85 | margin-bottom: 20px; 86 | } 87 | .dropdown-item { 88 | padding: 10px; 89 | margin-top: 20px; 90 | background: #e2e2e2; 91 | } 92 | .logo { 93 | padding-bottom: 10px; 94 | margin-bottom: 20px; 95 | border-bottom: 1px solid #e2e2e2; 96 | width: 200px; 97 | } 98 | ``` 99 | 100 | ## API 101 | 102 | #### ReactOffcanvasComponent 103 | 104 | | Prop | Type | Default | Required | Description | 105 | | ---------------- | --------- | --------------------------------------------- | -------- | ----------------------------------------------------------------------------- | 106 | | open | `Boolean` | `false` | yes | Setting values as `true` will Reveal Sidebar and `false` will Hide Sidebar | 107 | | className | `String` | no | no | Override the styles applied to the component with css | 108 | | style | `Object` | no | no | Override the styles applied to the component with inline styles | 109 | | openAnimation | `Object` | [See Default Animations](#default-animations) | no | Override the default open animation | 110 | | closeAnimation | `Object` | [See Default Animations](#default-animations) | no | Override the default close animation | 111 | | overlay | `Boolean` | false | no | Show / Hide background overlay | 112 | | overlayClassName | `String` | no | no | Override the default styles applied to the Overlay component with css classes | 113 | 114 | #### AnimatedPanel 115 | 116 | | Prop | Type | Default | Required | Description | 117 | | -------------- | -------- | --------------------------------------------- | -------- | --------------------------------------------------------------- | 118 | | className | `String` | no | no | Override the styles applied to the component with css | 119 | | style | `Object` | no | no | Override the styles applied to the component with inline styles | 120 | | openAnimation | `Object` | [See Default Animations](#default-animations) | no | Override the default open animation | 121 | | closeAnimation | `Object` | [See Default Animations](#default-animations) | no | Override the default close animation | 122 | 123 | #### CloseButton 124 | 125 | | Prop | Type | Default | Required | Description | 126 | | --------- | ---------- | ------- | -------- | --------------------------------------------------------------- | 127 | | onClick | `Function` | no | no | `function(event: object) => void` | 128 | | style | `Object` | no | no | Override the styles applied to the component with inline styles | 129 | | className | `String` | no | no | Override the styles applied to the component with css | 130 | 131 | #### DropdownMenu 132 | 133 | | Prop | Type | Default | Required | Description | 134 | | --------- | -------- | ------- | -------- | --------------------------------------------------------------- | 135 | | style | `Object` | no | no | Override the styles applied to the component with inline styles | 136 | | className | `String` | no | no | Override the styles applied to the component with css | 137 | 138 | #### DropdownMenu 139 | 140 | | Prop | Type | Default | Required | Description | 141 | | --------- | -------- | ------- | -------- | ----------------------------------------------------- | 142 | | className | `String` | no | no | Override the styles applied to the component with css | 143 | 144 | #### MenuItem 145 | 146 | | Prop | Type | Default | Required | Description | 147 | | --------------------- | ---------- | ------- | -------- | ----------------------------------------------------------------- | 148 | | style | `Object` | no | no | Override the styles applied to the component with inline styles | 149 | | className | `String` | no | no | Override the styles applied to the component with css | 150 | | onClick | `Function` | no | no | `function(event: object) => void` | 151 | | dropdownIconClassName | `String` | no | no | Override the styles applied to the dropdown menu icon with css | 152 | | hasDropdown | `Boolean` | no | no | If the MenuItem contains a dropdown menu set this value to `true` | 153 | 154 | ### Default Animations 155 | 156 | I have used [react-pose](https://popmotion.io/pose/) to create the animations. A tons of customization possible via pose api. 157 | 158 | #### ReactOffcanvasComponent 159 | 160 | **Open** 161 | 162 | ```js 163 | { 164 | x: "-100px", 165 | transition: { 166 | ease: [0.175, 0.885, 0.32, 1.275], 167 | duration: 300 168 | }, 169 | delayChildren: 150, 170 | staggerChildren: 100 171 | } 172 | ``` 173 | 174 | **Close** 175 | 176 | ```js 177 | { 178 | x: "-100%", 179 | transition: { 180 | duration: 100 181 | }, 182 | afterChildren: true 183 | } 184 | ``` 185 | 186 | #### AnimatedPanel 187 | 188 | **Open** 189 | 190 | ```js 191 | { 192 | x: "0%", 193 | transition: { 194 | ease: [0.175, 0.885, 0.32, 1.275], 195 | duration: 300 196 | }, 197 | delayChildren: 300, 198 | staggerChildren: 100 199 | } 200 | ``` 201 | 202 | **Close** 203 | 204 | ```js 205 | { 206 | x: "-100%", 207 | transition: { 208 | duration: 100 209 | }, 210 | afterChildren: true 211 | } 212 | ``` 213 | 214 | #### Todo: 215 | 216 | - Add tests and coverage reports 217 | -------------------------------------------------------------------------------- /__mocks__/styles.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']} 2 | -------------------------------------------------------------------------------- /examples/cra/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/cra/README.md: -------------------------------------------------------------------------------- 1 | # Example using create-react-app 2 | -------------------------------------------------------------------------------- /examples/cra/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cra", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react-dom": "^16.8.6", 7 | "react-icons": "3.7.0", 8 | "react-offcanvas-component": "file:../..", 9 | "react-scripts": "3.0.1" 10 | }, 11 | "scripts": { 12 | "start": "SKIP_PREFLIGHT_CHECK=true react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "eject": "react-scripts eject" 16 | }, 17 | "eslintConfig": { 18 | "extends": "react-app" 19 | }, 20 | "browserslist": { 21 | "production": [ 22 | ">0.2%", 23 | "not dead", 24 | "not op_mini all" 25 | ], 26 | "development": [ 27 | "last 1 chrome version", 28 | "last 1 firefox version", 29 | "last 1 safari version" 30 | ] 31 | }, 32 | "devDependencies": {} 33 | } 34 | -------------------------------------------------------------------------------- /examples/cra/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmashTapsOS/react-offcanvas-component/7afc3e1a36ab6e17b9e2d9e7f90ac138f72cf503/examples/cra/public/favicon.ico -------------------------------------------------------------------------------- /examples/cra/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | 26 | React App 27 | 33 | 34 | 35 | 36 |
37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/cra/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/cra/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ThreePanels from "./ThreePanels/index.js"; 3 | import Basic from "./Basic"; 4 | function App() { 5 | return ( 6 |
7 | 8 | 9 |
10 | ); 11 | } 12 | 13 | export default App; 14 | -------------------------------------------------------------------------------- /examples/cra/src/Basic/Basic.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 400px; 3 | background: #fff; 4 | padding: 20px; 5 | } 6 | .menu-item { 7 | margin-bottom: 20px; 8 | } 9 | .dropdown { 10 | cursor: pointer; 11 | margin-bottom: 20px; 12 | } 13 | .dropdown-item { 14 | padding: 10px; 15 | margin-top: 20px; 16 | background: #e2e2e2; 17 | } 18 | .logo { 19 | padding-bottom: 10px; 20 | margin-bottom: 20px; 21 | border-bottom: 1px solid #e2e2e2; 22 | width: 200px; 23 | } 24 | -------------------------------------------------------------------------------- /examples/cra/src/Basic/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import ReactOffcanvasComponent from "react-offcanvas-component"; 3 | import "./Basic.css"; 4 | import { FaTimes } from "react-icons/fa"; 5 | 6 | const { Menu, DropdownMenu, CloseButton } = ReactOffcanvasComponent; 7 | 8 | const MenuItem = Menu.Item; 9 | 10 | function Basic() { 11 | const [visibility, setVisibility] = useState(false); 12 | 13 | const handleClick = () => { 14 | setVisibility(visibility => !visibility); 15 | }; 16 | 17 | const openAnimation = { 18 | x: 0, 19 | transition: { 20 | duration: 200 21 | } 22 | }; 23 | 24 | return ( 25 |
26 | 29 | 34 | 35 | 36 | 37 |
ROC
38 | 39 | Home 40 | Another Menu Item 41 | 42 | Dropdown 43 | 44 | Abc 45 | Abc 46 | Abc 47 | 48 | 49 | Is n't it awesome 50 | 51 |
52 |
53 | ); 54 | } 55 | 56 | export default Basic; 57 | -------------------------------------------------------------------------------- /examples/cra/src/ThreePanels/ThreePanels.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 460px; 3 | padding-left: 50px; 4 | background: #2a2a4e; 5 | -webkit-box-shadow: 10px 0px 5px -7px rgba(0, 0, 0, 0.32); 6 | -moz-box-shadow: 10px 0px 5px -7px rgba(0, 0, 0, 0.32); 7 | box-shadow: 10px 0px 5px -7px rgba(0, 0, 0, 0.32); 8 | } 9 | .panelMid { 10 | width: 390px; 11 | background: #1da6ff; 12 | -webkit-box-shadow: 10px 0px 5px -7px rgba(0, 0, 0, 0.32); 13 | -moz-box-shadow: 10px 0px 5px -7px rgba(0, 0, 0, 0.32); 14 | box-shadow: 10px 0px 5px -7px rgba(0, 0, 0, 0.32); 15 | } 16 | .panelTop { 17 | background: #ff5964; 18 | width: 370px; 19 | padding-left: 60px; 20 | -webkit-box-shadow: 10px 0px 5px -7px rgba(0, 0, 0, 0.32); 21 | -moz-box-shadow: 10px 0px 5px -7px rgba(0, 0, 0, 0.32); 22 | box-shadow: 10px 0px 5px -7px rgba(0, 0, 0, 0.32); 23 | } 24 | .menu > div { 25 | background: #fff; 26 | margin-top: 20px; 27 | width: 280px; 28 | padding: 10px; 29 | border-radius: 3px; 30 | cursor: pointer; 31 | color: #2f2829; 32 | } 33 | .menu > div > svg { 34 | color: #797979; 35 | position: relative; 36 | left: -2px; 37 | font-size: 12px; 38 | } 39 | .settingsPanel { 40 | display: flex; 41 | justify-content: space-evenly; 42 | position: absolute; 43 | bottom: 0; 44 | width: 100%; 45 | left: 20px; 46 | text-align: center; 47 | padding: 10px; 48 | box-sizing: border-box; 49 | color: #fff; 50 | } 51 | .settingsPanel > svg { 52 | cursor: pointer; 53 | } 54 | .dropdown > div { 55 | padding-left: 30px; 56 | background: #ece7e7; 57 | margin: 10px 0; 58 | padding: 10px; 59 | border-radius: 3px; 60 | } 61 | .dropdownIcon { 62 | top: 10px; 63 | right: 10px; 64 | } 65 | -------------------------------------------------------------------------------- /examples/cra/src/ThreePanels/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import ReactOffcanvasComponent from "react-offcanvas-component"; 3 | import classNames from "./ThreePanels.module.css"; 4 | import { 5 | FaCog, 6 | FaEnvelope, 7 | FaCommentAlt, 8 | FaMagic, 9 | FaUserAstronaut, 10 | FaHome, 11 | FaIntercom, 12 | FaBaby, 13 | FaShareAlt 14 | } from "react-icons/fa"; 15 | const { 16 | AnimatedPanel, 17 | CloseButton, 18 | DropdownMenu, 19 | Menu 20 | } = ReactOffcanvasComponent; 21 | 22 | const MenuItem = Menu.Item; 23 | 24 | function ThreePanels() { 25 | const [visibility, setVisibility] = useState(false); 26 | 27 | const handleClick = () => { 28 | setVisibility(visibility => !visibility); 29 | }; 30 | return ( 31 |
32 | 35 | 41 | 42 | 43 | 44 | x 45 | 46 |

Logo

47 | 48 | 49 | Astro 50 | 51 | 52 | Home 53 | 54 | 55 | Settings 56 | 57 | 62 | Dropdown 63 | 64 | Abc 65 | Abc 66 | Abc 67 | 68 | 69 | 70 | Baby{" "} 71 | 72 | 77 | Dropdown 78 | 79 | Abc 80 | Abc 81 | Abc 82 | 83 | 84 | 85 |
86 | 87 | 88 | 89 | 90 |
91 |
92 |
93 |
94 |
95 | ); 96 | } 97 | 98 | export default ThreePanels; 99 | -------------------------------------------------------------------------------- /examples/cra/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import * as serviceWorker from "./serviceWorker"; 5 | 6 | ReactDOM.render(, document.getElementById("root")); 7 | 8 | // If you want your app to work offline and load faster, you can change 9 | // unregister() to register() below. Note this comes with some pitfalls. 10 | // Learn more about service workers: https://bit.ly/CRA-PWA 11 | serviceWorker.unregister(); 12 | -------------------------------------------------------------------------------- /examples/cra/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-offcanvas-component", 3 | "version": "0.0.0-development", 4 | "description": "Offcanvas Sidebar component for React", 5 | "main": "./dist/bundle.cjs.js", 6 | "module": "./dist/bundle.esm.js", 7 | "scripts": { 8 | "lint": "eslint 'src/**/*.js'", 9 | "lint:fix": "eslint 'src/**/*.js' --fix", 10 | "prettify": "prettier 'src/**/*.js' --write", 11 | "test": "jest", 12 | "test:watch": "jest --watch", 13 | "test:coverage": "jest --coverage", 14 | "test:coveralls": "jest --coverage --coverageReporters=text-lcov | coveralls", 15 | "commit": "git-cz", 16 | "build": "rollup -c", 17 | "build:watch": "rollup -c -w", 18 | "prebuild": "rimraf dist", 19 | "semantic-release": "semantic-release" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/samAbeywickrama/react-offcanvas-component.git" 24 | }, 25 | "keywords": [ 26 | "react", 27 | "reactjs-offcanvas", 28 | "offcanvas", 29 | "react-offcanvas-menu", 30 | "react-offcanvas-sidebar", 31 | "offcanvas", 32 | "react-offcanvas-menu", 33 | "react-sidebar-menu" 34 | ], 35 | "author": "Sameera C Abeywickrama", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/samAbeywickrama/react-offcanvas-component/issues" 39 | }, 40 | "homepage": "https://github.com/samAbeywickrama/react-offcanvas-component#readme", 41 | "devDependencies": { 42 | "@babel/core": "^7.5.5", 43 | "@babel/preset-env": "^7.5.5", 44 | "@babel/preset-react": "^7.0.0", 45 | "@commitlint/cli": "^8.1.0", 46 | "@commitlint/config-conventional": "^8.1.0", 47 | "babel-jest": "^24.8.0", 48 | "commitizen": "^4.0.3", 49 | "coveralls": "^3.0.5", 50 | "cz-conventional-changelog": "3.0.2", 51 | "eslint": "^6.1.0", 52 | "eslint-config-prettier": "^6.0.0", 53 | "eslint-plugin-jest": "^22.14.1", 54 | "eslint-plugin-prettier": "^3.1.0", 55 | "eslint-plugin-react": "^7.14.3", 56 | "git-cz": "^3.2.1", 57 | "husky": "^3.0.2", 58 | "identity-obj-proxy": "^3.0.0", 59 | "jest": "^24.8.0", 60 | "prettier": "1.18.2", 61 | "react": "16.9.0", 62 | "react-test-renderer": "^16.8.6", 63 | "rimraf": "^2.6.3", 64 | "rollup": "^1.17.0", 65 | "rollup-plugin-babel": "^4.3.3", 66 | "rollup-plugin-commonjs": "10.0.2", 67 | "rollup-plugin-node-resolve": "^5.2.0", 68 | "rollup-plugin-peer-deps-external": "^2.2.0", 69 | "rollup-plugin-postcss": "^2.0.3", 70 | "rollup-plugin-terser": "^5.1.1", 71 | "semantic-release": "^15.13.21" 72 | }, 73 | "husky": { 74 | "hooks": { 75 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 76 | "pre-commit": "npm run prettify && npm run lint" 77 | } 78 | }, 79 | "config": { 80 | "commitizen": { 81 | "path": "./node_modules/cz-conventional-changelog" 82 | } 83 | }, 84 | "peerDependencies": { 85 | "react": "^16.8.6" 86 | }, 87 | "engines": { 88 | "node": ">=10" 89 | }, 90 | "jest": { 91 | "moduleNameMapper": { 92 | "\\.(css)$": "/__mocks__/styles.js", 93 | "\\.(css|less)$": "identity-obj-proxy" 94 | } 95 | }, 96 | "dependencies": { 97 | "react-pose": "4.0.8" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from "rollup-plugin-babel"; 2 | import resolve from "rollup-plugin-node-resolve"; 3 | import peerDepsExternal from "rollup-plugin-peer-deps-external"; 4 | import postcss from "rollup-plugin-postcss"; 5 | import commonjs from "rollup-plugin-commonjs"; 6 | import { terser } from "rollup-plugin-terser"; 7 | 8 | const name = "ReactOffcanvasComponent"; 9 | const dist = "dist"; 10 | const bundleName = "bundle"; 11 | const prod = !process.env.ROLLUP_WATCH; 12 | 13 | export default { 14 | input: "src/index.js", 15 | output: [ 16 | { 17 | file: `${dist}/${bundleName}.cjs.js`, 18 | format: "cjs" 19 | }, 20 | { 21 | file: `${dist}/${bundleName}.esm.js`, 22 | format: "esm" 23 | }, 24 | { 25 | name: name, 26 | globals: { 27 | react: "React" 28 | }, 29 | file: `${dist}/${bundleName}.umd.js`, 30 | format: "umd" 31 | } 32 | ], 33 | plugins: [ 34 | peerDepsExternal(), 35 | resolve(), 36 | postcss({ 37 | modules: true 38 | }), 39 | babel({ 40 | exclude: "node_modules/**" 41 | }), 42 | prod && terser(), 43 | commonjs() 44 | ] 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/AnimatedPanel/AnimatedPanel.module.css: -------------------------------------------------------------------------------- 1 | .animatedPanel { 2 | width: 100%; 3 | height: 100%; 4 | box-sizing: border-box; 5 | position: fixed; 6 | top: 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/AnimatedPanel/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import posed from "react-pose"; 3 | import PropTypes from "prop-types"; 4 | import classNames from "./AnimatedPanel.module.css"; 5 | 6 | const PoseComponent = posed.div({ 7 | open: { 8 | x: ({ open }) => open.x, 9 | transition: ({ open }) => ({ ...open.transition }), 10 | delayChildren: ({ open }) => open.delayChildren, 11 | staggerChildren: ({ open }) => open.staggerChildren 12 | }, 13 | close: { 14 | x: ({ close }) => close.x, 15 | transition: ({ close }) => ({ ...close.transition }), 16 | afterChildren: ({ close }) => ({ ...close.afterChildren }) 17 | } 18 | }); 19 | 20 | function AnimatedPanel({ 21 | children, 22 | style, 23 | closeAnimation, 24 | openAnimation, 25 | className 26 | }) { 27 | const openDefaultAnimation = { 28 | x: "0%", 29 | transition: { 30 | ease: [0.175, 0.885, 0.32, 1.275], 31 | duration: 300 32 | }, 33 | delayChildren: 300, 34 | staggerChildren: 100 35 | }; 36 | 37 | const open = Object.assign({}, openDefaultAnimation, openAnimation, style); 38 | 39 | const closeDefaultAnimation = { 40 | x: "-100%", 41 | transition: { 42 | duration: 100 43 | }, 44 | afterChildren: true 45 | }; 46 | 47 | const close = Object.assign({}, closeDefaultAnimation, closeAnimation); 48 | 49 | return ( 50 | 56 | {children} 57 | 58 | ); 59 | } 60 | 61 | AnimatedPanel.propTypes = { 62 | children: PropTypes.oneOfType([ 63 | PropTypes.arrayOf(PropTypes.node), 64 | PropTypes.node 65 | ]), 66 | style: PropTypes.object, 67 | closeAnimation: PropTypes.object, 68 | openAnimation: PropTypes.object, 69 | className: PropTypes.string 70 | }; 71 | 72 | export default AnimatedPanel; 73 | -------------------------------------------------------------------------------- /src/components/CloseButton/CloseButton.module.css: -------------------------------------------------------------------------------- 1 | .closeButton { 2 | padding: 0; 3 | font-size: 16px; 4 | border: none; 5 | position: absolute; 6 | right: 5px; 7 | top: 5px; 8 | background: transparent; 9 | outline: none; 10 | cursor: pointer; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/CloseButton/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "./CloseButton.module.css"; 4 | 5 | function CloseButton({ children, onClick, style, className }) { 6 | return ( 7 | 14 | ); 15 | } 16 | 17 | CloseButton.propTypes = { 18 | children: PropTypes.oneOfType([ 19 | PropTypes.arrayOf(PropTypes.node), 20 | PropTypes.node 21 | ]).isRequired, 22 | onClick: PropTypes.func.isRequired, 23 | style: PropTypes.object, 24 | className: PropTypes.string 25 | }; 26 | 27 | export default CloseButton; 28 | -------------------------------------------------------------------------------- /src/components/Menu/DropdownMenu.js: -------------------------------------------------------------------------------- 1 | import React, { Children, cloneElement } from "react"; 2 | import PropTypes from "prop-types"; 3 | import posed from "react-pose"; 4 | import classNames from "./DropdownMenu.module.css"; 5 | 6 | // Todo: Add an icon to indicate the dropdown 7 | 8 | const DropdownWrapper = posed.div({ 9 | dropdownOpen: { 10 | height: "auto" 11 | }, 12 | dropdownClose: { 13 | height: 0 14 | } 15 | }); 16 | 17 | function DropDownMenu({ children, open = false, style, className }) { 18 | const handleClick = child => event => { 19 | event.stopPropagation(); 20 | if (child.props.onClick) { 21 | child.props.onClick(event); 22 | } 23 | }; 24 | 25 | const updatedChildren = Children.map(children, child => 26 | cloneElement(child, { 27 | onClick: handleClick(child) 28 | }) 29 | ); 30 | 31 | return ( 32 | 37 | {updatedChildren} 38 | 39 | ); 40 | } 41 | 42 | DropDownMenu.__name = "dropdown"; 43 | 44 | DropDownMenu.propTypes = { 45 | children: PropTypes.oneOfType([ 46 | PropTypes.arrayOf(PropTypes.node), 47 | PropTypes.node 48 | ]), 49 | style: PropTypes.object, 50 | open: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), 51 | className: PropTypes.string 52 | }; 53 | 54 | export default DropDownMenu; 55 | -------------------------------------------------------------------------------- /src/components/Menu/DropdownMenu.module.css: -------------------------------------------------------------------------------- 1 | .dropdown { 2 | overflow: hidden; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/Menu/Menu.js: -------------------------------------------------------------------------------- 1 | import React, { cloneElement, Children, useState } from "react"; 2 | import MenuItem from "./MenuItem"; 3 | import PropTypes from "prop-types"; 4 | 5 | function Menu({ el: El = "div", className, ...props }) { 6 | const [isDropdownOpen, setIsDropdownOpen] = useState(false); 7 | 8 | const handleClick = child => event => { 9 | if (child.props.onClick) { 10 | child.props.onClick(event); 11 | } 12 | if (child.props.hasDropdown) { 13 | setIsDropdownOpen( 14 | isDropdownOpen === child.props.name ? false : child.props.name 15 | ); 16 | } 17 | }; 18 | 19 | const children = Children.map(props.children, child => { 20 | return cloneElement(child, { 21 | onClick: handleClick(child), 22 | dropdownOpen: child.props.hasDropdown 23 | ? isDropdownOpen === child.props.name 24 | : null 25 | }); 26 | }); 27 | 28 | return {children}; 29 | } 30 | 31 | Menu.propTypes = { 32 | el: PropTypes.string, 33 | className: PropTypes.string, 34 | children: PropTypes.oneOfType([ 35 | PropTypes.arrayOf(PropTypes.node), 36 | PropTypes.node 37 | ]) 38 | }; 39 | 40 | Menu.Item = MenuItem; 41 | 42 | export default Menu; 43 | -------------------------------------------------------------------------------- /src/components/Menu/MenuItem.js: -------------------------------------------------------------------------------- 1 | import React, { Children, cloneElement } from "react"; 2 | import posed from "react-pose"; 3 | import PropTypes from "prop-types"; 4 | import classNames from "./MenuItem.module.css"; 5 | 6 | // TODO: Accept dropdown icon classname via props 7 | 8 | const Item = posed.div({ 9 | open: { 10 | y: 0, 11 | opacity: 1 12 | }, 13 | close: { y: 20, opacity: 0 } 14 | }); 15 | 16 | function MenuItem({ 17 | children, 18 | onClick, 19 | dropdownOpen, 20 | style, 21 | className, 22 | hasDropdown, 23 | dropdownIconClassName 24 | }) { 25 | const updatedChildren = Children.map(children, child => { 26 | if (child.type && child.type.__name === "dropdown") { 27 | return cloneElement(child, { 28 | open: dropdownOpen 29 | }); 30 | } 31 | return child; 32 | }); 33 | return ( 34 | 39 | {updatedChildren}{" "} 40 | {hasDropdown && 41 | (dropdownOpen ? ( 42 |
47 | - 48 |
49 | ) : ( 50 |
55 | + 56 |
57 | ))} 58 |
59 | ); 60 | } 61 | 62 | MenuItem.__name = "MenuItem"; 63 | 64 | MenuItem.propTypes = { 65 | children: PropTypes.oneOfType([ 66 | PropTypes.arrayOf(PropTypes.node), 67 | PropTypes.node 68 | ]), 69 | style: PropTypes.object, 70 | onClick: PropTypes.func, 71 | dropdownOpen: PropTypes.bool, 72 | className: PropTypes.string, 73 | dropdownIconClassName: PropTypes.string, 74 | hasDropdown: PropTypes.bool 75 | }; 76 | 77 | export default MenuItem; 78 | -------------------------------------------------------------------------------- /src/components/Menu/MenuItem.module.css: -------------------------------------------------------------------------------- 1 | .dropdownIcon { 2 | position: absolute; 3 | top: 0; 4 | right: 0; 5 | } 6 | .menuItem { 7 | position: relative; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Menu/index.js: -------------------------------------------------------------------------------- 1 | export { default as DropdownMenu } from "./DropdownMenu"; 2 | export { default } from "./Menu"; 3 | -------------------------------------------------------------------------------- /src/components/Overlay/Overlay.module.css: -------------------------------------------------------------------------------- 1 | .overlay { 2 | position: fixed; 3 | background: rgba(0, 0, 0, 0.5); 4 | z-index: 888; 5 | top: 0; 6 | bottom: 0; 7 | left: 0; 8 | right: 0; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Overlay/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import posed, { PoseGroup } from "react-pose"; 3 | import PropTypes from "prop-types"; 4 | import classNames from "./Overlay.module.css"; 5 | 6 | const AnimatedOverlay = posed.div({ 7 | enter: { opacity: 1 }, 8 | exit: { opacity: 0, delay: 500 } 9 | }); 10 | 11 | const Overlay = ({ className, open }) => { 12 | return ( 13 | 14 | {open && ( 15 | 20 | )} 21 | 22 | ); 23 | }; 24 | 25 | Overlay.propTypes = { 26 | className: PropTypes.string, 27 | open: PropTypes.bool 28 | }; 29 | 30 | export default Overlay; 31 | -------------------------------------------------------------------------------- /src/components/ReactOffcanvasComponent/ReactOffcanvasComponent.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import posed from "react-pose"; 3 | import PropTypes from "prop-types"; 4 | import classNames from "./ReactOffcanvasComponent.module.css"; 5 | import Overlay from "../Overlay"; 6 | 7 | const SidebarWrapperPanel = posed.section({ 8 | open: { 9 | x: ({ open }) => open.x, 10 | transition: ({ open }) => ({ ...open.transition }), 11 | delayChildren: ({ open }) => open.delayChildren, 12 | staggerChildren: ({ open }) => open.staggerChildren 13 | }, 14 | close: { 15 | x: ({ close }) => close.x, 16 | delayChildren: ({ close }) => close.delayChildren, 17 | transition: ({ close }) => ({ ...close.transition }), 18 | afterChildren: ({ close }) => close.afterChildren 19 | } 20 | }); 21 | 22 | function SidebarWrapper({ 23 | children, 24 | open = false, 25 | className, 26 | style, 27 | openAnimation, 28 | closeAnimation, 29 | overlay, 30 | overlayClassName 31 | }) { 32 | const defaultOpenAnimation = { 33 | x: "-100px", 34 | transition: { 35 | ease: [0.175, 0.885, 0.32, 1.275], 36 | duration: 300 37 | }, 38 | delayChildren: 150, 39 | staggerChildren: 100 40 | }; 41 | 42 | const defaultCloseAnimation = { 43 | x: "-100%", 44 | transition: { 45 | duration: 100 46 | }, 47 | afterChildren: true 48 | }; 49 | 50 | const updatedOpenAnimation = Object.assign( 51 | {}, 52 | defaultOpenAnimation, 53 | openAnimation 54 | ); 55 | 56 | const updatedCloseAnimation = Object.assign( 57 | {}, 58 | defaultCloseAnimation, 59 | closeAnimation 60 | ); 61 | 62 | const props = { 63 | className: `${classNames.wrapper} ${className}`, 64 | pose: open ? "open" : "close", 65 | style, 66 | open: updatedOpenAnimation, 67 | close: updatedCloseAnimation 68 | }; 69 | 70 | if (overlay) { 71 | return ( 72 | <> 73 | {" "} 74 | {children} 75 | 76 | ); 77 | } 78 | 79 | return {children}; 80 | } 81 | 82 | SidebarWrapper.propTypes = { 83 | children: PropTypes.oneOfType([ 84 | PropTypes.arrayOf(PropTypes.node), 85 | PropTypes.node 86 | ]), 87 | open: PropTypes.bool.isRequired, 88 | className: PropTypes.string, 89 | style: PropTypes.object, 90 | openAnimation: PropTypes.object, 91 | closeAnimation: PropTypes.object, 92 | overlay: PropTypes.bool, 93 | overlayClassName: PropTypes.bool 94 | }; 95 | 96 | export default SidebarWrapper; 97 | -------------------------------------------------------------------------------- /src/components/ReactOffcanvasComponent/ReactOffcanvasComponent.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | box-sizing: border-box; 3 | height: 100vh; 4 | position: fixed; 5 | width: 400px; 6 | top: 0; 7 | left: 0; 8 | z-index: 999; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import ReactOffcanvasComponent from "./ReactOffcanvasComponent/ReactOffcanvasComponent"; 2 | import CloseButton from "./CloseButton"; 3 | import AnimatedPanel from "./AnimatedPanel"; 4 | import Menu, { DropdownMenu } from "./Menu"; 5 | 6 | ReactOffcanvasComponent.AnimatedPanel = AnimatedPanel; 7 | ReactOffcanvasComponent.CloseButton = CloseButton; 8 | ReactOffcanvasComponent.DropdownMenu = DropdownMenu; 9 | ReactOffcanvasComponent.Menu = Menu; 10 | 11 | export default ReactOffcanvasComponent; 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./components"; 2 | --------------------------------------------------------------------------------