├── .babelrc ├── .eslintrc ├── .github └── workflows │ └── semgrep.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── .gitignore ├── animated │ ├── index.html │ ├── script.js │ └── styles.css ├── basic │ ├── index.html │ ├── script.js │ └── styles.css ├── redux │ ├── index.html │ ├── script.js │ └── styles.css └── universal │ ├── Application.js │ ├── browser.js │ ├── server.js │ └── styles.css ├── karma.conf.js ├── package.json ├── src ├── Modal.js └── index.js └── test ├── .eslintrc └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["cf"], 3 | "plugins": [] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | # vim: set ft=yaml: 2 | --- 3 | env: 4 | browser: true 5 | node: true 6 | plugins: 7 | - "cflint" 8 | rules: 9 | # Possible Errors 10 | no-reserved-keys: 2 11 | quotes: 12 | - 2 13 | - "single" 14 | # Best Practices 15 | block-scoped-var: 2 16 | default-case: 2 17 | guard-for-in: 2 18 | no-else-return: 2 19 | no-floating-decimal: 2 20 | no-self-compare: 2 21 | no-void: 2 22 | radix: 2 23 | ## Possible that this will switch to a "2" later 24 | wrap-iife: 2 25 | # Strict Mode 26 | global-strict: 27 | - 2 28 | - "always" 29 | # Variables 30 | no-catch-shadow: 2 31 | # Node.js 32 | handle-callback-err: 2 33 | # Stylistic Issues 34 | brace-style: 35 | - 1 36 | - "1tbs" 37 | camelcase: 0 38 | comma-style: 39 | - 2 40 | - "last" 41 | no-lonely-if: 2 42 | no-nested-ternary: 2 43 | no-underscore-dangle: 0 44 | quote-props: 45 | - 1 46 | - "as-needed" 47 | semi: 48 | - 2 49 | - "always" 50 | space-unary-ops: 2 51 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: {} 3 | workflow_dispatch: {} 4 | push: 5 | branches: 6 | - main 7 | - master 8 | schedule: 9 | - cron: '0 0 * * *' 10 | name: Semgrep config 11 | jobs: 12 | semgrep: 13 | name: semgrep/ci 14 | runs-on: ubuntu-latest 15 | env: 16 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 17 | SEMGREP_URL: https://cloudflare.semgrep.dev 18 | SEMGREP_APP_URL: https://cloudflare.semgrep.dev 19 | SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version 20 | container: 21 | image: semgrep/semgrep 22 | steps: 23 | - uses: actions/checkout@v4 24 | - run: semgrep ci 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | lib 4 | coverage 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | src 4 | coverage 5 | test 6 | example 7 | karma.conf.js 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v5.0.2 4 | - Fixing regression introduced in v5.0.1 that affected event propagation [#33](https://github.com/cloudflare/react-modal2/pull/33) 5 | 6 | ## v5.0.1 7 | - Prevent modal from closing when backdrop is partial target of click [#30](https://github.com/cloudflare/react-modal2/pull/30) 8 | 9 | ## v5.0.0 10 | - Version bump. No changes from v3.2.0. 11 | 12 | ## v4.0.0 13 | DEPRECATED. Do not use. 14 | 15 | ## v3.2.0 16 | - Replace string refs with callback refs [#19](https://github.com/cloudflare/react-modal2/pull/19) 17 | - Use `prop-types` package instead of `React.PropTypes` (React 15.5.0 compatibility) 18 | 19 | ## v3.1.0 20 | - Replace `React.createClass` with ES6 Class (React 15.0.0 compatibility) 21 | 22 | ## v3.0.2 23 | - Remove unnecessary `bind` on `keydown` handler [#10](https://github.com/cloudflare/react-modal2/pull/10) 24 | 25 | ## v3.0.1 26 | - Docs update only 27 | 28 | ## v3.0.0 29 | - Universal/Isomorphic modals 30 | 31 | ### BREAKING CHANGES 32 | - Removed `ReactModal2.setApplicationElement()`. You must now [override](https://github.com/cloudflare/react-modal2/blob/v3.0.1/README.md#accessibility) `React.getApplicationElement()` in your application. 33 | 34 | ## v2.0.0 35 | - Remove `ReactGateway` from automatically being included. This is an implementation detail that should be in your modal component. 36 | - Allows for alternatives other than `ReactGateway` 37 | - Allows wrapping `ReactModal` inside `ReactGateway` 38 | 39 | ## v1.0.2 40 | - Fix `styles` => `style` 41 | 42 | ## v1.0.1 43 | - Fix references 44 | 45 | ## v1.0.0 46 | - Initial version 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, CloudFlare, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-modal2 2 | 3 | > Simple modal component for React. 4 | 5 | - Unopionated 6 | - Stateless (dumb component) 7 | - Accessible 8 | - Universal/Isomorphic 9 | - Built via [reusable](https://github.com/cloudflare/react-gateway) [collection](https://github.com/cloudflare/a11y-focus-scope) of [modules](https://github.com/cloudflare/a11y-focus-store) 10 | 11 | ## Installation 12 | 13 | ```js 14 | $ npm install --save react-modal2 15 | ``` 16 | 17 | ## Usage 18 | 19 | ReactModal2 tries to be as minimal as possible. This means it requires a little 20 | bit of setup, but gives you complete flexibility to do what you want. 21 | 22 | Let's start off with the actual API of ReactModal2: 23 | 24 | ```js 25 | 43 | ... 44 | 45 | ``` 46 | 47 | If we use it like this it will simply render those two elements in the dom like 48 | this: 49 | 50 | ```html 51 |
52 |
...
53 |
54 | ``` 55 | 56 | However, you likely want to render the modal somewhere else in the DOM (in most 57 | cases at the end of the `document.body`. 58 | 59 | For this there is a separate library called 60 | [React Gateway](https://github.com/cloudflare/react-gateway). You can use it 61 | like this: 62 | 63 | ```js 64 | import { 65 | Gateway, 66 | GatewayDest, 67 | GatewayProvider 68 | } from 'react-gateway'; 69 | import ReactModal2 from 'react-modal2'; 70 | 71 | class Application extends React.Component { 72 | render() { 73 | return ( 74 | 75 |
76 |
77 |

My Application

78 | 79 | 80 | ... 81 | 82 | 83 |
84 | 85 |
86 |
87 | ); 88 | } 89 | } 90 | ``` 91 | 92 | Which will render as: 93 | 94 | ```html 95 |
96 |
97 |

