├── .babelrc ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── test.yml ├── .gitignore ├── .log ├── ti-16339.log ├── ti-28128.log ├── ti-39437.log ├── ti-88235.log └── tsserver.log ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── UPGRADE_GUIDE.md ├── bower.json ├── dist ├── react-modal.js └── react-modal.min.js ├── docs ├── accessibility │ └── index.md ├── contributing │ ├── development.md │ └── index.md ├── examples │ ├── css_classes.md │ ├── global_overrides.md │ ├── index.md │ ├── inline_styles.md │ ├── minimal.md │ ├── on_request_close.md │ ├── set_app_element.md │ └── should_close_on_overlay_click.md ├── index.md ├── pygments.css ├── styles │ ├── classes.md │ ├── index.md │ └── transitions.md └── testing │ └── index.md ├── examples ├── base.css ├── basic │ ├── app.css │ ├── app.js │ ├── forms │ │ └── index.js │ ├── index.html │ ├── multiple_modals │ │ └── index.js │ ├── nested_modals │ │ └── index.js │ ├── react-router │ │ └── index.js │ └── simple_usage │ │ ├── index.js │ │ └── modal.js ├── bootstrap │ ├── app.css │ ├── app.js │ └── index.html ├── index.html └── wc │ ├── app.css │ ├── app.js │ └── index.html ├── karma.conf.js ├── mkdocs.yml ├── package-lock.json ├── package.json ├── scripts ├── changelog.py ├── defaultConfig.js ├── repo_status ├── version ├── webpack.config.js ├── webpack.dist.config.js └── webpack.test.config.js ├── specs ├── Modal.events.spec.js ├── Modal.helpers.spec.js ├── Modal.spec.js ├── Modal.style.spec.js ├── Modal.testability.spec.js ├── helper.js └── index.js └── src ├── components ├── Modal.js └── ModalPortal.js ├── helpers ├── ariaAppHider.js ├── bodyTrap.js ├── classList.js ├── focusManager.js ├── portalOpenInstances.js ├── safeHTMLElement.js ├── scopeTab.js └── tabbable.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "react", 5 | "stage-2" 6 | ], 7 | "plugins": [ 8 | "add-module-exports" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true, 4 | "browser": true 5 | }, 6 | 7 | "parser": "babel-eslint", 8 | 9 | "parserOptions": { 10 | "ecmaVersion": 7, 11 | "ecmaFeatures": { 12 | "jsx": true 13 | }, 14 | "sourceType": "module" 15 | }, 16 | 17 | "settings": { 18 | "react": { 19 | "createClass": "createReactClass", 20 | "pragma": "React", 21 | "version": "15.0" 22 | }, 23 | "propWrapperFunctions": [ "forbidExtraProps" ], 24 | "import/resolver": "webpack" 25 | }, 26 | 27 | "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:import/recommended", "prettier"], 28 | 29 | "plugins": ["prettier"], 30 | 31 | "globals": { 32 | "process": true 33 | }, 34 | 35 | "rules": { 36 | "quotes": [0], 37 | "comma-dangle": [2, "only-multiline"], 38 | "max-len": [1, {"code": 80}], 39 | "no-unused-expressions": [0], 40 | "no-continue": [0], 41 | "no-plusplus": [0], 42 | "func-names": [0], 43 | "arrow-parens": [0], 44 | "space-before-function-paren": [0], 45 | "jsx-a11y/no-static-element-interactions": [0], 46 | "prettier/prettier": "error", 47 | "react/no-find-dom-node": [0], 48 | "react/jsx-closing-bracket-location": [0], 49 | "react/require-default-props": 0, 50 | "import/no-extraneous-dependencies": [2, { 51 | "devDependencies": ["specs/**"] 52 | }] 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Summary: 2 | 3 | ### Steps to reproduce: 4 | 5 | 1. 6 | 2. 7 | 3. 8 | 9 | ### Expected behavior: 10 | 11 | ### Link to example of issue: 12 | 16 | 17 | ### Additional notes: 18 | 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Acceptance Checklist: 2 | - [ ] Tests 3 | - [ ] Documentation and examples (if needed) 4 | 5 | Fixes #[issue number]. 6 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | - v4 11 | - chore/github-actions 12 | 13 | jobs: 14 | main: 15 | name: Test 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 1 21 | 22 | - uses: actions/setup-node@v2 23 | with: 24 | node-version: 16 25 | cache: 'npm' 26 | cache-dependency-path: '**/package-lock.json' 27 | - run: make deps-project tests-ci 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .version 2 | .branch 3 | .changelog_update 4 | scripts/__pycache__/ 5 | examples/**/*-bundle.js 6 | node_modules/ 7 | .idea/ 8 | .vscode 9 | _book 10 | *.patch 11 | *.diff 12 | *.orig 13 | *.rej 14 | .log 15 | examples/__build__ 16 | coverage 17 | yarn.lock 18 | 19 | ## Built folders 20 | lib 21 | -------------------------------------------------------------------------------- /.log/ti-28128.log: -------------------------------------------------------------------------------- 1 | [20:30:21.552] Global cache location '/Users/diasbruno/Library/Caches/typescript/4.2', safe file path '/users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/typingsafelist.json', types map path /users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/typesmap.json 2 | [20:30:21.555] Processing cache location '/Users/diasbruno/Library/Caches/typescript/4.2' 3 | [20:30:21.555] Trying to find '/Users/diasbruno/Library/Caches/typescript/4.2/package.json'... 4 | [20:30:21.557] Loaded content of '/Users/diasbruno/Library/Caches/typescript/4.2/package.json': {"private":true,"dependencies":{"types-registry":"^0.1.541"},"devDependencies":{"@types/brace-expansion":"^1.1.0","@types/d":"^1.0.0","@types/exenv":"^1.2.0","@types/lodash.get":"^4.4.6","@types/lolex":"^5.1.0","@types/minimatch":"^3.0.4","@types/nise":"^1.4.0","@types/node":"^15.3.0","@types/object-assign":"^4.0.30","@types/prop-types":"^15.7.3","@types/react":"^17.0.5","@types/react-dom":"^17.0.5","@types/react-is":"^17.0.0","@types/react-lifecycles-compat":"^3.0.1","@types/react-modal":"^3.12.0","@types/scheduler":"^0.16.1","@types/sinon":"^10.0.0","@types/warning":"^3.0.0"}} 5 | [20:30:21.557] Loaded content of '/Users/diasbruno/Library/Caches/typescript/4.2/package-lock.json' 6 | [20:30:21.571] Adding entry into typings cache: 'brace-expansion' => '/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/brace-expansion/index.d.ts' 7 | [20:30:21.576] Adding entry into typings cache: 'd' => '/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/d/index.d.ts' 8 | [20:30:21.579] Adding entry into typings cache: 'exenv' => '/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/exenv/index.d.ts' 9 | [20:30:21.583] Adding entry into typings cache: 'lodash.get' => '/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/lodash.get/index.d.ts' 10 | [20:30:21.590] Adding entry into typings cache: 'lolex' => '/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/lolex/index.d.ts' 11 | [20:30:21.594] Adding entry into typings cache: 'minimatch' => '/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/minimatch/index.d.ts' 12 | [20:30:21.597] Adding entry into typings cache: 'nise' => '/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/nise/index.d.ts' 13 | [20:30:21.607] Adding entry into typings cache: 'node' => '/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/node/index.d.ts' 14 | [20:30:21.610] Adding entry into typings cache: 'object-assign' => '/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/object-assign/index.d.ts' 15 | [20:30:21.614] Adding entry into typings cache: 'prop-types' => '/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/prop-types/index.d.ts' 16 | [20:30:21.618] Adding entry into typings cache: 'react' => '/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/react/index.d.ts' 17 | [20:30:21.621] Adding entry into typings cache: 'react-dom' => '/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/react-dom/index.d.ts' 18 | [20:30:21.623] Adding entry into typings cache: 'react-is' => '/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/react-is/index.d.ts' 19 | [20:30:21.626] Adding entry into typings cache: 'react-lifecycles-compat' => '/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/react-lifecycles-compat/index.d.ts' 20 | [20:30:21.629] Adding entry into typings cache: 'react-modal' => '/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/react-modal/index.d.ts' 21 | [20:30:21.632] Adding entry into typings cache: 'scheduler' => '/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/scheduler/index.d.ts' 22 | [20:30:21.634] Adding entry into typings cache: 'sinon' => '/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/sinon/index.d.ts' 23 | [20:30:21.638] Adding entry into typings cache: 'warning' => '/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/warning/index.d.ts' 24 | [20:30:21.638] Finished processing cache location '/Users/diasbruno/Library/Caches/typescript/4.2' 25 | [20:30:21.639] Process id: 28129 26 | [20:30:21.639] NPM location: /nix/store/lnfcmzafcsnv7jwk1zpvqhs8rzfq8xqg-nodejs-14.16.1/bin/npm (explicit '--npmLocation' not provided) 27 | [20:30:21.639] validateDefaultNpmLocation: false 28 | [20:30:21.640] Npm config file: /Users/diasbruno/Library/Caches/typescript/4.2/package.json 29 | [20:30:21.640] Updating types-registry npm package... 30 | [20:30:21.640] Exec: /nix/store/lnfcmzafcsnv7jwk1zpvqhs8rzfq8xqg-nodejs-14.16.1/bin/npm install --ignore-scripts types-registry@latest 31 | [20:30:23.453] Succeeded. stdout: 32 | + types-registry@0.1.541 33 | updated 1 package and audited 25 packages in 0.952s 34 | found 0 vulnerabilities 35 | 36 | 37 | [20:30:23.454] Updated types-registry npm package 38 | [20:30:24.054] Got install request {"projectName":"/dev/null/inferredProject1*","fileNames":["/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es5.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2016.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.dom.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.dom.iterable.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.webworker.importscripts.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.scripthost.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.core.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.collection.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.generator.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.iterable.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.promise.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.proxy.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.reflect.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.symbol.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2016.array.include.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2016.full.d.ts","/usr/local/src/react-modal/src/helpers/tabbable.js","/usr/local/src/react-modal/src/helpers/focusManager.js","/usr/local/src/react-modal/src/helpers/scopeTab.js","/usr/local/src/react-modal/src/helpers/safeHTMLElement.js","/usr/local/src/react-modal/src/helpers/ariaAppHider.js","/usr/local/src/react-modal/src/helpers/classList.js","/usr/local/src/react-modal/src/helpers/portalOpenInstances.js","/usr/local/src/react-modal/src/helpers/bodyTrap.js","/usr/local/src/react-modal/src/components/ModalPortal.js","/usr/local/src/react-modal/src/components/Modal.js","/usr/local/src/react-modal/specs/helper.js","/usr/local/src/react-modal/specs/Modal.events.spec.js"],"compilerOptions":{"module":1,"target":3,"jsx":1,"allowJs":true,"allowSyntheticDefaultImports":true,"allowNonTsExtensions":true,"noEmitForJsFiles":true,"maxNodeModuleJsDepth":2},"typeAcquisition":{"enable":true,"include":[],"exclude":[]},"unresolvedImports":[],"projectRootPath":"/usr/local/src/react-modal/specs","kind":"discover"} 39 | [20:30:24.080] Loaded safelist from types map file '/users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/typesmap.json' 40 | [20:30:24.082] Explicitly included types: [] 41 | [20:30:24.084] Inferred typings from unresolved imports: [] 42 | [20:30:24.084] Result: {"cachedTypingPaths":[],"newTypingNames":[],"filesToWatch":["/usr/local/src/react-modal/src/helpers/bower_components","/usr/local/src/react-modal/src/helpers/node_modules","/usr/local/src/react-modal/src/components/bower_components","/usr/local/src/react-modal/src/components/node_modules","/usr/local/src/react-modal/specs/bower_components","/usr/local/src/react-modal/specs/node_modules"]} 43 | [20:30:24.084] Finished typings discovery: {"cachedTypingPaths":[],"newTypingNames":[],"filesToWatch":["/usr/local/src/react-modal/src/helpers/bower_components","/usr/local/src/react-modal/src/helpers/node_modules","/usr/local/src/react-modal/src/components/bower_components","/usr/local/src/react-modal/src/components/node_modules","/usr/local/src/react-modal/specs/bower_components","/usr/local/src/react-modal/specs/node_modules"]} 44 | [20:30:24.085] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/helpers/bower_components 45 | [20:30:24.086] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/helpers/bower_components 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false 46 | [20:30:24.090] Elapsed:: 4.4382399916648865ms DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/helpers/bower_components 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false 47 | [20:30:24.091] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/helpers/node_modules 48 | [20:30:24.091] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/helpers/node_modules 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false 49 | [20:30:24.091] Elapsed:: 0.16435399651527405ms DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/helpers/node_modules 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false 50 | [20:30:24.092] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/components/bower_components 51 | [20:30:24.092] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/components/bower_components 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false 52 | [20:30:24.092] Elapsed:: 0.21012499928474426ms DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/components/bower_components 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false 53 | [20:30:24.092] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/components/node_modules 54 | [20:30:24.093] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/components/node_modules 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false 55 | [20:30:24.093] Elapsed:: 0.14466500282287598ms DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/components/node_modules 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false 56 | [20:30:24.093] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/specs/bower_components 57 | [20:30:24.093] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/specs/bower_components 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false 58 | [20:30:24.093] Elapsed:: 0.13554999232292175ms DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/specs/bower_components 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false 59 | [20:30:24.094] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/specs/node_modules 60 | [20:30:24.094] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/specs/node_modules 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false 61 | [20:30:24.095] Elapsed:: 0.20177200436592102ms DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/specs/node_modules 1 undefined Project: /dev/null/inferredProject1* watcher already invoked: false 62 | [20:30:24.095] Sending response: 63 | {"projectName":"/dev/null/inferredProject1*","typeAcquisition":{"enable":true,"include":[],"exclude":[]},"compilerOptions":{"module":1,"target":3,"jsx":1,"allowJs":true,"allowSyntheticDefaultImports":true,"allowNonTsExtensions":true,"noEmitForJsFiles":true,"maxNodeModuleJsDepth":2},"typings":[],"unresolvedImports":[],"kind":"action::set"} 64 | [20:30:24.096] Response has been sent. 65 | [20:30:24.096] No new typings were requested as a result of typings discovery 66 | [20:50:30.241] Got install request {"projectName":"/dev/null/inferredProject2*","fileNames":["/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es5.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2016.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.dom.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.dom.iterable.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.webworker.importscripts.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.scripthost.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.core.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.collection.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.generator.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.iterable.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.promise.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.proxy.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.reflect.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.symbol.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2016.array.include.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2016.full.d.ts","/usr/local/src/react-modal/src/helpers/tabbable.js","/usr/local/src/react-modal/src/helpers/focusManager.js","/usr/local/src/react-modal/src/helpers/scopeTab.js","/usr/local/src/react-modal/src/helpers/safeHTMLElement.js","/usr/local/src/react-modal/src/helpers/ariaAppHider.js","/usr/local/src/react-modal/src/helpers/classList.js","/usr/local/src/react-modal/src/helpers/portalOpenInstances.js","/usr/local/src/react-modal/src/helpers/bodyTrap.js","/usr/local/src/react-modal/src/components/ModalPortal.js","/usr/local/src/react-modal/src/components/Modal.js","/usr/local/src/react-modal/specs/helper.js","/usr/local/src/react-modal/specs/Modal.spec.js"],"compilerOptions":{"module":1,"target":3,"jsx":1,"allowJs":true,"allowSyntheticDefaultImports":true,"allowNonTsExtensions":true,"noEmitForJsFiles":true,"maxNodeModuleJsDepth":2},"typeAcquisition":{"enable":true,"include":[],"exclude":[]},"unresolvedImports":["react-modal"],"projectRootPath":"/usr/local/src/react-modal/specs","kind":"discover"} 67 | [20:50:30.242] Explicitly included types: [] 68 | [20:50:30.246] Inferred typings from unresolved imports: ["react-modal"] 69 | [20:50:30.246] Result: {"cachedTypingPaths":["/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/react-modal/index.d.ts"],"newTypingNames":[],"filesToWatch":["/usr/local/src/react-modal/src/helpers/bower_components","/usr/local/src/react-modal/src/helpers/node_modules","/usr/local/src/react-modal/src/components/bower_components","/usr/local/src/react-modal/src/components/node_modules","/usr/local/src/react-modal/specs/bower_components","/usr/local/src/react-modal/specs/node_modules"]} 70 | [20:50:30.246] Finished typings discovery: {"cachedTypingPaths":["/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/react-modal/index.d.ts"],"newTypingNames":[],"filesToWatch":["/usr/local/src/react-modal/src/helpers/bower_components","/usr/local/src/react-modal/src/helpers/node_modules","/usr/local/src/react-modal/src/components/bower_components","/usr/local/src/react-modal/src/components/node_modules","/usr/local/src/react-modal/specs/bower_components","/usr/local/src/react-modal/specs/node_modules"]} 71 | [20:50:30.247] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/helpers/bower_components 72 | [20:50:30.247] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/helpers/bower_components 1 undefined Project: /dev/null/inferredProject2* watcher already invoked: false 73 | [20:50:30.247] Elapsed:: 0.11894398927688599ms DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/helpers/bower_components 1 undefined Project: /dev/null/inferredProject2* watcher already invoked: false 74 | [20:50:30.247] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/helpers/node_modules 75 | [20:50:30.248] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/helpers/node_modules 1 undefined Project: /dev/null/inferredProject2* watcher already invoked: false 76 | [20:50:30.249] Elapsed:: 0.12288302183151245ms DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/helpers/node_modules 1 undefined Project: /dev/null/inferredProject2* watcher already invoked: false 77 | [20:50:30.249] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/components/bower_components 78 | [20:50:30.249] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/components/bower_components 1 undefined Project: /dev/null/inferredProject2* watcher already invoked: false 79 | [20:50:30.249] Elapsed:: 0.09933501482009888ms DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/components/bower_components 1 undefined Project: /dev/null/inferredProject2* watcher already invoked: false 80 | [20:50:30.250] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/components/node_modules 81 | [20:50:30.250] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/components/node_modules 1 undefined Project: /dev/null/inferredProject2* watcher already invoked: false 82 | [20:50:30.250] Elapsed:: 0.14720699191093445ms DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/src/components/node_modules 1 undefined Project: /dev/null/inferredProject2* watcher already invoked: false 83 | [20:50:30.250] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/specs/bower_components 84 | [20:50:30.251] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/specs/bower_components 1 undefined Project: /dev/null/inferredProject2* watcher already invoked: false 85 | [20:50:30.251] Elapsed:: 0.13086000084877014ms DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/specs/bower_components 1 undefined Project: /dev/null/inferredProject2* watcher already invoked: false 86 | [20:50:30.251] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/specs/node_modules 87 | [20:50:30.251] DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/specs/node_modules 1 undefined Project: /dev/null/inferredProject2* watcher already invoked: false 88 | [20:50:30.252] Elapsed:: 0.1500920057296753ms DirectoryWatcher:: Added:: WatchInfo: /usr/local/src/react-modal/specs/node_modules 1 undefined Project: /dev/null/inferredProject2* watcher already invoked: false 89 | [20:50:30.252] Sending response: 90 | {"projectName":"/dev/null/inferredProject2*","typeAcquisition":{"enable":true,"include":[],"exclude":[]},"compilerOptions":{"module":1,"target":3,"jsx":1,"allowJs":true,"allowSyntheticDefaultImports":true,"allowNonTsExtensions":true,"noEmitForJsFiles":true,"maxNodeModuleJsDepth":2},"typings":["/Users/diasbruno/Library/Caches/typescript/4.2/node_modules/@types/react-modal/index.d.ts"],"unresolvedImports":["react-modal"],"kind":"action::set"} 91 | [20:50:30.252] Response has been sent. 92 | [20:50:30.252] No new typings were requested as a result of typings discovery 93 | [21:08:31.182] Got install request {"projectName":"/dev/null/inferredProject1*","fileNames":["/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es5.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2016.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.dom.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.dom.iterable.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.webworker.importscripts.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.scripthost.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.core.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.collection.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.generator.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.iterable.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.promise.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.proxy.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.reflect.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.symbol.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2016.array.include.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2016.full.d.ts","/usr/local/src/react-modal/src/helpers/tabbable.js","/usr/local/src/react-modal/src/helpers/focusManager.js","/usr/local/src/react-modal/src/helpers/scopeTab.js","/usr/local/src/react-modal/src/helpers/safeHTMLElement.js","/usr/local/src/react-modal/src/helpers/ariaAppHider.js","/usr/local/src/react-modal/src/helpers/classList.js","/usr/local/src/react-modal/src/helpers/portalOpenInstances.js","/usr/local/src/react-modal/src/helpers/bodyTrap.js","/usr/local/src/react-modal/src/components/ModalPortal.js","/usr/local/src/react-modal/src/components/Modal.js","/usr/local/src/react-modal/specs/helper.js","/usr/local/src/react-modal/specs/Modal.events.spec.js"],"compilerOptions":{"module":1,"target":3,"jsx":1,"allowJs":true,"allowSyntheticDefaultImports":true,"allowNonTsExtensions":true,"noEmitForJsFiles":true,"maxNodeModuleJsDepth":2},"typeAcquisition":{"enable":true,"include":[],"exclude":[]},"unresolvedImports":["react-"],"projectRootPath":"/usr/local/src/react-modal/specs","kind":"discover"} 94 | [21:08:31.187] Explicitly included types: [] 95 | [21:08:31.188] Inferred typings from unresolved imports: ["react-"] 96 | [21:08:31.188] Result: {"cachedTypingPaths":[],"newTypingNames":["react-"],"filesToWatch":["/usr/local/src/react-modal/src/helpers/bower_components","/usr/local/src/react-modal/src/helpers/node_modules","/usr/local/src/react-modal/src/components/bower_components","/usr/local/src/react-modal/src/components/node_modules","/usr/local/src/react-modal/specs/bower_components","/usr/local/src/react-modal/specs/node_modules"]} 97 | [21:08:31.188] Finished typings discovery: {"cachedTypingPaths":[],"newTypingNames":["react-"],"filesToWatch":["/usr/local/src/react-modal/src/helpers/bower_components","/usr/local/src/react-modal/src/helpers/node_modules","/usr/local/src/react-modal/src/components/bower_components","/usr/local/src/react-modal/src/components/node_modules","/usr/local/src/react-modal/specs/bower_components","/usr/local/src/react-modal/specs/node_modules"]} 98 | [21:08:31.189] Installing typings ["react-"] 99 | [21:08:31.189] 'react-':: Entry for package 'react-' does not exist in local types registry - skipping... 100 | [21:08:31.190] All typings are known to be missing or invalid - no need to install more typings 101 | [21:08:31.190] Sending response: 102 | {"projectName":"/dev/null/inferredProject1*","typeAcquisition":{"enable":true,"include":[],"exclude":[]},"compilerOptions":{"module":1,"target":3,"jsx":1,"allowJs":true,"allowSyntheticDefaultImports":true,"allowNonTsExtensions":true,"noEmitForJsFiles":true,"maxNodeModuleJsDepth":2},"typings":[],"unresolvedImports":["react-"],"kind":"action::set"} 103 | [21:08:31.190] Response has been sent. 104 | [21:08:31.686] Got install request {"projectName":"/dev/null/inferredProject1*","fileNames":["/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es5.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2016.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.dom.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.dom.iterable.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.webworker.importscripts.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.scripthost.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.core.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.collection.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.generator.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.iterable.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.promise.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.proxy.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.reflect.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.symbol.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2016.array.include.d.ts","/Users/diasbruno/.emacs.d/.cache/lsp/npm/typescript/lib/node_modules/typescript/lib/lib.es2016.full.d.ts","/usr/local/src/react-modal/src/helpers/tabbable.js","/usr/local/src/react-modal/src/helpers/focusManager.js","/usr/local/src/react-modal/src/helpers/scopeTab.js","/usr/local/src/react-modal/src/helpers/safeHTMLElement.js","/usr/local/src/react-modal/src/helpers/ariaAppHider.js","/usr/local/src/react-modal/src/helpers/classList.js","/usr/local/src/react-modal/src/helpers/portalOpenInstances.js","/usr/local/src/react-modal/src/helpers/bodyTrap.js","/usr/local/src/react-modal/src/components/ModalPortal.js","/usr/local/src/react-modal/src/components/Modal.js","/usr/local/src/react-modal/specs/helper.js","/usr/local/src/react-modal/specs/Modal.events.spec.js"],"compilerOptions":{"module":1,"target":3,"jsx":1,"allowJs":true,"allowSyntheticDefaultImports":true,"allowNonTsExtensions":true,"noEmitForJsFiles":true,"maxNodeModuleJsDepth":2},"typeAcquisition":{"enable":true,"include":[],"exclude":[]},"unresolvedImports":[],"projectRootPath":"/usr/local/src/react-modal/specs","kind":"discover"} 105 | [21:08:31.686] Explicitly included types: [] 106 | [21:08:31.688] Inferred typings from unresolved imports: [] 107 | [21:08:31.688] Result: {"cachedTypingPaths":[],"newTypingNames":[],"filesToWatch":["/usr/local/src/react-modal/src/helpers/bower_components","/usr/local/src/react-modal/src/helpers/node_modules","/usr/local/src/react-modal/src/components/bower_components","/usr/local/src/react-modal/src/components/node_modules","/usr/local/src/react-modal/specs/bower_components","/usr/local/src/react-modal/specs/node_modules"]} 108 | [21:08:31.689] Finished typings discovery: {"cachedTypingPaths":[],"newTypingNames":[],"filesToWatch":["/usr/local/src/react-modal/src/helpers/bower_components","/usr/local/src/react-modal/src/helpers/node_modules","/usr/local/src/react-modal/src/components/bower_components","/usr/local/src/react-modal/src/components/node_modules","/usr/local/src/react-modal/specs/bower_components","/usr/local/src/react-modal/specs/node_modules"]} 109 | [21:08:31.689] Sending response: 110 | {"projectName":"/dev/null/inferredProject1*","typeAcquisition":{"enable":true,"include":[],"exclude":[]},"compilerOptions":{"module":1,"target":3,"jsx":1,"allowJs":true,"allowSyntheticDefaultImports":true,"allowNonTsExtensions":true,"noEmitForJsFiles":true,"maxNodeModuleJsDepth":2},"typings":[],"unresolvedImports":[],"kind":"action::set"} 111 | [21:08:31.690] Response has been sent. 112 | [21:08:31.690] No new typings were requested as a result of typings discovery 113 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | CONTRIBUTING.md 2 | .babelrc 3 | .travis.yml 4 | .babelrc 5 | .eslintrc.js 6 | *.orig 7 | *.rej 8 | .log 9 | .changelog_update 10 | Makefile 11 | book.json 12 | bootstrap.sh 13 | bower.json 14 | karma.conf.js 15 | yarn.lock 16 | webpack.* 17 | .idea/* 18 | .github/* 19 | coverage 20 | docs 21 | src 22 | scripts 23 | specs 24 | _book 25 | examples 26 | mkdocs.yml 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | cache: yarn 5 | services: 6 | - xvfb 7 | before_script: 8 | - export DISPLAY=:99.0 9 | script: 10 | - make tests-ci 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Commit Subjects 2 | 3 | Patches will be only accepted if they have a corresponding issue 4 | on GitHub. 5 | 6 | Having a corresponding issue is better to track 7 | and discuss ideas and propose changes. 8 | 9 | ### Docs 10 | 11 | Please update the README with any API changes, the code and docs should 12 | always be in sync. 13 | 14 | ### Development 15 | 16 | - `npm start` runs the dev server to run/develop examples 17 | - `npm test` will run the tests. 18 | - `scripts/test` same as `npm test` but keeps karma running and watches 19 | for changes 20 | 21 | ## Miscellaneous 22 | 23 | if you faced the below issue, make sure you use node version < 18 24 | ```node:internal/crypto/hash:71 25 | this[kHandle] = new _Hash(algorithm, xofLen); 26 | ^ 27 | 28 | Error: error:0308010C:digital envelope routines::unsupported 29 | at new Hash (node:internal/crypto/hash:71:19) 30 | at Object.createHash (node:crypto:133:10)``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Ryan Florence 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NODE=$(shell which node 2> /dev/null) 2 | NPM=$(shell which npm 2> /dev/null) 3 | YARN=$(shell which yarn 2> /dev/null) 4 | JQ=$(shell which jq 2> /dev/null) 5 | 6 | PKM?=$(if $(YARN),$(YARN),$(shell which npm)) 7 | 8 | BABEL=./node_modules/.bin/babel 9 | COVERALLS=./node_modules/coveralls/bin/coveralls.js 10 | REMOTE="git@github.com:reactjs/react-modal" 11 | CURRENT_VERSION:=$(shell jq ".version" package.json) 12 | COVERAGE?=true 13 | 14 | BRANCH=$(shell git rev-parse --abbrev-ref HEAD) 15 | CURRENT_VERSION:=$(shell jq ".version" package.json) 16 | 17 | VERSION:=$(if $(RELEASE),$(shell read -p "Release $(CURRENT_VERSION) -> " V && echo $$V),"HEAD") 18 | 19 | help: info 20 | @echo 21 | @echo "Current version: $(CURRENT_VERSION)" 22 | @echo 23 | @echo "List of commands:" 24 | @echo 25 | @echo " make info - display node, npm and yarn versions..." 26 | @echo " make deps - install all dependencies." 27 | @echo " make serve - start the server." 28 | @echo " make tests - run tests." 29 | @echo " make tests-single-run - run tests (used by continuous integration)." 30 | @echo " make coveralls - show coveralls." 31 | @echo " make lint - run lint." 32 | @echo " make docs - build and serve the docs." 33 | @echo " make build - build project artifacts." 34 | @echo " make publish - build and publish version on npm." 35 | @echo " make publish-docs - build the docs and publish to gh-pages." 36 | @echo " make publish-all - publish version and docs." 37 | 38 | info: 39 | @[[ ! -z "$(NODE)" ]] && echo node version: `$(NODE) --version` "$(NODE)" 40 | @[[ ! -z "$(PKM)" ]] && echo $(shell basename $(PKM)) version: `$(PKM) --version` "$(PKM)" 41 | @[[ ! -z "$(JQ)" ]] && echo jq version: `$(JQ) --version` "$(JQ)" 42 | 43 | deps: deps-project deps-docs 44 | 45 | deps-project: 46 | @$(PKM) install 47 | 48 | deps-docs: 49 | @pip install mkdocs mkdocs-material jsx-lexer 50 | 51 | # Rules for development 52 | 53 | serve: 54 | @npm start 55 | 56 | tests: 57 | @npm run test 58 | 59 | tests-single-run: 60 | @npm run test -- --single-run 61 | 62 | coveralls: 63 | -cat ./coverage/lcov.info | $(COVERALLS) 2>/dev/null 64 | 65 | tests-ci: clean lint 66 | @COVERAGE=$(COVERAGE) make tests-single-run coveralls 67 | 68 | lint: 69 | @npm run lint 70 | 71 | docs: build-docs 72 | pygmentize -S default -f html -a .codehilite > docs/pygments.css 73 | mkdocs serve 74 | 75 | # Rules for build and publish 76 | 77 | check-working-tree: 78 | @[ -z "`git status -s`" ] && \ 79 | echo "Stopping publish. There are change to commit or discard." || echo "Worktree is clean." 80 | 81 | compile: 82 | @echo "[Compiling source]" 83 | $(BABEL) src --out-dir lib 84 | 85 | build: compile 86 | @echo "[Building dists]" 87 | @npx webpack --config ./scripts/webpack.dist.config.js 88 | 89 | pre-release-commit: 90 | git commit --allow-empty -m "Release v$(VERSION)." 91 | 92 | changelog: 93 | @echo "[Updating CHANGELOG.md $(CURRENT_VERSION) > $(VERSION)]" 94 | python ./scripts/changelog.py -a $(VERSION) > CHANGELOG.md 95 | 96 | update-package-version: 97 | cat package.json | jq '.version="$(VERSION)"' > tmp; mv -f tmp package.json 98 | 99 | release-commit: pre-release-commit update-package-version changelog 100 | @git add . 101 | @git commit --amend -m "`git log -1 --format=%s`" 102 | 103 | release-tag: 104 | git tag "v$(VERSION)" -m "`python ./scripts/changelog.py -c $(VERSION)`" 105 | 106 | publish-version: release-commit release-tag 107 | @echo "[Publishing]" 108 | git push $(REMOTE) "$(BRANCH)" "v$(VERSION)" 109 | npm publish 110 | 111 | pre-publish: clean 112 | pre-build: deps-project tests-single-run build 113 | 114 | publish: check-working-tree pre-publish pre-build publish-version publish-finished 115 | 116 | publish-finished: clean 117 | 118 | # Rules for documentation 119 | 120 | init-docs-repo: 121 | @mkdir _book 122 | 123 | build-docs: 124 | @echo "[Building documentation]" 125 | @rm -rf _book 126 | @mkdocs build 127 | 128 | pre-publish-docs: clean-docs init-docs-repo deps-docs 129 | 130 | publish-docs: clean pre-publish-docs build-docs 131 | @echo "[Publishing docs]" 132 | @make -C _book -f ../Makefile _publish-docs 133 | 134 | _publish-docs: 135 | git init . 136 | git commit --allow-empty -m 'update book' 137 | git checkout -b gh-pages 138 | touch .nojekyll 139 | git add . 140 | git commit -am 'update book' 141 | git push git@github.com:reactjs/react-modal gh-pages --force 142 | 143 | # Run for a full publish 144 | 145 | publish-all: publish publish-docs 146 | 147 | # Rules for clean up 148 | 149 | clean-docs: 150 | @rm -rf _book 151 | 152 | clean-coverage: 153 | @rm -rf ./coverage/* 154 | 155 | clean-build: 156 | @rm -rf lib/* 157 | 158 | clean: clean-build clean-docs clean-coverage 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-modal 2 | 3 | Accessible modal dialog component for React.JS 4 | 5 | [![Build Status](https://img.shields.io/github/actions/workflow/status/reactjs/react-modal/test.yml?branch=master)](https://github.com/reactjs/react-modal/actions/workflows/test.yml) 6 | [![Coverage Status](https://coveralls.io/repos/github/reactjs/react-modal/badge.svg?branch=master)](https://coveralls.io/github/reactjs/react-modal?branch=master) 7 | ![gzip size](http://img.badgesize.io/https://unpkg.com/react-modal/dist/react-modal.min.js?compression=gzip) 8 | [![Join the chat at https://gitter.im/react-modal/Lobby](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/react-modal/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 9 | 10 | ## Table of Contents 11 | 12 | * [Installation](#installation) 13 | * [API documentation](#api-documentation) 14 | * [Examples](#examples) 15 | * [Demos](#demos) 16 | 17 | ## Installation 18 | 19 | To install, you can use [npm](https://npmjs.org/) or [yarn](https://yarnpkg.com): 20 | 21 | 22 | $ npm install --save react-modal 23 | $ yarn add react-modal 24 | 25 | To install react-modal in React CDN app: 26 | 27 | - Add this CDN script tag after React CDN scripts and before your JS files (for example from [cdnjs](https://cdnjs.com/)): 28 | 29 | 33 | 34 | - Use `` tag inside your React CDN app. 35 | 36 | 37 | ## API documentation 38 | 39 | The primary documentation for react-modal is the 40 | [reference book](https://reactjs.github.io/react-modal), which describes the API 41 | and gives examples of its usage. 42 | 43 | ## Examples 44 | 45 | Here is a simple example of react-modal being used in an app with some custom 46 | styles and focusable input elements within the modal content: 47 | 48 | ```jsx 49 | import React from 'react'; 50 | import ReactDOM from 'react-dom'; 51 | import Modal from 'react-modal'; 52 | 53 | const customStyles = { 54 | content: { 55 | top: '50%', 56 | left: '50%', 57 | right: 'auto', 58 | bottom: 'auto', 59 | marginRight: '-50%', 60 | transform: 'translate(-50%, -50%)', 61 | }, 62 | }; 63 | 64 | // Make sure to bind modal to your appElement (https://reactcommunity.org/react-modal/accessibility/) 65 | Modal.setAppElement('#yourAppElement'); 66 | 67 | function App() { 68 | let subtitle; 69 | const [modalIsOpen, setIsOpen] = React.useState(false); 70 | 71 | function openModal() { 72 | setIsOpen(true); 73 | } 74 | 75 | function afterOpenModal() { 76 | // references are now sync'd and can be accessed. 77 | subtitle.style.color = '#f00'; 78 | } 79 | 80 | function closeModal() { 81 | setIsOpen(false); 82 | } 83 | 84 | return ( 85 |
86 | 87 | 94 |

(subtitle = _subtitle)}>Hello

