├── .babelrc.js ├── .eslintignore ├── .eslintrc.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .size-snapshot.json ├── .storybook ├── main.js └── preview.js ├── CHANGELOG.md ├── LICENSE ├── Migration.md ├── README.md ├── package.json ├── prettier.config.js ├── rollup.config.js ├── src ├── CSSTransition.js ├── ReplaceTransition.js ├── SwitchTransition.js ├── Transition.js ├── TransitionGroup.js ├── TransitionGroupContext.js ├── config.js ├── index.js └── utils │ ├── ChildMapping.js │ ├── PropTypes.js │ ├── SimpleSet.js │ └── reflow.js ├── stories ├── .eslintrc.yml ├── CSSTransition.js ├── CSSTransitionGroupFixture.js ├── NestedTransition.js ├── ReplaceTransition.js ├── StoryFixture.js ├── Transition.js ├── TransitionGroup.js ├── index.js └── transitions │ ├── Bootstrap.js │ ├── CSSFade.js │ ├── CSSFadeForTransitionGroup.js │ └── Scale.js ├── test ├── .eslintrc.yml ├── CSSTransition-test.js ├── CSSTransitionGroup-test.js ├── ChildMapping-test.js ├── SSR-test.js ├── SwitchTransition-test.js ├── Transition-test.js ├── TransitionGroup-test.js ├── setup.js ├── setupAfterEnv.js └── utils.js ├── www ├── .babelrc.js ├── .gitignore ├── .npmrc ├── gatsby-config.js ├── gatsby-node.js ├── package.json ├── src │ ├── components │ │ ├── Example.js │ │ └── Layout.js │ ├── css │ │ ├── _variables.scss │ │ ├── bootstrap.scss │ │ └── prism-theme.scss │ ├── pages │ │ ├── index.js │ │ ├── testing.js │ │ └── with-react-router.js │ └── templates │ │ └── component.js └── yarn.lock └── yarn.lock /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['babel-preset-jason', { runtime: false }]], 3 | plugins: [ 4 | ['babel-plugin-transform-react-remove-prop-types', { mode: 'wrap' }], 5 | ], 6 | env: { 7 | esm: { 8 | presets: [['babel-preset-jason', { modules: false }]], 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | www/.cache/ 3 | www/public/ 4 | lib 5 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parser: babel-eslint 2 | extends: 3 | - jason/react 4 | - plugin:jsx-a11y/recommended 5 | - prettier 6 | settings: 7 | react: 8 | version: detect 9 | env: 10 | node: true 11 | browser: true 12 | plugins: 13 | - jsx-a11y 14 | 15 | overrides: 16 | - files: www/**/* 17 | env: 18 | es6: true 19 | - files: stories/**/* 20 | rules: 21 | no-console: off 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Something isn't working as expected. 4 | --- 5 | 6 | > What is the current behavior? 7 | 8 | 9 | 10 | > What is the expected behavior? 11 | 12 | 13 | 14 | 15 | 16 | > Could you provide a [CodeSandbox](https://codesandbox.io/) demo reproducing the bug? 17 | 18 | 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: I have a suggestion on how to improve the library. 4 | --- 5 | 6 | > What would you like improved? 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master, alpha] 6 | pull_request: 7 | branches: [master, alpha] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | # Otherwise how would we know if a specific React version caused the failure? 15 | fail-fast: false 16 | matrix: 17 | REACT_DIST: [16, 17, 18] 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js 14 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: 14 24 | cache: 'npm' 25 | - run: yarn 26 | - run: yarn add react@${{ matrix.REACT_DIST }} react-dom@${{ matrix.REACT_DIST }} 27 | - run: yarn add @testing-library/react@12 28 | if: matrix.REACT_DIST == '17' || matrix.REACT_DIST == '16' 29 | - run: yarn --cwd www 30 | # Test whether the web page can be built successfully or not 31 | - run: yarn --cwd www build 32 | - run: yarn test 33 | - name: Dry release 34 | uses: cycjimmy/semantic-release-action@v2 35 | with: 36 | dry_run: true 37 | semantic_version: 17 38 | branches: | 39 | [ 40 | 'master', 41 | {name: 'alpha', prerelease: true} 42 | ] 43 | env: 44 | # These are not available on forks but we need them on actual runs to verify everything is set up. 45 | # Otherwise we might fail in the middle of a release 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 48 | 49 | release: 50 | needs: test 51 | runs-on: ubuntu-latest 52 | if: ${{ github.repository == 'reactjs/react-transition-group' && 53 | contains('refs/heads/master,refs/heads/alpha', 54 | github.ref) && github.event_name == 'push' }} 55 | steps: 56 | - uses: styfle/cancel-workflow-action@0.9.0 57 | - uses: actions/checkout@v2 58 | - name: Use Node.js 14 59 | uses: actions/setup-node@v2 60 | with: 61 | node-version: 14 62 | cache: 'npm' 63 | - run: yarn 64 | - run: yarn build 65 | - name: Release 66 | uses: cycjimmy/semantic-release-action@v2 67 | with: 68 | semantic_version: 17 69 | branches: | 70 | [ 71 | 'master', 72 | {name: 'alpha', prerelease: true} 73 | ] 74 | env: 75 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 76 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lib 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | 41 | # Visual Studio Code configuration 42 | .vscode 43 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | www/.cache/ 3 | www/public/ 4 | lib 5 | *.md -------------------------------------------------------------------------------- /.size-snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "./lib/dist/react-transition-group.js": { 3 | "bundled": 82684, 4 | "minified": 22426, 5 | "gzipped": 6876 6 | }, 7 | "./lib/dist/react-transition-group.min.js": { 8 | "bundled": 47269, 9 | "minified": 14623, 10 | "gzipped": 4616 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const { plugins, rules } = require('webpack-atoms'); 2 | 3 | module.exports = { 4 | stories: ['../stories/index.js'], 5 | webpackFinal: (config) => { 6 | config.module = { 7 | rules: [rules.js(), rules.astroturf(), rules.css({ extract: false })], 8 | }; 9 | 10 | config.plugins.push(plugins.extractCss({ disable: true })); 11 | 12 | return config; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const decorators = [ 4 | (Story) => ( 5 | 6 | 7 | 8 | ), 9 | ]; 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [4.4.5](https://github.com/reactjs/react-transition-group/compare/v4.4.4...v4.4.5) (2022-08-01) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * apply entering animation synchronously when unmountOnExit or mountOnEnter is enabled ([#847](https://github.com/reactjs/react-transition-group/issues/847)) ([1043549](https://github.com/reactjs/react-transition-group/commit/10435492f5a5675b0e80ca6a435834ce4a0f270e)) 7 | 8 | ## [4.4.4](https://github.com/reactjs/react-transition-group/compare/v4.4.3...v4.4.4) (2022-07-30) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * missing build files ([#845](https://github.com/reactjs/react-transition-group/issues/845)) ([97af789](https://github.com/reactjs/react-transition-group/commit/97af7893b0a5bbf69211bc3287aee814123ddeea)) 14 | 15 | ## [4.4.3](https://github.com/reactjs/react-transition-group/compare/v4.4.2...v4.4.3) (2022-07-30) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * enter animations with mountOnEnter or unmountOnExit ([#749](https://github.com/reactjs/react-transition-group/issues/749)) ([51bdceb](https://github.com/reactjs/react-transition-group/commit/51bdceb96c8b6a79f417c32326ef1b31160edb97)) 21 | 22 | ## [4.4.2](https://github.com/reactjs/react-transition-group/compare/v4.4.1...v4.4.2) (2021-05-29) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * `nodeRef` prop type for cross-realm elements ([#732](https://github.com/reactjs/react-transition-group/issues/732)) ([8710c01](https://github.com/reactjs/react-transition-group/commit/8710c01549e09f55eeefec2aadb3af0a23a00f82)) 28 | 29 | ## [4.4.1](https://github.com/reactjs/react-transition-group/compare/v4.4.0...v4.4.1) (2020-05-06) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * transition SSR ([#619](https://github.com/reactjs/react-transition-group/issues/619)) ([2722bb6](https://github.com/reactjs/react-transition-group/commit/2722bb6b755943b8292f0f2bc2fdca55df5c28f0)) 35 | 36 | # [4.4.0](https://github.com/reactjs/react-transition-group/compare/v4.3.0...v4.4.0) (2020-05-05) 37 | 38 | 39 | ### Features 40 | 41 | * add `nodeRef` alternative instead of internal `findDOMNode` ([#559](https://github.com/reactjs/react-transition-group/issues/559)) ([85016bf](https://github.com/reactjs/react-transition-group/commit/85016bfddd3831e6d7bb27926f9f178d25502913)) 42 | - react-transition-group internally uses `findDOMNode`, which is deprecated and produces warnings in [Strict Mode](https://reactjs.org/docs/strict-mode.html), so now you can optionally pass `nodeRef` to `Transition` and `CSSTransition`, it's a ref object that should point to the transitioning child: 43 | 44 | ```jsx 45 | import React from "react" 46 | import { CSSTransition } from "react-transition-group" 47 | 48 | const MyComponent = () => { 49 | const nodeRef = React.useRef(null) 50 | return ( 51 | 52 |
Fade
53 |
54 | ) 55 | } 56 | ``` 57 | ### Bug Fixes 58 | 59 | * set the values of constants attached to `Transition` to match the exported ones ([#554](https://github.com/reactjs/react-transition-group/pull/554)) 60 | 61 | # [4.3.0](https://github.com/reactjs/react-transition-group/compare/v4.2.2...v4.3.0) (2019-09-05) 62 | 63 | 64 | ### Features 65 | 66 | * upgrade dom-helpers ([#549](https://github.com/reactjs/react-transition-group/issues/549)) ([b017e18](https://github.com/reactjs/react-transition-group/commit/b017e18)) 67 | 68 | ## [4.2.2](https://github.com/reactjs/react-transition-group/compare/v4.2.1...v4.2.2) (2019-08-02) 69 | 70 | 71 | ### Bug Fixes 72 | 73 | * Fix imports to play nicely with rollup ([#530](https://github.com/reactjs/react-transition-group/issues/530)) ([3d9003e](https://github.com/reactjs/react-transition-group/commit/3d9003e)) 74 | 75 | ## [4.2.1](https://github.com/reactjs/react-transition-group/compare/v4.2.0...v4.2.1) (2019-07-02) 76 | 77 | 78 | ### Bug Fixes 79 | 80 | * updated SwitchTransition component to be default export and exported from index.js ([#516](https://github.com/reactjs/react-transition-group/issues/516)) ([cfd0070](https://github.com/reactjs/react-transition-group/commit/cfd0070)) 81 | 82 | # [4.2.0](https://github.com/reactjs/react-transition-group/compare/v4.1.1...v4.2.0) (2019-06-28) 83 | 84 | 85 | ### Features 86 | 87 | * add SwitchTransition component ([#470](https://github.com/reactjs/react-transition-group/issues/470)) ([c5e379d](https://github.com/reactjs/react-transition-group/commit/c5e379d)) 88 | 89 | ## [4.1.1](https://github.com/reactjs/react-transition-group/compare/v4.1.0...v4.1.1) (2019-06-10) 90 | 91 | 92 | ### Bug Fixes 93 | 94 | * adds missing dependency [@babel](https://github.com/babel)/runtime ([#507](https://github.com/reactjs/react-transition-group/issues/507)) ([228bf5f](https://github.com/reactjs/react-transition-group/commit/228bf5f)) 95 | 96 | # [4.1.0](https://github.com/reactjs/react-transition-group/compare/v4.0.1...v4.1.0) (2019-05-30) 97 | 98 | 99 | ### Features 100 | 101 | * add global transition disable switch ([#506](https://github.com/reactjs/react-transition-group/issues/506)) ([4c5ba98](https://github.com/reactjs/react-transition-group/commit/4c5ba98)) 102 | 103 | ## [4.0.1](https://github.com/reactjs/react-transition-group/compare/v4.0.0...v4.0.1) (2019-05-09) 104 | 105 | 106 | ### Bug Fixes 107 | 108 | * issue with dynamically applied classes not being properly removed for reentering items ([#499](https://github.com/reactjs/react-transition-group/issues/499)) ([129cb11](https://github.com/reactjs/react-transition-group/commit/129cb11)) 109 | 110 | # [4.0.0](https://github.com/reactjs/react-transition-group/compare/v3.0.0...v4.0.0) (2019-04-16) 111 | 112 | 113 | ### Features 114 | 115 | * support esm via package.json routes ([#488](https://github.com/reactjs/react-transition-group/issues/488)) ([6337bf5](https://github.com/reactjs/react-transition-group/commit/6337bf5)), closes [#77](https://github.com/reactjs/react-transition-group/issues/77) 116 | 117 | 118 | ### BREAKING CHANGES 119 | 120 | * in environments where esm is supported importing from commonjs requires explicitly adding the `.default` after `require()` when resolving to the esm build 121 | 122 | # [3.0.0](https://github.com/reactjs/react-transition-group/compare/v2.9.0...v3.0.0) (2019-04-15) 123 | 124 | 125 | ### Features 126 | 127 | * use stable context API ([#471](https://github.com/reactjs/react-transition-group/issues/471)) ([aee4901](https://github.com/reactjs/react-transition-group/commit/aee4901)), closes [#429](https://github.com/reactjs/react-transition-group/issues/429) 128 | 129 | 130 | ### BREAKING CHANGES 131 | 132 | * use new style react context 133 | 134 | ```diff 135 | // package.json 136 | -"react": "^15.0.0", 137 | +"react": "^16.6.0", 138 | -"react-dom": "^15.0.0", 139 | +"react-dom": "^16.6.0", 140 | ``` 141 | 142 | # [2.9.0](https://github.com/reactjs/react-transition-group/compare/v2.8.0...v2.9.0) (2019-04-06) 143 | 144 | 145 | ### Features 146 | 147 | * **CSSTransition:** add "done" class for appear ([fe3c156](https://github.com/reactjs/react-transition-group/commit/fe3c156)), closes [#383](https://github.com/reactjs/react-transition-group/issues/383) [#327](https://github.com/reactjs/react-transition-group/issues/327) [#327](https://github.com/reactjs/react-transition-group/issues/327) 148 | 149 | 150 | ### Reverts 151 | 152 | * bump semantic release dependencies ([1bdcaec](https://github.com/reactjs/react-transition-group/commit/1bdcaec)) 153 | 154 | # [2.8.0](https://github.com/reactjs/react-transition-group/compare/v2.7.1...v2.8.0) (2019-04-02) 155 | 156 | 157 | ### Features 158 | 159 | * add support for empty classNames ([#481](https://github.com/reactjs/react-transition-group/issues/481)) ([d755dc6](https://github.com/reactjs/react-transition-group/commit/d755dc6)) 160 | 161 | ## [2.7.1](https://github.com/reactjs/react-transition-group/compare/v2.7.0...v2.7.1) (2019-03-25) 162 | 163 | 164 | ### Bug Fixes 165 | 166 | * revert tree-shaking support because it was a breaking change ([271364c](https://github.com/reactjs/react-transition-group/commit/271364c)) 167 | 168 | # [2.7.0](https://github.com/reactjs/react-transition-group/compare/v2.6.1...v2.7.0) (2019-03-22) 169 | 170 | 171 | ### Features 172 | 173 | * support ESM (tree-shaking) ([#455](https://github.com/reactjs/react-transition-group/issues/455)) ([ef3e357](https://github.com/reactjs/react-transition-group/commit/ef3e357)) 174 | 175 | ## [2.6.1](https://github.com/reactjs/react-transition-group/compare/v2.6.0...v2.6.1) (2019-03-14) 176 | 177 | 178 | ### Bug Fixes 179 | 180 | * **Transition:** make `exit` key optional when passing an object to the `timeout` prop ([#464](https://github.com/reactjs/react-transition-group/pull/464)) ([3a4cf9c](https://github.com/reactjs/react-transition-group/commit/3a4cf9c91ab5f25caaa9501b129bce66ec9bb56b)) 181 | * **package.json:** mark react-transition-group as side-effect free for webpack tree shaking ([#472](https://github.com/reactjs/react-transition-group/issues/472)) ([b81dc89](https://github.com/reactjs/react-transition-group/commit/b81dc89)) 182 | 183 | # [2.6.0](https://github.com/reactjs/react-transition-group/compare/v2.5.3...v2.6.0) (2019-02-26) 184 | 185 | 186 | ### Features 187 | 188 | * add appear timeout ([#462](https://github.com/reactjs/react-transition-group/issues/462)) ([52cdc34](https://github.com/reactjs/react-transition-group/commit/52cdc34)) 189 | 190 | ## [2.5.3](https://github.com/reactjs/react-transition-group/compare/v2.5.2...v2.5.3) (2019-01-14) 191 | 192 | 193 | ### Bug Fixes 194 | 195 | * strip custom prop-types in production ([#448](https://github.com/reactjs/react-transition-group/issues/448)) ([46fa20f](https://github.com/reactjs/react-transition-group/commit/46fa20f)) 196 | 197 | ## [2.5.2](https://github.com/reactjs/react-transition-group/compare/v2.5.1...v2.5.2) (2018-12-20) 198 | 199 | 200 | ### Bug Fixes 201 | 202 | * pass appear to CSSTransition callbacks ([#441](https://github.com/reactjs/react-transition-group/issues/441)) ([df7adb4](https://github.com/reactjs/react-transition-group/commit/df7adb4)), closes [#143](https://github.com/reactjs/react-transition-group/issues/143) 203 | 204 | ## [2.5.1](https://github.com/reactjs/react-transition-group/compare/v2.5.0...v2.5.1) (2018-12-10) 205 | 206 | 207 | ### Bug Fixes 208 | 209 | * prevent calling setState in TransitionGroup if it has been unmounted ([#435](https://github.com/reactjs/react-transition-group/issues/435)) ([6d46b69](https://github.com/reactjs/react-transition-group/commit/6d46b69)) 210 | 211 | # [2.5.0](https://github.com/reactjs/react-transition-group/compare/v2.4.0...v2.5.0) (2018-09-26) 212 | 213 | 214 | ### Features 215 | 216 | * update build and package dependencies ([#413](https://github.com/reactjs/react-transition-group/issues/413)) ([af3d45a](https://github.com/reactjs/react-transition-group/commit/af3d45a)) 217 | 218 | # [2.4.0](https://github.com/reactjs/react-transition-group/compare/v2.3.1...v2.4.0) (2018-06-27) 219 | 220 | 221 | ### Features 222 | 223 | * remove deprecated lifecycle hooks and polyfill for older react versions ([c1ab1cf](https://github.com/reactjs/react-transition-group/commit/c1ab1cf)) 224 | 225 | 226 | ### Performance Improvements 227 | 228 | * don't reflow when there's no class to add ([d7b898d](https://github.com/reactjs/react-transition-group/commit/d7b898d)) 229 | 230 | 231 | ## [2.3.1](https://github.com/reactjs/react-transition-group/compare/v2.3.0...v2.3.1) (2018-04-14) 232 | 233 | 234 | ### Bug Fixes 235 | 236 | * **deps:** Move loose-envify and semantic-release to devDependencies ([#319](https://github.com/reactjs/react-transition-group/issues/319)) ([b4ec774](https://github.com/reactjs/react-transition-group/commit/b4ec774)) 237 | 238 | ## [v2.3.0] 239 | 240 | > 2018-03-28 241 | 242 | * Added `*-done` classes to CSS Transition ([#269]) 243 | * Reorganize docs with more interesting examples! ([#304]) 244 | * A bunch of bug fixes 245 | 246 | [#269]: https://github.com/reactjs/react-transition-group/pull/269 247 | [#304]: https://github.com/reactjs/react-transition-group/pull/304 248 | [v2.3.0]: https://github.com/reactjs/react-transition-group/compare/v2.2.1...2.3.0 249 | 250 | ## [v2.2.1] 251 | 252 | > 2017-09-29 253 | 254 | * **Patch:** Allow React v16 ([#198]) 255 | 256 | [#198]: https://github.com/reactjs/react-transition-group/pull/198 257 | [v2.2.1]: https://github.com/reactjs/react-transition-group/compare/v2.2.0...2.2.1 258 | 259 | ## [v2.2.0] 260 | 261 | > 2017-07-21 262 | 263 | * **Feature:** Support multiple classes in `classNames` ([#124]) 264 | * **Docs:** fix broken link ([#127]) 265 | * **Bugfix:** Fix Transition props pass-through ([#123]) 266 | 267 | [#124]: https://github.com/reactjs/react-transition-group/pull/124 268 | [#123]: https://github.com/reactjs/react-transition-group/pull/123 269 | [#127]: https://github.com/reactjs/react-transition-group/pull/127 270 | [v2.2.0]: https://github.com/reactjs/react-transition-group/compare/v2.1.0...2.2.0 271 | 272 | ## [v2.1.0] 273 | 274 | > 2017-07-06 275 | 276 | * **Feature:** Add back `childFactory` on `` ([#113]) 277 | * **Bugfix:** Ensure child specified `onExited` fires in a `` ([#113]) 278 | 279 | [#113]: https://github.com/reactjs/react-transition-group/pull/113 280 | [v2.1.0]: https://github.com/reactjs/react-transition-group/compare/v2.0.1...2.1.0 281 | 282 | ## v2.0.2 283 | 284 | > 2017-07-06 285 | 286 | * **Fix documentation npm:** No code changes 287 | 288 | ## v2.0.1 289 | 290 | > 2017-07-06 291 | 292 | * **Fix documentation on npm:** No code changes 293 | 294 | ## [v2.0.0] 295 | 296 | > 2017-07-06 297 | 298 | * **Feature:** New API! ([#24]), migration guide at [https://github.com/reactjs/react-transition-group/blob/master/Migration.md](https://github.com/reactjs/react-transition-group/blob/master/Migration.md) 299 | 300 | [#24]: https://github.com/reactjs/react-transition-group/pull/24 301 | [v2.0.0]: https://github.com/reactjs/react-transition-group/compare/v1.2.0...v2.0.0 302 | 303 | ## [v1.2.0] 304 | 305 | > 2017-06-12 306 | 307 | * **Feature:** Dist build now includes both production and development builds ([#64]) 308 | * **Feature:** PropTypes are now wrapped allowing for lighter weight production builds ([#69]) 309 | 310 | [#64]: https://github.com/reactjs/react-transition-group/issues/64 311 | [#69]: https://github.com/reactjs/react-transition-group/issues/69 312 | [v1.1.x]: https://github.com/reactjs/react-transition-group/compare/v1.1.3...master 313 | 314 | ## [v1.1.3] 315 | 316 | > 2017-05-02 317 | 318 | * bonus release, no additions 319 | 320 | [v1.1.3]: https://github.com/reactjs/react-transition-group/compare/v1.1.2...v1.1.3 321 | 322 | ## [v1.1.2] 323 | 324 | > 2017-05-02 325 | 326 | * **Bugfix:** Fix refs on children ([#39]) 327 | 328 | [v1.1.2]: https://github.com/reactjs/react-transition-group/compare/v1.1.1...v1.1.2 329 | [#39]: https://github.com/reactjs/react-transition-group/pull/39 330 | 331 | ## [v1.1.1] 332 | 333 | > 2017-03-16 334 | 335 | * **Chore:** Add a prebuilt version of the library for jsbin and the like. 336 | 337 | [v1.1.1]: https://github.com/reactjs/react-transition-group/compare/v1.1.0...v1.1.1 338 | 339 | ## [v1.1.0] 340 | 341 | > 2017-03-16 342 | 343 | * **Feature:** Support refs on children ([#9]) 344 | * **Feature:** TransitionChild to passes props through ([#4]) 345 | * **Bugfix:** Fix TransitionGroup error on quick toggle of components ([#15]) 346 | * **Bugfix:** Fix to work enter animation with CSSTransitionGroup ([#13]) 347 | 348 | [v1.1.0]: https://github.com/reactjs/react-transition-group/compare/v1.0.0...v1.1.0 349 | [#15]: https://github.com/reactjs/react-transition-group/pull/15 350 | [#13]: https://github.com/reactjs/react-transition-group/pull/13 351 | [#9]: https://github.com/reactjs/react-transition-group/pull/9 352 | [#4]: https://github.com/reactjs/react-transition-group/pull/4 353 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, React Community 4 | Forked from React (https://github.com/facebook/react) Copyright 2013-present, Facebook, Inc. 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /Migration.md: -------------------------------------------------------------------------------- 1 | # Migration Guide from v1 to v2 2 | 3 | _A few notes to help with migrating from v1 to v2._ 4 | 5 | The `` component has been removed. A `` component has been added for use with the new `` component to accomplish the same tasks. 6 | 7 | ### tl;dr: 8 | 9 | - `transitionName` -> `classNames` 10 | - `transitionEnterTimeout` and `transitionLeaveTimeout` -> `timeout={{ exit, enter }}` 11 | - `transitionAppear` -> `appear` 12 | - `transitionEnter` -> `enter` 13 | - `transitionLeave` -> `exit` 14 | 15 | ## Walkthrough 16 | 17 | Let's take the [original docs example](https://github.com/reactjs/react-transition-group/tree/v1-stable/#high-level-api-csstransitiongroup) and migrate it. 18 | 19 | Starting with this CSS: 20 | 21 | ```css 22 | .example-enter { 23 | opacity: 0.01; 24 | } 25 | 26 | .example-enter.example-enter-active { 27 | opacity: 1; 28 | transition: opacity 500ms ease-in; 29 | } 30 | 31 | .example-leave { 32 | opacity: 1; 33 | } 34 | 35 | .example-leave.example-leave-active { 36 | opacity: 0.01; 37 | transition: opacity 300ms ease-in; 38 | } 39 | ``` 40 | 41 | And this component: 42 | 43 | ```js 44 | class TodoList extends React.Component { 45 | constructor(props) { 46 | super(props); 47 | this.state = {items: ['hello', 'world', 'click', 'me']}; 48 | this.handleAdd = this.handleAdd.bind(this); 49 | } 50 | 51 | handleAdd() { 52 | const newItems = this.state.items.concat([ 53 | prompt('Enter some text') 54 | ]); 55 | this.setState({items: newItems}); 56 | } 57 | 58 | handleRemove(i) { 59 | let newItems = this.state.items.slice(); 60 | newItems.splice(i, 1); 61 | this.setState({items: newItems}); 62 | } 63 | 64 | render() { 65 | const items = this.state.items.map((item, i) => ( 66 |
this.handleRemove(i)}> 67 | {item} 68 |
69 | )); 70 | 71 | return ( 72 |
73 | 74 | 78 | {items} 79 | 80 |
81 | ); 82 | } 83 | } 84 | ``` 85 | 86 | The most straightforward way to migrate is to use `` instead of ``: 87 | 88 | ```diff 89 | render() { 90 | const items = this.state.items.map((item, i) => ( 91 |
this.handleRemove(i)}> 92 | {item} 93 |
94 | )); 95 | 96 | return ( 97 |
98 | 99 | - 103 | + 104 | {items} 105 | - 106 | + 107 |
108 | ) 109 | } 110 | ``` 111 | 112 | That doesn't get us much, since we haven't included anything to do the animation. For that, we'll need to wrap each item in a ``. First, though, let's adjust our CSS: 113 | 114 | ```diff 115 | .example-enter { 116 | opacity: 0.01; 117 | } 118 | 119 | .example-enter.example-enter-active { 120 | opacity: 1; 121 | transition: opacity 500ms ease-in; 122 | } 123 | 124 | -.example-leave { 125 | +.example-exit { 126 | opacity: 1; 127 | } 128 | 129 | -.example-leave.example-leave-active { 130 | +.example-exit.example-exit-active { 131 | opacity: 0.01; 132 | transition: opacity 300ms ease-in; 133 | } 134 | ``` 135 | 136 | All we did was replace `leave` with `exit`. v2 uses "exit" instead of "leave" to be more symmetric, avoiding awkwardness with English tenses (like with "entered" and "leaved"). 137 | 138 | Now we add the `` component: 139 | 140 | ```diff 141 | render() { 142 | const items = this.state.items.map((item, i) => ( 143 | + 148 |
this.handleRemove(i)}> 149 | {item} 150 |
151 | +
152 | )); 153 | 154 | return ( 155 |
156 | 157 | 158 | {items} 159 | 160 |
161 | ) 162 | } 163 | ``` 164 | 165 | Note that we replaced `transitionName` with `classNames`. `` otherwise has essentially the same signature as ``. We also replaced `transitionEnterTimeout` and `transitionLeaveTimeout` with a single `timeout` prop with an object. 166 | 167 | > **Hint:** If your enter and exit timeouts are the same you can use the shorthand `timeout={500}`. 168 | 169 | If we want to make this a bit more encapsulated, we can wrap our `` into a separate component for reuse later: 170 | 171 | ```js 172 | const FadeTransition = (props) => ( 173 | 178 | ); 179 | ``` 180 | 181 | We can then use it like: 182 | 183 | ```diff 184 | render() { 185 | const items = this.state.items.map((item, i) => ( 186 | - 191 | + 192 |
this.handleRemove(i)}> 193 | {item} 194 |
195 | -
196 | + 197 | )); 198 | 199 | return ( 200 |
201 | 202 | 203 | {items} 204 | 205 |
206 | ) 207 | } 208 | ``` 209 | 210 | > **Hey!** You may not need `` at all! The lower level `` component is very flexible and may be easier to work with for simpler or more custom cases. Check out how we migrated [React-Bootstrap](https://react-bootstrap.github.io/)'s simple transitions to v2 for the [``](https://github.com/react-bootstrap/react-bootstrap/pull/2676/files#diff-4f938f648d04d4859be417d6590ca7c4) and [``](https://github.com/react-bootstrap/react-bootstrap/pull/2676/files#diff-8f766132cbd9f8de55ee05d63d75abd8) components. 211 | 212 | 213 | ## Wrapping `` Components 214 | 215 | The old `` component managed transitions through custom static lifecycle methods on its children. In v2 we removed that API in favor of requiring that `` be used with a `` component, and using traditional prop passing to communicate between the two. 216 | 217 | This means that ``s inject their children with ``-specific props that _must_ be passed through to the `` component for the transition to work. 218 | 219 | ```js 220 | const MyTransition = ({ children: child, ...props }) => ( 221 | // NOTICE THE SPREAD! THIS IS REQUIRED! 222 | 223 | {transitionState => React.cloneElement(child, { 224 | style: getStyleForTransitionState(transitionState) 225 | })} 226 | 227 | ); 228 | 229 | const MyList = () => ( 230 | 231 | {items.map(item => ( 232 | {item} 233 | )} 234 | 235 | ); 236 | ``` 237 | 238 | Note how `` passes all props other than its own to ``. 239 | 240 | 241 | ## Lifecycle Callbacks 242 | 243 | As noted, child lifecycle methods have been removed. If you do need to do some work when the `` changes from one state to another, use the lifecycle callback props. 244 | 245 | ```js 246 | 255 | ``` 256 | 257 | Each callback is called with the DOM node of the transition component. Note also that there are now _three_ states per enter/exit transition instead of the original two. See the [full documentation](https://reactcommunity.org/react-transition-group/#Transition) for more details. 258 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-transition-group [![npm][npm-badge]][npm] 2 | 3 | > **ATTENTION!** To address many issues that have come up over the years, the API in v2 and above is not backwards compatible with the original [`React addon (v1-stable)`](https://github.com/reactjs/react-transition-group/tree/v1-stable). 4 | > 5 | > **For a drop-in replacement for `react-addons-transition-group` and `react-addons-css-transition-group`, use the v1 release. Documentation and code for that release are available on the [`v1-stable`](https://github.com/reactjs/react-transition-group/tree/v1-stable) branch.** 6 | > 7 | > We are no longer updating the v1 codebase, please upgrade to the latest version when possible 8 | 9 | A set of components for managing component states (including mounting and unmounting) over time, specifically designed with animation in mind. 10 | 11 | ## Documentation 12 | 13 | - [**Main documentation**](https://reactcommunity.org/react-transition-group/) 14 | - [Migration guide from v1](/Migration.md) 15 | 16 | ## TypeScript 17 | TypeScript definitions are published via [**DefinitelyTyped**](https://github.com/DefinitelyTyped/DefinitelyTyped) and can be installed via the following command: 18 | 19 | ``` 20 | npm install @types/react-transition-group 21 | ``` 22 | 23 | ## Examples 24 | 25 | Clone the repo first: 26 | 27 | ``` 28 | git@github.com:reactjs/react-transition-group.git 29 | ``` 30 | 31 | Then run `npm install` (or `yarn`), and finally `npm run storybook` to start a storybook instance that you can navigate to in your browser to see the examples. 32 | 33 | [npm-badge]: https://img.shields.io/npm/v/react-transition-group.svg 34 | [npm]: https://www.npmjs.org/package/react-transition-group 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-transition-group", 3 | "version": "4.4.5", 4 | "description": "A react component toolset for managing animations", 5 | "main": "lib/cjs/index.js", 6 | "module": "lib/esm/index.js", 7 | "scripts": { 8 | "test": "npm run lint && npm run testonly", 9 | "testonly": "jest --verbose", 10 | "tdd": "jest --watch", 11 | "build": "rimraf lib && yarn build:cjs && yarn build:esm && yarn build:pick && yarn build:dist && cp README.md LICENSE ./lib", 12 | "build:docs": "yarn --cwd www run build", 13 | "build:cjs": "babel src --out-dir lib/cjs", 14 | "build:esm": "cross-env BABEL_ENV=esm babel src --out-dir lib/esm", 15 | "build:pick": "cherry-pick --cwd=lib --input-dir=../src --cjs-dir=cjs --esm-dir=esm", 16 | "build:dist": "cross-env BABEL_ENV=esm rollup -c", 17 | "bootstrap": "yarn && yarn --cwd www", 18 | "fix": "run-s fix:eslint fix:prettier", 19 | "fix:eslint": "yarn lint:eslint --fix", 20 | "fix:prettier": "yarn lint:prettier --write", 21 | "lint": "run-p lint:*", 22 | "lint:eslint": "eslint .", 23 | "lint:prettier": "prettier . --check", 24 | "release": "release", 25 | "release:next": "release --preid beta --tag next", 26 | "deploy-docs": "yarn --cwd www run deploy", 27 | "start": "yarn --cwd www run develop", 28 | "storybook": "start-storybook -p 6006", 29 | "build-storybook": "build-storybook", 30 | "semantic-release": "semantic-release" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/reactjs/react-transition-group.git" 35 | }, 36 | "keywords": [ 37 | "react", 38 | "transition", 39 | "addons", 40 | "transition-group", 41 | "animation", 42 | "css", 43 | "transitions" 44 | ], 45 | "author": "", 46 | "license": "BSD-3-Clause", 47 | "bugs": { 48 | "url": "https://github.com/reactjs/react-transition-group/issues" 49 | }, 50 | "homepage": "https://github.com/reactjs/react-transition-group#readme", 51 | "jest": { 52 | "testRegex": "-test\\.js", 53 | "setupFiles": [ 54 | "./test/setup.js" 55 | ], 56 | "setupFilesAfterEnv": [ 57 | "./test/setupAfterEnv.js" 58 | ], 59 | "roots": [ 60 | "/test" 61 | ] 62 | }, 63 | "peerDependencies": { 64 | "react": ">=16.6.0", 65 | "react-dom": ">=16.6.0" 66 | }, 67 | "dependencies": { 68 | "@babel/runtime": "^7.5.5", 69 | "dom-helpers": "^5.0.1", 70 | "loose-envify": "^1.4.0", 71 | "prop-types": "^15.6.2" 72 | }, 73 | "devDependencies": { 74 | "@babel/cli": "^7.8.4", 75 | "@babel/core": "^7.9.0", 76 | "@restart/hooks": "^0.3.22", 77 | "@semantic-release/changelog": "^5.0.1", 78 | "@semantic-release/git": "^9.0.0", 79 | "@semantic-release/github": "^7.0.5", 80 | "@semantic-release/npm": "^7.0.5", 81 | "@storybook/addon-actions": "^6.3.4", 82 | "@storybook/react": "^6.3.4", 83 | "@testing-library/react": "alpha", 84 | "@typescript-eslint/eslint-plugin": "^4.26.1", 85 | "astroturf": "^0.10.4", 86 | "babel-eslint": "^10.1.0", 87 | "babel-loader": "^8.1.0", 88 | "babel-plugin-transform-react-remove-prop-types": "^0.4.24", 89 | "babel-preset-jason": "^6.2.0", 90 | "cherry-pick": "^0.5.0", 91 | "cross-env": "^7.0.2", 92 | "eslint": "^7.28.0", 93 | "eslint-config-jason": "^8.1.1", 94 | "eslint-config-prettier": "^8.3.0", 95 | "eslint-plugin-import": "^2.23.4", 96 | "eslint-plugin-jsx-a11y": "^6.4.1", 97 | "eslint-plugin-react": "^7.24.0", 98 | "eslint-plugin-react-hooks": "^4.2.0", 99 | "jest": "^25.3.0", 100 | "npm-run-all": "^4.1.5", 101 | "prettier": "^2.3.1", 102 | "react": "^18.0.0", 103 | "react-dom": "^18.0.0", 104 | "release-script": "^1.0.2", 105 | "rimraf": "^3.0.2", 106 | "rollup": "^2.6.1", 107 | "rollup-plugin-babel": "^4.4.0", 108 | "rollup-plugin-commonjs": "^10.1.0", 109 | "rollup-plugin-node-resolve": "^5.2.0", 110 | "rollup-plugin-replace": "^2.2.0", 111 | "rollup-plugin-size-snapshot": "^0.11.0", 112 | "rollup-plugin-terser": "^5.3.0", 113 | "semantic-release": "^17.0.6", 114 | "semantic-release-alt-publish-dir": "^3.0.0", 115 | "typescript": "^4.3.2", 116 | "webpack-atoms": "14.0.0" 117 | }, 118 | "release": { 119 | "pkgRoot": "lib", 120 | "verifyConditions": [ 121 | "@semantic-release/changelog", 122 | "semantic-release-alt-publish-dir", 123 | "@semantic-release/git", 124 | "@semantic-release/github" 125 | ], 126 | "prepare": [ 127 | "@semantic-release/changelog", 128 | "semantic-release-alt-publish-dir", 129 | "@semantic-release/npm", 130 | "@semantic-release/git" 131 | ] 132 | }, 133 | "browserify": { 134 | "transform": [ 135 | "loose-envify" 136 | ] 137 | }, 138 | "sideEffects": false 139 | } 140 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | }; 4 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from 'rollup-plugin-node-resolve'; 2 | import babel from 'rollup-plugin-babel'; 3 | import commonjs from 'rollup-plugin-commonjs'; 4 | import replace from 'rollup-plugin-replace'; 5 | import { sizeSnapshot } from 'rollup-plugin-size-snapshot'; 6 | import { terser } from 'rollup-plugin-terser'; 7 | 8 | const input = './src/index.js'; 9 | const name = 'ReactTransitionGroup'; 10 | const globals = { 11 | react: 'React', 12 | 'react-dom': 'ReactDOM', 13 | }; 14 | 15 | const babelOptions = { 16 | exclude: /node_modules/, 17 | runtimeHelpers: true, 18 | }; 19 | 20 | const commonjsOptions = { 21 | include: /node_modules/, 22 | namedExports: { 23 | 'prop-types': ['object', 'oneOfType', 'element', 'bool', 'func'], 24 | }, 25 | }; 26 | 27 | export default [ 28 | { 29 | input, 30 | output: { 31 | file: './lib/dist/react-transition-group.js', 32 | format: 'umd', 33 | name, 34 | globals, 35 | }, 36 | external: Object.keys(globals), 37 | plugins: [ 38 | nodeResolve(), 39 | babel(babelOptions), 40 | commonjs(commonjsOptions), 41 | replace({ 'process.env.NODE_ENV': JSON.stringify('development') }), 42 | sizeSnapshot(), 43 | ], 44 | }, 45 | 46 | { 47 | input, 48 | output: { 49 | file: './lib/dist/react-transition-group.min.js', 50 | format: 'umd', 51 | name, 52 | globals, 53 | }, 54 | external: Object.keys(globals), 55 | plugins: [ 56 | nodeResolve(), 57 | babel(babelOptions), 58 | commonjs(commonjsOptions), 59 | replace({ 'process.env.NODE_ENV': JSON.stringify('production') }), 60 | sizeSnapshot(), 61 | terser(), 62 | ], 63 | }, 64 | ]; 65 | -------------------------------------------------------------------------------- /src/CSSTransition.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import addOneClass from 'dom-helpers/addClass'; 3 | 4 | import removeOneClass from 'dom-helpers/removeClass'; 5 | import React from 'react'; 6 | 7 | import Transition from './Transition'; 8 | import { classNamesShape } from './utils/PropTypes'; 9 | import { forceReflow } from './utils/reflow'; 10 | 11 | const addClass = (node, classes) => 12 | node && classes && classes.split(' ').forEach((c) => addOneClass(node, c)); 13 | const removeClass = (node, classes) => 14 | node && classes && classes.split(' ').forEach((c) => removeOneClass(node, c)); 15 | 16 | /** 17 | * A transition component inspired by the excellent 18 | * [ng-animate](https://docs.angularjs.org/api/ngAnimate) library, you should 19 | * use it if you're using CSS transitions or animations. It's built upon the 20 | * [`Transition`](https://reactcommunity.org/react-transition-group/transition) 21 | * component, so it inherits all of its props. 22 | * 23 | * `CSSTransition` applies a pair of class names during the `appear`, `enter`, 24 | * and `exit` states of the transition. The first class is applied and then a 25 | * second `*-active` class in order to activate the CSS transition. After the 26 | * transition, matching `*-done` class names are applied to persist the 27 | * transition state. 28 | * 29 | * ```jsx 30 | * function App() { 31 | * const [inProp, setInProp] = useState(false); 32 | * const nodeRef = useRef(null); 33 | * return ( 34 | *
35 | * 36 | *
37 | * {"I'll receive my-node-* classes"} 38 | *
39 | *
40 | * 43 | *
44 | * ); 45 | * } 46 | * ``` 47 | * 48 | * When the `in` prop is set to `true`, the child component will first receive 49 | * the class `example-enter`, then the `example-enter-active` will be added in 50 | * the next tick. `CSSTransition` [forces a 51 | * reflow](https://github.com/reactjs/react-transition-group/blob/5007303e729a74be66a21c3e2205e4916821524b/src/CSSTransition.js#L208-L215) 52 | * between before adding the `example-enter-active`. This is an important trick 53 | * because it allows us to transition between `example-enter` and 54 | * `example-enter-active` even though they were added immediately one after 55 | * another. Most notably, this is what makes it possible for us to animate 56 | * _appearance_. 57 | * 58 | * ```css 59 | * .my-node-enter { 60 | * opacity: 0; 61 | * } 62 | * .my-node-enter-active { 63 | * opacity: 1; 64 | * transition: opacity 200ms; 65 | * } 66 | * .my-node-exit { 67 | * opacity: 1; 68 | * } 69 | * .my-node-exit-active { 70 | * opacity: 0; 71 | * transition: opacity 200ms; 72 | * } 73 | * ``` 74 | * 75 | * `*-active` classes represent which styles you want to animate **to**, so it's 76 | * important to add `transition` declaration only to them, otherwise transitions 77 | * might not behave as intended! This might not be obvious when the transitions 78 | * are symmetrical, i.e. when `*-enter-active` is the same as `*-exit`, like in 79 | * the example above (minus `transition`), but it becomes apparent in more 80 | * complex transitions. 81 | * 82 | * **Note**: If you're using the 83 | * [`appear`](http://reactcommunity.org/react-transition-group/transition#Transition-prop-appear) 84 | * prop, make sure to define styles for `.appear-*` classes as well. 85 | */ 86 | class CSSTransition extends React.Component { 87 | static defaultProps = { 88 | classNames: '', 89 | }; 90 | 91 | appliedClasses = { 92 | appear: {}, 93 | enter: {}, 94 | exit: {}, 95 | }; 96 | 97 | onEnter = (maybeNode, maybeAppearing) => { 98 | const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing); 99 | this.removeClasses(node, 'exit'); 100 | this.addClass(node, appearing ? 'appear' : 'enter', 'base'); 101 | 102 | if (this.props.onEnter) { 103 | this.props.onEnter(maybeNode, maybeAppearing); 104 | } 105 | }; 106 | 107 | onEntering = (maybeNode, maybeAppearing) => { 108 | const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing); 109 | const type = appearing ? 'appear' : 'enter'; 110 | this.addClass(node, type, 'active'); 111 | 112 | if (this.props.onEntering) { 113 | this.props.onEntering(maybeNode, maybeAppearing); 114 | } 115 | }; 116 | 117 | onEntered = (maybeNode, maybeAppearing) => { 118 | const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing); 119 | const type = appearing ? 'appear' : 'enter'; 120 | this.removeClasses(node, type); 121 | this.addClass(node, type, 'done'); 122 | 123 | if (this.props.onEntered) { 124 | this.props.onEntered(maybeNode, maybeAppearing); 125 | } 126 | }; 127 | 128 | onExit = (maybeNode) => { 129 | const [node] = this.resolveArguments(maybeNode); 130 | this.removeClasses(node, 'appear'); 131 | this.removeClasses(node, 'enter'); 132 | this.addClass(node, 'exit', 'base'); 133 | 134 | if (this.props.onExit) { 135 | this.props.onExit(maybeNode); 136 | } 137 | }; 138 | 139 | onExiting = (maybeNode) => { 140 | const [node] = this.resolveArguments(maybeNode); 141 | this.addClass(node, 'exit', 'active'); 142 | 143 | if (this.props.onExiting) { 144 | this.props.onExiting(maybeNode); 145 | } 146 | }; 147 | 148 | onExited = (maybeNode) => { 149 | const [node] = this.resolveArguments(maybeNode); 150 | this.removeClasses(node, 'exit'); 151 | this.addClass(node, 'exit', 'done'); 152 | 153 | if (this.props.onExited) { 154 | this.props.onExited(maybeNode); 155 | } 156 | }; 157 | 158 | // when prop `nodeRef` is provided `node` is excluded 159 | resolveArguments = (maybeNode, maybeAppearing) => 160 | this.props.nodeRef 161 | ? [this.props.nodeRef.current, maybeNode] // here `maybeNode` is actually `appearing` 162 | : [maybeNode, maybeAppearing]; // `findDOMNode` was used 163 | 164 | getClassNames = (type) => { 165 | const { classNames } = this.props; 166 | const isStringClassNames = typeof classNames === 'string'; 167 | const prefix = isStringClassNames && classNames ? `${classNames}-` : ''; 168 | 169 | let baseClassName = isStringClassNames 170 | ? `${prefix}${type}` 171 | : classNames[type]; 172 | 173 | let activeClassName = isStringClassNames 174 | ? `${baseClassName}-active` 175 | : classNames[`${type}Active`]; 176 | 177 | let doneClassName = isStringClassNames 178 | ? `${baseClassName}-done` 179 | : classNames[`${type}Done`]; 180 | 181 | return { 182 | baseClassName, 183 | activeClassName, 184 | doneClassName, 185 | }; 186 | }; 187 | 188 | addClass(node, type, phase) { 189 | let className = this.getClassNames(type)[`${phase}ClassName`]; 190 | const { doneClassName } = this.getClassNames('enter'); 191 | 192 | if (type === 'appear' && phase === 'done' && doneClassName) { 193 | className += ` ${doneClassName}`; 194 | } 195 | 196 | // This is to force a repaint, 197 | // which is necessary in order to transition styles when adding a class name. 198 | if (phase === 'active') { 199 | if (node) forceReflow(node); 200 | } 201 | 202 | if (className) { 203 | this.appliedClasses[type][phase] = className; 204 | addClass(node, className); 205 | } 206 | } 207 | 208 | removeClasses(node, type) { 209 | const { 210 | base: baseClassName, 211 | active: activeClassName, 212 | done: doneClassName, 213 | } = this.appliedClasses[type]; 214 | 215 | this.appliedClasses[type] = {}; 216 | 217 | if (baseClassName) { 218 | removeClass(node, baseClassName); 219 | } 220 | if (activeClassName) { 221 | removeClass(node, activeClassName); 222 | } 223 | if (doneClassName) { 224 | removeClass(node, doneClassName); 225 | } 226 | } 227 | 228 | render() { 229 | const { classNames: _, ...props } = this.props; 230 | 231 | return ( 232 | 241 | ); 242 | } 243 | } 244 | 245 | CSSTransition.propTypes = { 246 | ...Transition.propTypes, 247 | 248 | /** 249 | * The animation classNames applied to the component as it appears, enters, 250 | * exits or has finished the transition. A single name can be provided, which 251 | * will be suffixed for each stage, e.g. `classNames="fade"` applies: 252 | * 253 | * - `fade-appear`, `fade-appear-active`, `fade-appear-done` 254 | * - `fade-enter`, `fade-enter-active`, `fade-enter-done` 255 | * - `fade-exit`, `fade-exit-active`, `fade-exit-done` 256 | * 257 | * A few details to note about how these classes are applied: 258 | * 259 | * 1. They are _joined_ with the ones that are already defined on the child 260 | * component, so if you want to add some base styles, you can use 261 | * `className` without worrying that it will be overridden. 262 | * 263 | * 2. If the transition component mounts with `in={false}`, no classes are 264 | * applied yet. You might be expecting `*-exit-done`, but if you think 265 | * about it, a component cannot finish exiting if it hasn't entered yet. 266 | * 267 | * 2. `fade-appear-done` and `fade-enter-done` will _both_ be applied. This 268 | * allows you to define different behavior for when appearing is done and 269 | * when regular entering is done, using selectors like 270 | * `.fade-enter-done:not(.fade-appear-done)`. For example, you could apply 271 | * an epic entrance animation when element first appears in the DOM using 272 | * [Animate.css](https://daneden.github.io/animate.css/). Otherwise you can 273 | * simply use `fade-enter-done` for defining both cases. 274 | * 275 | * Each individual classNames can also be specified independently like: 276 | * 277 | * ```js 278 | * classNames={{ 279 | * appear: 'my-appear', 280 | * appearActive: 'my-active-appear', 281 | * appearDone: 'my-done-appear', 282 | * enter: 'my-enter', 283 | * enterActive: 'my-active-enter', 284 | * enterDone: 'my-done-enter', 285 | * exit: 'my-exit', 286 | * exitActive: 'my-active-exit', 287 | * exitDone: 'my-done-exit', 288 | * }} 289 | * ``` 290 | * 291 | * If you want to set these classes using CSS Modules: 292 | * 293 | * ```js 294 | * import styles from './styles.css'; 295 | * ``` 296 | * 297 | * you might want to use camelCase in your CSS file, that way could simply 298 | * spread them instead of listing them one by one: 299 | * 300 | * ```js 301 | * classNames={{ ...styles }} 302 | * ``` 303 | * 304 | * @type {string | { 305 | * appear?: string, 306 | * appearActive?: string, 307 | * appearDone?: string, 308 | * enter?: string, 309 | * enterActive?: string, 310 | * enterDone?: string, 311 | * exit?: string, 312 | * exitActive?: string, 313 | * exitDone?: string, 314 | * }} 315 | */ 316 | classNames: classNamesShape, 317 | 318 | /** 319 | * A `` callback fired immediately after the 'enter' or 'appear' class is 320 | * applied. 321 | * 322 | * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. 323 | * 324 | * @type Function(node: HtmlElement, isAppearing: bool) 325 | */ 326 | onEnter: PropTypes.func, 327 | 328 | /** 329 | * A `` callback fired immediately after the 'enter-active' or 330 | * 'appear-active' class is applied. 331 | * 332 | * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. 333 | * 334 | * @type Function(node: HtmlElement, isAppearing: bool) 335 | */ 336 | onEntering: PropTypes.func, 337 | 338 | /** 339 | * A `` callback fired immediately after the 'enter' or 340 | * 'appear' classes are **removed** and the `done` class is added to the DOM node. 341 | * 342 | * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. 343 | * 344 | * @type Function(node: HtmlElement, isAppearing: bool) 345 | */ 346 | onEntered: PropTypes.func, 347 | 348 | /** 349 | * A `` callback fired immediately after the 'exit' class is 350 | * applied. 351 | * 352 | * **Note**: when `nodeRef` prop is passed, `node` is not passed 353 | * 354 | * @type Function(node: HtmlElement) 355 | */ 356 | onExit: PropTypes.func, 357 | 358 | /** 359 | * A `` callback fired immediately after the 'exit-active' is applied. 360 | * 361 | * **Note**: when `nodeRef` prop is passed, `node` is not passed 362 | * 363 | * @type Function(node: HtmlElement) 364 | */ 365 | onExiting: PropTypes.func, 366 | 367 | /** 368 | * A `` callback fired immediately after the 'exit' classes 369 | * are **removed** and the `exit-done` class is added to the DOM node. 370 | * 371 | * **Note**: when `nodeRef` prop is passed, `node` is not passed 372 | * 373 | * @type Function(node: HtmlElement) 374 | */ 375 | onExited: PropTypes.func, 376 | }; 377 | 378 | export default CSSTransition; 379 | -------------------------------------------------------------------------------- /src/ReplaceTransition.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import TransitionGroup from './TransitionGroup'; 5 | 6 | /** 7 | * The `` component is a specialized `Transition` component 8 | * that animates between two children. 9 | * 10 | * ```jsx 11 | * 12 | *
I appear first
13 | *
I replace the above
14 | *
15 | * ``` 16 | */ 17 | class ReplaceTransition extends React.Component { 18 | handleEnter = (...args) => this.handleLifecycle('onEnter', 0, args); 19 | handleEntering = (...args) => this.handleLifecycle('onEntering', 0, args); 20 | handleEntered = (...args) => this.handleLifecycle('onEntered', 0, args); 21 | 22 | handleExit = (...args) => this.handleLifecycle('onExit', 1, args); 23 | handleExiting = (...args) => this.handleLifecycle('onExiting', 1, args); 24 | handleExited = (...args) => this.handleLifecycle('onExited', 1, args); 25 | 26 | handleLifecycle(handler, idx, originalArgs) { 27 | const { children } = this.props; 28 | const child = React.Children.toArray(children)[idx]; 29 | 30 | if (child.props[handler]) child.props[handler](...originalArgs); 31 | if (this.props[handler]) { 32 | const maybeNode = child.props.nodeRef 33 | ? undefined 34 | : ReactDOM.findDOMNode(this); 35 | 36 | this.props[handler](maybeNode); 37 | } 38 | } 39 | 40 | render() { 41 | const { children, in: inProp, ...props } = this.props; 42 | const [first, second] = React.Children.toArray(children); 43 | 44 | delete props.onEnter; 45 | delete props.onEntering; 46 | delete props.onEntered; 47 | delete props.onExit; 48 | delete props.onExiting; 49 | delete props.onExited; 50 | 51 | return ( 52 | 53 | {inProp 54 | ? React.cloneElement(first, { 55 | key: 'first', 56 | onEnter: this.handleEnter, 57 | onEntering: this.handleEntering, 58 | onEntered: this.handleEntered, 59 | }) 60 | : React.cloneElement(second, { 61 | key: 'second', 62 | onEnter: this.handleExit, 63 | onEntering: this.handleExiting, 64 | onEntered: this.handleExited, 65 | })} 66 | 67 | ); 68 | } 69 | } 70 | 71 | ReplaceTransition.propTypes = { 72 | in: PropTypes.bool.isRequired, 73 | children(props, propName) { 74 | if (React.Children.count(props[propName]) !== 2) 75 | return new Error( 76 | `"${propName}" must be exactly two transition components.` 77 | ); 78 | 79 | return null; 80 | }, 81 | }; 82 | 83 | export default ReplaceTransition; 84 | -------------------------------------------------------------------------------- /src/SwitchTransition.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ENTERED, ENTERING, EXITING } from './Transition'; 4 | import TransitionGroupContext from './TransitionGroupContext'; 5 | 6 | function areChildrenDifferent(oldChildren, newChildren) { 7 | if (oldChildren === newChildren) return false; 8 | if ( 9 | React.isValidElement(oldChildren) && 10 | React.isValidElement(newChildren) && 11 | oldChildren.key != null && 12 | oldChildren.key === newChildren.key 13 | ) { 14 | return false; 15 | } 16 | return true; 17 | } 18 | 19 | /** 20 | * Enum of modes for SwitchTransition component 21 | * @enum { string } 22 | */ 23 | export const modes = { 24 | out: 'out-in', 25 | in: 'in-out', 26 | }; 27 | 28 | const callHook = 29 | (element, name, cb) => 30 | (...args) => { 31 | element.props[name] && element.props[name](...args); 32 | cb(); 33 | }; 34 | 35 | const leaveRenders = { 36 | [modes.out]: ({ current, changeState }) => 37 | React.cloneElement(current, { 38 | in: false, 39 | onExited: callHook(current, 'onExited', () => { 40 | changeState(ENTERING, null); 41 | }), 42 | }), 43 | [modes.in]: ({ current, changeState, children }) => [ 44 | current, 45 | React.cloneElement(children, { 46 | in: true, 47 | onEntered: callHook(children, 'onEntered', () => { 48 | changeState(ENTERING); 49 | }), 50 | }), 51 | ], 52 | }; 53 | 54 | const enterRenders = { 55 | [modes.out]: ({ children, changeState }) => 56 | React.cloneElement(children, { 57 | in: true, 58 | onEntered: callHook(children, 'onEntered', () => { 59 | changeState(ENTERED, React.cloneElement(children, { in: true })); 60 | }), 61 | }), 62 | [modes.in]: ({ current, children, changeState }) => [ 63 | React.cloneElement(current, { 64 | in: false, 65 | onExited: callHook(current, 'onExited', () => { 66 | changeState(ENTERED, React.cloneElement(children, { in: true })); 67 | }), 68 | }), 69 | React.cloneElement(children, { 70 | in: true, 71 | }), 72 | ], 73 | }; 74 | 75 | /** 76 | * A transition component inspired by the [vue transition modes](https://vuejs.org/v2/guide/transitions.html#Transition-Modes). 77 | * You can use it when you want to control the render between state transitions. 78 | * Based on the selected mode and the child's key which is the `Transition` or `CSSTransition` component, the `SwitchTransition` makes a consistent transition between them. 79 | * 80 | * If the `out-in` mode is selected, the `SwitchTransition` waits until the old child leaves and then inserts a new child. 81 | * If the `in-out` mode is selected, the `SwitchTransition` inserts a new child first, waits for the new child to enter and then removes the old child. 82 | * 83 | * **Note**: If you want the animation to happen simultaneously 84 | * (that is, to have the old child removed and a new child inserted **at the same time**), 85 | * you should use 86 | * [`TransitionGroup`](https://reactcommunity.org/react-transition-group/transition-group) 87 | * instead. 88 | * 89 | * ```jsx 90 | * function App() { 91 | * const [state, setState] = useState(false); 92 | * const helloRef = useRef(null); 93 | * const goodbyeRef = useRef(null); 94 | * const nodeRef = state ? goodbyeRef : helloRef; 95 | * return ( 96 | * 97 | * node.addEventListener("transitionend", done, false)} 101 | * classNames='fade' 102 | * > 103 | * 106 | * 107 | * 108 | * ); 109 | * } 110 | * ``` 111 | * 112 | * ```css 113 | * .fade-enter{ 114 | * opacity: 0; 115 | * } 116 | * .fade-exit{ 117 | * opacity: 1; 118 | * } 119 | * .fade-enter-active{ 120 | * opacity: 1; 121 | * } 122 | * .fade-exit-active{ 123 | * opacity: 0; 124 | * } 125 | * .fade-enter-active, 126 | * .fade-exit-active{ 127 | * transition: opacity 500ms; 128 | * } 129 | * ``` 130 | */ 131 | class SwitchTransition extends React.Component { 132 | state = { 133 | status: ENTERED, 134 | current: null, 135 | }; 136 | 137 | appeared = false; 138 | 139 | componentDidMount() { 140 | this.appeared = true; 141 | } 142 | 143 | static getDerivedStateFromProps(props, state) { 144 | if (props.children == null) { 145 | return { 146 | current: null, 147 | }; 148 | } 149 | 150 | if (state.status === ENTERING && props.mode === modes.in) { 151 | return { 152 | status: ENTERING, 153 | }; 154 | } 155 | 156 | if (state.current && areChildrenDifferent(state.current, props.children)) { 157 | return { 158 | status: EXITING, 159 | }; 160 | } 161 | 162 | return { 163 | current: React.cloneElement(props.children, { 164 | in: true, 165 | }), 166 | }; 167 | } 168 | 169 | changeState = (status, current = this.state.current) => { 170 | this.setState({ 171 | status, 172 | current, 173 | }); 174 | }; 175 | 176 | render() { 177 | const { 178 | props: { children, mode }, 179 | state: { status, current }, 180 | } = this; 181 | 182 | const data = { children, current, changeState: this.changeState, status }; 183 | let component; 184 | switch (status) { 185 | case ENTERING: 186 | component = enterRenders[mode](data); 187 | break; 188 | case EXITING: 189 | component = leaveRenders[mode](data); 190 | break; 191 | case ENTERED: 192 | component = current; 193 | } 194 | 195 | return ( 196 | 197 | {component} 198 | 199 | ); 200 | } 201 | } 202 | 203 | SwitchTransition.propTypes = { 204 | /** 205 | * Transition modes. 206 | * `out-in`: Current element transitions out first, then when complete, the new element transitions in. 207 | * `in-out`: New element transitions in first, then when complete, the current element transitions out. 208 | * 209 | * @type {'out-in'|'in-out'} 210 | */ 211 | mode: PropTypes.oneOf([modes.in, modes.out]), 212 | /** 213 | * Any `Transition` or `CSSTransition` component. 214 | */ 215 | children: PropTypes.oneOfType([PropTypes.element.isRequired]), 216 | }; 217 | 218 | SwitchTransition.defaultProps = { 219 | mode: modes.out, 220 | }; 221 | 222 | export default SwitchTransition; 223 | -------------------------------------------------------------------------------- /src/Transition.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | import config from './config'; 6 | import { timeoutsShape } from './utils/PropTypes'; 7 | import TransitionGroupContext from './TransitionGroupContext'; 8 | import { forceReflow } from './utils/reflow'; 9 | 10 | export const UNMOUNTED = 'unmounted'; 11 | export const EXITED = 'exited'; 12 | export const ENTERING = 'entering'; 13 | export const ENTERED = 'entered'; 14 | export const EXITING = 'exiting'; 15 | 16 | /** 17 | * The Transition component lets you describe a transition from one component 18 | * state to another _over time_ with a simple declarative API. Most commonly 19 | * it's used to animate the mounting and unmounting of a component, but can also 20 | * be used to describe in-place transition states as well. 21 | * 22 | * --- 23 | * 24 | * **Note**: `Transition` is a platform-agnostic base component. If you're using 25 | * transitions in CSS, you'll probably want to use 26 | * [`CSSTransition`](https://reactcommunity.org/react-transition-group/css-transition) 27 | * instead. It inherits all the features of `Transition`, but contains 28 | * additional features necessary to play nice with CSS transitions (hence the 29 | * name of the component). 30 | * 31 | * --- 32 | * 33 | * By default the `Transition` component does not alter the behavior of the 34 | * component it renders, it only tracks "enter" and "exit" states for the 35 | * components. It's up to you to give meaning and effect to those states. For 36 | * example we can add styles to a component when it enters or exits: 37 | * 38 | * ```jsx 39 | * import { Transition } from 'react-transition-group'; 40 | * import { useRef } from 'react'; 41 | * 42 | * const duration = 300; 43 | * 44 | * const defaultStyle = { 45 | * transition: `opacity ${duration}ms ease-in-out`, 46 | * opacity: 0, 47 | * } 48 | * 49 | * const transitionStyles = { 50 | * entering: { opacity: 1 }, 51 | * entered: { opacity: 1 }, 52 | * exiting: { opacity: 0 }, 53 | * exited: { opacity: 0 }, 54 | * }; 55 | * 56 | * function Fade({ in: inProp }) { 57 | * const nodeRef = useRef(null); 58 | * return ( 59 | * 60 | * {state => ( 61 | *
65 | * I'm a fade Transition! 66 | *
67 | * )} 68 | *
69 | * ); 70 | * } 71 | * ``` 72 | * 73 | * There are 4 main states a Transition can be in: 74 | * - `'entering'` 75 | * - `'entered'` 76 | * - `'exiting'` 77 | * - `'exited'` 78 | * 79 | * Transition state is toggled via the `in` prop. When `true` the component 80 | * begins the "Enter" stage. During this stage, the component will shift from 81 | * its current transition state, to `'entering'` for the duration of the 82 | * transition and then to the `'entered'` stage once it's complete. Let's take 83 | * the following example (we'll use the 84 | * [useState](https://reactjs.org/docs/hooks-reference.html#usestate) hook): 85 | * 86 | * ```jsx 87 | * import { Transition } from 'react-transition-group'; 88 | * import { useState, useRef } from 'react'; 89 | * 90 | * function App() { 91 | * const [inProp, setInProp] = useState(false); 92 | * const nodeRef = useRef(null); 93 | * return ( 94 | *
95 | * 96 | * {state => ( 97 | * // ... 98 | * )} 99 | * 100 | * 103 | *
104 | * ); 105 | * } 106 | * ``` 107 | * 108 | * When the button is clicked the component will shift to the `'entering'` state 109 | * and stay there for 500ms (the value of `timeout`) before it finally switches 110 | * to `'entered'`. 111 | * 112 | * When `in` is `false` the same thing happens except the state moves from 113 | * `'exiting'` to `'exited'`. 114 | */ 115 | class Transition extends React.Component { 116 | static contextType = TransitionGroupContext; 117 | 118 | constructor(props, context) { 119 | super(props, context); 120 | 121 | let parentGroup = context; 122 | // In the context of a TransitionGroup all enters are really appears 123 | let appear = 124 | parentGroup && !parentGroup.isMounting ? props.enter : props.appear; 125 | 126 | let initialStatus; 127 | 128 | this.appearStatus = null; 129 | 130 | if (props.in) { 131 | if (appear) { 132 | initialStatus = EXITED; 133 | this.appearStatus = ENTERING; 134 | } else { 135 | initialStatus = ENTERED; 136 | } 137 | } else { 138 | if (props.unmountOnExit || props.mountOnEnter) { 139 | initialStatus = UNMOUNTED; 140 | } else { 141 | initialStatus = EXITED; 142 | } 143 | } 144 | 145 | this.state = { status: initialStatus }; 146 | 147 | this.nextCallback = null; 148 | } 149 | 150 | static getDerivedStateFromProps({ in: nextIn }, prevState) { 151 | if (nextIn && prevState.status === UNMOUNTED) { 152 | return { status: EXITED }; 153 | } 154 | return null; 155 | } 156 | 157 | // getSnapshotBeforeUpdate(prevProps) { 158 | // let nextStatus = null 159 | 160 | // if (prevProps !== this.props) { 161 | // const { status } = this.state 162 | 163 | // if (this.props.in) { 164 | // if (status !== ENTERING && status !== ENTERED) { 165 | // nextStatus = ENTERING 166 | // } 167 | // } else { 168 | // if (status === ENTERING || status === ENTERED) { 169 | // nextStatus = EXITING 170 | // } 171 | // } 172 | // } 173 | 174 | // return { nextStatus } 175 | // } 176 | 177 | componentDidMount() { 178 | this.updateStatus(true, this.appearStatus); 179 | } 180 | 181 | componentDidUpdate(prevProps) { 182 | let nextStatus = null; 183 | if (prevProps !== this.props) { 184 | const { status } = this.state; 185 | 186 | if (this.props.in) { 187 | if (status !== ENTERING && status !== ENTERED) { 188 | nextStatus = ENTERING; 189 | } 190 | } else { 191 | if (status === ENTERING || status === ENTERED) { 192 | nextStatus = EXITING; 193 | } 194 | } 195 | } 196 | this.updateStatus(false, nextStatus); 197 | } 198 | 199 | componentWillUnmount() { 200 | this.cancelNextCallback(); 201 | } 202 | 203 | getTimeouts() { 204 | const { timeout } = this.props; 205 | let exit, enter, appear; 206 | 207 | exit = enter = appear = timeout; 208 | 209 | if (timeout != null && typeof timeout !== 'number') { 210 | exit = timeout.exit; 211 | enter = timeout.enter; 212 | // TODO: remove fallback for next major 213 | appear = timeout.appear !== undefined ? timeout.appear : enter; 214 | } 215 | return { exit, enter, appear }; 216 | } 217 | 218 | updateStatus(mounting = false, nextStatus) { 219 | if (nextStatus !== null) { 220 | // nextStatus will always be ENTERING or EXITING. 221 | this.cancelNextCallback(); 222 | 223 | if (nextStatus === ENTERING) { 224 | if (this.props.unmountOnExit || this.props.mountOnEnter) { 225 | const node = this.props.nodeRef 226 | ? this.props.nodeRef.current 227 | : ReactDOM.findDOMNode(this); 228 | // https://github.com/reactjs/react-transition-group/pull/749 229 | // With unmountOnExit or mountOnEnter, the enter animation should happen at the transition between `exited` and `entering`. 230 | // To make the animation happen, we have to separate each rendering and avoid being processed as batched. 231 | if (node) forceReflow(node); 232 | } 233 | this.performEnter(mounting); 234 | } else { 235 | this.performExit(); 236 | } 237 | } else if (this.props.unmountOnExit && this.state.status === EXITED) { 238 | this.setState({ status: UNMOUNTED }); 239 | } 240 | } 241 | 242 | performEnter(mounting) { 243 | const { enter } = this.props; 244 | const appearing = this.context ? this.context.isMounting : mounting; 245 | const [maybeNode, maybeAppearing] = this.props.nodeRef 246 | ? [appearing] 247 | : [ReactDOM.findDOMNode(this), appearing]; 248 | 249 | const timeouts = this.getTimeouts(); 250 | const enterTimeout = appearing ? timeouts.appear : timeouts.enter; 251 | // no enter animation skip right to ENTERED 252 | // if we are mounting and running this it means appear _must_ be set 253 | if ((!mounting && !enter) || config.disabled) { 254 | this.safeSetState({ status: ENTERED }, () => { 255 | this.props.onEntered(maybeNode); 256 | }); 257 | return; 258 | } 259 | 260 | this.props.onEnter(maybeNode, maybeAppearing); 261 | 262 | this.safeSetState({ status: ENTERING }, () => { 263 | this.props.onEntering(maybeNode, maybeAppearing); 264 | 265 | this.onTransitionEnd(enterTimeout, () => { 266 | this.safeSetState({ status: ENTERED }, () => { 267 | this.props.onEntered(maybeNode, maybeAppearing); 268 | }); 269 | }); 270 | }); 271 | } 272 | 273 | performExit() { 274 | const { exit } = this.props; 275 | const timeouts = this.getTimeouts(); 276 | const maybeNode = this.props.nodeRef 277 | ? undefined 278 | : ReactDOM.findDOMNode(this); 279 | 280 | // no exit animation skip right to EXITED 281 | if (!exit || config.disabled) { 282 | this.safeSetState({ status: EXITED }, () => { 283 | this.props.onExited(maybeNode); 284 | }); 285 | return; 286 | } 287 | 288 | this.props.onExit(maybeNode); 289 | 290 | this.safeSetState({ status: EXITING }, () => { 291 | this.props.onExiting(maybeNode); 292 | 293 | this.onTransitionEnd(timeouts.exit, () => { 294 | this.safeSetState({ status: EXITED }, () => { 295 | this.props.onExited(maybeNode); 296 | }); 297 | }); 298 | }); 299 | } 300 | 301 | cancelNextCallback() { 302 | if (this.nextCallback !== null) { 303 | this.nextCallback.cancel(); 304 | this.nextCallback = null; 305 | } 306 | } 307 | 308 | safeSetState(nextState, callback) { 309 | // This shouldn't be necessary, but there are weird race conditions with 310 | // setState callbacks and unmounting in testing, so always make sure that 311 | // we can cancel any pending setState callbacks after we unmount. 312 | callback = this.setNextCallback(callback); 313 | this.setState(nextState, callback); 314 | } 315 | 316 | setNextCallback(callback) { 317 | let active = true; 318 | 319 | this.nextCallback = (event) => { 320 | if (active) { 321 | active = false; 322 | this.nextCallback = null; 323 | 324 | callback(event); 325 | } 326 | }; 327 | 328 | this.nextCallback.cancel = () => { 329 | active = false; 330 | }; 331 | 332 | return this.nextCallback; 333 | } 334 | 335 | onTransitionEnd(timeout, handler) { 336 | this.setNextCallback(handler); 337 | const node = this.props.nodeRef 338 | ? this.props.nodeRef.current 339 | : ReactDOM.findDOMNode(this); 340 | 341 | const doesNotHaveTimeoutOrListener = 342 | timeout == null && !this.props.addEndListener; 343 | if (!node || doesNotHaveTimeoutOrListener) { 344 | setTimeout(this.nextCallback, 0); 345 | return; 346 | } 347 | 348 | if (this.props.addEndListener) { 349 | const [maybeNode, maybeNextCallback] = this.props.nodeRef 350 | ? [this.nextCallback] 351 | : [node, this.nextCallback]; 352 | this.props.addEndListener(maybeNode, maybeNextCallback); 353 | } 354 | 355 | if (timeout != null) { 356 | setTimeout(this.nextCallback, timeout); 357 | } 358 | } 359 | 360 | render() { 361 | const status = this.state.status; 362 | 363 | if (status === UNMOUNTED) { 364 | return null; 365 | } 366 | 367 | const { 368 | children, 369 | // filter props for `Transition` 370 | in: _in, 371 | mountOnEnter: _mountOnEnter, 372 | unmountOnExit: _unmountOnExit, 373 | appear: _appear, 374 | enter: _enter, 375 | exit: _exit, 376 | timeout: _timeout, 377 | addEndListener: _addEndListener, 378 | onEnter: _onEnter, 379 | onEntering: _onEntering, 380 | onEntered: _onEntered, 381 | onExit: _onExit, 382 | onExiting: _onExiting, 383 | onExited: _onExited, 384 | nodeRef: _nodeRef, 385 | ...childProps 386 | } = this.props; 387 | 388 | return ( 389 | // allows for nested Transitions 390 | 391 | {typeof children === 'function' 392 | ? children(status, childProps) 393 | : React.cloneElement(React.Children.only(children), childProps)} 394 | 395 | ); 396 | } 397 | } 398 | 399 | Transition.propTypes = { 400 | /** 401 | * A React reference to the DOM element that needs to transition: 402 | * https://stackoverflow.com/a/51127130/4671932 403 | * 404 | * - This prop is optional, but recommended in order to avoid defaulting to 405 | * [`ReactDOM.findDOMNode`](https://reactjs.org/docs/react-dom.html#finddomnode), 406 | * which is deprecated in `StrictMode` 407 | * - When `nodeRef` prop is used, `node` is not passed to callback functions 408 | * (e.g. `onEnter`) because user already has direct access to the node. 409 | * - When changing `key` prop of `Transition` in a `TransitionGroup` a new 410 | * `nodeRef` need to be provided to `Transition` with changed `key` prop 411 | * (see 412 | * [test/CSSTransition-test.js](https://github.com/reactjs/react-transition-group/blob/13435f897b3ab71f6e19d724f145596f5910581c/test/CSSTransition-test.js#L362-L437)). 413 | */ 414 | nodeRef: PropTypes.shape({ 415 | current: 416 | typeof Element === 'undefined' 417 | ? PropTypes.any 418 | : (propValue, key, componentName, location, propFullName, secret) => { 419 | const value = propValue[key]; 420 | 421 | return PropTypes.instanceOf( 422 | value && 'ownerDocument' in value 423 | ? value.ownerDocument.defaultView.Element 424 | : Element 425 | )(propValue, key, componentName, location, propFullName, secret); 426 | }, 427 | }), 428 | 429 | /** 430 | * A `function` child can be used instead of a React element. This function is 431 | * called with the current transition status (`'entering'`, `'entered'`, 432 | * `'exiting'`, `'exited'`), which can be used to apply context 433 | * specific props to a component. 434 | * 435 | * ```jsx 436 | * 437 | * {state => ( 438 | * 439 | * )} 440 | * 441 | * ``` 442 | */ 443 | children: PropTypes.oneOfType([ 444 | PropTypes.func.isRequired, 445 | PropTypes.element.isRequired, 446 | ]).isRequired, 447 | 448 | /** 449 | * Show the component; triggers the enter or exit states 450 | */ 451 | in: PropTypes.bool, 452 | 453 | /** 454 | * By default the child component is mounted immediately along with 455 | * the parent `Transition` component. If you want to "lazy mount" the component on the 456 | * first `in={true}` you can set `mountOnEnter`. After the first enter transition the component will stay 457 | * mounted, even on "exited", unless you also specify `unmountOnExit`. 458 | */ 459 | mountOnEnter: PropTypes.bool, 460 | 461 | /** 462 | * By default the child component stays mounted after it reaches the `'exited'` state. 463 | * Set `unmountOnExit` if you'd prefer to unmount the component after it finishes exiting. 464 | */ 465 | unmountOnExit: PropTypes.bool, 466 | 467 | /** 468 | * By default the child component does not perform the enter transition when 469 | * it first mounts, regardless of the value of `in`. If you want this 470 | * behavior, set both `appear` and `in` to `true`. 471 | * 472 | * > **Note**: there are no special appear states like `appearing`/`appeared`, this prop 473 | * > only adds an additional enter transition. However, in the 474 | * > `` component that first enter transition does result in 475 | * > additional `.appear-*` classes, that way you can choose to style it 476 | * > differently. 477 | */ 478 | appear: PropTypes.bool, 479 | 480 | /** 481 | * Enable or disable enter transitions. 482 | */ 483 | enter: PropTypes.bool, 484 | 485 | /** 486 | * Enable or disable exit transitions. 487 | */ 488 | exit: PropTypes.bool, 489 | 490 | /** 491 | * The duration of the transition, in milliseconds. 492 | * Required unless `addEndListener` is provided. 493 | * 494 | * You may specify a single timeout for all transitions: 495 | * 496 | * ```jsx 497 | * timeout={500} 498 | * ``` 499 | * 500 | * or individually: 501 | * 502 | * ```jsx 503 | * timeout={{ 504 | * appear: 500, 505 | * enter: 300, 506 | * exit: 500, 507 | * }} 508 | * ``` 509 | * 510 | * - `appear` defaults to the value of `enter` 511 | * - `enter` defaults to `0` 512 | * - `exit` defaults to `0` 513 | * 514 | * @type {number | { enter?: number, exit?: number, appear?: number }} 515 | */ 516 | timeout: (props, ...args) => { 517 | let pt = timeoutsShape; 518 | if (!props.addEndListener) pt = pt.isRequired; 519 | return pt(props, ...args); 520 | }, 521 | 522 | /** 523 | * Add a custom transition end trigger. Called with the transitioning 524 | * DOM node and a `done` callback. Allows for more fine grained transition end 525 | * logic. Timeouts are still used as a fallback if provided. 526 | * 527 | * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `done` is being passed as the first argument. 528 | * 529 | * ```jsx 530 | * addEndListener={(node, done) => { 531 | * // use the css transitionend event to mark the finish of a transition 532 | * node.addEventListener('transitionend', done, false); 533 | * }} 534 | * ``` 535 | */ 536 | addEndListener: PropTypes.func, 537 | 538 | /** 539 | * Callback fired before the "entering" status is applied. An extra parameter 540 | * `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount 541 | * 542 | * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. 543 | * 544 | * @type Function(node: HtmlElement, isAppearing: bool) -> void 545 | */ 546 | onEnter: PropTypes.func, 547 | 548 | /** 549 | * Callback fired after the "entering" status is applied. An extra parameter 550 | * `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount 551 | * 552 | * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. 553 | * 554 | * @type Function(node: HtmlElement, isAppearing: bool) 555 | */ 556 | onEntering: PropTypes.func, 557 | 558 | /** 559 | * Callback fired after the "entered" status is applied. An extra parameter 560 | * `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount 561 | * 562 | * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. 563 | * 564 | * @type Function(node: HtmlElement, isAppearing: bool) -> void 565 | */ 566 | onEntered: PropTypes.func, 567 | 568 | /** 569 | * Callback fired before the "exiting" status is applied. 570 | * 571 | * **Note**: when `nodeRef` prop is passed, `node` is not passed. 572 | * 573 | * @type Function(node: HtmlElement) -> void 574 | */ 575 | onExit: PropTypes.func, 576 | 577 | /** 578 | * Callback fired after the "exiting" status is applied. 579 | * 580 | * **Note**: when `nodeRef` prop is passed, `node` is not passed. 581 | * 582 | * @type Function(node: HtmlElement) -> void 583 | */ 584 | onExiting: PropTypes.func, 585 | 586 | /** 587 | * Callback fired after the "exited" status is applied. 588 | * 589 | * **Note**: when `nodeRef` prop is passed, `node` is not passed 590 | * 591 | * @type Function(node: HtmlElement) -> void 592 | */ 593 | onExited: PropTypes.func, 594 | }; 595 | 596 | // Name the function so it is clearer in the documentation 597 | function noop() {} 598 | 599 | Transition.defaultProps = { 600 | in: false, 601 | mountOnEnter: false, 602 | unmountOnExit: false, 603 | appear: false, 604 | enter: true, 605 | exit: true, 606 | 607 | onEnter: noop, 608 | onEntering: noop, 609 | onEntered: noop, 610 | 611 | onExit: noop, 612 | onExiting: noop, 613 | onExited: noop, 614 | }; 615 | 616 | Transition.UNMOUNTED = UNMOUNTED; 617 | Transition.EXITED = EXITED; 618 | Transition.ENTERING = ENTERING; 619 | Transition.ENTERED = ENTERED; 620 | Transition.EXITING = EXITING; 621 | 622 | export default Transition; 623 | -------------------------------------------------------------------------------- /src/TransitionGroup.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import TransitionGroupContext from './TransitionGroupContext'; 4 | 5 | import { 6 | getChildMapping, 7 | getInitialChildMapping, 8 | getNextChildMapping, 9 | } from './utils/ChildMapping'; 10 | 11 | const values = Object.values || ((obj) => Object.keys(obj).map((k) => obj[k])); 12 | 13 | const defaultProps = { 14 | component: 'div', 15 | childFactory: (child) => child, 16 | }; 17 | 18 | /** 19 | * The `` component manages a set of transition components 20 | * (`` and ``) in a list. Like with the transition 21 | * components, `` is a state machine for managing the mounting 22 | * and unmounting of components over time. 23 | * 24 | * Consider the example below. As items are removed or added to the TodoList the 25 | * `in` prop is toggled automatically by the ``. 26 | * 27 | * Note that `` does not define any animation behavior! 28 | * Exactly _how_ a list item animates is up to the individual transition 29 | * component. This means you can mix and match animations across different list 30 | * items. 31 | */ 32 | class TransitionGroup extends React.Component { 33 | constructor(props, context) { 34 | super(props, context); 35 | 36 | const handleExited = this.handleExited.bind(this); 37 | 38 | // Initial children should all be entering, dependent on appear 39 | this.state = { 40 | contextValue: { isMounting: true }, 41 | handleExited, 42 | firstRender: true, 43 | }; 44 | } 45 | 46 | componentDidMount() { 47 | this.mounted = true; 48 | this.setState({ 49 | contextValue: { isMounting: false }, 50 | }); 51 | } 52 | 53 | componentWillUnmount() { 54 | this.mounted = false; 55 | } 56 | 57 | static getDerivedStateFromProps( 58 | nextProps, 59 | { children: prevChildMapping, handleExited, firstRender } 60 | ) { 61 | return { 62 | children: firstRender 63 | ? getInitialChildMapping(nextProps, handleExited) 64 | : getNextChildMapping(nextProps, prevChildMapping, handleExited), 65 | firstRender: false, 66 | }; 67 | } 68 | 69 | // node is `undefined` when user provided `nodeRef` prop 70 | handleExited(child, node) { 71 | let currentChildMapping = getChildMapping(this.props.children); 72 | 73 | if (child.key in currentChildMapping) return; 74 | 75 | if (child.props.onExited) { 76 | child.props.onExited(node); 77 | } 78 | 79 | if (this.mounted) { 80 | this.setState((state) => { 81 | let children = { ...state.children }; 82 | 83 | delete children[child.key]; 84 | return { children }; 85 | }); 86 | } 87 | } 88 | 89 | render() { 90 | const { component: Component, childFactory, ...props } = this.props; 91 | const { contextValue } = this.state; 92 | const children = values(this.state.children).map(childFactory); 93 | 94 | delete props.appear; 95 | delete props.enter; 96 | delete props.exit; 97 | 98 | if (Component === null) { 99 | return ( 100 | 101 | {children} 102 | 103 | ); 104 | } 105 | return ( 106 | 107 | {children} 108 | 109 | ); 110 | } 111 | } 112 | 113 | TransitionGroup.propTypes = { 114 | /** 115 | * `` renders a `
` by default. You can change this 116 | * behavior by providing a `component` prop. 117 | * If you use React v16+ and would like to avoid a wrapping `
` element 118 | * you can pass in `component={null}`. This is useful if the wrapping div 119 | * borks your css styles. 120 | */ 121 | component: PropTypes.any, 122 | /** 123 | * A set of `` components, that are toggled `in` and out as they 124 | * leave. the `` will inject specific transition props, so 125 | * remember to spread them through if you are wrapping the `` as 126 | * with our `` example. 127 | * 128 | * While this component is meant for multiple `Transition` or `CSSTransition` 129 | * children, sometimes you may want to have a single transition child with 130 | * content that you want to be transitioned out and in when you change it 131 | * (e.g. routes, images etc.) In that case you can change the `key` prop of 132 | * the transition child as you change its content, this will cause 133 | * `TransitionGroup` to transition the child out and back in. 134 | */ 135 | children: PropTypes.node, 136 | 137 | /** 138 | * A convenience prop that enables or disables appear animations 139 | * for all children. Note that specifying this will override any defaults set 140 | * on individual children Transitions. 141 | */ 142 | appear: PropTypes.bool, 143 | /** 144 | * A convenience prop that enables or disables enter animations 145 | * for all children. Note that specifying this will override any defaults set 146 | * on individual children Transitions. 147 | */ 148 | enter: PropTypes.bool, 149 | /** 150 | * A convenience prop that enables or disables exit animations 151 | * for all children. Note that specifying this will override any defaults set 152 | * on individual children Transitions. 153 | */ 154 | exit: PropTypes.bool, 155 | 156 | /** 157 | * You may need to apply reactive updates to a child as it is exiting. 158 | * This is generally done by using `cloneElement` however in the case of an exiting 159 | * child the element has already been removed and not accessible to the consumer. 160 | * 161 | * If you do need to update a child as it leaves you can provide a `childFactory` 162 | * to wrap every child, even the ones that are leaving. 163 | * 164 | * @type Function(child: ReactElement) -> ReactElement 165 | */ 166 | childFactory: PropTypes.func, 167 | }; 168 | 169 | TransitionGroup.defaultProps = defaultProps; 170 | 171 | export default TransitionGroup; 172 | -------------------------------------------------------------------------------- /src/TransitionGroupContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default React.createContext(null); 4 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | disabled: false, 3 | }; 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as CSSTransition } from './CSSTransition'; 2 | export { default as ReplaceTransition } from './ReplaceTransition'; 3 | export { default as SwitchTransition } from './SwitchTransition'; 4 | export { default as TransitionGroup } from './TransitionGroup'; 5 | export { default as Transition } from './Transition'; 6 | export { default as config } from './config'; 7 | -------------------------------------------------------------------------------- /src/utils/ChildMapping.js: -------------------------------------------------------------------------------- 1 | import { Children, cloneElement, isValidElement } from 'react'; 2 | 3 | /** 4 | * Given `this.props.children`, return an object mapping key to child. 5 | * 6 | * @param {*} children `this.props.children` 7 | * @return {object} Mapping of key to child 8 | */ 9 | export function getChildMapping(children, mapFn) { 10 | let mapper = (child) => 11 | mapFn && isValidElement(child) ? mapFn(child) : child; 12 | 13 | let result = Object.create(null); 14 | if (children) 15 | Children.map(children, (c) => c).forEach((child) => { 16 | // run the map function here instead so that the key is the computed one 17 | result[child.key] = mapper(child); 18 | }); 19 | return result; 20 | } 21 | 22 | /** 23 | * When you're adding or removing children some may be added or removed in the 24 | * same render pass. We want to show *both* since we want to simultaneously 25 | * animate elements in and out. This function takes a previous set of keys 26 | * and a new set of keys and merges them with its best guess of the correct 27 | * ordering. In the future we may expose some of the utilities in 28 | * ReactMultiChild to make this easy, but for now React itself does not 29 | * directly have this concept of the union of prevChildren and nextChildren 30 | * so we implement it here. 31 | * 32 | * @param {object} prev prev children as returned from 33 | * `ReactTransitionChildMapping.getChildMapping()`. 34 | * @param {object} next next children as returned from 35 | * `ReactTransitionChildMapping.getChildMapping()`. 36 | * @return {object} a key set that contains all keys in `prev` and all keys 37 | * in `next` in a reasonable order. 38 | */ 39 | export function mergeChildMappings(prev, next) { 40 | prev = prev || {}; 41 | next = next || {}; 42 | 43 | function getValueForKey(key) { 44 | return key in next ? next[key] : prev[key]; 45 | } 46 | 47 | // For each key of `next`, the list of keys to insert before that key in 48 | // the combined list 49 | let nextKeysPending = Object.create(null); 50 | 51 | let pendingKeys = []; 52 | for (let prevKey in prev) { 53 | if (prevKey in next) { 54 | if (pendingKeys.length) { 55 | nextKeysPending[prevKey] = pendingKeys; 56 | pendingKeys = []; 57 | } 58 | } else { 59 | pendingKeys.push(prevKey); 60 | } 61 | } 62 | 63 | let i; 64 | let childMapping = {}; 65 | for (let nextKey in next) { 66 | if (nextKeysPending[nextKey]) { 67 | for (i = 0; i < nextKeysPending[nextKey].length; i++) { 68 | let pendingNextKey = nextKeysPending[nextKey][i]; 69 | childMapping[nextKeysPending[nextKey][i]] = 70 | getValueForKey(pendingNextKey); 71 | } 72 | } 73 | childMapping[nextKey] = getValueForKey(nextKey); 74 | } 75 | 76 | // Finally, add the keys which didn't appear before any key in `next` 77 | for (i = 0; i < pendingKeys.length; i++) { 78 | childMapping[pendingKeys[i]] = getValueForKey(pendingKeys[i]); 79 | } 80 | 81 | return childMapping; 82 | } 83 | 84 | function getProp(child, prop, props) { 85 | return props[prop] != null ? props[prop] : child.props[prop]; 86 | } 87 | 88 | export function getInitialChildMapping(props, onExited) { 89 | return getChildMapping(props.children, (child) => { 90 | return cloneElement(child, { 91 | onExited: onExited.bind(null, child), 92 | in: true, 93 | appear: getProp(child, 'appear', props), 94 | enter: getProp(child, 'enter', props), 95 | exit: getProp(child, 'exit', props), 96 | }); 97 | }); 98 | } 99 | 100 | export function getNextChildMapping(nextProps, prevChildMapping, onExited) { 101 | let nextChildMapping = getChildMapping(nextProps.children); 102 | let children = mergeChildMappings(prevChildMapping, nextChildMapping); 103 | 104 | Object.keys(children).forEach((key) => { 105 | let child = children[key]; 106 | 107 | if (!isValidElement(child)) return; 108 | 109 | const hasPrev = key in prevChildMapping; 110 | const hasNext = key in nextChildMapping; 111 | 112 | const prevChild = prevChildMapping[key]; 113 | const isLeaving = isValidElement(prevChild) && !prevChild.props.in; 114 | 115 | // item is new (entering) 116 | if (hasNext && (!hasPrev || isLeaving)) { 117 | // console.log('entering', key) 118 | children[key] = cloneElement(child, { 119 | onExited: onExited.bind(null, child), 120 | in: true, 121 | exit: getProp(child, 'exit', nextProps), 122 | enter: getProp(child, 'enter', nextProps), 123 | }); 124 | } else if (!hasNext && hasPrev && !isLeaving) { 125 | // item is old (exiting) 126 | // console.log('leaving', key) 127 | children[key] = cloneElement(child, { in: false }); 128 | } else if (hasNext && hasPrev && isValidElement(prevChild)) { 129 | // item hasn't changed transition states 130 | // copy over the last transition props; 131 | // console.log('unchanged', key) 132 | children[key] = cloneElement(child, { 133 | onExited: onExited.bind(null, child), 134 | in: prevChild.props.in, 135 | exit: getProp(child, 'exit', nextProps), 136 | enter: getProp(child, 'enter', nextProps), 137 | }); 138 | } 139 | }); 140 | 141 | return children; 142 | } 143 | -------------------------------------------------------------------------------- /src/utils/PropTypes.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export const timeoutsShape = 4 | process.env.NODE_ENV !== 'production' 5 | ? PropTypes.oneOfType([ 6 | PropTypes.number, 7 | PropTypes.shape({ 8 | enter: PropTypes.number, 9 | exit: PropTypes.number, 10 | appear: PropTypes.number, 11 | }).isRequired, 12 | ]) 13 | : null; 14 | 15 | export const classNamesShape = 16 | process.env.NODE_ENV !== 'production' 17 | ? PropTypes.oneOfType([ 18 | PropTypes.string, 19 | PropTypes.shape({ 20 | enter: PropTypes.string, 21 | exit: PropTypes.string, 22 | active: PropTypes.string, 23 | }), 24 | PropTypes.shape({ 25 | enter: PropTypes.string, 26 | enterDone: PropTypes.string, 27 | enterActive: PropTypes.string, 28 | exit: PropTypes.string, 29 | exitDone: PropTypes.string, 30 | exitActive: PropTypes.string, 31 | }), 32 | ]) 33 | : null; 34 | -------------------------------------------------------------------------------- /src/utils/SimpleSet.js: -------------------------------------------------------------------------------- 1 | export default class SimpleSet { 2 | constructor() { 3 | this.v = []; 4 | } 5 | clear() { 6 | this.v.length = 0; 7 | } 8 | has(k) { 9 | return this.v.indexOf(k) !== -1; 10 | } 11 | add(k) { 12 | if (this.has(k)) return; 13 | this.v.push(k); 14 | } 15 | delete(k) { 16 | const idx = this.v.indexOf(k); 17 | if (idx === -1) return false; 18 | this.v.splice(idx, 1); 19 | return true; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/reflow.js: -------------------------------------------------------------------------------- 1 | export const forceReflow = (node) => node.scrollTop; 2 | -------------------------------------------------------------------------------- /stories/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | react/prop-types: off 3 | no-unused-vars: 4 | - error 5 | - varsIgnorePattern: ^_$ 6 | import/no-extraneous-dependencies: 7 | - error 8 | - devDependencies: true 9 | -------------------------------------------------------------------------------- /stories/CSSTransition.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import StoryFixture from './StoryFixture'; 5 | import Fade from './transitions/CSSFade'; 6 | 7 | function ToggleFixture({ defaultIn, description, children }) { 8 | const [show, setShow] = useState(defaultIn || false); 9 | 10 | return ( 11 | 12 |
13 | 20 |
21 | {React.cloneElement(children, { in: show })} 22 |
23 | ); 24 | } 25 | 26 | storiesOf('CSSTransition', module) 27 | .add('Fade', () => ( 28 | 29 | asaghasg asgasg 30 | 31 | )) 32 | .add('Fade with appear', () => ( 33 | 34 | asaghasg asgasg 35 | 36 | )) 37 | .add('Fade with mountOnEnter', () => { 38 | return ( 39 | 40 | Fade with mountOnEnter 41 | 42 | ); 43 | }) 44 | .add('Fade with unmountOnExit', () => { 45 | return ( 46 | 47 | Fade with unmountOnExit 48 | 49 | ); 50 | }); 51 | -------------------------------------------------------------------------------- /stories/CSSTransitionGroupFixture.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import TransitionGroup from '../src/TransitionGroup'; 4 | import StoryFixture from './StoryFixture'; 5 | 6 | class CSSTransitionGroupFixture extends React.Component { 7 | static defaultProps = { 8 | items: [], 9 | }; 10 | 11 | count = this.props.items.length; 12 | state = { 13 | items: this.props.items, 14 | }; 15 | 16 | handleAddItem = () => { 17 | this.setState(({ items }) => ({ 18 | items: [...items, `Item number: ${++this.count}`], 19 | })); 20 | }; 21 | 22 | handleRemoveItems = () => { 23 | this.setState(({ items }) => { 24 | items = items.slice(); 25 | items.splice(1, 3); 26 | return { items }; 27 | }); 28 | }; 29 | 30 | handleRemoveItem = (item) => { 31 | this.setState(({ items }) => ({ 32 | items: items.filter((i) => i !== item), 33 | })); 34 | }; 35 | 36 | render() { 37 | const { items: _, description, children, ...rest } = this.props; 38 | // e.g. `Fade`, see where `CSSTransitionGroupFixture` is used 39 | const { type: TransitionType, props: transitionTypeProps } = 40 | React.Children.only(children); 41 | 42 | return ( 43 | 44 |
45 | {' '} 46 | 47 |
48 | 49 | {this.state.items.map((item) => ( 50 | 51 | {item} 52 | 55 | 56 | ))} 57 | 58 |
59 | ); 60 | } 61 | } 62 | 63 | export default CSSTransitionGroupFixture; 64 | -------------------------------------------------------------------------------- /stories/NestedTransition.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import StoryFixture from './StoryFixture'; 4 | import Fade from './transitions/CSSFadeForTransitionGroup'; 5 | import Scale from './transitions/Scale'; 6 | 7 | function FadeAndScale(props) { 8 | return ( 9 | 10 |
I will fade
11 | {/* 12 | We also want to scale in at the same time so we pass the `in` state here as well, so it enters 13 | at the same time as the Fade. 14 | 15 | Note also the `appear` since the Fade will happen when the item mounts, the Scale transition 16 | will mount at the same time as the div we want to scale, so we need to tell it to animate as 17 | it _appears_. 18 | */} 19 | 20 | I should scale 21 | 22 |
23 | ); 24 | } 25 | 26 | function Example() { 27 | const [showNested, setShowNested] = useState(false); 28 | 29 | return ( 30 | 31 |

Nested Animations

32 | 39 | 40 |
41 | ); 42 | } 43 | 44 | export default Example; 45 | -------------------------------------------------------------------------------- /stories/ReplaceTransition.js: -------------------------------------------------------------------------------- 1 | import { css } from 'astroturf'; 2 | import React, { useState } from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | 5 | import ReplaceTransition from '../src/ReplaceTransition'; 6 | import CSSTransition from '../src/CSSTransition'; 7 | 8 | const FADE_TIMEOUT = 1000; 9 | 10 | const styles = css` 11 | .enter { 12 | opacity: 0.01; 13 | } 14 | 15 | .enter.enter-active { 16 | position: absolute; 17 | left: 0; 18 | right: 0; 19 | opacity: 1; 20 | transition: opacity ${FADE_TIMEOUT * 2}ms ease-in; 21 | transition-delay: ${FADE_TIMEOUT}ms; 22 | } 23 | 24 | .exit { 25 | opacity: 1; 26 | } 27 | .exit.exit-active { 28 | opacity: 0.01; 29 | 30 | transition: opacity ${FADE_TIMEOUT}ms ease-in; 31 | } 32 | 33 | .box { 34 | padding: 20px; 35 | background-color: #ccc; 36 | } 37 | .container { 38 | position: relative; 39 | } 40 | `; 41 | 42 | const defaultProps = { 43 | in: false, 44 | timeout: FADE_TIMEOUT * 2, 45 | }; 46 | 47 | function Fade(props) { 48 | return ( 49 | 50 | ); 51 | } 52 | 53 | Fade.defaultProps = defaultProps; 54 | 55 | function Example({ children }) { 56 | const [show, setShow] = useState(false); 57 | 58 | return ( 59 |
60 | 67 | {React.cloneElement(children, { in: show })} 68 |
69 | ); 70 | } 71 | 72 | storiesOf('Replace Transition', module).add('Animates on all', () => { 73 | const firstNodeRef = React.createRef(); 74 | const secondNodeRef = React.createRef(); 75 | return ( 76 | 77 | console.log('onEnter')} 81 | onEntering={() => console.log('onEntering')} 82 | onEntered={() => console.log('onEntered')} 83 | onExit={() => console.log('onExit')} 84 | onExiting={() => console.log('onExiting')} 85 | onExited={() => console.log('onExited')} 86 | > 87 | 88 |
in True
89 |
90 | 91 |
in False
92 |
93 |
94 |
95 | ); 96 | }); 97 | -------------------------------------------------------------------------------- /stories/StoryFixture.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | const propTypes = { 5 | description: PropTypes.string, 6 | }; 7 | 8 | function StoryFixture({ description, children }) { 9 | return ( 10 |
11 |

{description}

12 | 13 | {children} 14 |
15 | ); 16 | } 17 | 18 | StoryFixture.propTypes = propTypes; 19 | 20 | export default StoryFixture; 21 | -------------------------------------------------------------------------------- /stories/Transition.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import StoryFixture from './StoryFixture'; 5 | import { 6 | Fade, 7 | Collapse, 8 | FadeForwardRef, 9 | FadeInnerRef, 10 | } from './transitions/Bootstrap'; 11 | 12 | function ToggleFixture({ defaultIn, description, children }) { 13 | const [show, setShow] = useState(defaultIn); 14 | 15 | return ( 16 | 17 |
18 | 25 |
26 | {React.cloneElement(children, { in: show })} 27 |
28 | ); 29 | } 30 | 31 | storiesOf('Transition', module) 32 | .add('Bootstrap Fade', () => ( 33 | 34 | asaghasg asgasg 35 | 36 | )) 37 | .add('Bootstrap Collapse', () => ( 38 | 39 | 40 | asaghasg asgasg 41 |
foo
42 |
bar
43 |
44 |
45 | )) 46 | .add('Fade using React.forwardRef', () => { 47 | const nodeRef = React.createRef(); 48 | return ( 49 | 50 | 51 | Fade using React.forwardRef 52 | 53 | 54 | ); 55 | }) 56 | .add('Fade using innerRef', () => { 57 | const nodeRef = React.createRef(); 58 | return ( 59 | 60 | Fade using innerRef 61 | 62 | ); 63 | }) 64 | .add('Fade with mountOnEnter', () => { 65 | return ( 66 | 67 | Fade with mountOnEnter 68 | 69 | ); 70 | }) 71 | .add('Fade with unmountOnExit', () => { 72 | return ( 73 | 74 | Fade with unmountOnExit 75 | 76 | ); 77 | }); 78 | -------------------------------------------------------------------------------- /stories/TransitionGroup.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | 4 | import TransitionGroup from '../src/TransitionGroup'; 5 | 6 | import CSSTransitionGroupFixture from './CSSTransitionGroupFixture'; 7 | import NestedTransition from './NestedTransition'; 8 | import StoryFixture from './StoryFixture'; 9 | import Fade, { FADE_TIMEOUT } from './transitions/CSSFadeForTransitionGroup'; 10 | 11 | storiesOf('Css Transition Group', module) 12 | .add('Animates on all', () => ( 13 | 21 | 22 | 23 | )) 24 | .add('Animates on enter', () => ( 25 | 34 | 35 | 36 | )) 37 | .add('Animates on exit', () => ( 38 | 45 | 46 | 47 | )) 48 | .add('Animates on appear', () => ( 49 | 56 | 57 | 58 | )) 59 | .add('Dynamic props', () => ( 60 | 65 | 66 | 67 | )) 68 | .add('Re-entering while leaving', () => ( 69 | 74 | 75 | 76 | )) 77 | .add('Nested Transitions', () => ); 78 | 79 | class DynamicTransition extends React.Component { 80 | state = { count: 0 }; 81 | handleClick = () => { 82 | this.setState({ hide: !this.state.hide }); 83 | }; 84 | 85 | componentDidMount() { 86 | this.interval = setInterval(() => { 87 | this.setState({ count: this.state.count + 1 }); 88 | }, 700); 89 | } 90 | componentWillUnmount() { 91 | clearInterval(this.interval); 92 | } 93 | 94 | render() { 95 | const { hide, count } = this.state; 96 | return ( 97 |
98 | 99 | 100 | {!hide && Changing! {count}} 101 | 102 |
103 | ); 104 | } 105 | } 106 | 107 | function ReEnterTransition() { 108 | const [hide, setHide] = useState(false); 109 | 110 | useEffect(() => { 111 | if (hide) { 112 | setTimeout(() => { 113 | console.log('re-entering!'); 114 | setHide(false); 115 | }, 0.5 * FADE_TIMEOUT); 116 | } 117 | }, [hide]); 118 | 119 | return ( 120 |
121 | 128 | 129 | {!hide && I'm entering!} 130 | 131 |
132 | ); 133 | } 134 | -------------------------------------------------------------------------------- /stories/index.js: -------------------------------------------------------------------------------- 1 | import './Transition'; 2 | import './CSSTransition'; 3 | import './TransitionGroup'; 4 | import './ReplaceTransition'; 5 | -------------------------------------------------------------------------------- /stories/transitions/Bootstrap.js: -------------------------------------------------------------------------------- 1 | import { css } from 'astroturf'; 2 | import React, { useEffect, useRef } from 'react'; 3 | import style from 'dom-helpers/css'; 4 | 5 | import Transition, { 6 | EXITED, 7 | ENTERED, 8 | ENTERING, 9 | EXITING, 10 | } from '../../src/Transition'; 11 | 12 | const styles = css` 13 | .fade { 14 | opacity: 0; 15 | transition: opacity 0.5s linear; 16 | } 17 | .fade.in { 18 | opacity: 1; 19 | } 20 | 21 | .collapse { 22 | display: none; 23 | } 24 | 25 | .collapse.in { 26 | display: block; 27 | } 28 | 29 | .collapsing { 30 | position: relative; 31 | height: 0; 32 | overflow: hidden; 33 | transition: 0.35s ease; 34 | transition-property: height, visibility; 35 | } 36 | `; 37 | 38 | const fadeStyles = { 39 | [ENTERING]: styles.in, 40 | [ENTERED]: styles.in, 41 | }; 42 | 43 | export function Fade(props) { 44 | const nodeRef = useRef(); 45 | return ( 46 | 52 | {(status) => ( 53 |
57 | {props.children} 58 |
59 | )} 60 |
61 | ); 62 | } 63 | 64 | function getHeight(elem) { 65 | let value = elem.offsetHeight; 66 | let margins = ['marginTop', 'marginBottom']; 67 | 68 | return ( 69 | value + 70 | parseInt(style(elem, margins[0]), 10) + 71 | parseInt(style(elem, margins[1]), 10) 72 | ); 73 | } 74 | 75 | const collapseStyles = { 76 | [EXITED]: styles.collapse, 77 | [EXITING]: styles.collapsing, 78 | [ENTERING]: styles.collapsing, 79 | [ENTERED]: `${styles.collapse} ${styles.in}`, 80 | }; 81 | 82 | export class Collapse extends React.Component { 83 | nodeRef = React.createRef(); 84 | 85 | /* -- Expanding -- */ 86 | handleEnter = () => { 87 | this.nodeRef.current.style.height = '0'; 88 | }; 89 | 90 | handleEntering = () => { 91 | this.nodeRef.current.style.height = `${this.nodeRef.current.scrollHeight}px`; 92 | }; 93 | 94 | handleEntered = () => { 95 | this.nodeRef.current.style.height = null; 96 | }; 97 | 98 | /* -- Collapsing -- */ 99 | handleExit = () => { 100 | this.nodeRef.current.style.height = getHeight(this.nodeRef.current) + 'px'; 101 | this.nodeRef.current.offsetHeight; // eslint-disable-line no-unused-expressions 102 | }; 103 | 104 | handleExiting = () => { 105 | this.nodeRef.current.style.height = '0'; 106 | }; 107 | 108 | render() { 109 | const { children, ...rest } = this.props; 110 | return ( 111 | 121 | {(state, props) => ( 122 |
123 | {children} 124 |
125 | )} 126 |
127 | ); 128 | } 129 | } 130 | 131 | export function FadeInnerRef(props) { 132 | const nodeRef = useMergedRef(props.innerRef); 133 | return ( 134 | 140 | {(status) => ( 141 |
145 | {props.children} 146 |
147 | )} 148 |
149 | ); 150 | } 151 | 152 | export const FadeForwardRef = React.forwardRef((props, ref) => { 153 | return ; 154 | }); 155 | 156 | /** 157 | * Compose multiple refs, there may be different implementations 158 | * This one is derived from 159 | * e.g. https://github.com/react-restart/hooks/blob/ed37bf3dfc8fc1d9234a6d8fe0af94d69fad3b74/src/useMergedRefs.ts 160 | * Also here are good discussion about this 161 | * https://github.com/facebook/react/issues/13029 162 | * @param ref 163 | * @returns {React.MutableRefObject} 164 | */ 165 | function useMergedRef(ref) { 166 | const nodeRef = React.useRef(); 167 | useEffect(function () { 168 | if (ref) { 169 | if (typeof ref === 'function') { 170 | ref(nodeRef.current); 171 | } else { 172 | ref.current = nodeRef.current; 173 | } 174 | } 175 | }); 176 | return nodeRef; 177 | } 178 | -------------------------------------------------------------------------------- /stories/transitions/CSSFade.js: -------------------------------------------------------------------------------- 1 | import { css } from 'astroturf'; 2 | import React, { useRef } from 'react'; 3 | 4 | import CSSTransition from '../../src/CSSTransition'; 5 | 6 | export const FADE_TIMEOUT = 1000; 7 | 8 | const styles = css` 9 | .default { 10 | opacity: 0; 11 | } 12 | .enter-done { 13 | opacity: 1; 14 | } 15 | 16 | .enter, 17 | .appear { 18 | opacity: 0.01; 19 | } 20 | 21 | .enter.enter-active, 22 | .appear.appear-active { 23 | opacity: 1; 24 | transition: opacity ${FADE_TIMEOUT}ms ease-in; 25 | } 26 | 27 | .exit { 28 | opacity: 1; 29 | } 30 | .exit.exit-active { 31 | opacity: 0.01; 32 | transition: opacity ${0.8 * FADE_TIMEOUT}ms ease-in; 33 | } 34 | `; 35 | 36 | const defaultProps = { 37 | in: false, 38 | timeout: FADE_TIMEOUT, 39 | }; 40 | function Fade(props) { 41 | const nodeRef = useRef(); 42 | return ( 43 | 44 |
45 | {props.children} 46 |
47 |
48 | ); 49 | } 50 | 51 | Fade.defaultProps = defaultProps; 52 | 53 | export default Fade; 54 | -------------------------------------------------------------------------------- /stories/transitions/CSSFadeForTransitionGroup.js: -------------------------------------------------------------------------------- 1 | import { css } from 'astroturf'; 2 | import React, { useRef } from 'react'; 3 | 4 | import CSSTransition from '../../src/CSSTransition'; 5 | 6 | export const FADE_TIMEOUT = 1000; 7 | 8 | const styles = css` 9 | .enter, 10 | .appear { 11 | opacity: 0.01; 12 | } 13 | 14 | .enter.enter-active, 15 | .appear.appear-active { 16 | opacity: 1; 17 | transition: opacity ${FADE_TIMEOUT}ms ease-in; 18 | } 19 | 20 | .exit { 21 | opacity: 1; 22 | } 23 | .exit.exit-active { 24 | opacity: 0.01; 25 | transition: opacity ${0.8 * FADE_TIMEOUT}ms ease-in; 26 | } 27 | `; 28 | 29 | const defaultProps = { 30 | in: false, 31 | timeout: FADE_TIMEOUT, 32 | }; 33 | 34 | function Fade(props) { 35 | const nodeRef = useRef(); 36 | return ( 37 | 38 |
{props.children}
39 |
40 | ); 41 | } 42 | 43 | Fade.defaultProps = defaultProps; 44 | 45 | export default Fade; 46 | -------------------------------------------------------------------------------- /stories/transitions/Scale.js: -------------------------------------------------------------------------------- 1 | import { css } from 'astroturf'; 2 | import React, { useRef } from 'react'; 3 | 4 | import CSSTransition from '../../src/CSSTransition'; 5 | 6 | export const SCALE_TIMEOUT = 1000; 7 | 8 | const styles = css` 9 | .enter, 10 | .appear { 11 | transform: scale(0); 12 | } 13 | .enter.enter-active, 14 | .appear.appear-active { 15 | transform: scale(1); 16 | transition: transform ${SCALE_TIMEOUT}ms; 17 | } 18 | 19 | .exit { 20 | transform: scale(1); 21 | } 22 | 23 | .exit.exit-active { 24 | transform: scale(0); 25 | transition: transform ${SCALE_TIMEOUT}ms; 26 | } 27 | `; 28 | 29 | const defaultProps = { 30 | in: false, 31 | timeout: SCALE_TIMEOUT, 32 | }; 33 | 34 | function Scale(props) { 35 | const nodeRef = useRef(); 36 | return ( 37 | 38 |
{props.children}
39 |
40 | ); 41 | } 42 | 43 | Scale.defaultProps = defaultProps; 44 | 45 | export default Scale; 46 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | jest: true 3 | es6: true 4 | rules: 5 | no-require: off 6 | global-require: off 7 | no-console: off 8 | react/no-multi-comp: off 9 | react/no-render-return-value: off 10 | react/no-find-dom-node: off 11 | react/prop-types: off 12 | react/prefer-stateless-function: off 13 | react/jsx-boolean-value: off 14 | react/no-string-refs: off 15 | import/no-extraneous-dependencies: 16 | - error 17 | - devDependencies: true 18 | -------------------------------------------------------------------------------- /test/CSSTransition-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, waitFor } from './utils'; 3 | 4 | import CSSTransition from '../src/CSSTransition'; 5 | import TransitionGroup from '../src/TransitionGroup'; 6 | 7 | describe('CSSTransition', () => { 8 | it('should flush new props to the DOM before initiating a transition', (done) => { 9 | const nodeRef = React.createRef(); 10 | const { setProps } = render( 11 | { 17 | expect(nodeRef.current.classList.contains('test-class')).toEqual( 18 | true 19 | ); 20 | expect(nodeRef.current.classList.contains('test-entering')).toEqual( 21 | false 22 | ); 23 | done(); 24 | }} 25 | > 26 |
27 | 28 | ); 29 | 30 | expect(nodeRef.current.classList.contains('test-class')).toEqual(false); 31 | 32 | setProps({ 33 | in: true, 34 | className: 'test-class', 35 | }); 36 | }); 37 | 38 | describe('entering', () => { 39 | it('should apply classes at each transition state', async () => { 40 | let count = 0; 41 | let done = false; 42 | const nodeRef = React.createRef(); 43 | const { setProps } = render( 44 | 45 |
46 | 47 | ); 48 | 49 | setProps({ 50 | in: true, 51 | 52 | onEnter() { 53 | count++; 54 | expect(nodeRef.current.className).toEqual('test-enter'); 55 | }, 56 | 57 | onEntering() { 58 | count++; 59 | expect(nodeRef.current.className).toEqual( 60 | 'test-enter test-enter-active' 61 | ); 62 | }, 63 | 64 | onEntered() { 65 | expect(nodeRef.current.className).toEqual('test-enter-done'); 66 | expect(count).toEqual(2); 67 | done = true; 68 | }, 69 | }); 70 | 71 | await waitFor(() => { 72 | expect(done).toBe(true); 73 | }); 74 | }); 75 | 76 | it('should apply custom classNames names', async () => { 77 | let count = 0; 78 | const nodeRef = React.createRef(); 79 | const { setProps } = render( 80 | 89 |
90 | 91 | ); 92 | 93 | setProps({ 94 | in: true, 95 | 96 | onEnter() { 97 | count++; 98 | expect(nodeRef.current.className).toEqual('custom'); 99 | }, 100 | 101 | onEntering() { 102 | count++; 103 | expect(nodeRef.current.className).toEqual( 104 | 'custom custom-super-active' 105 | ); 106 | }, 107 | 108 | onEntered() { 109 | expect(nodeRef.current.className).toEqual('custom-super-done'); 110 | }, 111 | }); 112 | 113 | await waitFor(() => { 114 | expect(count).toEqual(2); 115 | }); 116 | }); 117 | }); 118 | 119 | describe('appearing', () => { 120 | it('should apply appear classes at each transition state', async () => { 121 | let count = 0; 122 | const nodeRef = React.createRef(); 123 | render( 124 | { 131 | count++; 132 | expect(isAppearing).toEqual(true); 133 | expect(nodeRef.current.className).toEqual('appear-test-appear'); 134 | }} 135 | onEntering={(isAppearing) => { 136 | count++; 137 | expect(isAppearing).toEqual(true); 138 | expect(nodeRef.current.className).toEqual( 139 | 'appear-test-appear appear-test-appear-active' 140 | ); 141 | }} 142 | onEntered={(isAppearing) => { 143 | expect(isAppearing).toEqual(true); 144 | expect(nodeRef.current.className).toEqual( 145 | 'appear-test-appear-done appear-test-enter-done' 146 | ); 147 | }} 148 | > 149 |
150 | 151 | ); 152 | 153 | await waitFor(() => { 154 | expect(count).toEqual(2); 155 | }); 156 | }); 157 | 158 | it('should lose the "*-appear-done" class after leaving and entering again', async () => { 159 | const nodeRef = React.createRef(); 160 | let entered = false; 161 | let exited = false; 162 | const { setProps } = render( 163 | { 170 | entered = true; 171 | }} 172 | > 173 |
174 | 175 | ); 176 | 177 | await waitFor(() => { 178 | expect(entered).toEqual(true); 179 | }); 180 | setProps({ 181 | in: false, 182 | onEntered: () => {}, 183 | onExited: () => { 184 | exited = true; 185 | }, 186 | }); 187 | 188 | await waitFor(() => { 189 | expect(exited).toEqual(true); 190 | }); 191 | expect(nodeRef.current.className).toBe('appear-test-exit-done'); 192 | entered = false; 193 | setProps({ 194 | in: true, 195 | onEntered: () => { 196 | entered = true; 197 | }, 198 | }); 199 | 200 | await waitFor(() => { 201 | expect(entered).toEqual(true); 202 | }); 203 | expect(nodeRef.current.className).toBe('appear-test-enter-done'); 204 | }); 205 | 206 | it('should not add undefined when appearDone is not defined', async () => { 207 | const nodeRef = React.createRef(); 208 | let done = false; 209 | render( 210 | { 217 | expect(isAppearing).toEqual(true); 218 | expect(nodeRef.current.className).toEqual('appear-test'); 219 | }} 220 | onEntered={(isAppearing) => { 221 | expect(isAppearing).toEqual(true); 222 | expect(nodeRef.current.className).toEqual(''); 223 | done = true; 224 | }} 225 | > 226 |
227 | 228 | ); 229 | 230 | await waitFor(() => { 231 | expect(done).toEqual(true); 232 | }); 233 | }); 234 | 235 | it('should not be appearing in normal enter mode', async () => { 236 | let count = 0; 237 | const nodeRef = React.createRef(); 238 | render( 239 | 245 |
246 | 247 | ).setProps({ 248 | in: true, 249 | 250 | onEnter(isAppearing) { 251 | count++; 252 | expect(isAppearing).toEqual(false); 253 | expect(nodeRef.current.className).toEqual('not-appear-test-enter'); 254 | }, 255 | 256 | onEntering(isAppearing) { 257 | count++; 258 | expect(isAppearing).toEqual(false); 259 | expect(nodeRef.current.className).toEqual( 260 | 'not-appear-test-enter not-appear-test-enter-active' 261 | ); 262 | }, 263 | 264 | onEntered(isAppearing) { 265 | expect(isAppearing).toEqual(false); 266 | expect(nodeRef.current.className).toEqual( 267 | 'not-appear-test-enter-done' 268 | ); 269 | }, 270 | }); 271 | 272 | await waitFor(() => { 273 | expect(count).toEqual(2); 274 | }); 275 | }); 276 | 277 | it('should not enter the transition states when appear=false', () => { 278 | const nodeRef = React.createRef(); 279 | render( 280 | { 287 | throw Error('Enter called!'); 288 | }} 289 | onEntering={() => { 290 | throw Error('Entring called!'); 291 | }} 292 | onEntered={() => { 293 | throw Error('Entred called!'); 294 | }} 295 | > 296 |
297 | 298 | ); 299 | }); 300 | }); 301 | 302 | describe('exiting', () => { 303 | it('should apply classes at each transition state', async () => { 304 | let count = 0; 305 | const nodeRef = React.createRef(); 306 | const { setProps } = render( 307 | 308 |
309 | 310 | ); 311 | 312 | setProps({ 313 | in: false, 314 | 315 | onExit() { 316 | count++; 317 | expect(nodeRef.current.className).toEqual('test-exit'); 318 | }, 319 | 320 | onExiting() { 321 | count++; 322 | expect(nodeRef.current.className).toEqual( 323 | 'test-exit test-exit-active' 324 | ); 325 | }, 326 | 327 | onExited() { 328 | expect(nodeRef.current.className).toEqual('test-exit-done'); 329 | }, 330 | }); 331 | 332 | await waitFor(() => { 333 | expect(count).toEqual(2); 334 | }); 335 | }); 336 | 337 | it('should apply custom classNames names', async () => { 338 | let count = 0; 339 | const nodeRef = React.createRef(); 340 | const { setProps } = render( 341 | 351 |
352 | 353 | ); 354 | 355 | setProps({ 356 | in: false, 357 | 358 | onExit() { 359 | count++; 360 | expect(nodeRef.current.className).toEqual('custom'); 361 | }, 362 | 363 | onExiting() { 364 | count++; 365 | expect(nodeRef.current.className).toEqual( 366 | 'custom custom-super-active' 367 | ); 368 | }, 369 | 370 | onExited() { 371 | expect(nodeRef.current.className).toEqual('custom-super-done'); 372 | }, 373 | }); 374 | 375 | await waitFor(() => { 376 | expect(count).toEqual(2); 377 | }); 378 | }); 379 | 380 | it('should support empty prefix', async () => { 381 | let count = 0; 382 | 383 | const nodeRef = React.createRef(); 384 | const { setProps } = render( 385 | 386 |
387 | 388 | ); 389 | 390 | setProps({ 391 | in: false, 392 | 393 | onExit() { 394 | count++; 395 | expect(nodeRef.current.className).toEqual('exit'); 396 | }, 397 | 398 | onExiting() { 399 | count++; 400 | expect(nodeRef.current.className).toEqual('exit exit-active'); 401 | }, 402 | 403 | onExited() { 404 | expect(nodeRef.current.className).toEqual('exit-done'); 405 | }, 406 | }); 407 | 408 | await waitFor(() => { 409 | expect(count).toEqual(2); 410 | }); 411 | }); 412 | }); 413 | 414 | describe('reentering', () => { 415 | it('should remove dynamically applied classes', async () => { 416 | let count = 0; 417 | class Test extends React.Component { 418 | render() { 419 | const { direction, text, nodeRef, ...props } = this.props; 420 | 421 | return ( 422 | 425 | React.cloneElement(child, { 426 | classNames: direction, 427 | }) 428 | } 429 | > 430 | 436 | {text} 437 | 438 | 439 | ); 440 | } 441 | } 442 | 443 | const nodeRef = { 444 | foo: React.createRef(), 445 | bar: React.createRef(), 446 | }; 447 | 448 | const { setProps } = render( 449 | 450 | ); 451 | 452 | setProps({ 453 | direction: 'up', 454 | text: 'bar', 455 | nodeRef: nodeRef.bar, 456 | 457 | onEnter() { 458 | count++; 459 | expect(nodeRef.bar.current.className).toEqual('up-enter'); 460 | }, 461 | onEntering() { 462 | count++; 463 | expect(nodeRef.bar.current.className).toEqual( 464 | 'up-enter up-enter-active' 465 | ); 466 | }, 467 | }); 468 | 469 | await waitFor(() => { 470 | expect(count).toEqual(2); 471 | }); 472 | 473 | setProps({ 474 | direction: 'down', 475 | text: 'foo', 476 | nodeRef: nodeRef.foo, 477 | 478 | onEntering() { 479 | count++; 480 | expect(nodeRef.foo.current.className).toEqual( 481 | 'down-enter down-enter-active' 482 | ); 483 | }, 484 | onEntered() { 485 | count++; 486 | expect(nodeRef.foo.current.className).toEqual('down-enter-done'); 487 | }, 488 | }); 489 | 490 | await waitFor(() => { 491 | expect(count).toEqual(4); 492 | }); 493 | }); 494 | }); 495 | }); 496 | -------------------------------------------------------------------------------- /test/CSSTransitionGroup-test.js: -------------------------------------------------------------------------------- 1 | import hasClass from 'dom-helpers/hasClass'; 2 | import CSSTransition from '../src/CSSTransition'; 3 | 4 | let React; 5 | let ReactDOM; 6 | let TransitionGroup; 7 | let act; 8 | let render; 9 | 10 | // Most of the real functionality is covered in other unit tests, this just 11 | // makes sure we're wired up correctly. 12 | describe('CSSTransitionGroup', () => { 13 | let container; 14 | let consoleErrorSpy; 15 | 16 | function YoloTransition({ id, ...props }) { 17 | const nodeRef = React.useRef(); 18 | return ( 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | beforeEach(() => { 26 | jest.resetModuleRegistry(); 27 | jest.useFakeTimers(); 28 | 29 | React = require('react'); 30 | ReactDOM = require('react-dom'); 31 | const testUtils = require('./utils'); 32 | act = testUtils.act; 33 | const baseRender = testUtils.render; 34 | 35 | render = (element, container) => 36 | baseRender({element}, { container }); 37 | 38 | TransitionGroup = require('../src/TransitionGroup'); 39 | 40 | container = document.createElement('div'); 41 | consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); 42 | }); 43 | 44 | afterEach(() => { 45 | consoleErrorSpy.mockRestore(); 46 | jest.useRealTimers(); 47 | }); 48 | 49 | it('should clean-up silently after the timeout elapses', () => { 50 | render( 51 | 52 | 53 | , 54 | container 55 | ); 56 | 57 | const transitionGroupDiv = container.childNodes[0]; 58 | 59 | expect(transitionGroupDiv.childNodes.length).toBe(1); 60 | 61 | render( 62 | 63 | 64 | , 65 | container 66 | ); 67 | 68 | expect(transitionGroupDiv.childNodes.length).toBe(2); 69 | expect(transitionGroupDiv.childNodes[0].id).toBe('two'); 70 | expect(transitionGroupDiv.childNodes[1].id).toBe('one'); 71 | 72 | act(() => { 73 | jest.runAllTimers(); 74 | }); 75 | 76 | // No warnings 77 | expect(consoleErrorSpy).not.toHaveBeenCalled(); 78 | 79 | // The leaving child has been removed 80 | expect(transitionGroupDiv.childNodes.length).toBe(1); 81 | expect(transitionGroupDiv.childNodes[0].id).toBe('two'); 82 | }); 83 | 84 | it('should keep both sets of DOM nodes around', () => { 85 | render( 86 | 87 | 88 | , 89 | container 90 | ); 91 | 92 | const transitionGroupDiv = container.childNodes[0]; 93 | 94 | expect(transitionGroupDiv.childNodes.length).toBe(1); 95 | 96 | render( 97 | 98 | 99 | , 100 | container 101 | ); 102 | 103 | expect(transitionGroupDiv.childNodes.length).toBe(2); 104 | expect(transitionGroupDiv.childNodes[0].id).toBe('two'); 105 | expect(transitionGroupDiv.childNodes[1].id).toBe('one'); 106 | }); 107 | 108 | it('should switch transitionLeave from false to true', () => { 109 | render( 110 | 111 | 112 | , 113 | container 114 | ); 115 | 116 | const transitionGroupDiv = container.childNodes[0]; 117 | 118 | expect(transitionGroupDiv.childNodes.length).toBe(1); 119 | 120 | render( 121 | 122 | 123 | , 124 | container 125 | ); 126 | 127 | act(() => { 128 | jest.runAllTimers(); 129 | }); 130 | 131 | expect(transitionGroupDiv.childNodes.length).toBe(1); 132 | 133 | render( 134 | 135 | 136 | , 137 | container 138 | ); 139 | 140 | expect(transitionGroupDiv.childNodes.length).toBe(2); 141 | expect(transitionGroupDiv.childNodes[0].id).toBe('three'); 142 | expect(transitionGroupDiv.childNodes[1].id).toBe('two'); 143 | }); 144 | 145 | it('should work with a null child', () => { 146 | render({[null]}, container); 147 | }); 148 | 149 | it('should work with a child which renders as null', () => { 150 | const NullComponent = () => null; 151 | // Testing the whole lifecycle of entering and exiting, 152 | // because those lifecycle methods used to fail when the DOM node was null. 153 | render(, container); 154 | render( 155 | 156 | 157 | 158 | 159 | , 160 | container 161 | ); 162 | render(, container); 163 | }); 164 | 165 | it('should transition from one to null', () => { 166 | render( 167 | 168 | 169 | , 170 | container 171 | ); 172 | 173 | const transitionGroupDiv = container.childNodes[0]; 174 | 175 | expect(transitionGroupDiv.childNodes.length).toBe(1); 176 | 177 | render({null}, container); 178 | 179 | // (Here, we expect the original child to stick around but test that no 180 | // exception is thrown) 181 | expect(transitionGroupDiv.childNodes.length).toBe(1); 182 | expect(transitionGroupDiv.childNodes[0].id).toBe('one'); 183 | }); 184 | 185 | it('should transition from false to one', () => { 186 | render({false}, container); 187 | 188 | const transitionGroupDiv = container.childNodes[0]; 189 | 190 | expect(transitionGroupDiv.childNodes.length).toBe(0); 191 | 192 | render( 193 | 194 | 195 | , 196 | container 197 | ); 198 | 199 | expect(transitionGroupDiv.childNodes.length).toBe(1); 200 | expect(transitionGroupDiv.childNodes[0].id).toBe('one'); 201 | }); 202 | 203 | it('should clear transition timeouts when unmounted', () => { 204 | class Component extends React.Component { 205 | render() { 206 | return {this.props.children}; 207 | } 208 | } 209 | 210 | render(, container); 211 | render( 212 | 213 | 214 | , 215 | container 216 | ); 217 | 218 | ReactDOM.unmountComponentAtNode(container); 219 | 220 | // Testing that no exception is thrown here, as the timeout has been cleared. 221 | act(() => { 222 | jest.runAllTimers(); 223 | }); 224 | }); 225 | 226 | it('should handle unmounted elements properly', () => { 227 | class Child extends React.Component { 228 | render() { 229 | if (!this.props.show) return null; 230 | return
; 231 | } 232 | } 233 | 234 | class Component extends React.Component { 235 | state = { showChild: true }; 236 | 237 | componentDidMount() { 238 | this.setState({ showChild: false }); 239 | } 240 | 241 | render() { 242 | return ( 243 | 244 | 245 | 246 | ); 247 | } 248 | } 249 | 250 | render(, container); 251 | 252 | // Testing that no exception is thrown here, as the timeout has been cleared. 253 | act(() => { 254 | jest.runAllTimers(); 255 | }); 256 | }); 257 | 258 | it('should work with custom component wrapper cloning children', () => { 259 | const extraClassNameProp = 'wrapper-item'; 260 | class Wrapper extends React.Component { 261 | render() { 262 | return ( 263 |
264 | {React.Children.map(this.props.children, (child) => 265 | React.cloneElement(child, { className: extraClassNameProp }) 266 | )} 267 |
268 | ); 269 | } 270 | } 271 | 272 | class Child extends React.Component { 273 | render() { 274 | return
; 275 | } 276 | } 277 | 278 | class Component extends React.Component { 279 | render() { 280 | return ( 281 | 282 | 283 | 284 | ); 285 | } 286 | } 287 | 288 | render(, container); 289 | const transitionGroupDiv = container.childNodes[0]; 290 | transitionGroupDiv.childNodes.forEach((child) => { 291 | expect(hasClass(child, extraClassNameProp)).toBe(true); 292 | }); 293 | 294 | // Testing that no exception is thrown here, as the timeout has been cleared. 295 | act(() => { 296 | jest.runAllTimers(); 297 | }); 298 | }); 299 | }); 300 | -------------------------------------------------------------------------------- /test/ChildMapping-test.js: -------------------------------------------------------------------------------- 1 | let React; 2 | let ChildMapping; 3 | 4 | describe('ChildMapping', () => { 5 | beforeEach(() => { 6 | React = require('react'); 7 | ChildMapping = require('../src/utils/ChildMapping'); 8 | }); 9 | 10 | it('should support getChildMapping', () => { 11 | let oneone =
; 12 | let onetwo =
; 13 | let one = ( 14 |
15 | {oneone} 16 | {onetwo} 17 |
18 | ); 19 | let two =
foo
; 20 | let component = ( 21 |
22 | {one} 23 | {two} 24 |
25 | ); 26 | 27 | let mapping = ChildMapping.getChildMapping(component.props.children); 28 | 29 | expect(mapping['.$one'].props).toEqual(one.props); 30 | expect(mapping['.$two'].props).toEqual(two.props); 31 | }); 32 | 33 | it('should support mergeChildMappings for adding keys', () => { 34 | let prev = { 35 | one: true, 36 | two: true, 37 | }; 38 | let next = { 39 | one: true, 40 | two: true, 41 | three: true, 42 | }; 43 | expect(ChildMapping.mergeChildMappings(prev, next)).toEqual({ 44 | one: true, 45 | two: true, 46 | three: true, 47 | }); 48 | }); 49 | 50 | it('should support mergeChildMappings for removing keys', () => { 51 | let prev = { 52 | one: true, 53 | two: true, 54 | three: true, 55 | }; 56 | let next = { 57 | one: true, 58 | two: true, 59 | }; 60 | expect(ChildMapping.mergeChildMappings(prev, next)).toEqual({ 61 | one: true, 62 | two: true, 63 | three: true, 64 | }); 65 | }); 66 | 67 | it('should support mergeChildMappings for adding and removing', () => { 68 | let prev = { 69 | one: true, 70 | two: true, 71 | three: true, 72 | }; 73 | let next = { 74 | one: true, 75 | two: true, 76 | four: true, 77 | }; 78 | expect(ChildMapping.mergeChildMappings(prev, next)).toEqual({ 79 | one: true, 80 | two: true, 81 | three: true, 82 | four: true, 83 | }); 84 | }); 85 | 86 | it('should reconcile overlapping insertions and deletions', () => { 87 | let prev = { 88 | one: true, 89 | two: true, 90 | four: true, 91 | five: true, 92 | }; 93 | let next = { 94 | one: true, 95 | two: true, 96 | three: true, 97 | five: true, 98 | }; 99 | expect(ChildMapping.mergeChildMappings(prev, next)).toEqual({ 100 | one: true, 101 | two: true, 102 | three: true, 103 | four: true, 104 | five: true, 105 | }); 106 | }); 107 | 108 | it('should support mergeChildMappings with undefined input', () => { 109 | let prev = { 110 | one: true, 111 | two: true, 112 | }; 113 | 114 | let next; 115 | 116 | expect(ChildMapping.mergeChildMappings(prev, next)).toEqual({ 117 | one: true, 118 | two: true, 119 | }); 120 | 121 | prev = undefined; 122 | 123 | next = { 124 | three: true, 125 | four: true, 126 | }; 127 | 128 | expect(ChildMapping.mergeChildMappings(prev, next)).toEqual({ 129 | three: true, 130 | four: true, 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/SSR-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | // test that import does not crash 6 | import * as ReactTransitionGroup from '../src'; // eslint-disable-line no-unused-vars 7 | 8 | describe('SSR', () => { 9 | it('should import react-transition-group in node env', () => {}); 10 | }); 11 | -------------------------------------------------------------------------------- /test/SwitchTransition-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { act, render } from './utils'; 4 | 5 | import Transition, { ENTERED } from '../src/Transition'; 6 | import SwitchTransition from '../src/SwitchTransition'; 7 | 8 | describe('SwitchTransition', () => { 9 | let log, Parent; 10 | beforeEach(() => { 11 | log = []; 12 | let events = { 13 | onEnter: (m) => log.push(m ? 'appear' : 'enter'), 14 | onEntering: (m) => log.push(m ? 'appearing' : 'entering'), 15 | onEntered: (m) => log.push(m ? 'appeared' : 'entered'), 16 | onExit: () => log.push('exit'), 17 | onExiting: () => log.push('exiting'), 18 | onExited: () => log.push('exited'), 19 | }; 20 | 21 | const nodeRef = React.createRef(); 22 | Parent = function Parent({ on, rendered = true }) { 23 | return ( 24 | 25 | {rendered ? ( 26 | 32 | {on ? 'first' : 'second'} 33 | 34 | ) : null} 35 | 36 | ); 37 | }; 38 | 39 | jest.useFakeTimers(); 40 | }); 41 | 42 | afterEach(() => { 43 | jest.useRealTimers(); 44 | }); 45 | 46 | it('should have default status ENTERED', () => { 47 | const nodeRef = React.createRef(); 48 | render( 49 | 50 | 51 | {(status) => { 52 | return status: {status}; 53 | }} 54 | 55 | 56 | ); 57 | 58 | expect(nodeRef.current.textContent).toBe(`status: ${ENTERED}`); 59 | }); 60 | 61 | it('should have default mode: out-in', () => { 62 | const firstNodeRef = React.createRef(); 63 | const secondNodeRef = React.createRef(); 64 | const { rerender } = render( 65 | 66 | 67 | {(status) => { 68 | return first status: {status}; 69 | }} 70 | 71 | 72 | ); 73 | rerender( 74 | 75 | 76 | {(status) => { 77 | return second status: {status}; 78 | }} 79 | 80 | 81 | ); 82 | 83 | expect(firstNodeRef.current.textContent).toBe('first status: exiting'); 84 | expect(secondNodeRef.current).toBe(null); 85 | }); 86 | 87 | it('should work without childs', () => { 88 | const nodeRef = React.createRef(); 89 | expect(() => { 90 | render( 91 | 92 | 93 | 94 | 95 | 96 | ); 97 | }).not.toThrow(); 98 | }); 99 | 100 | it('should switch between components on change state', () => { 101 | const { container, setProps } = render(); 102 | 103 | expect(container.textContent).toBe('first'); 104 | setProps({ on: false }); 105 | expect(log).toEqual(['exit', 'exiting']); 106 | act(() => { 107 | jest.runAllTimers(); 108 | }); 109 | act(() => { 110 | jest.runAllTimers(); 111 | }); 112 | expect(log).toEqual([ 113 | 'exit', 114 | 'exiting', 115 | 'exited', 116 | 'enter', 117 | 'entering', 118 | 'entered', 119 | ]); 120 | expect(container.textContent).toBe('second'); 121 | }); 122 | 123 | it('should switch between null and component', () => { 124 | const { container, setProps } = render( 125 | 126 | ); 127 | 128 | expect(container.textContent).toBe(''); 129 | 130 | jest.useFakeTimers(); 131 | 132 | setProps({ rendered: true }); 133 | act(() => { 134 | jest.runAllTimers(); 135 | }); 136 | expect(log).toEqual(['enter', 'entering', 'entered']); 137 | expect(container.textContent).toBe('first'); 138 | 139 | setProps({ on: false, rendered: true }); 140 | act(() => { 141 | jest.runAllTimers(); 142 | }); 143 | act(() => { 144 | jest.runAllTimers(); 145 | }); 146 | expect(log).toEqual([ 147 | 'enter', 148 | 'entering', 149 | 'entered', 150 | 'exit', 151 | 'exiting', 152 | 'exited', 153 | 'enter', 154 | 'entering', 155 | 'entered', 156 | ]); 157 | 158 | expect(container.textContent).toBe('second'); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /test/Transition-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { render, waitFor } from './utils'; 5 | 6 | import Transition, { 7 | UNMOUNTED, 8 | EXITED, 9 | ENTERING, 10 | ENTERED, 11 | EXITING, 12 | } from '../src/Transition'; 13 | 14 | expect.extend({ 15 | toExist(received) { 16 | const pass = received != null; 17 | return pass 18 | ? { 19 | message: () => `expected ${received} to be null or undefined`, 20 | pass: true, 21 | } 22 | : { 23 | message: () => `expected ${received} not to be null or undefined`, 24 | pass: false, 25 | }; 26 | }, 27 | }); 28 | 29 | describe('Transition', () => { 30 | it('should not transition on mount', () => { 31 | const nodeRef = React.createRef(); 32 | render( 33 | { 38 | throw new Error('should not Enter'); 39 | }} 40 | > 41 | {(status) =>
status: {status}
} 42 |
43 | ); 44 | 45 | expect(nodeRef.current.textContent).toEqual(`status: ${ENTERED}`); 46 | }); 47 | 48 | it('should transition on mount with `appear`', (done) => { 49 | const nodeRef = React.createRef(); 50 | render( 51 | { 56 | throw Error('Animated!'); 57 | }} 58 | > 59 |
60 | 61 | ); 62 | 63 | render( 64 | done()} 70 | > 71 |
72 | 73 | ); 74 | }); 75 | 76 | it('should pass filtered props to children', () => { 77 | class Child extends React.Component { 78 | render() { 79 | return ( 80 |
81 | foo: {this.props.foo}, bar: {this.props.bar} 82 |
83 | ); 84 | } 85 | } 86 | const nodeRef = React.createRef(); 87 | render( 88 | {}} 100 | onEnter={() => {}} 101 | onEntering={() => {}} 102 | onEntered={() => {}} 103 | onExit={() => {}} 104 | onExiting={() => {}} 105 | onExited={() => {}} 106 | > 107 | 108 | 109 | ); 110 | 111 | expect(nodeRef.current.textContent).toBe('foo: foo, bar: bar'); 112 | }); 113 | 114 | it('should allow addEndListener instead of timeouts', async () => { 115 | let listener = jest.fn((end) => setTimeout(end, 0)); 116 | let done = false; 117 | 118 | const nodeRef = React.createRef(); 119 | const { setProps } = render( 120 | { 124 | expect(listener).toHaveBeenCalledTimes(1); 125 | done = true; 126 | }} 127 | > 128 |
129 | 130 | ); 131 | 132 | setProps({ in: true }); 133 | 134 | await waitFor(() => { 135 | expect(done).toEqual(true); 136 | }); 137 | }); 138 | 139 | it('should fallback to timeouts with addEndListener', async () => { 140 | let calledEnd = false; 141 | let done = false; 142 | let listener = (end) => 143 | setTimeout(() => { 144 | calledEnd = true; 145 | end(); 146 | }, 100); 147 | 148 | const nodeRef = React.createRef(); 149 | const { setProps } = render( 150 | { 155 | expect(calledEnd).toEqual(false); 156 | done = true; 157 | }} 158 | > 159 |
160 | 161 | ); 162 | 163 | setProps({ in: true }); 164 | 165 | await waitFor(() => { 166 | expect(done).toEqual(true); 167 | }); 168 | }); 169 | 170 | it('should mount/unmount immediately if not have enter/exit timeout', async () => { 171 | const nodeRef = React.createRef(); 172 | let done = false; 173 | const { setProps } = render( 174 | 175 | {(status) =>
status: {status}
} 176 |
177 | ); 178 | 179 | expect(nodeRef.current.textContent).toEqual(`status: ${ENTERED}`); 180 | let calledAfterTimeout = false; 181 | setTimeout(() => { 182 | calledAfterTimeout = true; 183 | }, 10); 184 | setProps({ 185 | in: false, 186 | onExited() { 187 | expect(nodeRef.current.textContent).toEqual(`status: ${EXITED}`); 188 | if (calledAfterTimeout) { 189 | throw new Error('wrong timeout'); 190 | } 191 | done = true; 192 | }, 193 | }); 194 | 195 | await waitFor(() => { 196 | expect(done).toEqual(true); 197 | }); 198 | }); 199 | 200 | it('should use `React.findDOMNode` when `nodeRef` is not provided', () => { 201 | const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); 202 | const findDOMNodeSpy = jest.spyOn(ReactDOM, 'findDOMNode'); 203 | 204 | render( 205 | 206 |
207 | 208 | ); 209 | 210 | expect(findDOMNodeSpy).toHaveBeenCalled(); 211 | findDOMNodeSpy.mockRestore(); 212 | consoleSpy.mockRestore(); 213 | }); 214 | 215 | it('should not use `React.findDOMNode` when `nodeRef` is provided', () => { 216 | const findDOMNodeSpy = jest.spyOn(ReactDOM, 'findDOMNode'); 217 | 218 | const nodeRef = React.createRef(); 219 | render( 220 | 221 |
222 | 223 | ); 224 | 225 | expect(findDOMNodeSpy).not.toHaveBeenCalled(); 226 | findDOMNodeSpy.mockRestore(); 227 | }); 228 | 229 | describe('appearing timeout', () => { 230 | it('should use enter timeout if appear not set', async () => { 231 | let calledBeforeEntered = false; 232 | let done = false; 233 | setTimeout(() => { 234 | calledBeforeEntered = true; 235 | }, 10); 236 | const nodeRef = React.createRef(); 237 | const { setProps } = render( 238 | 244 |
245 | 246 | ); 247 | 248 | setProps({ 249 | onEntered() { 250 | if (calledBeforeEntered) { 251 | done = true; 252 | } else { 253 | throw new Error('wrong timeout'); 254 | } 255 | }, 256 | }); 257 | 258 | await waitFor(() => { 259 | expect(done).toEqual(true); 260 | }); 261 | }); 262 | 263 | it('should use appear timeout if appear is set', async () => { 264 | let done = false; 265 | const nodeRef = React.createRef(); 266 | const { setProps } = render( 267 | 273 |
274 | 275 | ); 276 | 277 | let isCausedLate = false; 278 | setTimeout(() => { 279 | isCausedLate = true; 280 | }, 15); 281 | 282 | setProps({ 283 | onEntered() { 284 | if (isCausedLate) { 285 | throw new Error('wrong timeout'); 286 | } else { 287 | done = true; 288 | } 289 | }, 290 | }); 291 | 292 | await waitFor(() => { 293 | expect(done).toEqual(true); 294 | }); 295 | }); 296 | }); 297 | 298 | describe('entering', () => { 299 | it('should fire callbacks', async () => { 300 | let callOrder = []; 301 | let done = false; 302 | let onEnter = jest.fn(() => callOrder.push('onEnter')); 303 | let onEntering = jest.fn(() => callOrder.push('onEntering')); 304 | const nodeRef = React.createRef(); 305 | const { setProps } = render( 306 | 307 | {(status) =>
status: {status}
} 308 |
309 | ); 310 | 311 | expect(nodeRef.current.textContent).toEqual(`status: ${EXITED}`); 312 | 313 | setProps({ 314 | in: true, 315 | 316 | onEnter, 317 | 318 | onEntering, 319 | 320 | onEntered() { 321 | expect(onEnter).toHaveBeenCalledTimes(1); 322 | expect(onEntering).toHaveBeenCalledTimes(1); 323 | expect(callOrder).toEqual(['onEnter', 'onEntering']); 324 | done = true; 325 | }, 326 | }); 327 | 328 | await waitFor(() => { 329 | expect(done).toEqual(true); 330 | }); 331 | }); 332 | 333 | it('should move to each transition state', async () => { 334 | let count = 0; 335 | const nodeRef = React.createRef(); 336 | const { setProps } = render( 337 | 338 | {(status) =>
status: {status}
} 339 |
340 | ); 341 | 342 | expect(nodeRef.current.textContent).toEqual(`status: ${EXITED}`); 343 | 344 | setProps({ 345 | in: true, 346 | 347 | onEnter() { 348 | count++; 349 | expect(nodeRef.current.textContent).toEqual(`status: ${EXITED}`); 350 | }, 351 | 352 | onEntering() { 353 | count++; 354 | expect(nodeRef.current.textContent).toEqual(`status: ${ENTERING}`); 355 | }, 356 | 357 | onEntered() { 358 | expect(nodeRef.current.textContent).toEqual(`status: ${ENTERED}`); 359 | }, 360 | }); 361 | 362 | await waitFor(() => { 363 | expect(count).toEqual(2); 364 | }); 365 | }); 366 | }); 367 | 368 | describe('exiting', () => { 369 | it('should fire callbacks', async () => { 370 | let callOrder = []; 371 | let done = false; 372 | let onExit = jest.fn(() => callOrder.push('onExit')); 373 | let onExiting = jest.fn(() => callOrder.push('onExiting')); 374 | const nodeRef = React.createRef(); 375 | const { setProps } = render( 376 | 377 | {(status) =>
status: {status}
} 378 |
379 | ); 380 | 381 | expect(nodeRef.current.textContent).toEqual(`status: ${ENTERED}`); 382 | 383 | setProps({ 384 | in: false, 385 | 386 | onExit, 387 | 388 | onExiting, 389 | 390 | onExited() { 391 | expect(onExit).toHaveBeenCalledTimes(1); 392 | expect(onExiting).toHaveBeenCalledTimes(1); 393 | expect(callOrder).toEqual(['onExit', 'onExiting']); 394 | done = true; 395 | }, 396 | }); 397 | 398 | await waitFor(() => { 399 | expect(done).toEqual(true); 400 | }); 401 | }); 402 | 403 | it('should move to each transition state', async () => { 404 | let count = 0; 405 | let done = false; 406 | const nodeRef = React.createRef(); 407 | const { setProps } = render( 408 | 409 | {(status) =>
status: {status}
} 410 |
411 | ); 412 | 413 | expect(nodeRef.current.textContent).toEqual(`status: ${ENTERED}`); 414 | 415 | setProps({ 416 | in: false, 417 | 418 | onExit() { 419 | count++; 420 | expect(nodeRef.current.textContent).toEqual(`status: ${ENTERED}`); 421 | }, 422 | 423 | onExiting() { 424 | count++; 425 | expect(nodeRef.current.textContent).toEqual(`status: ${EXITING}`); 426 | }, 427 | 428 | onExited() { 429 | expect(nodeRef.current.textContent).toEqual(`status: ${EXITED}`); 430 | expect(count).toEqual(2); 431 | done = true; 432 | }, 433 | }); 434 | 435 | await waitFor(() => { 436 | expect(done).toEqual(true); 437 | }); 438 | }); 439 | }); 440 | 441 | describe('mountOnEnter', () => { 442 | class MountTransition extends React.Component { 443 | nodeRef = React.createRef(); 444 | 445 | render() { 446 | const { ...props } = this.props; 447 | delete props.initialIn; 448 | 449 | return ( 450 | 452 | (this.transition = this.transition || transition) 453 | } 454 | nodeRef={this.nodeRef} 455 | mountOnEnter 456 | in={this.props.in} 457 | timeout={10} 458 | {...props} 459 | > 460 | {(status) =>
status: {status}
} 461 |
462 | ); 463 | } 464 | 465 | getStatus = () => { 466 | return this.transition.state.status; 467 | }; 468 | } 469 | 470 | it('should mount when entering', (done) => { 471 | const { container, setProps } = render( 472 | { 475 | expect(container.textContent).toEqual(`status: ${EXITED}`); 476 | done(); 477 | }} 478 | /> 479 | ); 480 | 481 | expect(container.textContent).toEqual(''); 482 | 483 | setProps({ in: true }); 484 | }); 485 | 486 | it('should stay mounted after exiting', async () => { 487 | let entered = false; 488 | let exited = false; 489 | const { container, setProps } = render( 490 | { 493 | entered = true; 494 | }} 495 | onExited={() => { 496 | exited = true; 497 | }} 498 | /> 499 | ); 500 | 501 | expect(container.textContent).toEqual(''); 502 | setProps({ in: true }); 503 | 504 | await waitFor(() => { 505 | expect(entered).toEqual(true); 506 | }); 507 | expect(container.textContent).toEqual(`status: ${ENTERED}`); 508 | 509 | setProps({ in: false }); 510 | 511 | await waitFor(() => { 512 | expect(exited).toEqual(true); 513 | }); 514 | expect(container.textContent).toEqual(`status: ${EXITED}`); 515 | }); 516 | }); 517 | 518 | describe('unmountOnExit', () => { 519 | class UnmountTransition extends React.Component { 520 | nodeRef = React.createRef(); 521 | 522 | render() { 523 | const { ...props } = this.props; 524 | delete props.initialIn; 525 | 526 | return ( 527 | 529 | (this.transition = this.transition || transition) 530 | } 531 | nodeRef={this.nodeRef} 532 | unmountOnExit 533 | in={this.props.in} 534 | timeout={10} 535 | {...props} 536 | > 537 |
538 | 539 | ); 540 | } 541 | 542 | getStatus = () => { 543 | return this.transition.state.status; 544 | }; 545 | } 546 | 547 | it('should mount when entering', async () => { 548 | let done = false; 549 | const instanceRef = React.createRef(); 550 | const { setProps } = render( 551 | { 555 | expect(instanceRef.current.getStatus()).toEqual(EXITED); 556 | expect(instanceRef.current.nodeRef.current).toExist(); 557 | 558 | done = true; 559 | }} 560 | /> 561 | ); 562 | 563 | expect(instanceRef.current.getStatus()).toEqual(UNMOUNTED); 564 | expect(instanceRef.current.nodeRef.current).toBeNull(); 565 | 566 | setProps({ in: true }); 567 | 568 | await waitFor(() => { 569 | expect(done).toEqual(true); 570 | }); 571 | }); 572 | 573 | it('should unmount after exiting', async () => { 574 | let exited = false; 575 | const instanceRef = React.createRef(); 576 | const { setProps } = render( 577 | { 581 | setTimeout(() => { 582 | exited = true; 583 | }); 584 | }} 585 | /> 586 | ); 587 | 588 | expect(instanceRef.current.getStatus()).toEqual(ENTERED); 589 | expect(instanceRef.current.nodeRef.current).toExist(); 590 | 591 | setProps({ in: false }); 592 | 593 | await waitFor(() => { 594 | expect(exited).toEqual(true); 595 | }); 596 | 597 | expect(instanceRef.current.getStatus()).toEqual(UNMOUNTED); 598 | expect(instanceRef.current.nodeRef.current).not.toExist(); 599 | }); 600 | }); 601 | }); 602 | -------------------------------------------------------------------------------- /test/TransitionGroup-test.js: -------------------------------------------------------------------------------- 1 | let React; 2 | let TransitionGroup; 3 | let Transition; 4 | 5 | // Most of the real functionality is covered in other unit tests, this just 6 | // makes sure we're wired up correctly. 7 | describe('TransitionGroup', () => { 8 | let act, container, log, Child, renderStrict, render; 9 | 10 | beforeEach(() => { 11 | React = require('react'); 12 | Transition = require('../src/Transition').default; 13 | TransitionGroup = require('../src/TransitionGroup'); 14 | const testUtils = require('./utils'); 15 | act = testUtils.act; 16 | render = testUtils.render; 17 | 18 | renderStrict = (element, container) => 19 | render({element}, { container }); 20 | 21 | container = document.createElement('div'); 22 | 23 | log = []; 24 | let events = { 25 | onEnter: (m) => log.push(m ? 'appear' : 'enter'), 26 | onEntering: (m) => log.push(m ? 'appearing' : 'entering'), 27 | onEntered: (m) => log.push(m ? 'appeared' : 'entered'), 28 | onExit: () => log.push('exit'), 29 | onExiting: () => log.push('exiting'), 30 | onExited: () => log.push('exited'), 31 | }; 32 | 33 | const nodeRef = React.createRef(); 34 | Child = function Child(props) { 35 | return ( 36 | 37 | 38 | 39 | ); 40 | }; 41 | }); 42 | 43 | it('should allow null components', () => { 44 | function FirstChild(props) { 45 | const childrenArray = React.Children.toArray(props.children); 46 | return childrenArray[0] || null; 47 | } 48 | 49 | render( 50 | 51 | 52 | 53 | ); 54 | }); 55 | 56 | it('should allow callback refs', () => { 57 | const ref = jest.fn(); 58 | 59 | class Child extends React.Component { 60 | render() { 61 | return ; 62 | } 63 | } 64 | 65 | render( 66 | 67 | 68 | 69 | ); 70 | 71 | expect(ref).toHaveBeenCalled(); 72 | }); 73 | 74 | it('should work with no children', () => { 75 | renderStrict(, container); 76 | }); 77 | 78 | it('should handle transitioning correctly', () => { 79 | function Parent({ count = 1 }) { 80 | let children = []; 81 | for (let i = 0; i < count; i++) children.push(); 82 | return ( 83 | 84 | {children} 85 | 86 | ); 87 | } 88 | 89 | jest.useFakeTimers(); 90 | renderStrict(, container); 91 | 92 | act(() => { 93 | jest.runAllTimers(); 94 | }); 95 | expect(log).toEqual( 96 | // React 18 StrictEffects will call `componentDidMount` twice causing two `onEnter` calls. 97 | React.useTransition !== undefined 98 | ? ['appear', 'appear', 'appearing', 'appeared'] 99 | : ['appear', 'appearing', 'appeared'] 100 | ); 101 | 102 | log = []; 103 | renderStrict(, container); 104 | act(() => { 105 | jest.runAllTimers(); 106 | }); 107 | expect(log).toEqual( 108 | // React 18 StrictEffects will call `componentDidMount` twice causing two `onEnter` calls. 109 | React.useTransition !== undefined 110 | ? ['enter', 'enter', 'entering', 'entered'] 111 | : ['enter', 'entering', 'entered'] 112 | ); 113 | 114 | log = []; 115 | renderStrict(, container); 116 | act(() => { 117 | jest.runAllTimers(); 118 | }); 119 | expect(log).toEqual(['exit', 'exiting', 'exited']); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | global.requestAnimationFrame = function (callback) { 2 | setTimeout(callback, 0); 3 | }; 4 | -------------------------------------------------------------------------------- /test/setupAfterEnv.js: -------------------------------------------------------------------------------- 1 | import { cleanup } from '@testing-library/react/pure'; 2 | 3 | afterEach(() => { 4 | cleanup(); 5 | }); 6 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | import { render as baseRender } from '@testing-library/react/pure'; 2 | import React from 'react'; 3 | 4 | export * from '@testing-library/react'; 5 | export function render(element, options) { 6 | const result = baseRender(element, options); 7 | 8 | return { 9 | ...result, 10 | setProps(props) { 11 | result.rerender(React.cloneElement(element, props)); 12 | }, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /www/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['babel-preset-gatsby'], 3 | }; 4 | -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | public -------------------------------------------------------------------------------- /www/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /www/gatsby-config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | pathPrefix: `/react-transition-group`, 5 | siteMetadata: { 6 | title: 'React Transition Group Documentation', 7 | author: 'Jason Quense', 8 | componentPages: [ 9 | { 10 | path: '/transition', 11 | displayName: 'Transition', 12 | codeSandboxId: null, 13 | }, 14 | { 15 | path: '/css-transition', 16 | displayName: 'CSSTransition', 17 | codeSandboxId: 'm77l2vp00x', 18 | }, 19 | { 20 | path: '/switch-transition', 21 | displayName: 'SwitchTransition', 22 | codeSandboxId: 'switchtransition-component-iqm0d', 23 | }, 24 | { 25 | path: '/transition-group', 26 | displayName: 'TransitionGroup', 27 | codeSandboxId: '00rqyo26kn', 28 | }, 29 | ], 30 | }, 31 | plugins: [ 32 | 'gatsby-plugin-react-helmet', 33 | { 34 | resolve: 'gatsby-source-filesystem', 35 | options: { 36 | path: path.join(__dirname, 'src/pages'), 37 | name: 'pages', 38 | }, 39 | }, 40 | { 41 | resolve: 'gatsby-source-filesystem', 42 | options: { 43 | path: path.join(__dirname, '../src'), 44 | name: 'components', 45 | }, 46 | }, 47 | { 48 | resolve: 'gatsby-transformer-remark', 49 | options: { 50 | plugins: ['gatsby-remark-prismjs'], 51 | }, 52 | }, 53 | 'gatsby-transformer-react-docgen', 54 | 'gatsby-plugin-sass', 55 | ], 56 | }; 57 | -------------------------------------------------------------------------------- /www/gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const config = require('./gatsby-config'); 3 | 4 | exports.createPages = ({ actions, graphql }) => { 5 | const { createPage } = actions; 6 | const componentTemplate = path.join( 7 | __dirname, 8 | 'src', 9 | 'templates', 10 | 'component.js' 11 | ); 12 | return new Promise((resolve, reject) => { 13 | resolve( 14 | graphql(` 15 | { 16 | allComponentMetadata { 17 | edges { 18 | node { 19 | displayName 20 | } 21 | } 22 | } 23 | } 24 | `).then((result) => { 25 | if (result.errors) { 26 | reject(result.errors); 27 | } 28 | const { componentPages } = config.siteMetadata; 29 | result.data.allComponentMetadata.edges 30 | .filter(({ node: { displayName } }) => 31 | componentPages.some((page) => page.displayName === displayName) 32 | ) 33 | .forEach(({ node: { displayName } }) => { 34 | createPage({ 35 | path: componentPages.find( 36 | (page) => page.displayName === displayName 37 | ).path, 38 | component: componentTemplate, 39 | context: { 40 | displayName, 41 | }, 42 | }); 43 | }); 44 | }) 45 | ); 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "react-transition-group-docs", 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "NODE_ENV=production gatsby build --prefix-paths", 10 | "deploy": "npm run build && gh-pages -d public", 11 | "develop": "gatsby develop" 12 | }, 13 | "author": "", 14 | "license": "MIT", 15 | "engines": { 16 | "node": "<=16" 17 | }, 18 | "dependencies": { 19 | "@babel/core": "^7.3.4", 20 | "babel-preset-gatsby": "^2.7.0", 21 | "bootstrap": "^4.3.1", 22 | "gatsby": "^2.1.22", 23 | "gatsby-plugin-react-helmet": "^3.0.10", 24 | "gatsby-plugin-sass": "^2.0.10", 25 | "gatsby-remark-prismjs": "^3.2.4", 26 | "gatsby-source-filesystem": "^2.0.23", 27 | "gatsby-transformer-react-docgen": "^3.0.5", 28 | "gatsby-transformer-remark": "^2.3.0", 29 | "lodash": "^4.17.19", 30 | "prismjs": "^1.25.0", 31 | "react": "^16.8.3", 32 | "react-bootstrap": "^1.0.0-beta.5", 33 | "react-dom": "^16.8.3", 34 | "react-helmet": "^5.2.0", 35 | "sass": "^1.49.7" 36 | }, 37 | "devDependencies": { 38 | "gh-pages": "^2.0.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /www/src/components/Example.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Container } from 'react-bootstrap'; 4 | 5 | const propTypes = { 6 | codeSandbox: PropTypes.shape({ 7 | title: PropTypes.string.isRequired, 8 | id: PropTypes.string.isRequired, 9 | }).isRequired, 10 | }; 11 | 12 | const Example = ({ codeSandbox }) => ( 13 |
14 | 15 |

Example

16 |
17 |