My Application

98 |
100 | 105 |
106 | ``` 107 | 108 | Now this might seem like a lot to do every time you want to render a modal, but 109 | this is by design. You are meant to wrap ReactModal2 with your own component 110 | that you use everywhere. Your component can add it's own DOM, styles, 111 | animations, and behavior. 112 | 113 | ```js 114 | import React from 'react'; 115 | import {Gateway} from 'react-gateway'; 116 | import ReactModal2 from 'react-modal2'; 117 | 118 | export default class MyCustomModal extends React.Component { 119 | static propTypes = { 120 | onClose: React.PropTypes.func.isRequired, 121 | closeOnEsc: React.PropTypes.bool, 122 | closeOnBackdropClick: React.PropTypes.bool 123 | }; 124 | 125 | getDefaultProps() { 126 | return { 127 | closeOnEsc: true, 128 | closeOnBackdropClick: true 129 | }; 130 | } 131 | 132 | render() { 133 | return ( 134 | 135 | 141 | {this.props.children} 142 | 143 | 144 | ); 145 | } 146 | } 147 | ``` 148 | 149 | Then simply setup your application once: 150 | 151 | ```js 152 | import { 153 | GatewayDest, 154 | GatewayProvider 155 | } from 'react-gateway'; 156 | 157 | export default class Application extends React.Component { 158 | render() { 159 | return ( 160 | 161 |
162 |
163 | ... 164 |
165 | 166 |
167 |
168 | ); 169 | } 170 | } 171 | ``` 172 | 173 | Then you have your own ideal API for working with modals in any of your 174 | components. 175 | 176 | ```js 177 | import MyCustomModal from './my-custom-modal'; 178 | 179 | export default class MyComponent extends React.Component { 180 | state = { 181 | isModalOpen: false 182 | }; 183 | 184 | handleOpen() { 185 | this.setState({ isModalOpen: true }); 186 | } 187 | 188 | handleClose() { 189 | this.setState({ isModalOpen: false }); 190 | } 191 | 192 | render() { 193 | return ( 194 |
195 | 196 | {this.state.isModalOpen && ( 197 | 198 |

Hello from Modal

199 | 200 |
201 | )} 202 |
203 | ); 204 | } 205 | } 206 | ``` 207 | 208 | ## Props 209 | | Name | Type | Description | 210 | | --- | --- | --- | 211 | | `onClose` | `Function` | **Required.** A callback to handle an event that is attempting to close the modal. | 212 | | `closeOnEsc` | `Boolean` | Should this modal call `onClose` when the `esc` key is pressed? | 213 | | `closeOnBackdropClick` | `Boolean` | Should this modal call `onClose` when the backdrop is clicked? | 214 | | `backdropClassName` | `String` | An optional `className` for the backdrop element. | 215 | | `modalClassName` | `String` | An optional `className` for the modal element. | 216 | | `backdropStyles` | `Object` | Optional `style` for the backdrop element. | 217 | | `modalStyles` | `Object` | Optional `style` for the modal element. | 218 | 219 | 220 | 221 | ## Accessibility 222 | 223 | One of ReactModal2's opinions is that modals should be as accessible as 224 | possible. It does much of the work for you, but there's one little thing you 225 | need to help it with. 226 | 227 | In order to "hide" your application from screenreaders while a modal is open 228 | you need to let ReactModal2 what the root element for your application is. 229 | 230 | > **Note:** The root element should not contain the `GatewayDest` or whereever 231 | > the modal is getting rendered. This will break all the things. 232 | 233 | ```js 234 | import ReactModal2 from 'react-modal2'; 235 | 236 | ReactModal2.getApplicationElement = () => document.getElementById('application'); 237 | ``` 238 | 239 | ## FAQ 240 | 241 | #### How do I close the modal? 242 | 243 | ReactModal2 is designed to have no state, if you put it in the DOM then it will 244 | render. So if you don't want to show it then simply do not render it in your 245 | parent component. For this reason there is no `isOpen` property to pass. 246 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | */bundle.js 2 | -------------------------------------------------------------------------------- /example/animated/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ReactModal2 Example: Animated 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/animated/script.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import {render} from 'react-dom'; 3 | import { 4 | Gateway, 5 | GatewayDest, 6 | GatewayProvider 7 | } from 'react-gateway'; 8 | import CSSTransitionGroup from 'react-addons-css-transition-group'; 9 | import ReactModal2 from '../../src/index'; 10 | 11 | class Modal extends React.Component { 12 | static propTypes = { 13 | isOpen: PropTypes.bool.isRequired, 14 | onClose: PropTypes.func.isRequired, 15 | closeOnEsc: PropTypes.bool, 16 | closeOnBackdropClick: PropTypes.bool 17 | }; 18 | 19 | static defaultProps = { 20 | closeOnEsc: true, 21 | closeOnBackdropClick: true 22 | }; 23 | 24 | handleClose() { 25 | this.props.onClose(); 26 | } 27 | 28 | render() { 29 | return ( 30 | 31 | 37 | {this.props.isOpen && ( 38 | 46 | {this.props.children} 47 | 48 | )} 49 | 50 | 51 | ); 52 | } 53 | } 54 | 55 | class Application extends React.Component { 56 | constructor() { 57 | super(); 58 | } 59 | 60 | state = { 61 | isModalOpen: false 62 | }; 63 | 64 | handleOpen() { 65 | this.setState({ isModalOpen: true }); 66 | } 67 | 68 | handleClose() { 69 | this.setState({ isModalOpen: false }); 70 | } 71 | 72 | render() { 73 | return ( 74 | 75 |
76 |
77 |

ReactModal2 Example: Animated

78 | 79 | 80 |

Hello from Modal

81 | 82 |
83 |
84 | 85 |
86 |
87 | ); 88 | } 89 | } 90 | 91 | ReactModal2.getApplicationElement = () => document.getElementById('application'); 92 | render(, document.getElementById('root')); 93 | -------------------------------------------------------------------------------- /example/animated/styles.css: -------------------------------------------------------------------------------- 1 | *, 2 | ::before, 3 | ::after { 4 | -webkit-box-sizing: border-box; 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | margin: 2em auto; 10 | padding: 0 2em; 11 | max-width: 600px; 12 | font: normal 1em/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 13 | text-align: center; 14 | } 15 | 16 | button { 17 | width: 100%; 18 | padding: 1em 2em; 19 | border: none; 20 | border-radius: 3px; 21 | background: #36c; 22 | color: white; 23 | font: inherit; 24 | } 25 | 26 | .modal-backdrop { 27 | position: absolute; 28 | top: 0; 29 | left: 0; 30 | width: 100%; 31 | height: 100%; 32 | background: rgba(0, 0, 0, 0.8); 33 | } 34 | 35 | .modal { 36 | position: relative; 37 | top: 50%; 38 | -webkit-transform: translate(0, -50%); 39 | -moz-transform: translate(0, -50%); 40 | transform: translate(0, -50%); 41 | max-width: 400px; 42 | margin: 0 auto; 43 | padding: 20px; 44 | 45 | background: white; 46 | border-radius: 4px; 47 | 48 | outline: none; 49 | } 50 | 51 | .modal h1 { 52 | margin-top: 0; 53 | } 54 | 55 | /** 56 | * Animations 57 | */ 58 | 59 | /** 60 | * Backdrop 61 | */ 62 | 63 | .modal-appear.modal-backdrop, 64 | .modal-enter.modal-backdrop, 65 | .modal-leave.modal-leave-active.modal-backdrop { 66 | background: transparent; 67 | } 68 | 69 | .modal-appear.modal-appear-active.modal-backdrop, 70 | .modal-enter.modal-enter-active.modal-backdrop, 71 | .modal-leave.modal-backdrop { 72 | background: rgba(0, 0, 0, 0.8); 73 | } 74 | 75 | .modal-appear-active.modal-backdrop, 76 | .modal-enter-active.modal-backdrop, 77 | .modal-leave-active.modal-backdrop { 78 | transition: background 200ms ease-in; 79 | } 80 | 81 | /** 82 | * Modal 83 | */ 84 | 85 | .modal-appear .modal, 86 | .modal-enter .modal, 87 | .modal-leave.modal-leave-active .modal { 88 | opacity: 0; 89 | transform: translate(0, -100%); 90 | } 91 | 92 | .modal-appear.modal-appear-active .modal, 93 | .modal-enter.modal-enter-active .modal, 94 | .modal-leave .modal { 95 | opacity: 1; 96 | transform: translate(0, -50%); 97 | } 98 | 99 | .modal-appear-active .modal, 100 | .modal-enter-active .modal, 101 | .modal-leave-active .modal { 102 | transition: opacity 200ms ease, transform 200ms ease; 103 | } 104 | -------------------------------------------------------------------------------- /example/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ReactModal2 Example: Basic 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/basic/script.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import {render} from 'react-dom'; 3 | import { 4 | Gateway, 5 | GatewayDest, 6 | GatewayProvider 7 | } from 'react-gateway'; 8 | import ReactModal2 from '../../src/index'; 9 | 10 | class Modal extends React.Component { 11 | static propTypes = { 12 | onClose: PropTypes.func.isRequired, 13 | closeOnEsc: PropTypes.bool, 14 | closeOnBackdropClick: PropTypes.bool 15 | }; 16 | 17 | static defaultProps = { 18 | closeOnEsc: true, 19 | closeOnBackdropClick: true 20 | }; 21 | 22 | handleClose() { 23 | this.props.onClose(); 24 | } 25 | 26 | render() { 27 | return ( 28 | 29 | 36 | {this.props.children} 37 | 38 | 39 | ); 40 | } 41 | } 42 | 43 | class Application extends React.Component { 44 | constructor() { 45 | super(); 46 | } 47 | 48 | state = { 49 | isModalOpen: false 50 | }; 51 | 52 | handleOpen() { 53 | this.setState({ isModalOpen: true }); 54 | } 55 | 56 | handleClose() { 57 | this.setState({ isModalOpen: false }); 58 | } 59 | 60 | render() { 61 | return ( 62 | 63 |
64 |
65 |

ReactModal2 Example: Basic

66 | 67 | {this.state.isModalOpen && ( 68 | 69 |

Hello from Modal

70 | 71 |
72 | )} 73 |
74 | 75 |
76 |
77 | ); 78 | } 79 | } 80 | 81 | ReactModal2.getApplicationElement = () => document.getElementById('application'); 82 | render(, document.getElementById('root')); 83 | -------------------------------------------------------------------------------- /example/basic/styles.css: -------------------------------------------------------------------------------- 1 | *, 2 | ::before, 3 | ::after { 4 | -webkit-box-sizing: border-box; 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | margin: 2em auto; 10 | padding: 0 2em; 11 | max-width: 600px; 12 | font: normal 1em/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 13 | text-align: center; 14 | } 15 | 16 | button { 17 | width: 100%; 18 | padding: 1em 2em; 19 | border: none; 20 | border-radius: 3px; 21 | background: #36c; 22 | color: white; 23 | font: inherit; 24 | } 25 | 26 | .modal-backdrop { 27 | position: absolute; 28 | top: 0; 29 | left: 0; 30 | width: 100%; 31 | height: 100%; 32 | background: rgba(0, 0, 0, 0.8); 33 | } 34 | 35 | .modal { 36 | position: relative; 37 | top: 50%; 38 | -webkit-transform: translate(0, -50%); 39 | -moz-transform: translate(0, -50%); 40 | transform: translate(0, -50%); 41 | max-width: 400px; 42 | margin: 0 auto; 43 | padding: 20px; 44 | 45 | background: white; 46 | border-radius: 4px; 47 | 48 | outline: none; 49 | } 50 | 51 | .modal h1 { 52 | margin-top: 0; 53 | } 54 | -------------------------------------------------------------------------------- /example/redux/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ReactModal2 Example: Redux 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/redux/script.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import {render} from 'react-dom'; 3 | import {createStore} from 'redux'; 4 | import {Provider, connect} from 'react-redux'; 5 | import { 6 | Gateway, 7 | GatewayDest, 8 | GatewayProvider 9 | } from 'react-gateway'; 10 | import ReactModal2 from '../../src/index'; 11 | 12 | /** 13 | * Constants 14 | */ 15 | 16 | const ActionTypes = { 17 | MODAL_OPEN: 'MODAL_OPEN', 18 | MODAL_CLOSE: 'MODAL_ClOSE' 19 | }; 20 | 21 | /** 22 | * Actions 23 | */ 24 | 25 | const actions = { 26 | modalOpen: () => ({ type: ActionTypes.MODAL_OPEN }), 27 | modalClose: () => ({ type: ActionTypes.MODAL_CLOSE }) 28 | }; 29 | 30 | /** 31 | * Reducer 32 | */ 33 | 34 | const initialState = { isModalOpen: false };; 35 | 36 | function reducer(state = initialState, action) { 37 | switch (action.type) { 38 | case ActionTypes.MODAL_OPEN: return { ...state, isModalOpen: true }; 39 | case ActionTypes.MODAL_CLOSE: return { ...state, isModalOpen: false }; 40 | default: return state; 41 | } 42 | } 43 | 44 | /** 45 | * Modal Component 46 | */ 47 | 48 | class Modal extends React.Component { 49 | static propTypes = { 50 | onClose: PropTypes.func.isRequired, 51 | closeOnEsc: PropTypes.bool, 52 | closeOnBackdropClick: PropTypes.bool 53 | }; 54 | 55 | static defaultProps = { 56 | closeOnEsc: true, 57 | closeOnBackdropClick: true 58 | }; 59 | 60 | handleClose() { 61 | this.props.onClose(); 62 | } 63 | 64 | render() { 65 | return ( 66 | 67 | 74 | {this.props.children} 75 | 76 | 77 | ); 78 | } 79 | } 80 | 81 | /** 82 | * Application Container 83 | */ 84 | 85 | class Application extends React.Component { 86 | handleOpen() { 87 | this.props.dispatch(actions.modalOpen()); 88 | } 89 | 90 | handleClose() { 91 | this.props.dispatch(actions.modalClose()); 92 | } 93 | 94 | render() { 95 | return ( 96 | 97 |
98 |
99 |

ReactModal2 Example: Redux

100 | 101 | {this.props.isModalOpen && ( 102 | 103 |

Hello from Modal

104 | 105 |
106 | )} 107 |
108 | 109 |
110 |
111 | ); 112 | } 113 | } 114 | 115 | Application = connect(state => ({ isModalOpen: state.isModalOpen }))(Application); 116 | 117 | /** 118 | * Init 119 | */ 120 | 121 | ReactModal2.getApplicationElement = () => document.getElementById('application'); 122 | 123 | const store = createStore(reducer); 124 | 125 | render( 126 | 127 | 128 | 129 | , document.getElementById('root')); 130 | -------------------------------------------------------------------------------- /example/redux/styles.css: -------------------------------------------------------------------------------- 1 | *, 2 | ::before, 3 | ::after { 4 | -webkit-box-sizing: border-box; 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | margin: 2em auto; 10 | padding: 0 2em; 11 | max-width: 600px; 12 | font: normal 1em/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 13 | text-align: center; 14 | } 15 | 16 | button { 17 | width: 100%; 18 | padding: 1em 2em; 19 | border: none; 20 | border-radius: 3px; 21 | background: #36c; 22 | color: white; 23 | font: inherit; 24 | } 25 | 26 | .modal-backdrop { 27 | position: absolute; 28 | top: 0; 29 | left: 0; 30 | width: 100%; 31 | height: 100%; 32 | background: rgba(0, 0, 0, 0.8); 33 | } 34 | 35 | .modal { 36 | position: relative; 37 | top: 50%; 38 | -webkit-transform: translate(0, -50%); 39 | -moz-transform: translate(0, -50%); 40 | transform: translate(0, -50%); 41 | max-width: 400px; 42 | margin: 0 auto; 43 | padding: 20px; 44 | 45 | background: white; 46 | border-radius: 4px; 47 | 48 | outline: none; 49 | } 50 | 51 | .modal h1 { 52 | margin-top: 0; 53 | } 54 | -------------------------------------------------------------------------------- /example/universal/Application.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import {render} from 'react-dom'; 3 | import { 4 | Gateway, 5 | GatewayDest, 6 | GatewayProvider 7 | } from 'react-gateway'; 8 | import ReactModal2 from '../../src/index'; 9 | 10 | class Modal extends React.Component { 11 | static propTypes = { 12 | onClose: PropTypes.func.isRequired, 13 | closeOnEsc: PropTypes.bool, 14 | closeOnBackdropClick: PropTypes.bool 15 | }; 16 | 17 | static defaultProps = { 18 | closeOnEsc: true, 19 | closeOnBackdropClick: true 20 | }; 21 | 22 | handleClose() { 23 | this.props.onClose(); 24 | } 25 | 26 | render() { 27 | return ( 28 | 29 | 36 | {this.props.children} 37 | 38 | 39 | ); 40 | } 41 | } 42 | 43 | export default class Application extends React.Component { 44 | constructor() { 45 | super(); 46 | } 47 | 48 | state = { 49 | isModalOpen: true 50 | }; 51 | 52 | handleOpen() { 53 | this.setState({ isModalOpen: true }); 54 | } 55 | 56 | handleClose() { 57 | this.setState({ isModalOpen: false }); 58 | } 59 | 60 | render() { 61 | return ( 62 | 63 |
64 |
65 |

ReactModal2 Example: Basic

66 | 67 | {this.state.isModalOpen && ( 68 | 69 |

Hello from Modal

70 | 71 |
72 | )} 73 |
74 | 75 |
76 |
77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /example/universal/browser.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Application from './Application'; 3 | import ReactDOM from 'react-dom'; 4 | import ReactModal2 from '../../src/index'; 5 | 6 | ReactModal2.getApplicationElement = () => document.getElementById('application'); 7 | ReactDOM.render(, document.getElementById('root')); 8 | -------------------------------------------------------------------------------- /example/universal/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | import React from 'react'; 4 | import ReactDOMServer from 'react-dom/server'; 5 | 6 | import Application from './Application'; 7 | 8 | const app = express(); 9 | 10 | app.get('/', (req, res) => { 11 | res.send(` 12 | 13 | 14 | 15 | React Gateway Universal Example 16 | 17 | 18 | 19 |
${ReactDOMServer.renderToString()}
20 | 21 | 22 | 23 | `); 24 | }); 25 | 26 | app.get('/bundle.js', (req, res) => res.sendFile(path.join(__dirname, 'bundle.js'))); 27 | app.get('/styles.css', (req, res) => res.sendFile(path.join(__dirname, 'styles.css'))); 28 | 29 | app.listen(3000); 30 | console.log('>> http://localhost:3000/'); 31 | -------------------------------------------------------------------------------- /example/universal/styles.css: -------------------------------------------------------------------------------- 1 | *, 2 | ::before, 3 | ::after { 4 | -webkit-box-sizing: border-box; 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | margin: 2em auto; 10 | padding: 0 2em; 11 | max-width: 600px; 12 | font: normal 1em/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 13 | text-align: center; 14 | } 15 | 16 | button { 17 | width: 100%; 18 | padding: 1em 2em; 19 | border: none; 20 | border-radius: 3px; 21 | background: #36c; 22 | color: white; 23 | font: inherit; 24 | } 25 | 26 | .modal-backdrop { 27 | position: absolute; 28 | top: 0; 29 | left: 0; 30 | width: 100%; 31 | height: 100%; 32 | background: rgba(0, 0, 0, 0.8); 33 | } 34 | 35 | .modal { 36 | position: relative; 37 | top: 50%; 38 | -webkit-transform: translate(0, -50%); 39 | -moz-transform: translate(0, -50%); 40 | transform: translate(0, -50%); 41 | max-width: 400px; 42 | margin: 0 auto; 43 | padding: 20px; 44 | 45 | background: white; 46 | border-radius: 4px; 47 | 48 | outline: none; 49 | } 50 | 51 | .modal h1 { 52 | margin-top: 0; 53 | } 54 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var customLaunchers = {}; 3 | 4 | var minimist = require('minimist'); 5 | var defined = require('defined'); 6 | 7 | var args = minimist(process.argv.slice(2), { 8 | string: ['env', 'build-branch', 'build-number', 'suace-username', 'sauce-key'], 9 | 'default': { 10 | env: process.env.NODE_ENV, 11 | 'build-branch': process.env.BUILD_BRANCH, 12 | 'build-number': process.env.BUILD_NUMBER, 13 | 'sauce-username': process.env.SAUCE_USERNAME, 14 | 'sauce-key': process.env.SAUCE_ACCESS_KEY 15 | } 16 | }); 17 | 18 | args.istanbul = defined(args.istanbul, args.env !== 'CI'); 19 | args['sauce-labs'] = defined(args['sauce-labs'], args.env === 'CI'); 20 | 21 | // Overridable arguments are denoted below. Other arguments can be found in the 22 | // [Karma configuration](http://karma-runner.github.io/0.12/config/configuration-file.html) 23 | 24 | ['chrome', 'firefox', 'iphone', 'ipad', 'android'].forEach(function(browser) { 25 | customLaunchers['sl_' + browser] = { 26 | base: 'SauceLabs', 27 | browserName: browser 28 | }; 29 | }); 30 | 31 | // Safari defaults to version 5 on Windows 7 (huh?) 32 | customLaunchers.sl_safari = { 33 | base: 'SauceLabs', 34 | browserName: 'safari', 35 | platform: 'OS X 10.9' 36 | }; 37 | 38 | [9, 10, 11].forEach(function(version) { 39 | customLaunchers['sl_ie_' + version] = { 40 | base: 'SauceLabs', 41 | browserName: 'internet explorer', 42 | version: version 43 | }; 44 | }); 45 | 46 | var reporters = ['mocha', 'beep']; 47 | 48 | if (args.istanbul) { 49 | reporters.push('coverage'); 50 | } 51 | 52 | module.exports = function(config) { 53 | config.set({ 54 | frameworks: ['browserify', 'mocha'], 55 | 56 | files: [ 57 | { 58 | pattern: 'http://cdnjs.cloudflare.com/ajax/libs/modernizr/2.6.2/modernizr.min.js', 59 | watched: false, 60 | included: true, 61 | served: false 62 | }, 63 | { 64 | pattern: 'test/**/*.js', 65 | watched: true, 66 | included: true, 67 | served: true 68 | } 69 | ], 70 | 71 | preprocessors: { 72 | 'test/**/*.js': ['browserify'] 73 | }, 74 | 75 | // Overridable with a comma-separated list with `--reporters` 76 | reporters: reporters, 77 | 78 | // Overridable with `[--no]-colors 79 | colors: true, 80 | 81 | /* Overridable with `--log-level=`. 82 | * 83 | * Possible 'level' options include: 84 | * * disable 85 | * * error 86 | * * warn 87 | * * info 88 | * * debug 89 | */ 90 | logLevel: config.LOG_INFO, 91 | 92 | // Overridable with a comma-separated list with `--browsers` 93 | browsers: args['sauce-labs'] ? Object.keys(customLaunchers) : [ 94 | 'Chrome', 95 | 'Firefox', 96 | 'Safari' 97 | ], 98 | 99 | sauceLabs: { 100 | username: args['sauce-username'], 101 | accessKey: args['sauce-key'], 102 | testName: require('./package.json').name, 103 | tags: args['build-branch'], 104 | build: args['build-number'] 105 | }, 106 | 107 | browserify: { 108 | debug: true, 109 | transform: [ 110 | 'babelify' 111 | ].concat(args.istanbul && [ 112 | ['browserify-istanbul', { 113 | ignore: ['**/*.handlebars'], 114 | instrumenter: require('isparta') 115 | }] 116 | ] || []) 117 | }, 118 | 119 | coverageReporter: { 120 | reporters: [ 121 | { 122 | type: 'html' 123 | }, 124 | { 125 | type: 'text' 126 | } 127 | ] 128 | }, 129 | 130 | client: { 131 | // '--grep' arguments are passed directly to mocha. 132 | args: args.grep && ['--grep', args.grep], 133 | mocha: { 134 | reporter: 'html' 135 | } 136 | }, 137 | 138 | customLaunchers: customLaunchers 139 | }); 140 | }; 141 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-modal2", 3 | "version": "5.0.2", 4 | "description": "Simple modal component for React.", 5 | "repository": "cloudflare/react-modal2", 6 | "main": "lib/index.js", 7 | "scripts": { 8 | "build": "babel src -d lib", 9 | "example": "npm run example:basic & npm run example:animated & npm run example:redux & npm run example:universal", 10 | "example:animated": "browserify --debug example/animated/script.js -o example/animated/bundle.js -t babelify", 11 | "example:basic": "browserify --debug example/basic/script.js -o example/basic/bundle.js -t babelify", 12 | "example:redux": "browserify --debug example/redux/script.js -o example/redux/bundle.js -t babelify", 13 | "example:universal": "browserify --debug example/universal/browser.js -o example/universal/bundle.js -t babelify && babel-node example/universal/server.js", 14 | "format": "jsfmt -w *.js src/ test/", 15 | "lint": "eslint *.js src/ test/", 16 | "prepublish": "npm run build", 17 | "test": "karma start" 18 | }, 19 | "keywords": [ 20 | "react", 21 | "modal", 22 | "dialog", 23 | "stateless" 24 | ], 25 | "author": "James Kyle ", 26 | "license": "BSD-3-Clause", 27 | "peerDependencies": { 28 | "react": "^0.14.9 || ^15.0.0-0 || ^16.0.0-0" 29 | }, 30 | "dependencies": { 31 | "a11y-focus-scope": "^1.1.0", 32 | "a11y-focus-store": "^1.0.0", 33 | "exenv": "^1.2.0", 34 | "prop-types": "^15.5.8" 35 | }, 36 | "devDependencies": { 37 | "babel-cli": "^6.1.1", 38 | "babel-core": "^6.0.20", 39 | "babel-preset-cf": "^1.2.0", 40 | "babel-preset-es2015": "^6.1.2", 41 | "babel-preset-react": "^6.1.2", 42 | "babel-preset-stage-1": "^6.1.2", 43 | "babelify": "^7.2.0", 44 | "browserify": "^12.0.1", 45 | "browserify-istanbul": "^0.2.1", 46 | "chai": "^3.4.1", 47 | "defined": "^1.0.0", 48 | "eslint": "^1.8.0", 49 | "eslint-plugin-cflint": "^1.0.0", 50 | "express": "^4.13.3", 51 | "isparta": "^4.0.0", 52 | "jsfmt": "^0.5.2", 53 | "karma": "^0.13.15", 54 | "karma-beep-reporter": "^0.1.4", 55 | "karma-browserify": "^4.4.0", 56 | "karma-chrome-launcher": "^0.2.1", 57 | "karma-coverage": "^0.5.3", 58 | "karma-firefox-launcher": "^0.1.6", 59 | "karma-mocha": "^0.2.0", 60 | "karma-mocha-reporter": "^1.1.1", 61 | "karma-safari-launcher": "^0.1.1", 62 | "karma-sauce-launcher": "^0.3.0", 63 | "karma-tape-reporter": "^1.0.3", 64 | "minimist": "^1.2.0", 65 | "mocha": "^2.3.3", 66 | "react": "^15.5.4", 67 | "react-addons-css-transition-group": "^15.0.0", 68 | "react-addons-test-utils": "^15.0.0", 69 | "react-dom": "^15.0.0", 70 | "react-gateway": "^2.2.0", 71 | "react-redux": "^4.0.0", 72 | "redux": "^3.0.4" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Modal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import focusScope from 'a11y-focus-scope'; 4 | import focusStore from 'a11y-focus-store'; 5 | import ExecutionEnvironment from 'exenv'; 6 | 7 | function setFocusOn(applicationElement, element) { 8 | focusStore.storeFocus(); 9 | if (applicationElement) applicationElement.setAttribute('aria-hidden', 'true'); 10 | focusScope.scopeFocus(element); 11 | } 12 | 13 | function resetFocus(applicationElement) { 14 | focusScope.unscopeFocus(); 15 | if (applicationElement) applicationElement.removeAttribute('aria-hidden'); 16 | focusStore.restoreFocus(); 17 | } 18 | 19 | export default class ReactModal2 extends React.Component { 20 | static getApplicationElement() { 21 | console.warn('`ReactModal2.getApplicationElement` needs to be set for accessibility reasons'); 22 | } 23 | 24 | static propTypes = { 25 | onClose: PropTypes.func.isRequired, 26 | 27 | closeOnEsc: PropTypes.bool, 28 | closeOnBackdropClick: PropTypes.bool, 29 | 30 | backdropClassName: PropTypes.string, 31 | backdropStyles: PropTypes.object, 32 | 33 | modalClassName: PropTypes.string, 34 | modalStyles: PropTypes.object 35 | }; 36 | 37 | static defaultProps = { 38 | closeOnEsc: true, 39 | closeOnBackdropClick: true 40 | }; 41 | 42 | backdropMouseDown = false 43 | backdropMouseUp = false 44 | 45 | componentDidMount() { 46 | if (ExecutionEnvironment.canUseDOM) { 47 | setFocusOn(ReactModal2.getApplicationElement(), this.modal); 48 | document.addEventListener('keydown', this.handleDocumentKeydown); 49 | } 50 | } 51 | 52 | componentWillUnmount() { 53 | if (ExecutionEnvironment.canUseDOM) { 54 | resetFocus(ReactModal2.getApplicationElement()); 55 | document.removeEventListener('keydown', this.handleDocumentKeydown); 56 | } 57 | } 58 | 59 | handleDocumentKeydown = event => { 60 | if (this.props.closeOnEsc && event.keyCode === 27) { 61 | this.props.onClose(); 62 | } 63 | } 64 | 65 | handleBackdropMouseDown = event => { 66 | this.backdropMouseDown = !this.modal.contains(event.target) 67 | } 68 | 69 | handleBackdropMouseUp = event => { 70 | this.backdropMouseUp = !this.modal.contains(event.target) 71 | } 72 | 73 | handleBackdropClick = () => { 74 | if ( 75 | this.props.closeOnBackdropClick && 76 | this.backdropMouseDown && 77 | this.backdropMouseUp 78 | ) { 79 | this.props.onClose(); 80 | } 81 | 82 | this.backdropMouseDown = false 83 | this.backdropMouseUp = false 84 | } 85 | 86 | handleModalClick = event => { 87 | event.stopPropagation(); 88 | } 89 | 90 | render() { 91 | return ( 92 |
this.backdrop = i} 93 | className={this.props.backdropClassName} 94 | style={this.props.backdropStyles} 95 | onMouseDown={this.handleBackdropMouseDown} 96 | onMouseUp={this.handleBackdropMouseUp} 97 | onClick={this.handleBackdropClick}> 98 |
this.modal = i} 99 | className={this.props.modalClassName} 100 | style={this.props.modalStyles} 101 | onClick={this.handleModalClick} 102 | tabIndex="-1"> 103 | {this.props.children} 104 |
105 |
106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export default from './Modal'; 2 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | # vim: set ft=yaml: 2 | --- 3 | globals: 4 | describe: false 5 | it: false 6 | before: false 7 | after: false 8 | beforeEach: false 9 | afterEach: false 10 | rules: 11 | no-unused-expressions: 0 12 | new-cap: 0 13 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import TestUtils from 'react-addons-test-utils'; 5 | 6 | import ReactModal2 from '../src/index'; 7 | 8 | console.log(ReactModal2); 9 | 10 | describe('ReactModal2', function() { 11 | beforeEach(function() { 12 | this.root = document.createElement('div'); 13 | document.body.appendChild(this.root); 14 | }); 15 | 16 | afterEach(function() { 17 | ReactDOM.unmountComponentAtNode(this.root); 18 | document.body.removeChild(this.root); 19 | delete this.root; 20 | }); 21 | 22 | it('should call `onClose` when the `esc` key is pressed', function() { 23 | var called = false; 24 | var onClose = function() { called = true; }; 25 | 26 | var dom = ; 27 | var instance = ReactDOM.render(dom, this.root); 28 | 29 | instance.handleDocumentKeydown({ keyCode: 27 }); 30 | expect(called).to.be.true; 31 | }); 32 | 33 | it('should not call `onClose` when the `esc` key is pressed but `closeOnEsc` is `false`', function() { 34 | var called = false; 35 | var onClose = function() { called = true; }; 36 | 37 | var dom = ; 38 | var instance = ReactDOM.render(dom, this.root); 39 | 40 | instance.handleDocumentKeydown({ keyCode: 27 }); 41 | expect(called).to.be.false; 42 | }); 43 | 44 | it('should call `onClose` when the backdrop is clicked', function() { 45 | var called = false; 46 | var onClose = function() { called = true; }; 47 | 48 | var dom = ; 49 | var instance = ReactDOM.render(dom, this.root); 50 | 51 | TestUtils.Simulate.mouseDown(instance.backdrop); 52 | TestUtils.Simulate.mouseUp(instance.backdrop); 53 | TestUtils.Simulate.click(instance.backdrop); 54 | 55 | expect(called).to.be.true; 56 | }); 57 | 58 | it('should not call `onClose` when the backdrop is clicked but `closeOnBackdropClick` is `false`', function() { 59 | var called = false; 60 | var onClose = function() { called = true; }; 61 | 62 | var dom = ; 63 | var instance = ReactDOM.render(dom, this.root); 64 | 65 | TestUtils.Simulate.mouseDown(instance.backdrop); 66 | TestUtils.Simulate.mouseUp(instance.backdrop); 67 | TestUtils.Simulate.click(instance.backdrop); 68 | 69 | expect(called).to.be.false; 70 | }); 71 | 72 | it('should not call `onClose` when the modal is the start target of a click', function() { 73 | var called = false; 74 | var onClose = function() { called = true; }; 75 | 76 | var dom = ; 77 | var instance = ReactDOM.render(dom, this.root); 78 | 79 | TestUtils.Simulate.mouseDown(instance.modal); 80 | TestUtils.Simulate.mouseUp(instance.backdrop); 81 | TestUtils.Simulate.click(instance.backdrop); 82 | 83 | expect(called).to.be.false; 84 | }); 85 | 86 | it('should not call `onClose` when the modal is the end target of a click', function() { 87 | var called = false; 88 | var onClose = function() { called = true; }; 89 | 90 | var dom = ; 91 | var instance = ReactDOM.render(dom, this.root); 92 | 93 | TestUtils.Simulate.mouseDown(instance.backdrop); 94 | TestUtils.Simulate.mouseUp(instance.modal); 95 | TestUtils.Simulate.click(instance.backdrop); 96 | 97 | expect(called).to.be.false; 98 | }); 99 | 100 | it('should scope the focus on the modal when mounted', function() { 101 | var input = document.createElement('input'); 102 | document.body.appendChild(input); 103 | input.focus(); 104 | 105 | var dom = ; 106 | var instance = ReactDOM.render(dom, this.root); 107 | 108 | expect(instance.modal).to.equal(document.activeElement); 109 | 110 | document.body.removeChild(input); 111 | }); 112 | 113 | it('should return the focus on the modal when unmounted', function() { 114 | var input = document.createElement('input'); 115 | document.body.appendChild(input); 116 | input.focus(); 117 | 118 | var dom = ; 119 | ReactDOM.render(dom, this.root); 120 | ReactDOM.unmountComponentAtNode(this.root); 121 | 122 | expect(input).to.equal(document.activeElement); 123 | 124 | document.body.removeChild(input); 125 | }); 126 | 127 | it('should "hide" the applicationElement when mounted', function() { 128 | var applicationElement = document.createElement('div'); 129 | document.body.appendChild(applicationElement); 130 | ReactModal2.getApplicationElement = () => applicationElement; 131 | 132 | var dom = ; 133 | ReactDOM.render(dom, this.root); 134 | 135 | expect(applicationElement.getAttribute('aria-hidden')).to.equal('true'); 136 | 137 | document.body.removeChild(applicationElement); 138 | ReactModal2.getApplicationElement = () => {}; 139 | }); 140 | 141 | it('should "unhide" the applicationElement when mounted', function() { 142 | var applicationElement = document.createElement('div'); 143 | document.body.appendChild(applicationElement); 144 | ReactModal2.getApplicationElement = () => applicationElement; 145 | 146 | var dom = ; 147 | ReactDOM.render(dom, this.root); 148 | ReactDOM.unmountComponentAtNode(this.root); 149 | 150 | expect(applicationElement.getAttribute('aria-hidden')).to.equal(null); 151 | 152 | document.body.removeChild(applicationElement); 153 | ReactModal2.getApplicationElement = () => {}; 154 | }); 155 | }); 156 | --------------------------------------------------------------------------------