95 | 96 |
I am a modal
97 |
98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 |
106 | ); 107 | } 108 | 109 | ReactDOM.render(, appElement); 110 | ``` 111 | 112 | You can find more examples in the `examples` directory, which you can run in a 113 | local development server using `npm start` or `yarn run start`. 114 | 115 | ## Demos 116 | 117 | There are several demos hosted on [CodePen](https://codepen.io) which 118 | demonstrate various features of react-modal: 119 | 120 | * [Minimal example](https://codepen.io/claydiffrient/pen/KNxgav) 121 | * [Using setAppElement](https://codepen.io/claydiffrient/pen/ENegGJ) 122 | * [Using onRequestClose](https://codepen.io/claydiffrient/pen/KNjVBx) 123 | * [Using shouldCloseOnOverlayClick](https://codepen.io/claydiffrient/pen/woLzwo) 124 | * [Using inline styles](https://codepen.io/claydiffrient/pen/ZBmyKz) 125 | * [Using CSS classes for styling](https://codepen.io/claydiffrient/pen/KNjVrG) 126 | * [Customizing the default styles](https://codepen.io/claydiffrient/pen/pNXgqQ) 127 | -------------------------------------------------------------------------------- /UPGRADE_GUIDE.md: -------------------------------------------------------------------------------- 1 | Upgrade Guide 2 | ============= 3 | 4 | To see discussion around these API changes, please refer to the 5 | [changelog](/CHANGELOG.md) and visit the commits and issues they 6 | reference. 7 | 8 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-modal", 3 | "version": "3.11.1", 4 | "homepage": "https://github.com/reactjs/react-modal", 5 | "authors": [ 6 | "Ryan Florence", 7 | "Michael Jackson" 8 | ], 9 | "description": "Accessible modal dialog component for React.JS", 10 | "main": "dist/react-modal.js", 11 | "keywords": [ 12 | "react", 13 | "modal", 14 | "dialog" 15 | ], 16 | "license": "MIT", 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "bower_components", 21 | "specs", 22 | "modules", 23 | "examples", 24 | "script", 25 | "CONTRIBUTING.md", 26 | "karma.conf.js", 27 | "package.json" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /docs/accessibility/index.md: -------------------------------------------------------------------------------- 1 | react-modal aims to be fully accessible, using the 2 | [WAI-ARIA](https://www.w3.org/WAI/intro/aria) guidelines to support users of 3 | assistive technologies. This page describes some of react-modal's 4 | accessibility-oriented features, along with their configuration options. 5 | 6 | ### [The app element](#app-element) 7 | 8 | It is important for users of screenreaders that other page content be hidden 9 | (via the `aria-hidden` attribute) while the modal is open. To allow 10 | react-modal to do this, you should call `Modal.setAppElement` with a query 11 | selector identifying the root of your app. For example, if your app content is 12 | located inside an element with the ID `root`, you could place the following 13 | call somewhere in your code before any modals are opened: 14 | 15 | ```jsx 16 | Modal.setAppElement('#root'); 17 | ``` 18 | 19 | You can also pass a DOM element directly, so that the above example could be 20 | rewritten: 21 | 22 | ```jsx 23 | Modal.setAppElement(document.getElementById('root')); 24 | ``` 25 | 26 | Using a selector that matches multiple elements or passing a list of DOM 27 | elements will hide all of the elements. Note that this list won't be 28 | automatically pruned if elements are removed from the DOM, so you may want to 29 | call `Modal.setAppElement` when any such changes are made, or pass a live 30 | HTMLCollection as the value. 31 | 32 | If you are already applying the `aria-hidden` attribute to your app content 33 | through other means, you can pass the `ariaHideApp={false}` prop to your modal 34 | to avoid getting a warning that your app element is not specified. 35 | 36 | Using `Modal.setAppElement` will not embed react-modal into your react app as 37 | a descendent component. It will just help boost up the app accessiblity. 38 | 39 | ### [Keyboard navigation](#keyboard) 40 | 41 | When the modal is opened, it restricts keyboard navigation using the tab key to 42 | elements within the modal content. This ensures that elements outside the 43 | modal (which are not visible while the modal is open) do not receive focus 44 | unexpectedly. 45 | 46 | By default, when the modal is closed, focus will be restored to the element 47 | that was focused before the modal was opened. To disable this behavior, you 48 | can pass the `shouldReturnFocusAfterClose={false}` prop to your modal. 49 | 50 | The modal can be closed using the escape key, unless the 51 | `shouldCloseOnEsc={false}` prop is passed. Disabling this behavior may cause 52 | accessibility issues for keyboard users, however, so it is not recommended. 53 | 54 | ### [ARIA attributes](#aria) 55 | 56 | Besides the `aria-hidden` attribute which is applied to the app element when 57 | the modal is shown, there are many other ARIA attributes which you can use to 58 | make your app more accessible. A complete list of ARIA attributes can be found 59 | in the [ARIA specification](https://www.w3.org/TR/wai-aria-1.1/#state_prop_def). 60 | 61 | One ARIA attribute is given a dedicated prop by react-modal: you should use the 62 | `contentLabel` prop to provide a label for the modal content (via `aria-label`) 63 | if there is no visible label on the screen. If the modal is already labeled 64 | with visible text, you should specify the element including the label with the 65 | `aria-labelledby` attribute using the `aria` prop described below. 66 | 67 | To pass other ARIA attributes to your modal, you can use the `aria` prop, which 68 | accepts an object whose keys are the attributes you want to set (without the 69 | leading `aria-` prefix). For example, you could have an alert modal with a 70 | title as well as a longer description: 71 | 72 | ```jsx 73 | 79 |

Alert

80 |
81 |

Description goes here.

82 |
83 |
84 | ``` 85 | -------------------------------------------------------------------------------- /docs/contributing/development.md: -------------------------------------------------------------------------------- 1 | `react-modal` uses `make` to build and publish new versions and documentation. 2 | 3 | It works as a checklist for the future releases to keep everything updated such as 4 | `CHANGELOG.md`, `package.json` and `bower.json` and so on. 5 | 6 | The minimun works as a normal `npm` scripts. 7 | 8 | #### [Usage](#usage) 9 | 10 | Once you clone `react-modal`, you can run `sh bootstrap.sh` to check 11 | and download dependencies not managed by `react-modal` such as `gitbook-cli`. 12 | 13 | It will also show information about the current versions of `node`, `npm`, 14 | `yarn` and `jq` available. 15 | 16 | #### [List of `npm` or `yarn` commands](#npm-yarn-commands) 17 | 18 | $ npm start 19 | $ npm run tests 20 | $ npm run lint 21 | 22 | #### [List of `make` commands](#make-commands) 23 | 24 | $ make help # show all make commands available 25 | $ make deps # npm install 26 | $ make serve # start a examples' web server 27 | $ make tests # use when developing 28 | $ make tests-ci # single run 29 | $ make lint # execute lint 30 | $ make publish # execute the entire pipeline to publish 31 | $ make publish-docs # execute the pipeline for docs 32 | -------------------------------------------------------------------------------- /docs/contributing/index.md: -------------------------------------------------------------------------------- 1 | ### Commit Subjects 2 | 3 | If your patch **changes the API or fixes a bug** please use one of the 4 | following prefixes in your commit subject: 5 | 6 | - `[fixed] ...` 7 | - `[changed] ...` 8 | - `[added] ...` 9 | - `[removed] ...` 10 | 11 | That ensures the subject line of your commit makes it into the 12 | auto-generated changelog. Do not use these tags if your change doesn't 13 | fix a bug and doesn't change the public API. 14 | 15 | Commits with changed, added, or removed, must be reviewed by another 16 | collaborator. 17 | 18 | #### When using `[changed]` or `[removed]`... 19 | 20 | Please include an upgrade path with example code in the commit message. 21 | If it doesn't make sense to do this, then it doesn't make sense to use 22 | `[changed]` or `[removed]` :) 23 | 24 | ### Docs 25 | 26 | Please update the README with any API changes, the code and docs should 27 | always be in sync. 28 | 29 | ### Development 30 | 31 | - `npm start` runs the dev server to run/develop examples 32 | - `npm test` will run the tests. 33 | - `scripts/test` same as `npm test` but keeps karma running and watches 34 | for changes 35 | 36 | ### Build 37 | 38 | Please do not include the output of `scripts/build` in your commits, we 39 | only do this when we release. (Also, you probably don't need to build 40 | anyway unless you are fixing something around our global build.) 41 | -------------------------------------------------------------------------------- /docs/examples/css_classes.md: -------------------------------------------------------------------------------- 1 | # Using CSS Classes for Styling 2 | 3 | If you prefer to use CSS to handle styling the modal you can. 4 | 5 | One thing to note is that by using the className property you will override all default styles. 6 | 7 | [CSS classes example](https://codepen.io/claydiffrient/pen/KNjVrG) 8 | -------------------------------------------------------------------------------- /docs/examples/global_overrides.md: -------------------------------------------------------------------------------- 1 | # Global Overrides 2 | 3 | If you'll be using several modals and want to adjust styling for all of them in one location you can by modifying `Modal.defaultStyles`. 4 | 5 | [Global overrides example](https://codepen.io/claydiffrient/pen/pNXgqQ) 6 | -------------------------------------------------------------------------------- /docs/examples/index.md: -------------------------------------------------------------------------------- 1 | The following sub-sections contain several examples of basic usage, hosted on 2 | [CodePen](https://codepen.io). 3 | 4 | The `examples` directory in the project root also contains some examples which 5 | you can run locally. To build and run those examples using a local development 6 | server, run either 7 | 8 | $ npm start 9 | 10 | or 11 | 12 | $ yarn start 13 | 14 | 15 | and then point your browser to `localhost:8080`. 16 | -------------------------------------------------------------------------------- /docs/examples/inline_styles.md: -------------------------------------------------------------------------------- 1 | # Using Inline Styles 2 | 3 | This example shows how to use inline styles to adjust the modal. 4 | 5 | [inline styles example](https://codepen.io/claydiffrient/pen/ZBmyKz) 6 | -------------------------------------------------------------------------------- /docs/examples/minimal.md: -------------------------------------------------------------------------------- 1 | # Minimal 2 | 3 | This example shows the minimal needed to get React Modal to work. 4 | 5 | [Minimal example](https://codepen.io/claydiffrient/pen/KNxgav) 6 | -------------------------------------------------------------------------------- /docs/examples/on_request_close.md: -------------------------------------------------------------------------------- 1 | # onRequestClose Callback 2 | 3 | This example shows how you can use the `onRequestClose` prop with a function to perform actions when closing. 4 | 5 | This is especially important for handling closing the modal via the escape key. 6 | 7 | Also more important if `shouldCloseOnOverlayClick` is set to `true`, when clicked on overlay it calls `onRequestClose`. 8 | 9 | [onRequestClose example](https://codepen.io/claydiffrient/pen/KNjVBx) 10 | -------------------------------------------------------------------------------- /docs/examples/set_app_element.md: -------------------------------------------------------------------------------- 1 | # Using setAppElement 2 | 3 | This example shows how to use setAppElement to properly hide your application from screenreaders and other assistive technologies while the modal is open. 4 | 5 | You'll notice in this example that the aria-hidden attribute is applied to the #main div rather than the document body. 6 | 7 | [setAppElement example](https://codepen.io/claydiffrient/pen/ENegGJ) 8 | -------------------------------------------------------------------------------- /docs/examples/should_close_on_overlay_click.md: -------------------------------------------------------------------------------- 1 | # Using shouldCloseOnOverlayClick 2 | 3 | When `shouldCloseOnOverlayClick` is `true` (default value for this property), 4 | it requires the `onRequestClose` to be defined in order to close the . 5 | This is due to the fact that the `react-modal` doesn't store the `isOpen` 6 | on its state (only for the internal `portal` (see [ModalPortal.js](https://github.com/reactjs/react-modal/blob/master/src/components/ModalPortal.js)). 7 | 8 | [disable 'close on overlay click', codepen by claydiffrient](https://codepen.io/claydiffrient/pen/woLzwo) 9 | 10 | [enable 'close on overlay click', codepen by sbgriffi](https://codepen.io/sbgriffi/pen/WMyBaR) 11 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # react-modal 2 | 3 | > Accessible modal dialog component for React.JS 4 | 5 | We maintain that accessibility is a key component of any modern web application. As such, we have created this modal in such a way that it fulfills the accessibility requirements of the modern web. We seek to keep the focus on accessibility while providing a functional, capable modal component for general use. 6 | 7 | ## [Installation](#installation) 8 | 9 | To install the stable version you can use [npm](https://npmjs.org/) or [yarn](https://yarnpkg.com): 10 | 11 | 12 | $ npm install react-modal 13 | $ yarn add react-modal 14 | 15 | To install react-modal in React CDN app: 16 | 17 | - Add this CDN script tag after React CDN scripts and before your JS files (for example from [cdnjs](https://cdnjs.com/)): 18 | 19 | 23 | 24 | - Use `` tag inside your React CDN app. 25 | 26 | 27 | ## [General Usage](#usage) 28 | 29 | The only required prop for the modal object is `isOpen`, which indicates 30 | whether the modal should be displayed. The following is an example of using 31 | react-modal specifying all the possible props and options: 32 | 33 | ```jsx 34 | import ReactModal from 'react-modal'; 35 | 36 | 179 | ``` 180 | 181 | ## [Using a custom parent node](#custom-parent) 182 | 183 | By default, the modal portal will be appended to the document's body. You can 184 | choose a different parent element by providing a function to the 185 | `parentSelector` prop that returns the element to be used: 186 | 187 | ```jsx 188 | document.querySelector('#root')}> 191 |

