├── .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 | [](https://travis-ci.org/samAbeywickrama/react-offcanvas-component)
2 | 
3 | [](https://github.com/prettier/prettier)
4 | [](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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------