Modal Content.

192 |
193 | ``` 194 | If you do this, please ensure that your 195 | [app element](accessibility/#app-element) is set correctly. The app 196 | element should not be a parent of the modal, to prevent modal content from 197 | being hidden to screenreaders while it is open. 198 | 199 | ## [Refs](#refs) 200 | 201 | You can use ref callbacks to get the overlay and content DOM nodes directly: 202 | 203 | ```jsx 204 | (this.overlayRef = node)} 207 | contentRef={node => (this.contentRef = node)}> 208 |

Modal Content.

209 |
210 | ``` 211 | 212 | ## [License](#license) 213 | 214 | MIT 215 | -------------------------------------------------------------------------------- /docs/pygments.css: -------------------------------------------------------------------------------- 1 | pre { line-height: 125%; } 2 | td.linenos pre { color: #000000; background-color: #f0f0f0; padding-left: 5px; padding-right: 5px; } 3 | span.linenos { color: #000000; background-color: #f0f0f0; padding-left: 5px; padding-right: 5px; } 4 | td.linenos pre.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } 5 | span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } 6 | .codehilite .hll { background-color: #ffffcc } 7 | .codehilite { background: #f8f8f8; } 8 | .codehilite .c { color: #408080; font-style: italic } /* Comment */ 9 | .codehilite .err { border: 1px solid #FF0000 } /* Error */ 10 | .codehilite .k { color: #008000; font-weight: bold } /* Keyword */ 11 | .codehilite .o { color: #666666 } /* Operator */ 12 | .codehilite .ch { color: #408080; font-style: italic } /* Comment.Hashbang */ 13 | .codehilite .cm { color: #408080; font-style: italic } /* Comment.Multiline */ 14 | .codehilite .cp { color: #BC7A00 } /* Comment.Preproc */ 15 | .codehilite .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */ 16 | .codehilite .c1 { color: #408080; font-style: italic } /* Comment.Single */ 17 | .codehilite .cs { color: #408080; font-style: italic } /* Comment.Special */ 18 | .codehilite .gd { color: #A00000 } /* Generic.Deleted */ 19 | .codehilite .ge { font-style: italic } /* Generic.Emph */ 20 | .codehilite .gr { color: #FF0000 } /* Generic.Error */ 21 | .codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 22 | .codehilite .gi { color: #00A000 } /* Generic.Inserted */ 23 | .codehilite .go { color: #888888 } /* Generic.Output */ 24 | .codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ 25 | .codehilite .gs { font-weight: bold } /* Generic.Strong */ 26 | .codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 27 | .codehilite .gt { color: #0044DD } /* Generic.Traceback */ 28 | .codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ 29 | .codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ 30 | .codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ 31 | .codehilite .kp { color: #008000 } /* Keyword.Pseudo */ 32 | .codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ 33 | .codehilite .kt { color: #B00040 } /* Keyword.Type */ 34 | .codehilite .m { color: #666666 } /* Literal.Number */ 35 | .codehilite .s { color: #BA2121 } /* Literal.String */ 36 | .codehilite .na { color: #7D9029 } /* Name.Attribute */ 37 | .codehilite .nb { color: #008000 } /* Name.Builtin */ 38 | .codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */ 39 | .codehilite .no { color: #880000 } /* Name.Constant */ 40 | .codehilite .nd { color: #AA22FF } /* Name.Decorator */ 41 | .codehilite .ni { color: #999999; font-weight: bold } /* Name.Entity */ 42 | .codehilite .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ 43 | .codehilite .nf { color: #0000FF } /* Name.Function */ 44 | .codehilite .nl { color: #A0A000 } /* Name.Label */ 45 | .codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ 46 | .codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */ 47 | .codehilite .nv { color: #19177C } /* Name.Variable */ 48 | .codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 49 | .codehilite .w { color: #bbbbbb } /* Text.Whitespace */ 50 | .codehilite .mb { color: #666666 } /* Literal.Number.Bin */ 51 | .codehilite .mf { color: #666666 } /* Literal.Number.Float */ 52 | .codehilite .mh { color: #666666 } /* Literal.Number.Hex */ 53 | .codehilite .mi { color: #666666 } /* Literal.Number.Integer */ 54 | .codehilite .mo { color: #666666 } /* Literal.Number.Oct */ 55 | .codehilite .sa { color: #BA2121 } /* Literal.String.Affix */ 56 | .codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */ 57 | .codehilite .sc { color: #BA2121 } /* Literal.String.Char */ 58 | .codehilite .dl { color: #BA2121 } /* Literal.String.Delimiter */ 59 | .codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ 60 | .codehilite .s2 { color: #BA2121 } /* Literal.String.Double */ 61 | .codehilite .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ 62 | .codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */ 63 | .codehilite .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ 64 | .codehilite .sx { color: #008000 } /* Literal.String.Other */ 65 | .codehilite .sr { color: #BB6688 } /* Literal.String.Regex */ 66 | .codehilite .s1 { color: #BA2121 } /* Literal.String.Single */ 67 | .codehilite .ss { color: #19177C } /* Literal.String.Symbol */ 68 | .codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */ 69 | .codehilite .fm { color: #0000FF } /* Name.Function.Magic */ 70 | .codehilite .vc { color: #19177C } /* Name.Variable.Class */ 71 | .codehilite .vg { color: #19177C } /* Name.Variable.Global */ 72 | .codehilite .vi { color: #19177C } /* Name.Variable.Instance */ 73 | .codehilite .vm { color: #19177C } /* Name.Variable.Magic */ 74 | .codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */ 75 | -------------------------------------------------------------------------------- /docs/styles/classes.md: -------------------------------------------------------------------------------- 1 | Sometimes it may be preferable to use CSS classes rather than inline styles. 2 | react-modal can be configured to use CSS classes to style the modal content and 3 | overlay, as well as the document body and the portal within which the modal is 4 | mounted. 5 | 6 | #### For the content and overlay 7 | 8 | You can use the `className` and `overlayClassName` props to control the CSS 9 | classes that are applied to the modal content and the overlay, respectively. 10 | Each of these props may be a single string containing the class name to apply 11 | to the component. 12 | 13 | Alternatively, you may pass an object with the `base`, `afterOpen` and 14 | `beforeClose` keys, where the value corresponding to each key is a class name. 15 | The `base` class will always be applied to the component, the `afterOpen` class 16 | will be applied after the modal has been opened and the `beforeClose` class 17 | will be applied after the modal has requested to be closed (e.g. when the user 18 | presses the escape key or clicks on the overlay). 19 | 20 | Please note that the `beforeClose` class will have no effect unless the 21 | `closeTimeoutMS` prop is set to a non-zero value, since otherwise the modal 22 | will be closed immediately when requested. Thus, if you are using the 23 | `afterOpen` and `beforeClose` classes to provide transitions, you may want to 24 | set `closeTimeoutMS` to the length (in milliseconds) of your closing 25 | transition. 26 | 27 | If you specify `className`, the [default content styles](index.md) will not be 28 | applied. Likewise, if you specify `overlayClassName`, the default overlay 29 | styles will not be applied. 30 | 31 | If no class names are specified for the overlay, the default classes 32 | `ReactModal__Overlay`, `ReactModal__Overlay--after-open` and 33 | `ReactModal__Overlay--before-close` will be applied; the default classes for 34 | the content use the analogous prefix `ReactModal__Content`. Please note that 35 | any styles applied using these default classes will not override the default 36 | styles as they would if specified using the `className` or `overlayClassName` 37 | props. 38 | 39 | #### For the document.body and html tag 40 | 41 | You can override the default class that is added to `document.body` when the 42 | modal is open by defining a property `bodyOpenClassName`. 43 | 44 | The `bodyOpenClassName` prop must be a *constant string*; otherwise, we would 45 | require a complex system to manage which class name should be added to or 46 | removed from `document.body` from which modal (if using multiple modals 47 | simultaneously). The default value is `ReactModal__Body--open`. 48 | 49 | `bodyOpenClassName` when set as `null` doesn't add any class to `document.body`. 50 | 51 | `bodyOpenClassName` can support adding multiple classes to `document.body` when 52 | the modal is open. Add as many class names as you desire, delineated by spaces. 53 | 54 | One potential application for the body class is to remove scrolling on the body 55 | when the modal is open. To do this for all modals (except those that specify a 56 | non-default `bodyOpenClassName`), you could use the following CSS: 57 | 58 | ```CSS 59 | .ReactModal__Body--open { 60 | overflow: hidden; 61 | } 62 | ``` 63 | 64 | You can define a class to be added to the html tag, using the `htmlOpenClassName` 65 | attribute, which can be helpeful to stop the page to scroll to the top when open 66 | a modal. The default value is `null`. 67 | 68 | This attribute follows the same rules as `bodyOpenClassName`, it must be a *constant string*; 69 | 70 | Here is an example that can help preventing this behavior: 71 | 72 | ```CSS 73 | .ReactModal__Body--open, 74 | .ReactModal__Html--open { 75 | overflow: hidden; 76 | } 77 | ``` 78 | 79 | #### For the entire portal 80 | 81 | To specify a class to be applied to the entire portal, you may use the 82 | `portalClassName` prop. By default, there are no styles applied to the portal 83 | itself. 84 | -------------------------------------------------------------------------------- /docs/styles/index.md: -------------------------------------------------------------------------------- 1 | Styles passed into the Modal via the `style` prop are merged with the defaults. 2 | The default styles are defined in the `Modal.defaultStyles` object and are 3 | shown below. 4 | 5 | ```jsx 6 | 34 | ``` 35 | 36 | You can change the default styles by modifying `Modal.defaultStyles`. Please 37 | note that specifying a [CSS class](classes.md) for the overlay or the content 38 | will disable the default styles for that component. 39 | -------------------------------------------------------------------------------- /docs/styles/transitions.md: -------------------------------------------------------------------------------- 1 | Using [CSS classes](classes.md), it is possible to implement transitions for 2 | when the modal is opened or closed. By placing the following CSS somewhere in 3 | your project's styles, you can make the modal content fade in when it is opened 4 | and fade out when it is closed: 5 | 6 | ```css 7 | .ReactModal__Overlay { 8 | opacity: 0; 9 | transition: opacity 2000ms ease-in-out; 10 | } 11 | 12 | .ReactModal__Overlay--after-open{ 13 | opacity: 1; 14 | } 15 | 16 | .ReactModal__Overlay--before-close{ 17 | opacity: 0; 18 | } 19 | ``` 20 | 21 | 22 | The above example will apply the fade transition globally, affecting all modals 23 | whose `afterOpen` and `beforeClose` classes have not been set via the 24 | `className` prop. To apply the transition to one modal only, you can change 25 | the above class names and pass an object to your modal's `className` prop as 26 | described in the [previous section](classes.md). 27 | 28 | In order for the fade transition to work, you need to inform the `` about the transition time required for the animation. 29 | 30 | Like this 31 | 32 | ```javascript 33 | 34 | ``` 35 | 36 | `closeTimeoutMS` is expressed in milliseconds. 37 | 38 | The `closeTimeoutMS` value and the value used in CSS or `style` prop passed to `` needs to be the same. 39 | 40 | Warning: if you are using **React 16**, the close transition works [only if you use](https://github.com/reactjs/react-modal/issues/530#issuecomment-335208533) the `isOpen` prop to toggle the visibility of the modal. 41 | 42 | Do not conditionally render the ``. 43 | 44 | Instead of this 45 | 46 | ```javascript 47 | { 48 | this.state.showModal && 49 | this.toggleModal()} 54 | > 55 |

Add modal content here

56 |
57 | } 58 | ``` 59 | 60 | *Do this* 61 | 62 | ```javascript 63 | { 64 | this.toggleModal()} 69 | > 70 |

Add modal content here

71 |
72 | } 73 | ``` 74 | 75 | React Modal has adopted the [stable Portal API](https://reactjs.org/docs/portals.html) as exposed in React 16. 76 | 77 | And `createProtal` API from React 16 [no longer allow](https://github.com/facebook/react/issues/10826#issuecomment-355719729) developers to intervene the unmounting of the portal component. 78 | -------------------------------------------------------------------------------- /docs/testing/index.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | When using React Test Utils with this library, here are some things to keep in mind: 4 | 5 | - You need to set `isOpen={true}` on the modal component for it to render its children. 6 | - You need to use the `.portal` property, as in `ReactDOM.findDOMNode(renderedModal.portal)` or `TestUtils.scryRenderedDOMComponentsWithClass(Modal.portal, 'my-modal-class')` to acquire a handle to the inner contents of your modal. 7 | -------------------------------------------------------------------------------- /examples/base.css: -------------------------------------------------------------------------------- 1 | h1, h2, h3 { 2 | font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif; 3 | font-weight: 200; 4 | } 5 | 6 | /* From http://instructure-react.github.io/library/shared.css */ 7 | 8 | .padbox { 9 | padding: 40px; 10 | } 11 | 12 | .branding { 13 | border-bottom: 1px solid hsl(200, 0%, 90%); 14 | } 15 | 16 | .btn:not(:last-child) { 17 | margin-right: 20px; 18 | } 19 | 20 | .example:not(:last-child) { 21 | margin-bottom: 40px; 22 | } 23 | -------------------------------------------------------------------------------- /examples/basic/app.css: -------------------------------------------------------------------------------- 1 | .ReactModal__Overlay { 2 | -webkit-perspective: 600; 3 | perspective: 600; 4 | opacity: 0; 5 | } 6 | 7 | .ReactModal__Overlay--after-open { 8 | opacity: 1; 9 | transition: opacity 150ms ease-out; 10 | } 11 | 12 | .ReactModal__Content { 13 | -webkit-transform: scale(0.5) rotateX(-30deg); 14 | transform: scale(0.5) rotateX(-30deg); 15 | } 16 | 17 | .ReactModal__Content--after-open { 18 | -webkit-transform: scale(1) rotateX(0deg); 19 | transform: scale(1) rotateX(0deg); 20 | transition: all 150ms ease-in; 21 | } 22 | 23 | .ReactModal__Overlay--before-close { 24 | opacity: 0; 25 | } 26 | 27 | .ReactModal__Content--before-close { 28 | -webkit-transform: scale(0.5) rotateX(30deg); 29 | transform: scale(0.5) rotateX(30deg); 30 | transition: all 150ms ease-in; 31 | } 32 | 33 | .ReactModal__Body--open, 34 | .ReactModal__Html--open { 35 | overflow: hidden; 36 | } 37 | -------------------------------------------------------------------------------- /examples/basic/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Modal from 'react-modal'; 4 | import SimpleUsage from './simple_usage'; 5 | import MultipleModals from './multiple_modals'; 6 | import Forms from './forms'; 7 | import ReactRouter from './react-router'; 8 | import NestedModals from './nested_modals'; 9 | 10 | const appElement = document.getElementById('example'); 11 | 12 | Modal.setAppElement('#example'); 13 | 14 | const examples = [ 15 | SimpleUsage, 16 | Forms, 17 | MultipleModals, 18 | NestedModals, 19 | ReactRouter 20 | ]; 21 | 22 | class App extends Component { 23 | render() { 24 | return ( 25 |
26 | {examples.map((example, key) => { 27 | const ExampleApp = example.app; 28 | return ( 29 |
30 |

{`#${key + 1}. ${example.label}`}

31 | 32 |
33 | ); 34 | })} 35 |
36 | ); 37 | } 38 | } 39 | 40 | ReactDOM.render(, appElement); 41 | -------------------------------------------------------------------------------- /examples/basic/forms/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Modal from 'react-modal'; 3 | 4 | const MODAL_A = 'modal_a'; 5 | const MODAL_B = 'modal_b'; 6 | 7 | const DEFAULT_TITLE = 'Default title'; 8 | 9 | class Forms extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { isOpen: false }; 14 | } 15 | 16 | toggleModal = event => { 17 | console.log(event); 18 | const { isOpen } = this.state; 19 | this.setState({ isOpen: !isOpen }); 20 | } 21 | 22 | render() { 23 | const { isOpen } = this.state; 24 | 25 | return ( 26 |
27 | 28 | 39 |

Forms!

40 |
41 |

This is a description of what it does: nothing :)

42 |
43 |
44 | 45 | 46 |
47 |
48 | Radio buttons 49 | 52 | 55 |
56 |
57 | Checkbox buttons 58 | 61 | 64 |
65 | 66 |
67 |
68 |
69 |
70 | ); 71 | } 72 | } 73 | 74 | export default { 75 | label: "Modal with forms fields.", 76 | app: Forms 77 | }; 78 | -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Basic Example 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

react-modal

13 |

an accessible React modal dialog component

14 |
15 |
16 | Fork me on GitHub 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/basic/multiple_modals/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Modal from 'react-modal'; 3 | 4 | class List extends React.Component { 5 | render() { 6 | return ( 7 |
8 | {this.props.items.map((x, i) => ( 9 |
10 | {x} 11 |
))} 12 |
13 | ); 14 | } 15 | } 16 | 17 | class MultipleModals extends Component { 18 | constructor(props) { 19 | super(props); 20 | this.state = { 21 | listItemsIsOpen: false, 22 | currentItem: -1, 23 | loading: false, 24 | items: [] 25 | }; 26 | } 27 | 28 | toggleModal = event => { 29 | event.preventDefault(); 30 | if (this.state.listItemsIsOpen) { 31 | this.handleModalCloseRequest(); 32 | return; 33 | } 34 | this.setState({ 35 | items: [], 36 | listItemsIsOpen: true, 37 | loading: true 38 | }); 39 | } 40 | 41 | handleModalCloseRequest = () => { 42 | // opportunity to validate something and keep the modal open even if it 43 | // requested to be closed 44 | this.setState({ 45 | listItemsIsOpen: false, 46 | loading: false 47 | }); 48 | } 49 | 50 | handleOnAfterOpenModal = () => { 51 | // when ready, we can access the available refs. 52 | (new Promise((resolve, reject) => { 53 | setTimeout(() => resolve(true), 500); 54 | })).then(res => { 55 | this.setState({ 56 | items: [1, 2, 3, 4, 5].map(x => `Item ${x}`), 57 | loading: false 58 | }); 59 | }); 60 | } 61 | 62 | onItemClick = index => event => { 63 | this.setState({ currentItem: index }); 64 | } 65 | 66 | cleanCurrentItem = () => { 67 | this.setState({ currentItem: -1 }); 68 | } 69 | 70 | render() { 71 | const { listItemsIsOpen } = this.state; 72 | return ( 73 |
74 | 75 | 82 |

List of items

83 | {this.state.loading ? ( 84 |

Loading...

85 | ) : ( 86 | 87 | )} 88 |
89 | -1} 94 | onRequestClose={this.cleanCurrentItem} 95 | aria={{ 96 | labelledby: "item_title", 97 | describedby: "item_info" 98 | }}> 99 |

Item: {this.state.items[this.state.currentItem]}

100 |
101 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur pulvinar varius auctor. Aliquam maximus et justo ut faucibus. Nullam sit amet urna molestie turpis bibendum accumsan a id sem. Proin ullamcorper nisl sapien, gravida dictum nibh congue vel. Vivamus convallis dolor vitae ipsum ultricies, vitae pulvinar justo tincidunt. Maecenas a nunc elit. Phasellus fermentum, tellus ut consectetur scelerisque, eros nunc lacinia eros, aliquet efficitur tellus arcu a nibh. Praesent quis consequat nulla. Etiam dapibus ac sem vel efficitur. Nunc faucibus efficitur leo vitae vulputate. Nunc at quam vitae felis pretium vehicula vel eu quam. Quisque sapien mauris, condimentum eget dictum ut, congue id dolor. Donec vitae varius orci, eu faucibus turpis. Morbi eleifend orci non urna bibendum, ac scelerisque augue efficitur.

102 | 103 |

Maecenas justo justo, laoreet vitae odio quis, lacinia porttitor arcu. Nunc nisl est, ultricies sed laoreet eu, semper in nisi. Phasellus lacinia porta purus, eu luctus neque. Nullam quis mi malesuada, vestibulum sem id, rhoncus purus. Aliquam erat volutpat. Duis nec turpis mi. Pellentesque eleifend nisl sed risus aliquet, eu feugiat elit auctor. Suspendisse ac neque vitae ligula consequat aliquam. Vivamus sit amet eros et ante mollis porta.

104 |
105 |
106 |
107 | ); 108 | } 109 | } 110 | 111 | export default { 112 | label: "Working with many modal.", 113 | app: MultipleModals 114 | }; 115 | -------------------------------------------------------------------------------- /examples/basic/nested_modals/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Modal from 'react-modal'; 3 | 4 | class Item extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | isOpen: false 9 | }; 10 | } 11 | 12 | toggleModal = index => event => { 13 | console.log("NESTED MODAL ITEM", event); 14 | this.setState({ 15 | itemNumber: !this.state.isOpen ? index : null, 16 | isOpen: !this.state.isOpen 17 | }); 18 | }; 19 | 20 | render() { 21 | const { isOpen, itemNumber } = this.state; 22 | const { number, index } = this.props; 23 | 24 | const toggleModal = this.toggleModal(index); 25 | 26 | return ( 27 |
28 | {number} 29 | 37 |

Item: {itemNumber + 1}

38 |
39 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur pulvinar varius auctor. Aliquam maximus et justo ut faucibus. Nullam sit amet urna molestie turpis bibendum accumsan a id sem. Proin ullamcorper nisl sapien, gravida dictum nibh congue vel. Vivamus convallis dolor vitae ipsum ultricies, vitae pulvinar justo tincidunt. Maecenas a nunc elit. Phasellus fermentum, tellus ut consectetur scelerisque, eros nunc lacinia eros, aliquet efficitur tellus arcu a nibh. Praesent quis consequat nulla. Etiam dapibus ac sem vel efficitur. Nunc faucibus efficitur leo vitae vulputate. Nunc at quam vitae felis pretium vehicula vel eu quam. Quisque sapien mauris, condimentum eget dictum ut, congue id dolor. Donec vitae varius orci, eu faucibus turpis. Morbi eleifend orci non urna bibendum, ac scelerisque augue efficitur.

40 |
41 |
42 |
43 | ); 44 | } 45 | } 46 | 47 | class List extends Component { 48 | render() { 49 | return this.props.items.map((n, index) => ( 50 | 51 | )); 52 | } 53 | } 54 | 55 | 56 | class NestedModals extends Component { 57 | constructor(props) { 58 | super(props); 59 | 60 | this.state = { 61 | isOpen: false, 62 | currentItem: -1, 63 | loading: false, 64 | items: [] 65 | }; 66 | } 67 | 68 | toggleModal = event => { 69 | event.preventDefault(); 70 | console.log("NESTEDMODAL", event); 71 | this.setState({ 72 | items: [], 73 | isOpen: !this.state.isOpen, 74 | loading: true 75 | }); 76 | } 77 | 78 | handleOnAfterOpenModal = () => { 79 | // when ready, we can access the available refs. 80 | (new Promise((resolve, reject) => { 81 | setTimeout(() => resolve(true), 500); 82 | })).then(res => { 83 | this.setState({ 84 | items: [1, 2, 3, 4, 5].map(x => `Item ${x}`), 85 | loading: false 86 | }); 87 | }); 88 | } 89 | 90 | render() { 91 | const { isOpen } = this.state; 92 | return ( 93 |
94 | 95 | 102 |

List of items

103 | {this.state.loading ? ( 104 |

Loading...

105 | ) : ( 106 | 107 | )} 108 |
109 |
110 | ); 111 | } 112 | } 113 | 114 | export default { 115 | label: "Working with nested modals.", 116 | app: NestedModals 117 | }; 118 | -------------------------------------------------------------------------------- /examples/basic/react-router/index.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | import createHistory from 'history/createBrowserHistory'; 4 | import { Router, Route, Switch } from 'react-router'; 5 | import { Link } from 'react-router-dom'; 6 | import Modal from 'react-modal'; 7 | 8 | const history = createHistory(); 9 | 10 | const Content = label => () =>

{`Content ${label}`}

; 11 | 12 | const shouldOpenModal = locationPath => /\bmodal\b/.test(locationPath); 13 | 14 | const ReactRouterModal = props => ( 15 | history.push("/basic")}> 18 |
19 | Link A
20 | Link B 21 |
22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 | ); 30 | 31 | class App extends Component { 32 | render() { 33 | return ( 34 | 35 |
36 | Modal 37 | 38 |
39 |
40 | ); 41 | } 42 | } 43 | 44 | export default { 45 | label: "react-modal and react-router.", 46 | app: App 47 | }; 48 | -------------------------------------------------------------------------------- /examples/basic/simple_usage/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Modal from 'react-modal'; 3 | import MyModal from './modal'; 4 | 5 | const MODAL_A = 'modal_a'; 6 | const MODAL_B = 'modal_b'; 7 | 8 | const DEFAULT_TITLE = 'Default title'; 9 | 10 | class SimpleUsage extends Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | title1: DEFAULT_TITLE, 15 | currentModal: null 16 | }; 17 | } 18 | 19 | toggleModal = key => event => { 20 | event.preventDefault(); 21 | if (this.state.currentModal) { 22 | this.handleModalCloseRequest(); 23 | return; 24 | } 25 | 26 | this.setState({ 27 | ...this.state, 28 | currentModal: key, 29 | title1: DEFAULT_TITLE 30 | }); 31 | } 32 | 33 | handleModalCloseRequest = () => { 34 | // opportunity to validate something and keep the modal open even if it 35 | // requested to be closed 36 | this.setState({ 37 | ...this.state, 38 | currentModal: null 39 | }); 40 | } 41 | 42 | handleInputChange = e => { 43 | let text = e.target.value; 44 | if (text == '') { 45 | text = DEFAULT_TITLE; 46 | } 47 | this.setState({ ...this.state, title1: text }); 48 | } 49 | 50 | handleOnAfterOpenModal = () => { 51 | // when ready, we can access the available refs. 52 | this.heading && (this.heading.style.color = '#F00'); 53 | } 54 | 55 | render() { 56 | const { currentModal } = this.state; 57 | 58 | return ( 59 |
60 | 61 | 62 | 69 | 82 |

this.heading = h1}>This is the modal 2!

83 |
84 |

This is a description of what it does: nothing :)

85 | 86 |
87 |
88 |
89 | ); 90 | } 91 | } 92 | 93 | export default { 94 | label: "Working with one modal at a time.", 95 | app: SimpleUsage 96 | }; 97 | -------------------------------------------------------------------------------- /examples/basic/simple_usage/modal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Modal from 'react-modal'; 3 | 4 | export default props => { 5 | const { 6 | title, isOpen, askToClose, 7 | onAfterOpen, onRequestClose, onChangeInput 8 | } = props; 9 | 10 | return ( 11 | 18 |

{title}

19 | 20 |
I am a modal. Use the first input to change the modal's title.
21 |
22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /examples/bootstrap/app.css: -------------------------------------------------------------------------------- 1 | .ReactModal__Overlay { 2 | -webkit-perspective: 600; 3 | perspective: 600; 4 | opacity: 0; 5 | overflow-x: hidden; 6 | overflow-y: auto; 7 | background-color: rgba(0, 0, 0, 0.5); 8 | } 9 | 10 | .ReactModal__Overlay--after-open { 11 | opacity: 1; 12 | transition: opacity 150ms ease-out; 13 | } 14 | 15 | .ReactModal__Content { 16 | -webkit-transform: scale(0.5) rotateX(-30deg); 17 | transform: scale(0.5) rotateX(-30deg); 18 | } 19 | 20 | .ReactModal__Content--after-open { 21 | -webkit-transform: scale(1) rotateX(0deg); 22 | transform: scale(1) rotateX(0deg); 23 | transition: all 150ms ease-in; 24 | } 25 | 26 | .ReactModal__Overlay--before-close { 27 | opacity: 0; 28 | } 29 | 30 | .ReactModal__Content--before-close { 31 | -webkit-transform: scale(0.5) rotateX(30deg); 32 | transform: scale(0.5) rotateX(30deg); 33 | transition: all 150ms ease-in; 34 | } 35 | 36 | .ReactModal__Content.modal-dialog { 37 | border: none; 38 | background-color: transparent; 39 | } 40 | -------------------------------------------------------------------------------- /examples/bootstrap/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Modal from 'react-modal'; 4 | 5 | var appElement = document.getElementById('example'); 6 | 7 | Modal.setAppElement(appElement); 8 | 9 | class App extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { modalIsOpen: false }; 13 | } 14 | 15 | openModal = () => { 16 | this.setState({modalIsOpen: true}); 17 | } 18 | 19 | closeModal = () => { 20 | this.setState({modalIsOpen: false}); 21 | } 22 | 23 | handleModalCloseRequest = () => { 24 | // opportunity to validate something and keep the modal open even if it 25 | // requested to be closed 26 | this.setState({modalIsOpen: false}); 27 | } 28 | 29 | handleSaveClicked = (e) => { 30 | alert('Save button was clicked'); 31 | } 32 | 33 | render() { 34 | return ( 35 |
36 | 37 | 43 |
44 |
45 |

Modal title

46 | 50 |
51 |
52 |

Really long content...

53 |

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus

54 |

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus

55 |

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus

56 |
57 |
58 | 59 | 60 |
61 |
62 |
63 |
64 | ); 65 | } 66 | } 67 | 68 | ReactDOM.render(, appElement); 69 | -------------------------------------------------------------------------------- /examples/bootstrap/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bootstrap-Style Example 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

react-modal

13 |

an accessible React modal dialog component

14 |
15 |
16 | Fork me on GitHub 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Basic Example 5 | 6 | 7 | 8 | 9 | 10 |
11 |

react-modal

12 |

an accessible React modal dialog component

13 |
14 |
15 | Basic 16 | Bootstrap 17 | Web Component 18 |
19 | Fork me on GitHub 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/wc/app.css: -------------------------------------------------------------------------------- 1 | .ReactModal__Overlay { 2 | -webkit-perspective: 600; 3 | perspective: 600; 4 | opacity: 0; 5 | overflow-x: hidden; 6 | overflow-y: auto; 7 | background-color: rgba(0, 0, 0, 0.5); 8 | } 9 | 10 | .ReactModal__Overlay--after-open { 11 | opacity: 1; 12 | transition: opacity 150ms ease-out; 13 | } 14 | 15 | .ReactModal__Content { 16 | -webkit-transform: scale(0.5) rotateX(-30deg); 17 | transform: scale(0.5) rotateX(-30deg); 18 | } 19 | 20 | .ReactModal__Content--after-open { 21 | -webkit-transform: scale(1) rotateX(0deg); 22 | transform: scale(1) rotateX(0deg); 23 | transition: all 150ms ease-in; 24 | } 25 | 26 | .ReactModal__Overlay--before-close { 27 | opacity: 0; 28 | } 29 | 30 | .ReactModal__Content--before-close { 31 | -webkit-transform: scale(0.5) rotateX(30deg); 32 | transform: scale(0.5) rotateX(30deg); 33 | transition: all 150ms ease-in; 34 | } 35 | 36 | .ReactModal__Content.modal-dialog { 37 | border: none; 38 | background-color: transparent; 39 | } 40 | -------------------------------------------------------------------------------- /examples/wc/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Modal from 'react-modal'; 4 | 5 | import '@webcomponents/custom-elements/src/native-shim'; 6 | 7 | var appElement = document.getElementById('example'); 8 | 9 | Modal.setAppElement(appElement); 10 | 11 | class App extends Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { modalIsOpen: false }; 15 | } 16 | 17 | openModal = () => { 18 | this.setState({modalIsOpen: true}); 19 | } 20 | 21 | closeModal = () => { 22 | this.setState({modalIsOpen: false}); 23 | } 24 | 25 | handleModalCloseRequest = () => { 26 | // opportunity to validate something and keep the modal open even if it 27 | // requested to be closed 28 | this.setState({modalIsOpen: false}); 29 | } 30 | 31 | handleSaveClicked = (e) => { 32 | alert('Save button was clicked'); 33 | } 34 | 35 | render() { 36 | return ( 37 |
38 | 39 | 45 |
46 |
47 |

Modal title

48 |
49 | 50 | 54 |
55 |
56 |
57 |

Really long content...

58 |

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus

59 |

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus

60 |

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus

61 |
62 |
63 | 64 | 65 |
66 |
67 |
68 |
69 | ); 70 | } 71 | } 72 | 73 | ReactDOM.render(, appElement); 74 | 75 | class AwesomeButton extends HTMLElement { 76 | constructor() { 77 | super(); 78 | } 79 | 80 | // this shows with no shadow root 81 | connectedCallback() { 82 | this.innerHTML = ` 83 | 84 | `; 85 | } 86 | } 87 | 88 | customElements.define("awesome-button", AwesomeButton); 89 | -------------------------------------------------------------------------------- /examples/wc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bootstrap-Style Example 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

react-modal

13 |

an accessible React modal dialog component

14 |
15 |
16 | Fork me on GitHub 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | let browsers = ['ChromeHeadless']; 2 | let coverageType = 'text'; 3 | 4 | if (process.env.CONTINUOUS_INTEGRATION) { 5 | browsers = ['Firefox']; 6 | coverageType = 'lcovonly'; 7 | } 8 | 9 | module.exports = function(config) { 10 | config.set({ 11 | frameworks: ['mocha'], 12 | 13 | preprocessors: { 14 | './src/*.js': ['coverage'], 15 | './src/**/*.js': ['coverage'], 16 | './specs/index.js': ['webpack', 'sourcemap'] 17 | }, 18 | 19 | files: ['./specs/index.js'], 20 | 21 | webpack: require('./scripts/webpack.test.config'), 22 | 23 | webpackMiddleware: { stats: 'errors-only' }, 24 | 25 | reporters: ['mocha', 'coverage'], 26 | 27 | mochaReporter: { showDiff: true }, 28 | 29 | coverageReporter: { 30 | type : coverageType, 31 | dir : 'coverage/', 32 | subdir: '.' 33 | }, 34 | 35 | port: 9876, 36 | 37 | colors: true, 38 | 39 | logLevel: config.LOG_INFO, 40 | 41 | autoWatch: true, 42 | 43 | browsers, 44 | 45 | // Increase timeouts to prevent the issue with disconnected tests (https://goo.gl/nstA69) 46 | captureTimeout: 4 * 60 * 1000, 47 | 48 | singleRun: (process.env.CONTINUOUS_INTEGRATION) 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: react-modal documentation 2 | site_dir: _book 3 | nav: 4 | - Overview: index.md 5 | - Accessibility: accessibility/index.md 6 | - Styles: 7 | - Inline Styles: styles/index.md 8 | - Classes: styles/classes.md 9 | - Transitions: styles/transitions.md 10 | - Examples: 11 | - Run local: examples/index.md 12 | - Minimal: examples/minimal.md 13 | - setAppElement: examples/set_app_element.md 14 | - shouldCloseOnOverlayClick: examples/should_close_on_overlay_click.md 15 | - onRequestClose: examples/on_request_close.md 16 | - Global Overrides: examples/global_overrides.md 17 | - Inline Styles: examples/inline_styles.md 18 | - Css Classes: examples/css_classes.md 19 | - Testing: testing/index.md 20 | - Contributing: 21 | - Overview: contributing/index.md 22 | - Development setup: contributing/development.md 23 | theme: 24 | name: 'material' 25 | markdown_extensions: 26 | - codehilite 27 | extra_css: [pygments.css] 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-modal", 3 | "version": "3.16.3", 4 | "description": "Accessible modal dialog component for React.JS", 5 | "main": "./lib/index.js", 6 | "module": "./lib/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/reactjs/react-modal.git" 10 | }, 11 | "homepage": "https://github.com/reactjs/react-modal", 12 | "bugs": "https://github.com/reactjs/react-modal/issues", 13 | "directories": { 14 | "example": "examples" 15 | }, 16 | "scripts": { 17 | "start": "npx webpack-dev-server --config ./scripts/webpack.config.js --inline --host 127.0.0.1 --content-base examples/", 18 | "test": "cross-env NODE_ENV=test karma start", 19 | "lint": "eslint src/" 20 | }, 21 | "authors": [ 22 | "Ryan Florence" 23 | ], 24 | "license": "MIT", 25 | "devDependencies": { 26 | "@webcomponents/custom-elements": "^1.5.0", 27 | "babel-cli": "^6.26.0", 28 | "babel-core": "^6.25.0", 29 | "babel-eslint": "^8.0.1", 30 | "babel-loader": "^7.1.2", 31 | "babel-plugin-add-module-exports": "^0.2.1", 32 | "babel-preset-env": "^1.6.0", 33 | "babel-preset-react": "^6.24.1", 34 | "babel-preset-stage-2": "^6.24.1", 35 | "coveralls": "^3.1.0", 36 | "cross-env": "^5.2.1", 37 | "eslint": "^4.8.0", 38 | "eslint-config-prettier": "^2.6.0", 39 | "eslint-import-resolver-webpack": "^0.9.0", 40 | "eslint-plugin-import": "^2.23.2", 41 | "eslint-plugin-jsx-a11y": "^6.4.1", 42 | "eslint-plugin-prettier": "^2.3.1", 43 | "eslint-plugin-react": "^7.23.2", 44 | "istanbul-instrumenter-loader": "^3.0.0", 45 | "karma": "^6.3.6", 46 | "karma-chrome-launcher": "2.2.0", 47 | "karma-coverage": "^2.0.3", 48 | "karma-firefox-launcher": "1.0.1", 49 | "karma-mocha": "^2.0.1", 50 | "karma-mocha-reporter": "^2.2.1", 51 | "karma-sourcemap-loader": "^0.3.8", 52 | "karma-webpack": "^2.0.4", 53 | "mocha": "^8.4.0", 54 | "npm-run-all": "^4.1.1", 55 | "prettier": "^1.19.1", 56 | "react": "^17.0.2", 57 | "react-dom": "^17.0.2", 58 | "react-router": "^4.2.0", 59 | "react-router-dom": "^4.2.2", 60 | "should": "^13.1.0", 61 | "sinon": "next", 62 | "uglify-js": "3.1.1", 63 | "webpack": "^4.46.0", 64 | "webpack-cli": "^3.3.12", 65 | "webpack-dev-server": "^3.11.2" 66 | }, 67 | "dependencies": { 68 | "exenv": "^1.2.0", 69 | "prop-types": "^15.7.2", 70 | "react-lifecycles-compat": "^3.0.0", 71 | "warning": "^4.0.3" 72 | }, 73 | "peerDependencies": { 74 | "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19", 75 | "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19" 76 | }, 77 | "tags": [ 78 | "react", 79 | "modal", 80 | "dialog" 81 | ], 82 | "keywords": [ 83 | "react", 84 | "react-component", 85 | "modal", 86 | "dialog" 87 | ] 88 | } 89 | -------------------------------------------------------------------------------- /scripts/changelog.py: -------------------------------------------------------------------------------- 1 | # Requires python3 to work since, python 3< does not implement %z. 2 | 3 | import sys 4 | 5 | sys.path += ["/Users/diasbruno/.local/lib/python3.7/site-packages"] 6 | 7 | from datetime import datetime 8 | from subprocess import Popen, PIPE 9 | import semver 10 | import functools 11 | 12 | 13 | # 1: version, 2: date, 3: dashes, 4: entries 14 | LOG_ENTRY = """{} 15 | {} 16 | 17 | {} 18 | """ 19 | 20 | 21 | head_version = "HEAD" 22 | 23 | 24 | def git_exec(args): 25 | p = Popen(" ".join(["git"] + args), shell=True, stdout=PIPE, stderr=PIPE) 26 | out, err = p.communicate() 27 | return out.decode('utf-8') 28 | 29 | 30 | def git_log(args): 31 | return git_exec(["log"] + args) 32 | 33 | 34 | def log_entry(entry): 35 | log = entry.split(' ') 36 | hash = log[0] 37 | log = ' '.join(log[1:]) 38 | 39 | return "- [%s](../../commit/%s) %s" % (hash, hash, log) 40 | 41 | 42 | def get_tags_date(tag): 43 | args = [tag, "-1", '--format="%ad"'] 44 | date_time = git_log(args).split('\n')[0] 45 | 46 | if date_time != '': 47 | dt = datetime.strptime(date_time, '%a %b %d %H:%M:%S %Y %z') 48 | else: 49 | dt = datetime.now() 50 | dt = dt.strftime('%a, %d %b %Y %H:%M:%S') 51 | return dt 52 | 53 | 54 | def log_in_between_versions(t): 55 | (a, b, logs) = t 56 | 57 | v = b and to_version(b) or head_version 58 | dt = get_tags_date(v) 59 | 60 | header = "{} - {} UTC".format(b or head_version, dt) 61 | dashes = ("-" * len(header)) 62 | 63 | def write_log(acc, log): 64 | if log[8:8+7] == 'Release' or log[8:8+7] == 'release': 65 | return acc 66 | acc.append(log_entry(log)) 67 | return acc 68 | 69 | actual_log = list(functools.reduce(write_log, 70 | logs.splitlines(), 71 | [])) 72 | 73 | if len(actual_log) == 0: 74 | entries = '-\n\n' 75 | else: 76 | entries = "\n".join(actual_log) 77 | 78 | return LOG_ENTRY.format(header, dashes, entries) 79 | 80 | 81 | def adjacents(ls, f, res): 82 | if len(ls) == 0: 83 | return res 84 | 85 | first = ls[0] 86 | if len(ls) == 1: 87 | next = None 88 | else: 89 | next = ls[1] 90 | 91 | res.append(f(first, next)) 92 | return adjacents(ls[1:], f, res) 93 | 94 | 95 | def to_version(tag): 96 | if not tag: 97 | return "HEAD" 98 | if tag.prerelease: 99 | return str(tag) 100 | return "v{}".format(tag) 101 | 102 | 103 | def logs_between(base, b): 104 | to = to_version(b) 105 | between = "{}..{}".format(to_version(base), to) 106 | logs = git_log([between, "--format='%h %s'"]) 107 | return (base, b, logs) 108 | 109 | 110 | def parse_version(version): 111 | if version == 'HEAD': 112 | return version 113 | if version[0] == 'v': 114 | version = version[1:] 115 | return semver.parse_version_info(version) 116 | 117 | 118 | def get_all_tags(): 119 | lines = git_exec(["tag", "-l"]) 120 | versions = map(parse_version, lines.splitlines()) 121 | return sorted(versions) 122 | 123 | 124 | def generate_current(): 125 | versions = get_all_tags() 126 | base = versions[-1] 127 | logs = logs_between(base, None) 128 | return [log_in_between_versions(logs)] 129 | 130 | 131 | def generate_all(): 132 | versions = get_all_tags() 133 | log_versions = adjacents(versions, logs_between, []) 134 | vs = map(log_in_between_versions, log_versions) 135 | return list(vs) 136 | 137 | 138 | if __name__ == "__main__": 139 | argc = len(sys.argv) 140 | 141 | if sys.argv[1] == '-a': # all 142 | head_version = sys.argv[2] if argc > 2 else "HEAD" 143 | log = generate_all() 144 | log.reverse() 145 | 146 | elif sys.argv[1] == '-c': # current 147 | head_version = sys.argv[2] 148 | log = generate_current() 149 | 150 | print("\n".join(log)) 151 | -------------------------------------------------------------------------------- /scripts/defaultConfig.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | output: { 6 | filename: '[name].js', 7 | path: path.resolve(__dirname, './examples/__build__'), 8 | publicPath: '/__build__/' 9 | }, 10 | module: { 11 | rules: [ 12 | { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader' } } 13 | ] 14 | }, 15 | resolve: { 16 | alias: { 17 | "react-modal": path.resolve(__dirname, "../src") 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /scripts/repo_status: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -z "`git status -s`" ]; then 4 | echo "Working tree is not clean" 5 | git status -s 6 | read -p "Proceed? [Y/n] " OK 7 | if [[ "$OK" -eq "n" || "$OK" -eq "N" || -z "$OK" ]]; then 8 | echo "Stopping publish" 9 | exit 1 10 | fi 11 | fi 12 | -------------------------------------------------------------------------------- /scripts/version: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | JQ=$(which jq) 4 | 5 | if [[ -z "$JQ" ]]; then 6 | echo "jq is missing." 7 | fi 8 | 9 | echo "Current version is: $1" 10 | 11 | read -p "Bump to: " NEW_VERSION 12 | 13 | if [[ ! -z "$(git tag -l | grep v${NEW_VERSION})" ]]; then 14 | echo "Tag $NEW_VERSION already exists." 15 | exit 1 16 | fi 17 | 18 | FILES="package.json bower.json" 19 | 20 | for F in $FILES; do 21 | $JQ ".version = \"${NEW_VERSION}\"" "$F" > up.json 22 | cat up.json > "$F" 23 | done 24 | 25 | rm up.json 26 | -------------------------------------------------------------------------------- /scripts/webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const defaultConfig = require('./defaultConfig'); 5 | 6 | var EXAMPLES_DIR = path.resolve(__dirname, '../examples'); 7 | 8 | function isDirectory(dir) { 9 | return fs.lstatSync(dir).isDirectory(); 10 | } 11 | 12 | function buildEntries() { 13 | return fs.readdirSync(EXAMPLES_DIR).reduce(function (entries, dir) { 14 | if (dir === 'build') 15 | return entries; 16 | 17 | var isDraft = dir.charAt(0) === '_'; 18 | 19 | if (!isDraft && isDirectory(path.join(EXAMPLES_DIR, dir))) 20 | entries[dir] = path.join(EXAMPLES_DIR, dir, 'app.js'); 21 | 22 | return entries; 23 | }, {}); 24 | } 25 | 26 | module.exports = { 27 | ...defaultConfig, 28 | entry: buildEntries(), 29 | }; 30 | -------------------------------------------------------------------------------- /scripts/webpack.dist.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const defaultConfig = require('./defaultConfig'); 4 | 5 | const reactExternal = { 6 | root: 'React', 7 | commonjs2: 'react', 8 | commonjs: 'react', 9 | amd: 'react' 10 | }; 11 | const reactDOMExternal = { 12 | root: 'ReactDOM', 13 | commonjs2: 'react-dom', 14 | commonjs: 'react-dom', 15 | amd: 'react-dom' 16 | }; 17 | 18 | module.exports = { 19 | ...defaultConfig, 20 | mode: 'production', 21 | entry: { 22 | 'react-modal': path.resolve(__dirname, '../src/index.js'), 23 | 'react-modal.min': path.resolve(__dirname, '../src/index.js') 24 | }, 25 | externals: { 26 | 'react': reactExternal, 27 | 'react-dom': reactDOMExternal 28 | }, 29 | output: { 30 | filename: '[name].js', 31 | chunkFilename: '[id].chunk.js', 32 | path: path.resolve(__dirname, '../dist'), 33 | publicPath: '/', 34 | libraryTarget: 'umd', 35 | library: 'ReactModal' 36 | }, 37 | optimization: { 38 | minimize: true 39 | }, 40 | plugins: [ 41 | new webpack.DefinePlugin({ 42 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 43 | }) 44 | ] 45 | }; 46 | -------------------------------------------------------------------------------- /scripts/webpack.test.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const defaultConfig = require('./defaultConfig'); 3 | 4 | module.exports = { 5 | ...defaultConfig, 6 | plugins: [], 7 | entry: path.resolve(__dirname, '../specs/index.js'), 8 | devtool: 'inline-source-map', 9 | module: { 10 | ...defaultConfig.module, 11 | rules: [ 12 | { 13 | test: /\.js$/, 14 | use: { loader: 'istanbul-instrumenter-loader' }, 15 | enforce: 'post', 16 | include: path.resolve(__dirname, '../src') 17 | }, 18 | ...defaultConfig.module.rules 19 | ] 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /specs/Modal.events.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import React from "react"; 3 | import ReactDOM from "react-dom"; 4 | import "should"; 5 | import sinon from "sinon"; 6 | import Modal from "react-modal"; 7 | import { 8 | moverlay, 9 | mcontent, 10 | clickAt, 11 | mouseDownAt, 12 | mouseUpAt, 13 | escKeyDown, 14 | escKeyDownWithCode, 15 | tabKeyDown, 16 | tabKeyDownWithCode, 17 | withModal, 18 | withElementCollector, 19 | createHTMLElement 20 | } from "./helper"; 21 | 22 | export default () => { 23 | it("should trigger the onAfterOpen callback", () => { 24 | const afterOpenCallback = sinon.spy(); 25 | withElementCollector(() => { 26 | const props = { isOpen: true, onAfterOpen: afterOpenCallback }; 27 | const node = createHTMLElement("div"); 28 | ReactDOM.render(, node); 29 | requestAnimationFrame(() => { 30 | afterOpenCallback.called.should.be.ok(); 31 | ReactDOM.unmountComponentAtNode(node); 32 | }); 33 | }); 34 | }); 35 | 36 | it("should call onAfterOpen with overlay and content references", () => { 37 | const afterOpenCallback = sinon.spy(); 38 | withElementCollector(() => { 39 | const props = { isOpen: true, onAfterOpen: afterOpenCallback }; 40 | const node = createHTMLElement("div"); 41 | const modal = ReactDOM.render(, node); 42 | requestAnimationFrame(() => { 43 | sinon.assert.calledWith(afterOpenCallback, { 44 | overlayEl: modal.portal.overlay, 45 | contentEl: modal.portal.content 46 | }); 47 | ReactDOM.unmountComponentAtNode(node); 48 | }); 49 | }); 50 | }); 51 | 52 | it("should trigger the onAfterClose callback", () => { 53 | const onAfterCloseCallback = sinon.spy(); 54 | withModal({ 55 | isOpen: true, 56 | onAfterClose: onAfterCloseCallback 57 | }); 58 | onAfterCloseCallback.called.should.be.ok(); 59 | }); 60 | 61 | it("should not trigger onAfterClose callback when unmounting a closed modal", () => { 62 | const onAfterCloseCallback = sinon.spy(); 63 | withModal({ isOpen: false, onAfterClose: onAfterCloseCallback }); 64 | onAfterCloseCallback.called.should.not.be.ok(); 65 | }); 66 | 67 | it("should trigger onAfterClose callback when unmounting an opened modal", () => { 68 | const onAfterCloseCallback = sinon.spy(); 69 | withModal({ isOpen: true, onAfterClose: onAfterCloseCallback }); 70 | onAfterCloseCallback.called.should.be.ok(); 71 | }); 72 | 73 | it("keeps focus inside the modal when child has no tabbable elements", () => { 74 | let tabPrevented = false; 75 | const props = { isOpen: true }; 76 | withModal(props, "hello", modal => { 77 | const content = mcontent(modal); 78 | document.activeElement.should.be.eql(content); 79 | tabKeyDown(content, { 80 | preventDefault() { 81 | tabPrevented = true; 82 | } 83 | }); 84 | tabPrevented.should.be.eql(true); 85 | }); 86 | }); 87 | 88 | it("handles case when child has no tabbable elements", () => { 89 | const props = { isOpen: true }; 90 | withModal(props, "hello", modal => { 91 | const content = mcontent(modal); 92 | tabKeyDown(content); 93 | document.activeElement.should.be.eql(content); 94 | }); 95 | }); 96 | 97 | it("traps tab in the modal on shift + tab", () => { 98 | const topButton = ; 99 | const bottomButton = ; 100 | const modalContent = ( 101 |
102 | {topButton} 103 | {bottomButton} 104 |
105 | ); 106 | const props = { isOpen: true }; 107 | withModal(props, modalContent, modal => { 108 | const content = mcontent(modal); 109 | tabKeyDown(content, { shiftKey: true }); 110 | document.activeElement.textContent.should.be.eql("bottom"); 111 | }); 112 | }); 113 | 114 | it("traps tab in the modal on shift + tab with KeyboardEvent.code", () => { 115 | const topButton = ; 116 | const bottomButton = ; 117 | const modalContent = ( 118 |
119 | {topButton} 120 | {bottomButton} 121 |
122 | ); 123 | const props = { isOpen: true }; 124 | withModal(props, modalContent, modal => { 125 | const content = mcontent(modal); 126 | tabKeyDownWithCode(content, { shiftKey: true }); 127 | document.activeElement.textContent.should.be.eql("bottom"); 128 | }); 129 | }); 130 | 131 | describe("shouldCloseOnEsc", () => { 132 | context("when true", () => { 133 | it("should close on Esc key event", () => { 134 | const requestCloseCallback = sinon.spy(); 135 | withModal( 136 | { 137 | isOpen: true, 138 | shouldCloseOnEsc: true, 139 | onRequestClose: requestCloseCallback 140 | }, 141 | null, 142 | modal => { 143 | escKeyDown(mcontent(modal)); 144 | requestCloseCallback.called.should.be.ok(); 145 | // Check if event is passed to onRequestClose callback. 146 | const event = requestCloseCallback.getCall(0).args[0]; 147 | event.should.be.ok(); 148 | } 149 | ); 150 | }); 151 | 152 | it("should close on Esc key event with KeyboardEvent.code", () => { 153 | const requestCloseCallback = sinon.spy(); 154 | withModal( 155 | { 156 | isOpen: true, 157 | shouldCloseOnEsc: true, 158 | onRequestClose: requestCloseCallback 159 | }, 160 | null, 161 | modal => { 162 | escKeyDownWithCode(mcontent(modal)); 163 | requestCloseCallback.called.should.be.ok(); 164 | // Check if event is passed to onRequestClose callback. 165 | const event = requestCloseCallback.getCall(0).args[0]; 166 | event.should.be.ok(); 167 | } 168 | ); 169 | }); 170 | }); 171 | 172 | context("when false", () => { 173 | it("should not close on Esc key event", () => { 174 | const requestCloseCallback = sinon.spy(); 175 | const props = { 176 | isOpen: true, 177 | shouldCloseOnEsc: false, 178 | onRequestClose: requestCloseCallback 179 | }; 180 | withModal(props, null, modal => { 181 | escKeyDown(mcontent(modal)); 182 | requestCloseCallback.called.should.be.false; 183 | }); 184 | }); 185 | }); 186 | }); 187 | 188 | describe("shouldCloseOnoverlayClick", () => { 189 | it("when false, click on overlay should not close", () => { 190 | const requestCloseCallback = sinon.spy(); 191 | const props = { 192 | isOpen: true, 193 | shouldCloseOnOverlayClick: false 194 | }; 195 | withModal(props, null, modal => { 196 | const overlay = moverlay(modal); 197 | clickAt(overlay); 198 | requestCloseCallback.called.should.not.be.ok(); 199 | }); 200 | }); 201 | 202 | it("when true, click on overlay must close", () => { 203 | const requestCloseCallback = sinon.spy(); 204 | const props = { 205 | isOpen: true, 206 | shouldCloseOnOverlayClick: true, 207 | onRequestClose: requestCloseCallback 208 | }; 209 | withModal(props, null, modal => { 210 | clickAt(moverlay(modal)); 211 | requestCloseCallback.called.should.be.ok(); 212 | }); 213 | }); 214 | 215 | it("overlay mouse down and content mouse up, should not close", () => { 216 | const requestCloseCallback = sinon.spy(); 217 | const props = { 218 | isOpen: true, 219 | shouldCloseOnOverlayClick: true, 220 | onRequestClose: requestCloseCallback 221 | }; 222 | withModal(props, null, modal => { 223 | mouseDownAt(moverlay(modal)); 224 | mouseUpAt(mcontent(modal)); 225 | requestCloseCallback.called.should.not.be.ok(); 226 | }); 227 | }); 228 | 229 | it("content mouse down and overlay mouse up, should not close", () => { 230 | const requestCloseCallback = sinon.spy(); 231 | const props = { 232 | isOpen: true, 233 | shouldCloseOnOverlayClick: true, 234 | onRequestClose: requestCloseCallback 235 | }; 236 | withModal(props, null, modal => { 237 | mouseDownAt(mcontent(modal)); 238 | mouseUpAt(moverlay(modal)); 239 | requestCloseCallback.called.should.not.be.ok(); 240 | }); 241 | }); 242 | }); 243 | 244 | it("should not stop event propagation", () => { 245 | let hasPropagated = false; 246 | const props = { 247 | isOpen: true, 248 | shouldCloseOnOverlayClick: true 249 | }; 250 | withModal(props, null, modal => { 251 | const propagated = () => (hasPropagated = true); 252 | window.addEventListener("click", propagated); 253 | const event = new MouseEvent("click", { bubbles: true }); 254 | moverlay(modal).dispatchEvent(event); 255 | hasPropagated.should.be.ok(); 256 | window.removeEventListener("click", propagated); 257 | }); 258 | }); 259 | 260 | it("verify event passing on overlay click", () => { 261 | const requestCloseCallback = sinon.spy(); 262 | const props = { 263 | isOpen: true, 264 | shouldCloseOnOverlayClick: true, 265 | onRequestClose: requestCloseCallback 266 | }; 267 | withModal(props, null, modal => { 268 | // click the overlay 269 | clickAt(moverlay(modal), { 270 | // Used to test that this was the event received 271 | fakeData: "ABC" 272 | }); 273 | requestCloseCallback.called.should.be.ok(); 274 | // Check if event is passed to onRequestClose callback. 275 | const event = requestCloseCallback.getCall(0).args[0]; 276 | event.should.be.ok(); 277 | }); 278 | }); 279 | 280 | it("on nested modals, only the topmost should handle ESC key.", () => { 281 | const requestCloseCallback = sinon.spy(); 282 | const innerRequestCloseCallback = sinon.spy(); 283 | let innerModal = null; 284 | let innerModalRef = ref => { 285 | innerModal = ref; 286 | }; 287 | 288 | withModal( 289 | { 290 | isOpen: true, 291 | onRequestClose: requestCloseCallback 292 | }, 293 | 298 | Test 299 | , 300 | () => { 301 | const content = mcontent(innerModal); 302 | escKeyDown(content); 303 | innerRequestCloseCallback.called.should.be.ok(); 304 | requestCloseCallback.called.should.not.be.ok(); 305 | } 306 | ); 307 | }); 308 | }; 309 | -------------------------------------------------------------------------------- /specs/Modal.helpers.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import "should"; 3 | import "@webcomponents/custom-elements/src/native-shim"; 4 | import tabbable from "../src/helpers/tabbable"; 5 | import "sinon"; 6 | 7 | export default () => { 8 | describe("tabbable", () => { 9 | describe("without tabbable descendents", () => { 10 | it("returns an empty array", () => { 11 | const elem = document.createElement("div"); 12 | tabbable(elem).should.deepEqual([]); 13 | }); 14 | }); 15 | 16 | describe("with tabbable descendents", () => { 17 | let elem; 18 | beforeEach(() => { 19 | elem = document.createElement("div"); 20 | document.body.appendChild(elem); 21 | }); 22 | 23 | afterEach(() => { 24 | document.body.removeChild(elem); 25 | }); 26 | 27 | it("includes descendent tabbable inputs", () => { 28 | const input = document.createElement("input"); 29 | elem.appendChild(input); 30 | tabbable(elem).should.containEql(input); 31 | }); 32 | 33 | it("includes tabbable non-input elements", () => { 34 | const div = document.createElement("div"); 35 | div.tabIndex = 1; 36 | elem.appendChild(div); 37 | tabbable(elem).should.containEql(div); 38 | }); 39 | 40 | it("includes links with an href", () => { 41 | const a = document.createElement("a"); 42 | a.href = "foobar"; 43 | a.innerHTML = "link"; 44 | elem.appendChild(a); 45 | tabbable(elem).should.containEql(a); 46 | }); 47 | 48 | it("excludes links without an href or a tabindex", () => { 49 | const a = document.createElement("a"); 50 | elem.appendChild(a); 51 | tabbable(elem).should.not.containEql(a); 52 | }); 53 | 54 | it("excludes descendent inputs if they are not tabbable", () => { 55 | const input = document.createElement("input"); 56 | input.tabIndex = -1; 57 | elem.appendChild(input); 58 | tabbable(elem).should.not.containEql(input); 59 | }); 60 | 61 | it("excludes descendent inputs if they are disabled", () => { 62 | const input = document.createElement("input"); 63 | input.disabled = true; 64 | elem.appendChild(input); 65 | tabbable(elem).should.not.containEql(input); 66 | }); 67 | 68 | it("excludes descendent inputs if they are not displayed", () => { 69 | const input = document.createElement("input"); 70 | input.style.display = "none"; 71 | elem.appendChild(input); 72 | tabbable(elem).should.not.containEql(input); 73 | }); 74 | 75 | it("excludes descendent inputs with 0 width and height", () => { 76 | const input = document.createElement("input"); 77 | input.style.width = "0"; 78 | input.style.height = "0"; 79 | input.style.border = "0"; 80 | input.style.padding = "0"; 81 | elem.appendChild(input); 82 | tabbable(elem).should.not.containEql(input); 83 | }); 84 | 85 | it("excludes descendents with hidden parents", () => { 86 | const input = document.createElement("input"); 87 | elem.style.display = "none"; 88 | elem.appendChild(input); 89 | tabbable(elem).should.not.containEql(input); 90 | }); 91 | 92 | it("excludes inputs with parents that have zero width and height", () => { 93 | const input = document.createElement("input"); 94 | elem.style.width = "0"; 95 | elem.style.height = "0"; 96 | elem.style.overflow = "hidden"; 97 | elem.appendChild(input); 98 | tabbable(elem).should.not.containEql(input); 99 | }); 100 | 101 | it("includes inputs visible because of overflow == visible", () => { 102 | const input = document.createElement("input"); 103 | input.style.width = "0"; 104 | input.style.height = "0"; 105 | input.style.overflow = "visible"; 106 | elem.appendChild(input); 107 | tabbable(elem).should.containEql(input); 108 | }); 109 | 110 | it("excludes elements with overflow == visible if there is no visible content", () => { 111 | const button = document.createElement("button"); 112 | button.innerHTML = "You can't see me!"; 113 | button.style.display = "none"; 114 | button.style.overflow = "visible"; 115 | elem.appendChild(button); 116 | tabbable(elem).should.not.containEql(button); 117 | }); 118 | 119 | it("excludes elements that contain reserved node names", () => { 120 | const button = document.createElement("button"); 121 | button.innerHTML = "I am a good button"; 122 | elem.appendChild(button); 123 | 124 | const badButton = document.createElement("bad-button"); 125 | badButton.innerHTML = "I am a bad button"; 126 | elem.appendChild(badButton); 127 | 128 | tabbable(elem).should.deepEqual([button]); 129 | }); 130 | 131 | it("includes elements that contain reserved node names with tabindex", () => { 132 | const trickButton = document.createElement("trick-button"); 133 | trickButton.innerHTML = "I am a good button"; 134 | trickButton.tabIndex = '0'; 135 | elem.appendChild(trickButton); 136 | 137 | tabbable(elem).should.deepEqual([trickButton]); 138 | }); 139 | 140 | describe("inside Web Components with shadow dom", () => { 141 | let wc; 142 | let input; 143 | class TestWebComponent extends HTMLElement { 144 | constructor() { 145 | super(); 146 | } 147 | 148 | connectedCallback() { 149 | this.attachShadow({ 150 | mode: "open" 151 | }); 152 | this.style.display = "block"; 153 | this.style.width = "100px"; 154 | this.style.height = "25px"; 155 | } 156 | } 157 | 158 | const registerTestComponent = () => { 159 | if (window.customElements.get("test-web-component")) { 160 | return; 161 | } 162 | window.customElements.define("test-web-component", TestWebComponent); 163 | }; 164 | 165 | beforeEach(() => { 166 | registerTestComponent(); 167 | wc = document.createElement("test-web-component"); 168 | 169 | input = document.createElement("input"); 170 | elem.appendChild(input); 171 | 172 | document.body.appendChild(wc); 173 | wc.shadowRoot.appendChild(elem); 174 | }); 175 | 176 | afterEach(() => { 177 | // re-add elem to body for the next afterEach 178 | document.body.appendChild(elem); 179 | 180 | // remove Web Component 181 | document.body.removeChild(wc); 182 | }); 183 | 184 | it("includes elements when inside a Shadow DOM", () => { 185 | tabbable(elem).should.containEql(input); 186 | }); 187 | 188 | it("excludes elements when hidden inside a Shadow DOM", () => { 189 | wc.style.display = "none"; 190 | tabbable(elem).should.not.containEql(input); 191 | }); 192 | }); 193 | 194 | describe("inside Web Components with no shadow dom", () => { 195 | let wc; 196 | let button; 197 | class ButtonWebComponent extends HTMLElement { 198 | constructor() { 199 | super(); 200 | } 201 | 202 | connectedCallback() { 203 | this.innerHTML = ''; 204 | this.style.display = "block"; 205 | this.style.width = "100px"; 206 | this.style.height = "25px"; 207 | } 208 | } 209 | 210 | const registerButtonComponent = () => { 211 | if (window.customElements.get("button-web-component")) { 212 | return; 213 | } 214 | window.customElements.define("button-web-component", ButtonWebComponent); 215 | }; 216 | 217 | beforeEach(() => { 218 | registerButtonComponent(); 219 | wc = document.createElement("button-web-component"); 220 | 221 | elem.appendChild(wc); 222 | }); 223 | 224 | afterEach(() => { 225 | // remove Web Component 226 | elem.removeChild(wc); 227 | }); 228 | 229 | it("includes only focusable elements", () => { 230 | button = wc.querySelector('button'); 231 | 232 | tabbable(elem).should.deepEqual([button]); 233 | }); 234 | }); 235 | }); 236 | }); 237 | }; 238 | -------------------------------------------------------------------------------- /specs/Modal.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import should from "should"; 3 | import React, { Component } from "react"; 4 | import ReactDOM from "react-dom"; 5 | import Modal from "react-modal"; 6 | import { 7 | setElement as ariaAppSetElement, 8 | resetState as ariaAppHiderResetState 9 | } from "react-modal/helpers/ariaAppHider"; 10 | import { resetState as bodyTrapReset } from "react-modal/helpers/bodyTrap"; 11 | import { resetState as classListReset } from "react-modal/helpers/classList"; 12 | import { resetState as focusManagerReset } from "react-modal/helpers/focusManager"; 13 | import { resetState as portalInstancesReset } from "react-modal/helpers/portalOpenInstances"; 14 | import { 15 | log, 16 | isDocumentWithReactModalOpenClass, 17 | isHtmlWithReactModalOpenClass, 18 | htmlClassList, 19 | contentAttribute, 20 | mcontent, 21 | moverlay, 22 | escKeyDown, 23 | withModal, 24 | documentClassList, 25 | withElementCollector, 26 | createHTMLElement 27 | } from "./helper"; 28 | 29 | Modal.setCreateHTMLElement(createHTMLElement); 30 | 31 | export default () => { 32 | beforeEach("check for leaks", () => log("before")); 33 | afterEach("clean up", () => ( 34 | log("after", true), 35 | bodyTrapReset(), 36 | classListReset(), 37 | focusManagerReset(), 38 | portalInstancesReset(), 39 | ariaAppHiderResetState() 40 | )); 41 | 42 | it("can be open initially", () => { 43 | const props = { isOpen: true }; 44 | withModal(props, "hello", modal => { 45 | mcontent(modal).should.be.ok(); 46 | }); 47 | }); 48 | 49 | it("can be closed initially", () => { 50 | const props = {}; 51 | withModal(props, "hello", modal => { 52 | should(ReactDOM.findDOMNode(mcontent(modal))).not.be.ok(); 53 | }); 54 | }); 55 | 56 | it("doesn't render the portal if modal is closed", () => { 57 | const props = {}; 58 | withModal(props, "hello", modal => { 59 | should(ReactDOM.findDOMNode(modal.portal)).not.be.ok(); 60 | }); 61 | }); 62 | 63 | it("has default props", () => { 64 | withElementCollector(() => { 65 | // eslint-disable-next-line react/no-render-return-value 66 | const modal = ; 67 | const props = modal.props; 68 | props.isOpen.should.not.be.ok(); 69 | props.ariaHideApp.should.be.ok(); 70 | props.closeTimeoutMS.should.be.eql(0); 71 | props.shouldFocusAfterRender.should.be.ok(); 72 | props.shouldCloseOnOverlayClick.should.be.ok(); 73 | props.preventScroll.should.be.false(); 74 | }); 75 | }); 76 | 77 | it("accepts appElement as a prop", () => { 78 | withElementCollector(() => { 79 | const el = createHTMLElement("div"); 80 | const props = { 81 | isOpen: true, 82 | ariaHideApp: true, 83 | appElement: el 84 | }; 85 | withModal(props, null, () => { 86 | el.getAttribute("aria-hidden").should.be.eql("true"); 87 | }); 88 | }); 89 | }); 90 | 91 | it("accepts array of appElement as a prop", () => { 92 | withElementCollector(() => { 93 | const el1 = createHTMLElement("div"); 94 | const el2 = createHTMLElement("div"); 95 | const node = createHTMLElement("div"); 96 | ReactDOM.render(, node); 97 | el1.getAttribute("aria-hidden").should.be.eql("true"); 98 | el2.getAttribute("aria-hidden").should.be.eql("true"); 99 | ReactDOM.unmountComponentAtNode(node); 100 | }); 101 | }); 102 | 103 | it("renders into the body, not in context", () => { 104 | withElementCollector(() => { 105 | const node = createHTMLElement("div"); 106 | Modal.setAppElement(node); 107 | ReactDOM.render(, node); 108 | document.body 109 | .querySelector(".ReactModalPortal") 110 | .parentNode.should.be.eql(document.body); 111 | ReactDOM.unmountComponentAtNode(node); 112 | }); 113 | }); 114 | 115 | it("allow setting appElement of type string", () => { 116 | withElementCollector(() => { 117 | const node = createHTMLElement("div"); 118 | const appElement = "body"; 119 | Modal.setAppElement(appElement); 120 | ReactDOM.render(, node); 121 | document.body 122 | .querySelector(".ReactModalPortal") 123 | .parentNode.should.be.eql(document.body); 124 | ReactDOM.unmountComponentAtNode(node); 125 | }); 126 | }); 127 | 128 | // eslint-disable-next-line max-len 129 | it("allow setting appElement of type string matching multiple elements", () => { 130 | withElementCollector(() => { 131 | const el1 = createHTMLElement("div"); 132 | el1.id = "id1"; 133 | document.body.appendChild(el1); 134 | const el2 = createHTMLElement("div"); 135 | el2.id = "id2"; 136 | document.body.appendChild(el2); 137 | const node = createHTMLElement("div"); 138 | const appElement = "#id1, #id2"; 139 | Modal.setAppElement(appElement); 140 | ReactDOM.render(, node); 141 | el1.getAttribute("aria-hidden").should.be.eql("true"); 142 | ReactDOM.unmountComponentAtNode(node); 143 | }); 144 | }); 145 | 146 | it("default parentSelector should be document.body.", () => { 147 | const props = { isOpen: true }; 148 | withModal(props, null, (modal) => { 149 | modal.props.parentSelector().should.be.eql(document.body); 150 | }); 151 | }); 152 | 153 | it("renders the modal content with a dialog aria role when provided ", () => { 154 | const child = "I am a child of Modal, and he has sent me here..."; 155 | const props = { isOpen: true, role: "dialog" }; 156 | withModal(props, child, (modal) => { 157 | contentAttribute(modal, "role").should.be.eql("dialog"); 158 | }); 159 | }); 160 | 161 | // eslint-disable-next-line max-len 162 | it("renders the modal content with the default aria role when not provided", () => { 163 | const child = "I am a child of Modal, and he has sent me here..."; 164 | const props = { isOpen: true }; 165 | withModal(props, child, modal => { 166 | contentAttribute(modal, "role").should.be.eql("dialog"); 167 | }); 168 | }); 169 | 170 | it("does not render the aria role when provided role with null", () => { 171 | const child = "I am a child of Modal, and he has sent me here..."; 172 | const props = { isOpen: true, role: null }; 173 | withModal(props, child, modal => { 174 | should(contentAttribute(modal, "role")).be.eql(null); 175 | }); 176 | }); 177 | 178 | it("sets aria-label based on the contentLabel prop", () => { 179 | const child = "I am a child of Modal, and he has sent me here..."; 180 | withModal( 181 | { 182 | isOpen: true, 183 | contentLabel: "Special Modal" 184 | }, 185 | child, 186 | modal => { 187 | contentAttribute(modal, "aria-label").should.be.eql("Special Modal"); 188 | } 189 | ); 190 | }); 191 | 192 | it("removes the portal node", () => { 193 | const props = { isOpen: true }; 194 | withModal(props, "hello"); 195 | should(document.querySelector(".ReactModalPortal")).not.be.ok(); 196 | }); 197 | 198 | it("removes the portal node after closeTimeoutMS", done => { 199 | const closeTimeoutMS = 100; 200 | 201 | function checkDOM(count) { 202 | const portal = document.querySelectorAll(".ReactModalPortal"); 203 | portal.length.should.be.eql(count); 204 | } 205 | 206 | const props = { isOpen: true, closeTimeoutMS }; 207 | withModal(props, "hello", () => { 208 | checkDOM(1); 209 | }); 210 | 211 | setTimeout(() => { 212 | // content is unmounted after specified timeout 213 | checkDOM(0); 214 | done(); 215 | }, closeTimeoutMS); 216 | }); 217 | 218 | it("focuses the modal content by default", () => { 219 | const props = { isOpen: true }; 220 | withModal(props, null, modal => { 221 | document.activeElement.should.be.eql(mcontent(modal)); 222 | }); 223 | }); 224 | 225 | it("does not focus modal content if shouldFocusAfterRender is false", () => { 226 | withModal( 227 | { isOpen: true, shouldFocusAfterRender: false }, 228 | null, 229 | modal => { 230 | document.activeElement.should.not.be.eql(mcontent(modal)); 231 | } 232 | ); 233 | }); 234 | 235 | it("give back focus to previous element or modal.", done => { 236 | withModal( 237 | { 238 | isOpen: true, 239 | className: "modal-a", 240 | onRequestClose: function() { done(); } 241 | }, 242 | null, 243 | modalA => { 244 | const modalContent = mcontent(modalA); 245 | document.activeElement.should.be.eql(modalContent); 246 | 247 | const modalB = withModal( 248 | { 249 | isOpen: true, 250 | className: "modal-b", 251 | onRequestClose() { 252 | const modalContent = mcontent(modalB); 253 | document.activeElement.should.be.eql(mcontent(modalA)); 254 | escKeyDown(modalContent); 255 | document.activeElement.should.be.eql(modalContent); 256 | } 257 | }, 258 | null 259 | ); 260 | escKeyDown(modalContent); 261 | } 262 | ); 263 | }); 264 | 265 | it("does not steel focus when a descendent is already focused", () => { 266 | let content; 267 | const input = ( 268 | { 270 | el && el.focus(); 271 | content = el; 272 | }} 273 | /> 274 | ); 275 | const props = { isOpen: true }; 276 | withModal(props, input, () => { 277 | document.activeElement.should.be.eql(content); 278 | }); 279 | }); 280 | 281 | it("supports id prop", () => { 282 | const props = { isOpen: true, id: "id" }; 283 | withModal(props, null, modal => { 284 | mcontent(modal) 285 | .id 286 | .should.be.eql("id"); 287 | }); 288 | }); 289 | 290 | it("supports portalClassName", () => { 291 | const props = { 292 | isOpen: true, 293 | portalClassName: "myPortalClass" 294 | }; 295 | withModal(props, null, modal => { 296 | modal.node.className.includes("myPortalClass").should.be.ok(); 297 | }); 298 | }); 299 | 300 | it("supports custom className", () => { 301 | const props = { isOpen: true, className: "myClass" }; 302 | withModal(props, null, modal => { 303 | mcontent(modal) 304 | .className.includes("myClass") 305 | .should.be.ok(); 306 | }); 307 | }); 308 | 309 | it("supports custom overlayElement", () => { 310 | const overlayElement = (props, contentElement) => ( 311 |
312 | {contentElement} 313 |
314 | ); 315 | 316 | const props = { isOpen: true, overlayElement }; 317 | withModal(props, null, modal => { 318 | const modalOverlay = moverlay(modal); 319 | modalOverlay.id.should.eql("custom"); 320 | }); 321 | }); 322 | 323 | it("supports custom contentElement", () => { 324 | const contentElement = (props, children) => ( 325 |
326 | {children} 327 |
328 | ); 329 | 330 | const props = { isOpen: true, contentElement }; 331 | withModal(props, "hello", modal => { 332 | const modalContent = mcontent(modal); 333 | modalContent.id.should.eql("custom"); 334 | modalContent.textContent.should.be.eql("hello"); 335 | }); 336 | }); 337 | 338 | it("supports overlayClassName", () => { 339 | const props = { 340 | isOpen: true, 341 | overlayClassName: "myOverlayClass" 342 | }; 343 | withModal(props, null, modal => { 344 | moverlay(modal) 345 | .className.includes("myOverlayClass") 346 | .should.be.ok(); 347 | }); 348 | }); 349 | 350 | it("overrides content classes with custom object className", () => { 351 | withElementCollector(() => { 352 | const props = { 353 | isOpen: true, 354 | className: { 355 | base: "myClass", 356 | afterOpen: "myClass_after-open", 357 | beforeClose: "myClass_before-close" 358 | } 359 | }; 360 | const node = createHTMLElement("div"); 361 | const modal = ReactDOM.render(, node); 362 | const request = requestAnimationFrame(() => { 363 | mcontent(modal).className.should.be.eql("myClass myClass_after-open"); 364 | ReactDOM.unmountComponentAtNode(node); 365 | }); 366 | cancelAnimationFrame(request); 367 | }); 368 | }); 369 | 370 | it("overrides overlay classes with custom object overlayClassName", () => { 371 | withElementCollector(() => { 372 | const props = { 373 | isOpen: true, 374 | overlayClassName: { 375 | base: "myOverlayClass", 376 | afterOpen: "myOverlayClass_after-open", 377 | beforeClose: "myOverlayClass_before-close" 378 | } 379 | }; 380 | const node = createHTMLElement("div"); 381 | const modal = ReactDOM.render(, node); 382 | const request = requestAnimationFrame(() => { 383 | moverlay(modal).className.should.be.eql( 384 | "myOverlayClass myOverlayClass_after-open" 385 | ); 386 | ReactDOM.unmountComponentAtNode(node); 387 | }); 388 | cancelAnimationFrame(request); 389 | }); 390 | }); 391 | 392 | it("supports overriding react modal open class in document.body.", () => { 393 | const props = { isOpen: true, bodyOpenClassName: "custom-modal-open" }; 394 | withModal(props, null, () => { 395 | (document.body.className.indexOf("custom-modal-open") > -1).should.be.ok(); 396 | }); 397 | }); 398 | 399 | it("supports setting react modal open class in .", () => { 400 | const props = { isOpen: true, htmlOpenClassName: "custom-modal-open" }; 401 | withModal(props, null, () => { 402 | isHtmlWithReactModalOpenClass("custom-modal-open").should.be.ok(); 403 | }); 404 | }); 405 | 406 | // eslint-disable-next-line max-len 407 | it("don't append class to document.body if modal is closed.", () => { 408 | const props = { isOpen: false }; 409 | withModal(props, null, () => { 410 | isDocumentWithReactModalOpenClass().should.not.be.ok(); 411 | }); 412 | }); 413 | 414 | // eslint-disable-next-line max-len 415 | it("don't append any class to document.body when bodyOpenClassName is null.", () => { 416 | const props = { isOpen: true, bodyOpenClassName: null }; 417 | withModal(props, null, () => { 418 | documentClassList().should.be.empty(); 419 | }); 420 | }); 421 | 422 | it("don't append class to if modal is closed.", () => { 423 | const props = { isOpen: false, htmlOpenClassName: "custom-modal-open" }; 424 | withModal(props, null, () => { 425 | isHtmlWithReactModalOpenClass().should.not.be.ok(); 426 | }); 427 | }); 428 | 429 | it("append class to document.body if modal is open.", () => { 430 | const props = { isOpen: true }; 431 | withModal(props, null, () => { 432 | isDocumentWithReactModalOpenClass().should.be.ok(); 433 | }); 434 | }); 435 | 436 | it("don't append class to if not defined.", () => { 437 | const props = { isOpen: true }; 438 | withModal(props, null, () => { 439 | htmlClassList().should.be.empty(); 440 | }); 441 | }); 442 | 443 | // eslint-disable-next-line max-len 444 | it("removes class from document.body when unmounted without closing", () => { 445 | withModal({ isOpen: true }); 446 | isDocumentWithReactModalOpenClass().should.not.be.ok(); 447 | }); 448 | 449 | it("remove class from document.body when no modals opened", () => { 450 | const propsA = { isOpen: true }; 451 | withModal(propsA, null, () => { 452 | isDocumentWithReactModalOpenClass().should.be.ok(); 453 | }); 454 | const propsB = { isOpen: true }; 455 | withModal(propsB, null, () => { 456 | isDocumentWithReactModalOpenClass().should.be.ok(); 457 | }); 458 | isDocumentWithReactModalOpenClass().should.not.be.ok(); 459 | isHtmlWithReactModalOpenClass().should.not.be.ok(); 460 | }); 461 | 462 | it("supports adding/removing multiple document.body classes", () => { 463 | const props = { 464 | isOpen: true, 465 | bodyOpenClassName: "A B C" 466 | }; 467 | withModal(props, null, () => { 468 | document.body.classList.contains("A", "B", "C").should.be.ok(); 469 | }); 470 | document.body.classList.contains("A", "B", "C").should.not.be.ok(); 471 | ; 472 | }); 473 | 474 | it("does not remove shared classes if more than one modal is open", () => { 475 | const props = { 476 | isOpen: true, 477 | bodyOpenClassName: "A" 478 | }; 479 | withModal(props, null, () => { 480 | isDocumentWithReactModalOpenClass("A").should.be.ok(); 481 | withModal({ 482 | isOpen: true, 483 | bodyOpenClassName: "A B" 484 | }, null, () => { 485 | isDocumentWithReactModalOpenClass("A B").should.be.ok(); 486 | }); 487 | isDocumentWithReactModalOpenClass("A").should.be.ok(); 488 | }); 489 | isDocumentWithReactModalOpenClass("A").should.not.be.ok(); 490 | }); 491 | 492 | it("should not add classes to document.body for unopened modals", () => { 493 | const props = { isOpen: true }; 494 | withModal(props, null, () => { 495 | isDocumentWithReactModalOpenClass().should.be.ok(); 496 | }); 497 | withModal({ isOpen: false, bodyOpenClassName: "testBodyClass" }); 498 | isDocumentWithReactModalOpenClass("testBodyClass").should.not.be.ok(); 499 | }); 500 | 501 | it("should not remove classes from document.body if modal is closed", () => { 502 | const props = { isOpen: true }; 503 | withModal(props, null, () => { 504 | isDocumentWithReactModalOpenClass().should.be.ok(); 505 | withModal({ isOpen: false, bodyOpenClassName: "testBodyClass" }, null, () => { 506 | isDocumentWithReactModalOpenClass("testBodyClass").should.not.be.ok(); 507 | }); 508 | isDocumentWithReactModalOpenClass().should.be.ok(); 509 | }); 510 | }); 511 | 512 | it("should not remove classes from if modal is closed", () => { 513 | const props = { isOpen: false }; 514 | withModal(props, null, () => { 515 | isHtmlWithReactModalOpenClass().should.not.be.ok(); 516 | withModal({ 517 | isOpen: true, 518 | htmlOpenClassName: "testHtmlClass" 519 | }, null, () => { 520 | isHtmlWithReactModalOpenClass("testHtmlClass").should.be.ok(); 521 | }); 522 | isHtmlWithReactModalOpenClass("testHtmlClass").should.not.be.ok(); 523 | }); 524 | }); 525 | 526 | it("additional aria attributes", () => { 527 | withModal( 528 | { isOpen: true, aria: { labelledby: "a" } }, 529 | "hello", 530 | modal => mcontent(modal) 531 | .getAttribute("aria-labelledby") 532 | .should.be.eql("a") 533 | ); 534 | }); 535 | 536 | it("additional data attributes", () => { 537 | withModal( 538 | { isOpen: true, data: { background: "green" } }, 539 | "hello", 540 | modal => mcontent(modal) 541 | .getAttribute("data-background") 542 | .should.be.eql("green") 543 | ); 544 | }); 545 | 546 | it("additional testId attribute", () => { 547 | withModal( 548 | { isOpen: true, testId: "foo-bar" }, 549 | "hello", 550 | modal => mcontent(modal) 551 | .getAttribute("data-testid") 552 | .should.be.eql("foo-bar") 553 | ) 554 | }); 555 | 556 | it("raises an exception if the appElement selector does not match", () => { 557 | should(() => ariaAppSetElement(".test")).throw(); 558 | }); 559 | 560 | it("removes aria-hidden from appElement when unmounted w/o closing", () => { 561 | withElementCollector(() => { 562 | const el = createHTMLElement("div"); 563 | const node = createHTMLElement("div"); 564 | ReactDOM.render(, node); 565 | el.getAttribute("aria-hidden").should.be.eql("true"); 566 | ReactDOM.unmountComponentAtNode(node); 567 | const request = requestAnimationFrame(() => { 568 | should(el.getAttribute('aria-hidden')).not.be.ok(); 569 | }); 570 | cancelAnimationFrame(request); 571 | }); 572 | }); 573 | 574 | // eslint-disable-next-line max-len 575 | it("removes aria-hidden when closed and another modal with ariaHideApp set to false is open", () => { 576 | withElementCollector(() => { 577 | const rootNode = createHTMLElement("div"); 578 | const appElement = createHTMLElement("div"); 579 | document.body.appendChild(rootNode); 580 | document.body.appendChild(appElement); 581 | 582 | Modal.setAppElement(appElement); 583 | 584 | const initialState = ( 585 |
586 | 587 | 588 |
589 | ); 590 | 591 | ReactDOM.render(initialState, rootNode); 592 | appElement.getAttribute("aria-hidden").should.be.eql("true"); 593 | 594 | const updatedState = ( 595 |
596 | 597 | 598 |
599 | ); 600 | 601 | const request = requestAnimationFrame(() => { 602 | ReactDOM.render(updatedState, rootNode); 603 | should(appElement.getAttribute("aria-hidden")).not.be.ok(); 604 | 605 | ReactDOM.unmountComponentAtNode(rootNode); 606 | }); 607 | cancelAnimationFrame(request); 608 | }); 609 | }); 610 | 611 | // eslint-disable-next-line max-len 612 | it("maintains aria-hidden when closed and another modal with ariaHideApp set to true is open", () => { 613 | withElementCollector(() => { 614 | const rootNode = createHTMLElement("div"); 615 | document.body.appendChild(rootNode); 616 | 617 | const appElement = createHTMLElement("div"); 618 | document.body.appendChild(appElement); 619 | 620 | Modal.setAppElement(appElement); 621 | 622 | const initialState = ( 623 |
624 | 625 | 626 |
627 | ); 628 | 629 | ReactDOM.render(initialState, rootNode); 630 | appElement.getAttribute("aria-hidden").should.be.eql("true"); 631 | 632 | const updatedState = ( 633 |
634 | 635 | 636 |
637 | ); 638 | 639 | ReactDOM.render(updatedState, rootNode); 640 | appElement.getAttribute("aria-hidden").should.be.eql("true"); 641 | 642 | ReactDOM.unmountComponentAtNode(rootNode); 643 | }); 644 | }); 645 | 646 | // eslint-disable-next-line max-len 647 | it("removes aria-hidden when unmounted without close and second modal with ariaHideApp=false is open", () => { 648 | withElementCollector(() => { 649 | const appElement = createHTMLElement("div"); 650 | document.body.appendChild(appElement); 651 | Modal.setAppElement(appElement); 652 | 653 | const propsA = { isOpen: true, ariaHideApp: false, id: "test-2-modal-1" }; 654 | withModal(propsA, null, () => { 655 | should(appElement.getAttribute("aria-hidden")).not.be.ok(); 656 | }); 657 | 658 | const propsB = { isOpen: true, ariaHideApp: true, id: "test-2-modal-2" }; 659 | withModal(propsB, null, () => { 660 | appElement.getAttribute("aria-hidden").should.be.eql("true"); 661 | }); 662 | 663 | const request = requestAnimationFrame(() => { 664 | should(appElement.getAttribute("aria-hidden")).not.be.ok(); 665 | }); 666 | cancelAnimationFrame(request); 667 | }); 668 | }); 669 | 670 | // eslint-disable-next-line max-len 671 | it("maintains aria-hidden when unmounted without close and second modal with ariaHideApp=true is open", () => { 672 | withElementCollector(() => { 673 | const appElement = createHTMLElement("div"); 674 | document.body.appendChild(appElement); 675 | Modal.setAppElement(appElement); 676 | 677 | const check = (tobe) => appElement.getAttribute("aria-hidden").should.be.eql(tobe); 678 | 679 | const props = { isOpen: true, ariaHideApp: true, id: "test-3-modal-1" }; 680 | withModal(props, null, () => { 681 | check("true"); 682 | withModal({ isOpen: true, ariaHideApp: true, id: "test-3-modal-2" }, null, () => { 683 | check("true"); 684 | }); 685 | check("true"); 686 | }); 687 | 688 | const request = requestAnimationFrame(() => { 689 | should(appElement.getAttribute("aria-hidden")).not.be.ok(); 690 | }); 691 | cancelAnimationFrame(request); 692 | }); 693 | }); 694 | 695 | it("adds --after-open for animations", () => { 696 | withElementCollector(() => { 697 | const rg = /--after-open/i; 698 | const props = { isOpen: true }; 699 | const node = createHTMLElement("div"); 700 | const modal = ReactDOM.render(, node); 701 | const request = requestAnimationFrame(() => { 702 | const contentName = modal.portal.content.className; 703 | const overlayName = modal.portal.overlay.className; 704 | rg.test(contentName).should.be.ok(); 705 | rg.test(overlayName).should.be.ok(); 706 | ReactDOM.unmountComponentAtNode(node); 707 | }); 708 | cancelAnimationFrame(request); 709 | }); 710 | }); 711 | 712 | it("adds --before-close for animations", () => { 713 | const closeTimeoutMS = 50; 714 | const props = { 715 | isOpen: true, 716 | closeTimeoutMS 717 | }; 718 | withModal(props, null, modal => { 719 | modal.portal.closeWithTimeout(); 720 | 721 | const rg = /--before-close/i; 722 | rg.test(moverlay(modal).className).should.be.ok(); 723 | rg.test(mcontent(modal).className).should.be.ok(); 724 | 725 | modal.portal.closeWithoutTimeout(); 726 | }); 727 | }); 728 | 729 | it("should not be open after close with time out and reopen it", () => { 730 | const props = { 731 | isOpen: true, 732 | closeTimeoutMS: 2000, 733 | onRequestClose() { } 734 | }; 735 | withModal(props, null, modal => { 736 | modal.portal.closeWithTimeout(); 737 | modal.portal.open(); 738 | modal.portal.closeWithoutTimeout(); 739 | modal.portal.state.isOpen.should.not.be.ok(); 740 | }); 741 | }); 742 | 743 | it("verify default prop of shouldCloseOnOverlayClick", () => { 744 | const props = { isOpen: true }; 745 | withModal(props, null, modal => { 746 | modal.props.shouldCloseOnOverlayClick.should.be.ok(); 747 | }); 748 | }); 749 | 750 | it("verify prop of shouldCloseOnOverlayClick", () => { 751 | const modalOpts = { isOpen: true, shouldCloseOnOverlayClick: false }; 752 | withModal(modalOpts, null, modal => { 753 | modal.props.shouldCloseOnOverlayClick.should.not.be.ok(); 754 | }); 755 | }); 756 | 757 | it("keeps the modal in the DOM until closeTimeoutMS elapses", done => { 758 | function checkDOM(count) { 759 | const overlay = document.querySelectorAll(".ReactModal__Overlay"); 760 | const content = document.querySelectorAll(".ReactModal__Content"); 761 | overlay.length.should.be.eql(count); 762 | content.length.should.be.eql(count); 763 | } 764 | withElementCollector(() => { 765 | const closeTimeoutMS = 100; 766 | const props = { isOpen: true, closeTimeoutMS }; 767 | const node = createHTMLElement("div"); 768 | const modal = ReactDOM.render(, node); 769 | 770 | modal.portal.closeWithTimeout(); 771 | checkDOM(1); 772 | 773 | setTimeout(() => { 774 | checkDOM(0); 775 | ReactDOM.unmountComponentAtNode(node); 776 | done(); 777 | }, closeTimeoutMS); 778 | }); 779 | }); 780 | 781 | it("verify that portalClassName is refreshed on component update", () => { 782 | withElementCollector(() => { 783 | const node = createHTMLElement("div"); 784 | let modal = null; 785 | 786 | class App extends Component { 787 | constructor(props) { 788 | super(props); 789 | this.state = { classModifier: "" }; 790 | } 791 | 792 | componentDidMount() { 793 | modal.node.className.should.be.eql("portal"); 794 | 795 | this.setState({ classModifier: "-modifier" }); 796 | } 797 | 798 | componentDidUpdate() { 799 | modal.node.className.should.be.eql("portal-modifier"); 800 | } 801 | 802 | render() { 803 | const { classModifier } = this.state; 804 | const portalClassName = `portal${classModifier}`; 805 | 806 | return ( 807 |
808 | { 810 | modal = modalComponent; 811 | }} 812 | isOpen 813 | portalClassName={portalClassName} 814 | > 815 | Test 816 | 817 |
818 | ); 819 | } 820 | } 821 | 822 | Modal.setAppElement(node); 823 | ReactDOM.render(, node); 824 | ReactDOM.unmountComponentAtNode(node); 825 | }); 826 | }); 827 | 828 | it("use overlayRef and contentRef", () => { 829 | let overlay = null; 830 | let content = null; 831 | 832 | const props = { 833 | isOpen: true, 834 | overlayRef: node => (overlay = node), 835 | contentRef: node => (content = node) 836 | }; 837 | withModal(props, null, () => { 838 | overlay.should.be.instanceOf(HTMLElement); 839 | content.should.be.instanceOf(HTMLElement); 840 | overlay.classList.contains("ReactModal__Overlay"); 841 | content.classList.contains("ReactModal__Content"); 842 | }); 843 | }); 844 | }; 845 | -------------------------------------------------------------------------------- /specs/Modal.style.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import "should"; 3 | import Modal from "react-modal"; 4 | import { mcontent, moverlay, withModal } from "./helper"; 5 | 6 | export default () => { 7 | it("overrides the default styles when a custom classname is used", () => { 8 | const props = { isOpen: true, className: "myClass" }; 9 | withModal(props, null, modal => { 10 | mcontent(modal).style.top.should.be.eql(""); 11 | }); 12 | }); 13 | 14 | it("overrides the default styles when using custom overlayClassName", () => { 15 | const overlayClassName = "myOverlayClass"; 16 | const props = { isOpen: true, overlayClassName }; 17 | withModal(props, null, modal => { 18 | moverlay(modal).style.backgroundColor.should.be.eql(""); 19 | }); 20 | }); 21 | 22 | it("supports adding style to the modal contents", () => { 23 | const style = { content: { width: "20px" } }; 24 | const props = { isOpen: true, style }; 25 | withModal(props, null, modal => { 26 | mcontent(modal).style.width.should.be.eql("20px"); 27 | }); 28 | }); 29 | 30 | it("supports overriding style on the modal contents", () => { 31 | const style = { content: { position: "static" } }; 32 | const props = { isOpen: true, style }; 33 | withModal(props, null, modal => { 34 | mcontent(modal).style.position.should.be.eql("static"); 35 | }); 36 | }); 37 | 38 | it("supports adding style on the modal overlay", () => { 39 | const style = { overlay: { width: "75px" } }; 40 | const props = { isOpen: true, style }; 41 | withModal(props, null, modal => { 42 | moverlay(modal).style.width.should.be.eql("75px"); 43 | }); 44 | }); 45 | 46 | it("supports overriding style on the modal overlay", () => { 47 | const style = { overlay: { position: "static" } }; 48 | const props = { isOpen: true, style }; 49 | withModal(props, null, modal => { 50 | moverlay(modal).style.position.should.be.eql("static"); 51 | }); 52 | }); 53 | 54 | it("supports overriding the default styles", () => { 55 | const previousStyle = Modal.defaultStyles.content.position; 56 | // Just in case the default style is already relative, 57 | // check that we can change it 58 | const newStyle = previousStyle === "relative" ? "static" : "relative"; 59 | Modal.defaultStyles.content.position = newStyle; 60 | const props = { isOpen: true }; 61 | withModal(props, null, modal => { 62 | modal.portal.content.style.position.should.be.eql(newStyle); 63 | Modal.defaultStyles.content.position = previousStyle; 64 | }); 65 | }); 66 | }; 67 | -------------------------------------------------------------------------------- /specs/Modal.testability.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import ReactDOM from "react-dom"; 3 | import sinon from "sinon"; 4 | import { withModal } from "./helper"; 5 | 6 | export default () => { 7 | it("allows ReactDOM.createPortal to be overridden in real-time", () => { 8 | const createPortalSpy = sinon.spy(ReactDOM, "createPortal"); 9 | const props = { isOpen: true }; 10 | withModal(props, "hello"); 11 | createPortalSpy.called.should.be.ok(); 12 | ReactDOM.createPortal.restore(); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /specs/helper.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import Modal, { bodyOpenClassName } from "../src/components/Modal"; 4 | import TestUtils from "react-dom/test-utils"; 5 | import { log as classListLog } from "../src/helpers/classList"; 6 | import { log as focusManagerLog } from "../src/helpers/focusManager"; 7 | import { log as ariaAppLog } from "../src/helpers/ariaAppHider"; 8 | import { log as bodyTrapLog } from "../src/helpers/bodyTrap"; 9 | import { log as portalInstancesLog } from "../src/helpers/portalOpenInstances"; 10 | 11 | const debug = false; 12 | 13 | let i = 0; 14 | 15 | /** 16 | * This log is used to see if there are leaks in between tests. 17 | */ 18 | export function log(label, spaces) { 19 | if (!debug) return; 20 | 21 | console.log(`${label} -----------------`); 22 | console.log(document.body.children.length); 23 | const logChildren = c => console.log(c.nodeName, c.className, c.id); 24 | document.body.children.forEach(logChildren); 25 | 26 | ariaAppLog(); 27 | bodyTrapLog(); 28 | classListLog(); 29 | focusManagerLog(); 30 | portalInstancesLog(); 31 | 32 | console.log(`end ${label} -----------------` + (!spaces ? '' : ` 33 | 34 | 35 | `)); 36 | } 37 | 38 | let elementPool = []; 39 | 40 | /** 41 | * Every HTMLElement must be requested using this function... 42 | * and inside `withElementCollector`. 43 | */ 44 | export function createHTMLElement(name) { 45 | const e = document.createElement(name); 46 | elementPool[elementPool.length - 1].push(e); 47 | e.className = `element_pool_${name}-${++i}`; 48 | return e; 49 | } 50 | 51 | /** 52 | * Remove every element from its parent and release the pool. 53 | */ 54 | export function drainPool(pool) { 55 | pool.forEach(e => e.parentNode && e.parentNode.removeChild(e)); 56 | } 57 | 58 | /** 59 | * Every HTMLElement must be requested inside this function... 60 | * The reason is that it provides a mechanism that disposes 61 | * all the elements (built with `createHTMLElement`) after a test. 62 | */ 63 | export function withElementCollector(work) { 64 | let r; 65 | let poolIndex = elementPool.length; 66 | elementPool[poolIndex] = []; 67 | try { 68 | r = work(); 69 | } finally { 70 | drainPool(elementPool[poolIndex]); 71 | elementPool = elementPool.slice( 72 | 0, poolIndex 73 | ); 74 | } 75 | return r; 76 | } 77 | 78 | /** 79 | * Polyfill for String.includes on some node versions. 80 | */ 81 | if (!String.prototype.includes) { 82 | String.prototype.includes = function(search, start) { 83 | if (typeof start !== "number") { 84 | start = 0; 85 | } 86 | 87 | if (start + search.length > this.length) { 88 | return false; 89 | } 90 | 91 | return this.indexOf(search, start) !== -1; 92 | }; 93 | } 94 | 95 | /** 96 | * Return the class list object from `document.body`. 97 | * @return {Array} 98 | */ 99 | export const documentClassList = () => document.body.classList; 100 | 101 | /** 102 | * Check if the document.body contains the react modal 103 | * open class. 104 | * @return {Boolean} 105 | */ 106 | export const isDocumentWithReactModalOpenClass = ( 107 | bodyClass = bodyOpenClassName 108 | ) => document.body.className.includes(bodyClass); 109 | 110 | /** 111 | * Return the class list object from . 112 | * @return {Array} 113 | */ 114 | export const htmlClassList = () => 115 | document.getElementsByTagName("html")[0].classList; 116 | 117 | /** 118 | * Check if the html contains the react modal 119 | * open class. 120 | * @return {Boolean} 121 | */ 122 | export const isHtmlWithReactModalOpenClass = htmlClass => 123 | htmlClassList().contains(htmlClass); 124 | 125 | /** 126 | * Returns a rendered dom element by class. 127 | * @param {React} element A react instance. 128 | * @param {String} className A class to find. 129 | * @return {DOMElement} 130 | */ 131 | export const findDOMWithClass = TestUtils.findRenderedDOMComponentWithClass; 132 | 133 | /** 134 | * Returns an attribut of a rendered react tree. 135 | * @param {React} component A react instance. 136 | * @return {String} 137 | */ 138 | const getModalAttribute = component => (instance, attr) => 139 | modalComponent(component)(instance).getAttribute(attr); 140 | 141 | /** 142 | * Return an element from a react component. 143 | * @param {React} A react instance. 144 | * @return {DOMElement} 145 | */ 146 | const modalComponent = component => instance => instance.portal[component]; 147 | 148 | /** 149 | * Returns the modal content. 150 | * @param {Modal} modal Modal instance. 151 | * @return {DOMElement} 152 | */ 153 | export const mcontent = modalComponent("content"); 154 | 155 | /** 156 | * Returns the modal overlay. 157 | * @param {Modal} modal Modal instance. 158 | * @return {DOMElement} 159 | */ 160 | export const moverlay = modalComponent("overlay"); 161 | 162 | /** 163 | * Return an attribute of modal content. 164 | * @param {Modal} modal Modal instance. 165 | * @return {String} 166 | */ 167 | export const contentAttribute = getModalAttribute("content"); 168 | 169 | /** 170 | * Return an attribute of modal overlay. 171 | * @param {Modal} modal Modal instance. 172 | * @return {String} 173 | */ 174 | export const overlayAttribute = getModalAttribute("overlay"); 175 | 176 | const Simulate = TestUtils.Simulate; 177 | 178 | const dispatchMockEvent = eventCtor => (key, code) => (element, opts) => 179 | eventCtor( 180 | element, 181 | Object.assign( 182 | {}, 183 | { 184 | key: key, 185 | which: code 186 | }, 187 | code, 188 | opts 189 | ) 190 | ); 191 | 192 | const dispatchMockKeyDownEvent = dispatchMockEvent(Simulate.keyDown); 193 | 194 | /** 195 | * @deprecated will be replaced by `escKeyDownWithCode` when `react-modal` 196 | * drops support for React <18. 197 | * 198 | * Dispatch an 'esc' key down event using the legacy KeyboardEvent.keyCode. 199 | */ 200 | export const escKeyDown = dispatchMockKeyDownEvent("ESC", { keyCode: 27 }); 201 | /** 202 | * Dispatch an 'esc' key down event. 203 | */ 204 | export const escKeyDownWithCode = dispatchMockKeyDownEvent("ESC", { 205 | code: "Escape" 206 | }); 207 | /** 208 | * @deprecated will be replaced by `escKeyDownWithCode` when `react-modal` 209 | * drops support for React <18. 210 | * 211 | * Dispatch a 'tab' key down event using the legacy KeyboardEvent.keyCode. 212 | */ 213 | export const tabKeyDown = dispatchMockKeyDownEvent("TAB", { keyCode: 9 }); 214 | /** 215 | * Dispatch a 'tab' key down event. 216 | */ 217 | export const tabKeyDownWithCode = dispatchMockKeyDownEvent("TAB", { 218 | code: "Tab" 219 | }); 220 | /** 221 | * Dispatch a 'click' event at a node. 222 | */ 223 | export const clickAt = Simulate.click; 224 | /** 225 | * Dispatch a 'mouse up' event at a node. 226 | */ 227 | export const mouseUpAt = Simulate.mouseUp; 228 | /** 229 | * Dispatch a 'mouse down' event at a node. 230 | */ 231 | export const mouseDownAt = Simulate.mouseDown; 232 | 233 | export const noop = () => {}; 234 | 235 | /** 236 | * Request a managed modal to run the tests on. 237 | * 238 | */ 239 | export const withModal = function(props, children, test = noop) { 240 | return withElementCollector(() => { 241 | const node = createHTMLElement(); 242 | const modalProps = { ariaHideApp: false, ...props }; 243 | let modal; 244 | try { 245 | ReactDOM.render( 246 | (modal = m)} {...modalProps}> 247 | {children} 248 | , 249 | node 250 | ); 251 | test(modal); 252 | } finally { 253 | ReactDOM.unmountComponentAtNode(node); 254 | } 255 | }); 256 | }; 257 | -------------------------------------------------------------------------------- /specs/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | import ModalState from "./Modal.spec"; 4 | import ModalEvents from "./Modal.events.spec"; 5 | import ModalStyle from "./Modal.style.spec"; 6 | import ModalHelpers from "./Modal.helpers.spec"; 7 | import ModalTestability from "./Modal.testability.spec"; 8 | 9 | describe("State", ModalState); 10 | describe("Style", ModalStyle); 11 | describe("Events", ModalEvents); 12 | describe("Helpers", ModalHelpers); 13 | describe("Testability", ModalTestability); 14 | -------------------------------------------------------------------------------- /src/components/Modal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import PropTypes from "prop-types"; 4 | import ModalPortal from "./ModalPortal"; 5 | import * as ariaAppHider from "../helpers/ariaAppHider"; 6 | import SafeHTMLElement, { 7 | SafeNodeList, 8 | SafeHTMLCollection, 9 | canUseDOM 10 | } from "../helpers/safeHTMLElement"; 11 | 12 | import { polyfill } from "react-lifecycles-compat"; 13 | 14 | export const portalClassName = "ReactModalPortal"; 15 | export const bodyOpenClassName = "ReactModal__Body--open"; 16 | 17 | const isReact16 = canUseDOM && ReactDOM.createPortal !== undefined; 18 | 19 | let createHTMLElement = name => document.createElement(name); 20 | 21 | const getCreatePortal = () => 22 | isReact16 23 | ? ReactDOM.createPortal 24 | : ReactDOM.unstable_renderSubtreeIntoContainer; 25 | 26 | function getParentElement(parentSelector) { 27 | return parentSelector(); 28 | } 29 | 30 | class Modal extends Component { 31 | static setAppElement(element) { 32 | ariaAppHider.setElement(element); 33 | } 34 | 35 | /* eslint-disable react/no-unused-prop-types */ 36 | static propTypes = { 37 | isOpen: PropTypes.bool.isRequired, 38 | style: PropTypes.shape({ 39 | content: PropTypes.object, 40 | overlay: PropTypes.object 41 | }), 42 | portalClassName: PropTypes.string, 43 | bodyOpenClassName: PropTypes.string, 44 | htmlOpenClassName: PropTypes.string, 45 | className: PropTypes.oneOfType([ 46 | PropTypes.string, 47 | PropTypes.shape({ 48 | base: PropTypes.string.isRequired, 49 | afterOpen: PropTypes.string.isRequired, 50 | beforeClose: PropTypes.string.isRequired 51 | }) 52 | ]), 53 | overlayClassName: PropTypes.oneOfType([ 54 | PropTypes.string, 55 | PropTypes.shape({ 56 | base: PropTypes.string.isRequired, 57 | afterOpen: PropTypes.string.isRequired, 58 | beforeClose: PropTypes.string.isRequired 59 | }) 60 | ]), 61 | appElement: PropTypes.oneOfType([ 62 | PropTypes.instanceOf(SafeHTMLElement), 63 | PropTypes.instanceOf(SafeHTMLCollection), 64 | PropTypes.instanceOf(SafeNodeList), 65 | PropTypes.arrayOf(PropTypes.instanceOf(SafeHTMLElement)) 66 | ]), 67 | onAfterOpen: PropTypes.func, 68 | onRequestClose: PropTypes.func, 69 | closeTimeoutMS: PropTypes.number, 70 | ariaHideApp: PropTypes.bool, 71 | shouldFocusAfterRender: PropTypes.bool, 72 | shouldCloseOnOverlayClick: PropTypes.bool, 73 | shouldReturnFocusAfterClose: PropTypes.bool, 74 | preventScroll: PropTypes.bool, 75 | parentSelector: PropTypes.func, 76 | aria: PropTypes.object, 77 | data: PropTypes.object, 78 | role: PropTypes.string, 79 | contentLabel: PropTypes.string, 80 | shouldCloseOnEsc: PropTypes.bool, 81 | overlayRef: PropTypes.func, 82 | contentRef: PropTypes.func, 83 | id: PropTypes.string, 84 | overlayElement: PropTypes.func, 85 | contentElement: PropTypes.func 86 | }; 87 | /* eslint-enable react/no-unused-prop-types */ 88 | 89 | static defaultProps = { 90 | isOpen: false, 91 | portalClassName, 92 | bodyOpenClassName, 93 | role: "dialog", 94 | ariaHideApp: true, 95 | closeTimeoutMS: 0, 96 | shouldFocusAfterRender: true, 97 | shouldCloseOnEsc: true, 98 | shouldCloseOnOverlayClick: true, 99 | shouldReturnFocusAfterClose: true, 100 | preventScroll: false, 101 | parentSelector: () => document.body, 102 | overlayElement: (props, contentEl) =>
{contentEl}
, 103 | contentElement: (props, children) =>
{children}
104 | }; 105 | 106 | static defaultStyles = { 107 | overlay: { 108 | position: "fixed", 109 | top: 0, 110 | left: 0, 111 | right: 0, 112 | bottom: 0, 113 | backgroundColor: "rgba(255, 255, 255, 0.75)" 114 | }, 115 | content: { 116 | position: "absolute", 117 | top: "40px", 118 | left: "40px", 119 | right: "40px", 120 | bottom: "40px", 121 | border: "1px solid #ccc", 122 | background: "#fff", 123 | overflow: "auto", 124 | WebkitOverflowScrolling: "touch", 125 | borderRadius: "4px", 126 | outline: "none", 127 | padding: "20px" 128 | } 129 | }; 130 | 131 | componentDidMount() { 132 | if (!canUseDOM) return; 133 | 134 | if (!isReact16) { 135 | this.node = createHTMLElement("div"); 136 | } 137 | this.node.className = this.props.portalClassName; 138 | 139 | const parent = getParentElement(this.props.parentSelector); 140 | parent.appendChild(this.node); 141 | 142 | !isReact16 && this.renderPortal(this.props); 143 | } 144 | 145 | getSnapshotBeforeUpdate(prevProps) { 146 | const prevParent = getParentElement(prevProps.parentSelector); 147 | const nextParent = getParentElement(this.props.parentSelector); 148 | return { prevParent, nextParent }; 149 | } 150 | 151 | componentDidUpdate(prevProps, _, snapshot) { 152 | if (!canUseDOM) return; 153 | const { isOpen, portalClassName } = this.props; 154 | 155 | if (prevProps.portalClassName !== portalClassName) { 156 | this.node.className = portalClassName; 157 | } 158 | 159 | const { prevParent, nextParent } = snapshot; 160 | if (nextParent !== prevParent) { 161 | prevParent.removeChild(this.node); 162 | nextParent.appendChild(this.node); 163 | } 164 | 165 | // Stop unnecessary renders if modal is remaining closed 166 | if (!prevProps.isOpen && !isOpen) return; 167 | 168 | !isReact16 && this.renderPortal(this.props); 169 | } 170 | 171 | componentWillUnmount() { 172 | if (!canUseDOM || !this.node || !this.portal) return; 173 | 174 | const state = this.portal.state; 175 | const now = Date.now(); 176 | const closesAt = 177 | state.isOpen && 178 | this.props.closeTimeoutMS && 179 | (state.closesAt || now + this.props.closeTimeoutMS); 180 | 181 | if (closesAt) { 182 | if (!state.beforeClose) { 183 | this.portal.closeWithTimeout(); 184 | } 185 | 186 | setTimeout(this.removePortal, closesAt - now); 187 | } else { 188 | this.removePortal(); 189 | } 190 | } 191 | 192 | removePortal = () => { 193 | !isReact16 && ReactDOM.unmountComponentAtNode(this.node); 194 | const parent = getParentElement(this.props.parentSelector); 195 | if (parent && parent.contains(this.node)) { 196 | parent.removeChild(this.node); 197 | } else { 198 | // eslint-disable-next-line no-console 199 | console.warn( 200 | 'React-Modal: "parentSelector" prop did not returned any DOM ' + 201 | "element. Make sure that the parent element is unmounted to " + 202 | "avoid any memory leaks." 203 | ); 204 | } 205 | }; 206 | 207 | portalRef = ref => { 208 | this.portal = ref; 209 | }; 210 | 211 | renderPortal = props => { 212 | const createPortal = getCreatePortal(); 213 | const portal = createPortal( 214 | this, 215 | , 216 | this.node 217 | ); 218 | this.portalRef(portal); 219 | }; 220 | 221 | render() { 222 | if (!canUseDOM || !isReact16) { 223 | return null; 224 | } 225 | 226 | if (!this.node && isReact16) { 227 | this.node = createHTMLElement("div"); 228 | } 229 | 230 | const createPortal = getCreatePortal(); 231 | return createPortal( 232 | , 237 | this.node 238 | ); 239 | } 240 | } 241 | 242 | polyfill(Modal); 243 | 244 | if (process.env.NODE_ENV !== "production") { 245 | Modal.setCreateHTMLElement = fn => (createHTMLElement = fn); 246 | } 247 | 248 | export default Modal; 249 | -------------------------------------------------------------------------------- /src/components/ModalPortal.js: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import * as focusManager from "../helpers/focusManager"; 4 | import scopeTab from "../helpers/scopeTab"; 5 | import * as ariaAppHider from "../helpers/ariaAppHider"; 6 | import * as classList from "../helpers/classList"; 7 | import SafeHTMLElement, { 8 | SafeHTMLCollection, 9 | SafeNodeList 10 | } from "../helpers/safeHTMLElement"; 11 | import portalOpenInstances from "../helpers/portalOpenInstances"; 12 | import "../helpers/bodyTrap"; 13 | 14 | // so that our CSS is statically analyzable 15 | const CLASS_NAMES = { 16 | overlay: "ReactModal__Overlay", 17 | content: "ReactModal__Content" 18 | }; 19 | 20 | /** 21 | * We need to support the deprecated `KeyboardEvent.keyCode` in addition to 22 | * `KeyboardEvent.code` for apps that still support IE11. Can be removed when 23 | * `react-modal` only supports React >18 (which dropped IE support). 24 | */ 25 | const isTabKey = event => event.code === "Tab" || event.keyCode === 9; 26 | const isEscKey = event => event.code === "Escape" || event.keyCode === 27; 27 | 28 | let ariaHiddenInstances = 0; 29 | 30 | export default class ModalPortal extends Component { 31 | static defaultProps = { 32 | style: { 33 | overlay: {}, 34 | content: {} 35 | }, 36 | defaultStyles: {} 37 | }; 38 | 39 | static propTypes = { 40 | isOpen: PropTypes.bool.isRequired, 41 | defaultStyles: PropTypes.shape({ 42 | content: PropTypes.object, 43 | overlay: PropTypes.object 44 | }), 45 | style: PropTypes.shape({ 46 | content: PropTypes.object, 47 | overlay: PropTypes.object 48 | }), 49 | className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), 50 | overlayClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), 51 | parentSelector: PropTypes.func, 52 | bodyOpenClassName: PropTypes.string, 53 | htmlOpenClassName: PropTypes.string, 54 | ariaHideApp: PropTypes.bool, 55 | appElement: PropTypes.oneOfType([ 56 | PropTypes.instanceOf(SafeHTMLElement), 57 | PropTypes.instanceOf(SafeHTMLCollection), 58 | PropTypes.instanceOf(SafeNodeList), 59 | PropTypes.arrayOf(PropTypes.instanceOf(SafeHTMLElement)) 60 | ]), 61 | onAfterOpen: PropTypes.func, 62 | onAfterClose: PropTypes.func, 63 | onRequestClose: PropTypes.func, 64 | closeTimeoutMS: PropTypes.number, 65 | shouldFocusAfterRender: PropTypes.bool, 66 | shouldCloseOnOverlayClick: PropTypes.bool, 67 | shouldReturnFocusAfterClose: PropTypes.bool, 68 | preventScroll: PropTypes.bool, 69 | role: PropTypes.string, 70 | contentLabel: PropTypes.string, 71 | aria: PropTypes.object, 72 | data: PropTypes.object, 73 | children: PropTypes.node, 74 | shouldCloseOnEsc: PropTypes.bool, 75 | overlayRef: PropTypes.func, 76 | contentRef: PropTypes.func, 77 | id: PropTypes.string, 78 | overlayElement: PropTypes.func, 79 | contentElement: PropTypes.func, 80 | testId: PropTypes.string 81 | }; 82 | 83 | constructor(props) { 84 | super(props); 85 | 86 | this.state = { 87 | afterOpen: false, 88 | beforeClose: false 89 | }; 90 | 91 | this.shouldClose = null; 92 | this.moveFromContentToOverlay = null; 93 | } 94 | 95 | componentDidMount() { 96 | if (this.props.isOpen) { 97 | this.open(); 98 | } 99 | } 100 | 101 | componentDidUpdate(prevProps, prevState) { 102 | if (process.env.NODE_ENV !== "production") { 103 | if (prevProps.bodyOpenClassName !== this.props.bodyOpenClassName) { 104 | // eslint-disable-next-line no-console 105 | console.warn( 106 | 'React-Modal: "bodyOpenClassName" prop has been modified. ' + 107 | "This may cause unexpected behavior when multiple modals are open." 108 | ); 109 | } 110 | if (prevProps.htmlOpenClassName !== this.props.htmlOpenClassName) { 111 | // eslint-disable-next-line no-console 112 | console.warn( 113 | 'React-Modal: "htmlOpenClassName" prop has been modified. ' + 114 | "This may cause unexpected behavior when multiple modals are open." 115 | ); 116 | } 117 | } 118 | 119 | if (this.props.isOpen && !prevProps.isOpen) { 120 | this.open(); 121 | } else if (!this.props.isOpen && prevProps.isOpen) { 122 | this.close(); 123 | } 124 | 125 | // Focus only needs to be set once when the modal is being opened 126 | if ( 127 | this.props.shouldFocusAfterRender && 128 | this.state.isOpen && 129 | !prevState.isOpen 130 | ) { 131 | this.focusContent(); 132 | } 133 | } 134 | 135 | componentWillUnmount() { 136 | if (this.state.isOpen) { 137 | this.afterClose(); 138 | } 139 | clearTimeout(this.closeTimer); 140 | cancelAnimationFrame(this.openAnimationFrame); 141 | } 142 | 143 | setOverlayRef = overlay => { 144 | this.overlay = overlay; 145 | this.props.overlayRef && this.props.overlayRef(overlay); 146 | }; 147 | 148 | setContentRef = content => { 149 | this.content = content; 150 | this.props.contentRef && this.props.contentRef(content); 151 | }; 152 | 153 | beforeOpen() { 154 | const { 155 | appElement, 156 | ariaHideApp, 157 | htmlOpenClassName, 158 | bodyOpenClassName, 159 | parentSelector 160 | } = this.props; 161 | 162 | const parentDocument = 163 | (parentSelector && parentSelector().ownerDocument) || document; 164 | 165 | // Add classes. 166 | bodyOpenClassName && classList.add(parentDocument.body, bodyOpenClassName); 167 | 168 | htmlOpenClassName && 169 | classList.add( 170 | parentDocument.getElementsByTagName("html")[0], 171 | htmlOpenClassName 172 | ); 173 | 174 | if (ariaHideApp) { 175 | ariaHiddenInstances += 1; 176 | ariaAppHider.hide(appElement); 177 | } 178 | 179 | portalOpenInstances.register(this); 180 | } 181 | 182 | afterClose = () => { 183 | const { 184 | appElement, 185 | ariaHideApp, 186 | htmlOpenClassName, 187 | bodyOpenClassName, 188 | parentSelector 189 | } = this.props; 190 | 191 | const parentDocument = 192 | (parentSelector && parentSelector().ownerDocument) || document; 193 | 194 | // Remove classes. 195 | bodyOpenClassName && 196 | classList.remove(parentDocument.body, bodyOpenClassName); 197 | 198 | htmlOpenClassName && 199 | classList.remove( 200 | parentDocument.getElementsByTagName("html")[0], 201 | htmlOpenClassName 202 | ); 203 | 204 | // Reset aria-hidden attribute if all modals have been removed 205 | if (ariaHideApp && ariaHiddenInstances > 0) { 206 | ariaHiddenInstances -= 1; 207 | 208 | if (ariaHiddenInstances === 0) { 209 | ariaAppHider.show(appElement); 210 | } 211 | } 212 | 213 | if (this.props.shouldFocusAfterRender) { 214 | if (this.props.shouldReturnFocusAfterClose) { 215 | focusManager.returnFocus(this.props.preventScroll); 216 | focusManager.teardownScopedFocus(); 217 | } else { 218 | focusManager.popWithoutFocus(); 219 | } 220 | } 221 | 222 | if (this.props.onAfterClose) { 223 | this.props.onAfterClose(); 224 | } 225 | 226 | portalOpenInstances.deregister(this); 227 | }; 228 | 229 | open = () => { 230 | this.beforeOpen(); 231 | if (this.state.afterOpen && this.state.beforeClose) { 232 | clearTimeout(this.closeTimer); 233 | this.setState({ beforeClose: false }); 234 | } else { 235 | if (this.props.shouldFocusAfterRender) { 236 | focusManager.setupScopedFocus(this.node); 237 | focusManager.markForFocusLater(); 238 | } 239 | 240 | this.setState({ isOpen: true }, () => { 241 | this.openAnimationFrame = requestAnimationFrame(() => { 242 | this.setState({ afterOpen: true }); 243 | 244 | if (this.props.isOpen && this.props.onAfterOpen) { 245 | this.props.onAfterOpen({ 246 | overlayEl: this.overlay, 247 | contentEl: this.content 248 | }); 249 | } 250 | }); 251 | }); 252 | } 253 | }; 254 | 255 | close = () => { 256 | if (this.props.closeTimeoutMS > 0) { 257 | this.closeWithTimeout(); 258 | } else { 259 | this.closeWithoutTimeout(); 260 | } 261 | }; 262 | 263 | // Don't steal focus from inner elements 264 | focusContent = () => 265 | this.content && 266 | !this.contentHasFocus() && 267 | this.content.focus({ preventScroll: true }); 268 | 269 | closeWithTimeout = () => { 270 | const closesAt = Date.now() + this.props.closeTimeoutMS; 271 | this.setState({ beforeClose: true, closesAt }, () => { 272 | this.closeTimer = setTimeout( 273 | this.closeWithoutTimeout, 274 | this.state.closesAt - Date.now() 275 | ); 276 | }); 277 | }; 278 | 279 | closeWithoutTimeout = () => { 280 | this.setState( 281 | { 282 | beforeClose: false, 283 | isOpen: false, 284 | afterOpen: false, 285 | closesAt: null 286 | }, 287 | this.afterClose 288 | ); 289 | }; 290 | 291 | handleKeyDown = event => { 292 | if (isTabKey(event)) { 293 | scopeTab(this.content, event); 294 | } 295 | 296 | if (this.props.shouldCloseOnEsc && isEscKey(event)) { 297 | event.stopPropagation(); 298 | this.requestClose(event); 299 | } 300 | }; 301 | 302 | handleOverlayOnClick = event => { 303 | if (this.shouldClose === null) { 304 | this.shouldClose = true; 305 | } 306 | 307 | if (this.shouldClose && this.props.shouldCloseOnOverlayClick) { 308 | if (this.ownerHandlesClose()) { 309 | this.requestClose(event); 310 | } else { 311 | this.focusContent(); 312 | } 313 | } 314 | this.shouldClose = null; 315 | }; 316 | 317 | handleContentOnMouseUp = () => { 318 | this.shouldClose = false; 319 | }; 320 | 321 | handleOverlayOnMouseDown = event => { 322 | if (!this.props.shouldCloseOnOverlayClick && event.target == this.overlay) { 323 | event.preventDefault(); 324 | } 325 | }; 326 | 327 | handleContentOnClick = () => { 328 | this.shouldClose = false; 329 | }; 330 | 331 | handleContentOnMouseDown = () => { 332 | this.shouldClose = false; 333 | }; 334 | 335 | requestClose = event => 336 | this.ownerHandlesClose() && this.props.onRequestClose(event); 337 | 338 | ownerHandlesClose = () => this.props.onRequestClose; 339 | 340 | shouldBeClosed = () => !this.state.isOpen && !this.state.beforeClose; 341 | 342 | contentHasFocus = () => 343 | document.activeElement === this.content || 344 | this.content.contains(document.activeElement); 345 | 346 | buildClassName = (which, additional) => { 347 | const classNames = 348 | typeof additional === "object" 349 | ? additional 350 | : { 351 | base: CLASS_NAMES[which], 352 | afterOpen: `${CLASS_NAMES[which]}--after-open`, 353 | beforeClose: `${CLASS_NAMES[which]}--before-close` 354 | }; 355 | let className = classNames.base; 356 | if (this.state.afterOpen) { 357 | className = `${className} ${classNames.afterOpen}`; 358 | } 359 | if (this.state.beforeClose) { 360 | className = `${className} ${classNames.beforeClose}`; 361 | } 362 | return typeof additional === "string" && additional 363 | ? `${className} ${additional}` 364 | : className; 365 | }; 366 | 367 | attributesFromObject = (prefix, items) => 368 | Object.keys(items).reduce((acc, name) => { 369 | acc[`${prefix}-${name}`] = items[name]; 370 | return acc; 371 | }, {}); 372 | 373 | render() { 374 | const { 375 | id, 376 | className, 377 | overlayClassName, 378 | defaultStyles, 379 | children 380 | } = this.props; 381 | const contentStyles = className ? {} : defaultStyles.content; 382 | const overlayStyles = overlayClassName ? {} : defaultStyles.overlay; 383 | 384 | if (this.shouldBeClosed()) { 385 | return null; 386 | } 387 | 388 | const overlayProps = { 389 | ref: this.setOverlayRef, 390 | className: this.buildClassName("overlay", overlayClassName), 391 | style: { ...overlayStyles, ...this.props.style.overlay }, 392 | onClick: this.handleOverlayOnClick, 393 | onMouseDown: this.handleOverlayOnMouseDown 394 | }; 395 | 396 | const contentProps = { 397 | id, 398 | ref: this.setContentRef, 399 | style: { ...contentStyles, ...this.props.style.content }, 400 | className: this.buildClassName("content", className), 401 | tabIndex: "-1", 402 | onKeyDown: this.handleKeyDown, 403 | onMouseDown: this.handleContentOnMouseDown, 404 | onMouseUp: this.handleContentOnMouseUp, 405 | onClick: this.handleContentOnClick, 406 | role: this.props.role, 407 | "aria-label": this.props.contentLabel, 408 | ...this.attributesFromObject("aria", { modal: true, ...this.props.aria }), 409 | ...this.attributesFromObject("data", this.props.data || {}), 410 | "data-testid": this.props.testId 411 | }; 412 | 413 | const contentElement = this.props.contentElement(contentProps, children); 414 | return this.props.overlayElement(overlayProps, contentElement); 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /src/helpers/ariaAppHider.js: -------------------------------------------------------------------------------- 1 | import warning from "warning"; 2 | import { canUseDOM } from "./safeHTMLElement"; 3 | 4 | let globalElement = null; 5 | 6 | /* eslint-disable no-console */ 7 | /* istanbul ignore next */ 8 | export function resetState() { 9 | if (globalElement) { 10 | if (globalElement.removeAttribute) { 11 | globalElement.removeAttribute("aria-hidden"); 12 | } else if (globalElement.length != null) { 13 | globalElement.forEach(element => element.removeAttribute("aria-hidden")); 14 | } else { 15 | document 16 | .querySelectorAll(globalElement) 17 | .forEach(element => element.removeAttribute("aria-hidden")); 18 | } 19 | } 20 | globalElement = null; 21 | } 22 | 23 | /* istanbul ignore next */ 24 | export function log() { 25 | if (process.env.NODE_ENV !== "production") { 26 | var check = globalElement || {}; 27 | console.log("ariaAppHider ----------"); 28 | console.log(check.nodeName, check.className, check.id); 29 | console.log("end ariaAppHider ----------"); 30 | } 31 | } 32 | /* eslint-enable no-console */ 33 | 34 | export function assertNodeList(nodeList, selector) { 35 | if (!nodeList || !nodeList.length) { 36 | throw new Error( 37 | `react-modal: No elements were found for selector ${selector}.` 38 | ); 39 | } 40 | } 41 | 42 | export function setElement(element) { 43 | let useElement = element; 44 | if (typeof useElement === "string" && canUseDOM) { 45 | const el = document.querySelectorAll(useElement); 46 | assertNodeList(el, useElement); 47 | useElement = el; 48 | } 49 | globalElement = useElement || globalElement; 50 | return globalElement; 51 | } 52 | 53 | export function validateElement(appElement) { 54 | const el = appElement || globalElement; 55 | if (el) { 56 | return Array.isArray(el) || 57 | el instanceof HTMLCollection || 58 | el instanceof NodeList 59 | ? el 60 | : [el]; 61 | } else { 62 | warning( 63 | false, 64 | [ 65 | "react-modal: App element is not defined.", 66 | "Please use `Modal.setAppElement(el)` or set `appElement={el}`.", 67 | "This is needed so screen readers don't see main content", 68 | "when modal is opened. It is not recommended, but you can opt-out", 69 | "by setting `ariaHideApp={false}`." 70 | ].join(" ") 71 | ); 72 | 73 | return []; 74 | } 75 | } 76 | 77 | export function hide(appElement) { 78 | for (let el of validateElement(appElement)) { 79 | el.setAttribute("aria-hidden", "true"); 80 | } 81 | } 82 | 83 | export function show(appElement) { 84 | for (let el of validateElement(appElement)) { 85 | el.removeAttribute("aria-hidden"); 86 | } 87 | } 88 | 89 | export function documentNotReadyOrSSRTesting() { 90 | globalElement = null; 91 | } 92 | -------------------------------------------------------------------------------- /src/helpers/bodyTrap.js: -------------------------------------------------------------------------------- 1 | import portalOpenInstances from "./portalOpenInstances"; 2 | // Body focus trap see Issue #742 3 | 4 | let before, 5 | after, 6 | instances = []; 7 | 8 | /* eslint-disable no-console */ 9 | /* istanbul ignore next */ 10 | export function resetState() { 11 | for (let item of [before, after]) { 12 | if (!item) continue; 13 | item.parentNode && item.parentNode.removeChild(item); 14 | } 15 | before = after = null; 16 | instances = []; 17 | } 18 | 19 | /* istanbul ignore next */ 20 | export function log() { 21 | console.log("bodyTrap ----------"); 22 | console.log(instances.length); 23 | for (let item of [before, after]) { 24 | let check = item || {}; 25 | console.log(check.nodeName, check.className, check.id); 26 | } 27 | console.log("edn bodyTrap ----------"); 28 | } 29 | /* eslint-enable no-console */ 30 | 31 | function focusContent() { 32 | if (instances.length === 0) { 33 | if (process.env.NODE_ENV !== "production") { 34 | // eslint-disable-next-line no-console 35 | console.warn(`React-Modal: Open instances > 0 expected`); 36 | } 37 | return; 38 | } 39 | instances[instances.length - 1].focusContent(); 40 | } 41 | 42 | function bodyTrap(eventType, openInstances) { 43 | if (!before && !after) { 44 | before = document.createElement("div"); 45 | before.setAttribute("data-react-modal-body-trap", ""); 46 | before.style.position = "absolute"; 47 | before.style.opacity = "0"; 48 | before.setAttribute("tabindex", "0"); 49 | before.addEventListener("focus", focusContent); 50 | after = before.cloneNode(); 51 | after.addEventListener("focus", focusContent); 52 | } 53 | 54 | instances = openInstances; 55 | 56 | if (instances.length > 0) { 57 | // Add focus trap 58 | if (document.body.firstChild !== before) { 59 | document.body.insertBefore(before, document.body.firstChild); 60 | } 61 | if (document.body.lastChild !== after) { 62 | document.body.appendChild(after); 63 | } 64 | } else { 65 | // Remove focus trap 66 | if (before.parentElement) { 67 | before.parentElement.removeChild(before); 68 | } 69 | if (after.parentElement) { 70 | after.parentElement.removeChild(after); 71 | } 72 | } 73 | } 74 | 75 | portalOpenInstances.subscribe(bodyTrap); 76 | -------------------------------------------------------------------------------- /src/helpers/classList.js: -------------------------------------------------------------------------------- 1 | let htmlClassList = {}; 2 | let docBodyClassList = {}; 3 | 4 | /* eslint-disable no-console */ 5 | /* istanbul ignore next */ 6 | function removeClass(at, cls) { 7 | at.classList.remove(cls); 8 | } 9 | 10 | /* istanbul ignore next */ 11 | export function resetState() { 12 | const htmlElement = document.getElementsByTagName("html")[0]; 13 | for (let cls in htmlClassList) { 14 | removeClass(htmlElement, htmlClassList[cls]); 15 | } 16 | 17 | const body = document.body; 18 | for (let cls in docBodyClassList) { 19 | removeClass(body, docBodyClassList[cls]); 20 | } 21 | 22 | htmlClassList = {}; 23 | docBodyClassList = {}; 24 | } 25 | 26 | /* istanbul ignore next */ 27 | export function log() { 28 | if (process.env.NODE_ENV !== "production") { 29 | let classes = document.getElementsByTagName("html")[0].className; 30 | let buffer = "Show tracked classes:\n\n"; 31 | 32 | buffer += ` (${classes}): 33 | `; 34 | for (let x in htmlClassList) { 35 | buffer += ` ${x} ${htmlClassList[x]} 36 | `; 37 | } 38 | 39 | classes = document.body.className; 40 | 41 | buffer += `\n\ndoc.body (${classes}): 42 | `; 43 | for (let x in docBodyClassList) { 44 | buffer += ` ${x} ${docBodyClassList[x]} 45 | `; 46 | } 47 | 48 | buffer += "\n"; 49 | 50 | console.log(buffer); 51 | } 52 | } 53 | /* eslint-enable no-console */ 54 | 55 | /** 56 | * Track the number of reference of a class. 57 | * @param {object} poll The poll to receive the reference. 58 | * @param {string} className The class name. 59 | * @return {string} 60 | */ 61 | const incrementReference = (poll, className) => { 62 | if (!poll[className]) { 63 | poll[className] = 0; 64 | } 65 | poll[className] += 1; 66 | return className; 67 | }; 68 | 69 | /** 70 | * Drop the reference of a class. 71 | * @param {object} poll The poll to receive the reference. 72 | * @param {string} className The class name. 73 | * @return {string} 74 | */ 75 | const decrementReference = (poll, className) => { 76 | if (poll[className]) { 77 | poll[className] -= 1; 78 | } 79 | return className; 80 | }; 81 | 82 | /** 83 | * Track a class and add to the given class list. 84 | * @param {Object} classListRef A class list of an element. 85 | * @param {Object} poll The poll to be used. 86 | * @param {Array} classes The list of classes to be tracked. 87 | */ 88 | const trackClass = (classListRef, poll, classes) => { 89 | classes.forEach(className => { 90 | incrementReference(poll, className); 91 | classListRef.add(className); 92 | }); 93 | }; 94 | 95 | /** 96 | * Untrack a class and remove from the given class list if the reference 97 | * reaches 0. 98 | * @param {Object} classListRef A class list of an element. 99 | * @param {Object} poll The poll to be used. 100 | * @param {Array} classes The list of classes to be untracked. 101 | */ 102 | const untrackClass = (classListRef, poll, classes) => { 103 | classes.forEach(className => { 104 | decrementReference(poll, className); 105 | poll[className] === 0 && classListRef.remove(className); 106 | }); 107 | }; 108 | 109 | /** 110 | * Public inferface to add classes to the document.body. 111 | * @param {string} bodyClass The class string to be added. 112 | * It may contain more then one class 113 | * with ' ' as separator. 114 | */ 115 | export const add = (element, classString) => 116 | trackClass( 117 | element.classList, 118 | element.nodeName.toLowerCase() == "html" ? htmlClassList : docBodyClassList, 119 | classString.split(" ") 120 | ); 121 | 122 | /** 123 | * Public inferface to remove classes from the document.body. 124 | * @param {string} bodyClass The class string to be added. 125 | * It may contain more then one class 126 | * with ' ' as separator. 127 | */ 128 | export const remove = (element, classString) => 129 | untrackClass( 130 | element.classList, 131 | element.nodeName.toLowerCase() == "html" ? htmlClassList : docBodyClassList, 132 | classString.split(" ") 133 | ); 134 | -------------------------------------------------------------------------------- /src/helpers/focusManager.js: -------------------------------------------------------------------------------- 1 | import findTabbable from "../helpers/tabbable"; 2 | 3 | let focusLaterElements = []; 4 | let modalElement = null; 5 | let needToFocus = false; 6 | 7 | /* eslint-disable no-console */ 8 | /* istanbul ignore next */ 9 | export function resetState() { 10 | focusLaterElements = []; 11 | } 12 | 13 | /* istanbul ignore next */ 14 | export function log() { 15 | if (process.env.NODE_ENV !== "production") { 16 | console.log("focusManager ----------"); 17 | focusLaterElements.forEach(f => { 18 | const check = f || {}; 19 | console.log(check.nodeName, check.className, check.id); 20 | }); 21 | console.log("end focusManager ----------"); 22 | } 23 | } 24 | /* eslint-enable no-console */ 25 | 26 | export function handleBlur() { 27 | needToFocus = true; 28 | } 29 | 30 | export function handleFocus() { 31 | if (needToFocus) { 32 | needToFocus = false; 33 | if (!modalElement) { 34 | return; 35 | } 36 | // need to see how jQuery shims document.on('focusin') so we don't need the 37 | // setTimeout, firefox doesn't support focusin, if it did, we could focus 38 | // the element outside of a setTimeout. Side-effect of this implementation 39 | // is that the document.body gets focus, and then we focus our element right 40 | // after, seems fine. 41 | setTimeout(() => { 42 | if (modalElement.contains(document.activeElement)) { 43 | return; 44 | } 45 | const el = findTabbable(modalElement)[0] || modalElement; 46 | el.focus(); 47 | }, 0); 48 | } 49 | } 50 | 51 | export function markForFocusLater() { 52 | focusLaterElements.push(document.activeElement); 53 | } 54 | 55 | /* eslint-disable no-console */ 56 | export function returnFocus(preventScroll = false) { 57 | let toFocus = null; 58 | try { 59 | if (focusLaterElements.length !== 0) { 60 | toFocus = focusLaterElements.pop(); 61 | toFocus.focus({ preventScroll }); 62 | } 63 | return; 64 | } catch (e) { 65 | console.warn( 66 | [ 67 | "You tried to return focus to", 68 | toFocus, 69 | "but it is not in the DOM anymore" 70 | ].join(" ") 71 | ); 72 | } 73 | } 74 | /* eslint-enable no-console */ 75 | 76 | export function popWithoutFocus() { 77 | focusLaterElements.length > 0 && focusLaterElements.pop(); 78 | } 79 | 80 | export function setupScopedFocus(element) { 81 | modalElement = element; 82 | 83 | if (window.addEventListener) { 84 | window.addEventListener("blur", handleBlur, false); 85 | document.addEventListener("focus", handleFocus, true); 86 | } else { 87 | window.attachEvent("onBlur", handleBlur); 88 | document.attachEvent("onFocus", handleFocus); 89 | } 90 | } 91 | 92 | export function teardownScopedFocus() { 93 | modalElement = null; 94 | 95 | if (window.addEventListener) { 96 | window.removeEventListener("blur", handleBlur); 97 | document.removeEventListener("focus", handleFocus); 98 | } else { 99 | window.detachEvent("onBlur", handleBlur); 100 | document.detachEvent("onFocus", handleFocus); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/helpers/portalOpenInstances.js: -------------------------------------------------------------------------------- 1 | // Tracks portals that are open and emits events to subscribers 2 | 3 | class PortalOpenInstances { 4 | constructor() { 5 | this.openInstances = []; 6 | this.subscribers = []; 7 | } 8 | 9 | register = openInstance => { 10 | if (this.openInstances.indexOf(openInstance) !== -1) { 11 | if (process.env.NODE_ENV !== "production") { 12 | // eslint-disable-next-line no-console 13 | console.warn( 14 | `React-Modal: Cannot register modal instance that's already open` 15 | ); 16 | } 17 | return; 18 | } 19 | this.openInstances.push(openInstance); 20 | this.emit("register"); 21 | }; 22 | 23 | deregister = openInstance => { 24 | const index = this.openInstances.indexOf(openInstance); 25 | if (index === -1) { 26 | if (process.env.NODE_ENV !== "production") { 27 | // eslint-disable-next-line no-console 28 | console.warn( 29 | `React-Modal: Unable to deregister ${openInstance} as ` + 30 | `it was never registered` 31 | ); 32 | } 33 | return; 34 | } 35 | this.openInstances.splice(index, 1); 36 | this.emit("deregister"); 37 | }; 38 | 39 | subscribe = callback => { 40 | this.subscribers.push(callback); 41 | }; 42 | 43 | emit = eventType => { 44 | this.subscribers.forEach(subscriber => 45 | subscriber( 46 | eventType, 47 | // shallow copy to avoid accidental mutation 48 | this.openInstances.slice() 49 | ) 50 | ); 51 | }; 52 | } 53 | 54 | let portalOpenInstances = new PortalOpenInstances(); 55 | 56 | /* eslint-disable no-console */ 57 | /* istanbul ignore next */ 58 | export function log() { 59 | console.log("portalOpenInstances ----------"); 60 | console.log(portalOpenInstances.openInstances.length); 61 | portalOpenInstances.openInstances.forEach(p => console.log(p)); 62 | console.log("end portalOpenInstances ----------"); 63 | } 64 | 65 | /* istanbul ignore next */ 66 | export function resetState() { 67 | portalOpenInstances = new PortalOpenInstances(); 68 | } 69 | /* eslint-enable no-console */ 70 | 71 | export default portalOpenInstances; 72 | -------------------------------------------------------------------------------- /src/helpers/safeHTMLElement.js: -------------------------------------------------------------------------------- 1 | import ExecutionEnvironment from "exenv"; 2 | 3 | const EE = ExecutionEnvironment; 4 | 5 | const SafeHTMLElement = EE.canUseDOM ? window.HTMLElement : {}; 6 | 7 | export const SafeHTMLCollection = EE.canUseDOM ? window.HTMLCollection : {}; 8 | 9 | export const SafeNodeList = EE.canUseDOM ? window.NodeList : {}; 10 | 11 | export const canUseDOM = EE.canUseDOM; 12 | 13 | export default SafeHTMLElement; 14 | -------------------------------------------------------------------------------- /src/helpers/scopeTab.js: -------------------------------------------------------------------------------- 1 | import findTabbable from "./tabbable"; 2 | 3 | function getActiveElement(el = document) { 4 | return el.activeElement.shadowRoot 5 | ? getActiveElement(el.activeElement.shadowRoot) 6 | : el.activeElement; 7 | } 8 | 9 | export default function scopeTab(node, event) { 10 | const tabbable = findTabbable(node); 11 | 12 | if (!tabbable.length) { 13 | // Do nothing, since there are no elements that can receive focus. 14 | event.preventDefault(); 15 | return; 16 | } 17 | 18 | let target; 19 | 20 | const shiftKey = event.shiftKey; 21 | const head = tabbable[0]; 22 | const tail = tabbable[tabbable.length - 1]; 23 | const activeElement = getActiveElement(); 24 | 25 | // proceed with default browser behavior on tab. 26 | // Focus on last element on shift + tab. 27 | if (node === activeElement) { 28 | if (!shiftKey) return; 29 | target = tail; 30 | } 31 | 32 | if (tail === activeElement && !shiftKey) { 33 | target = head; 34 | } 35 | 36 | if (head === activeElement && shiftKey) { 37 | target = tail; 38 | } 39 | 40 | if (target) { 41 | event.preventDefault(); 42 | target.focus(); 43 | return; 44 | } 45 | 46 | // Safari radio issue. 47 | // 48 | // Safari does not move the focus to the radio button, 49 | // so we need to force it to really walk through all elements. 50 | // 51 | // This is very error prone, since we are trying to guess 52 | // if it is a safari browser from the first occurence between 53 | // chrome or safari. 54 | // 55 | // The chrome user agent contains the first ocurrence 56 | // as the 'chrome/version' and later the 'safari/version'. 57 | const checkSafari = /(\bChrome\b|\bSafari\b)\//.exec(navigator.userAgent); 58 | const isSafariDesktop = 59 | checkSafari != null && 60 | checkSafari[1] != "Chrome" && 61 | /\biPod\b|\biPad\b/g.exec(navigator.userAgent) == null; 62 | 63 | // If we are not in safari desktop, let the browser control 64 | // the focus 65 | if (!isSafariDesktop) return; 66 | 67 | var x = tabbable.indexOf(activeElement); 68 | 69 | if (x > -1) { 70 | x += shiftKey ? -1 : 1; 71 | } 72 | 73 | target = tabbable[x]; 74 | 75 | // If the tabbable element does not exist, 76 | // focus head/tail based on shiftKey 77 | if (typeof target === "undefined") { 78 | event.preventDefault(); 79 | target = shiftKey ? tail : head; 80 | target.focus(); 81 | return; 82 | } 83 | 84 | event.preventDefault(); 85 | 86 | target.focus(); 87 | } 88 | -------------------------------------------------------------------------------- /src/helpers/tabbable.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Adapted from jQuery UI core 3 | * 4 | * http://jqueryui.com 5 | * 6 | * Copyright 2014 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | * 10 | * http://api.jqueryui.com/category/ui-core/ 11 | */ 12 | 13 | const DISPLAY_NONE = "none"; 14 | const DISPLAY_CONTENTS = "contents"; 15 | 16 | // match the whole word to prevent fuzzy searching 17 | const tabbableNode = /^(input|select|textarea|button|object|iframe)$/; 18 | 19 | function isNotOverflowing(element, style) { 20 | return ( 21 | style.getPropertyValue("overflow") !== "visible" || 22 | // if 'overflow: visible' set, check if there is actually any overflow 23 | (element.scrollWidth <= 0 && element.scrollHeight <= 0) 24 | ); 25 | } 26 | 27 | function hidesContents(element) { 28 | const zeroSize = element.offsetWidth <= 0 && element.offsetHeight <= 0; 29 | 30 | // If the node is empty, this is good enough 31 | if (zeroSize && !element.innerHTML) return true; 32 | 33 | try { 34 | // Otherwise we need to check some styles 35 | const style = window.getComputedStyle(element); 36 | const displayValue = style.getPropertyValue("display"); 37 | return zeroSize 38 | ? displayValue !== DISPLAY_CONTENTS && isNotOverflowing(element, style) 39 | : displayValue === DISPLAY_NONE; 40 | } catch (exception) { 41 | // eslint-disable-next-line no-console 42 | console.warn("Failed to inspect element style"); 43 | return false; 44 | } 45 | } 46 | 47 | function visible(element) { 48 | let parentElement = element; 49 | let rootNode = element.getRootNode && element.getRootNode(); 50 | while (parentElement) { 51 | if (parentElement === document.body) break; 52 | 53 | // if we are not hidden yet, skip to checking outside the Web Component 54 | if (rootNode && parentElement === rootNode) 55 | parentElement = rootNode.host.parentNode; 56 | 57 | if (hidesContents(parentElement)) return false; 58 | parentElement = parentElement.parentNode; 59 | } 60 | return true; 61 | } 62 | 63 | function focusable(element, isTabIndexNotNaN) { 64 | const nodeName = element.nodeName.toLowerCase(); 65 | const res = 66 | (tabbableNode.test(nodeName) && !element.disabled) || 67 | (nodeName === "a" ? element.href || isTabIndexNotNaN : isTabIndexNotNaN); 68 | return res && visible(element); 69 | } 70 | 71 | function tabbable(element) { 72 | let tabIndex = element.getAttribute("tabindex"); 73 | if (tabIndex === null) tabIndex = undefined; 74 | const isTabIndexNaN = isNaN(tabIndex); 75 | return (isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN); 76 | } 77 | 78 | export default function findTabbableDescendants(element) { 79 | const descendants = [].slice 80 | .call(element.querySelectorAll("*"), 0) 81 | .reduce( 82 | (finished, el) => 83 | finished.concat( 84 | !el.shadowRoot ? [el] : findTabbableDescendants(el.shadowRoot) 85 | ), 86 | [] 87 | ); 88 | return descendants.filter(tabbable); 89 | } 90 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Modal from "./components/Modal"; 2 | 3 | export default Modal; 4 | --------------------------------------------------------------------------------