diff --git a/.babelrc b/.babelrc deleted file mode 100644 index e868af14..00000000 --- a/.babelrc +++ /dev/null @@ -1,15 +0,0 @@ -{ - "presets": [ - ["latest", { - "es2015": { "loose": true } - }], - "stage-2", - "react" - ], - "plugins": [ - "dev-expression", - "add-module-exports", - "transform-object-assign", - ["transform-react-remove-prop-types", { "mode": "wrap" }] - ] -} diff --git a/.babelrc.js b/.babelrc.js new file mode 100644 index 00000000..a12cc6d4 --- /dev/null +++ b/.babelrc.js @@ -0,0 +1,11 @@ +module.exports = { + presets: [['babel-preset-jason', { runtime: false }]], + plugins: [ + ['babel-plugin-transform-react-remove-prop-types', { mode: 'wrap' }], + ], + env: { + esm: { + presets: [['babel-preset-jason', { modules: false }]], + }, + }, +}; diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..38ee6bb2 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +**/node_modules +www/.cache/ +www/public/ +lib diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 1062ba58..00000000 --- a/.eslintrc +++ /dev/null @@ -1,10 +0,0 @@ - -parser: babel-eslint -extends: - - jason/react - - plugin:jsx-a11y/recommended -env: - node: true - browser: true -plugins: - - jsx-a11y diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 00000000..0428d894 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,21 @@ +parser: babel-eslint +extends: + - jason/react + - plugin:jsx-a11y/recommended + - prettier +settings: + react: + version: detect +env: + node: true + browser: true +plugins: + - jsx-a11y + +overrides: + - files: www/**/* + env: + es6: true + - files: stories/**/* + rules: + no-console: off diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 52dbcddb..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,9 +0,0 @@ -**Do you want to request a *feature* or report a *bug*?** - -**What is the current behavior?** - -**If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://jsfiddle.net or similar (template: https://jsfiddle.net/m4mb9Lg3/4/).** - -**What is the expected behavior?** - -**Which versions, and which browser / OS are affected by this issue? Did this work in previous versions?** diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 00000000..b66cbd90 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,18 @@ +--- +name: Bug report +about: Something isn't working as expected. +--- + +> What is the current behavior? + + + +> What is the expected behavior? + + + + + +> Could you provide a [CodeSandbox](https://codesandbox.io/) demo reproducing the bug? + + diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 00000000..d21cee25 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,8 @@ +--- +name: Feature request +about: I have a suggestion on how to improve the library. +--- + +> What would you like improved? + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..08d7ad2d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,76 @@ +name: CI + +on: + push: + branches: [master, alpha] + pull_request: + branches: [master, alpha] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + # Otherwise how would we know if a specific React version caused the failure? + fail-fast: false + matrix: + REACT_DIST: [16, 17, 18] + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 14 + uses: actions/setup-node@v2 + with: + node-version: 14 + cache: 'npm' + - run: yarn + - run: yarn add react@${{ matrix.REACT_DIST }} react-dom@${{ matrix.REACT_DIST }} + - run: yarn add @testing-library/react@12 + if: matrix.REACT_DIST == '17' || matrix.REACT_DIST == '16' + - run: yarn --cwd www + # Test whether the web page can be built successfully or not + - run: yarn --cwd www build + - run: yarn test + - name: Dry release + uses: cycjimmy/semantic-release-action@v2 + with: + dry_run: true + semantic_version: 17 + branches: | + [ + 'master', + {name: 'alpha', prerelease: true} + ] + env: + # These are not available on forks but we need them on actual runs to verify everything is set up. + # Otherwise we might fail in the middle of a release + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + release: + needs: test + runs-on: ubuntu-latest + if: ${{ github.repository == 'reactjs/react-transition-group' && + contains('refs/heads/master,refs/heads/alpha', + github.ref) && github.event_name == 'push' }} + steps: + - uses: styfle/cancel-workflow-action@0.9.0 + - uses: actions/checkout@v2 + - name: Use Node.js 14 + uses: actions/setup-node@v2 + with: + node-version: 14 + cache: 'npm' + - run: yarn + - run: yarn build + - name: Release + uses: cycjimmy/semantic-release-action@v2 + with: + semantic_version: 17 + branches: | + [ + 'master', + {name: 'alpha', prerelease: true} + ] + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 2c99def8..66b7715d 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ jspm_packages # Optional REPL history .node_repl_history + +# Visual Studio Code configuration +.vscode diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..c979173c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +**/node_modules +www/.cache/ +www/public/ +lib +*.md \ No newline at end of file diff --git a/.size-snapshot.json b/.size-snapshot.json new file mode 100644 index 00000000..28cd868b --- /dev/null +++ b/.size-snapshot.json @@ -0,0 +1,12 @@ +{ + "./lib/dist/react-transition-group.js": { + "bundled": 82684, + "minified": 22426, + "gzipped": 6876 + }, + "./lib/dist/react-transition-group.min.js": { + "bundled": 47269, + "minified": 14623, + "gzipped": 4616 + } +} diff --git a/.storybook/config.js b/.storybook/config.js deleted file mode 100644 index 9154670a..00000000 --- a/.storybook/config.js +++ /dev/null @@ -1,7 +0,0 @@ -import { configure } from '@storybook/react'; - -function loadStories() { - require('../stories'); -} - -configure(loadStories, module); diff --git a/.storybook/main.js b/.storybook/main.js new file mode 100644 index 00000000..6d952e40 --- /dev/null +++ b/.storybook/main.js @@ -0,0 +1,14 @@ +const { plugins, rules } = require('webpack-atoms'); + +module.exports = { + stories: ['../stories/index.js'], + webpackFinal: (config) => { + config.module = { + rules: [rules.js(), rules.astroturf(), rules.css({ extract: false })], + }; + + config.plugins.push(plugins.extractCss({ disable: true })); + + return config; + }, +}; diff --git a/.storybook/preview.js b/.storybook/preview.js new file mode 100644 index 00000000..203766f9 --- /dev/null +++ b/.storybook/preview.js @@ -0,0 +1,9 @@ +import React from 'react'; + +export const decorators = [ + (Story) => ( + + + + ), +]; diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js deleted file mode 100644 index 738c133e..00000000 --- a/.storybook/webpack.config.js +++ /dev/null @@ -1,15 +0,0 @@ -const { plugins, rules } = require('webpack-atoms'); - -module.exports = (config) => { - config.module = { - rules: [ - rules.js.inlineCss(), - rules.css({ modules: true }), - ], - }; - - config.plugins.push( - plugins.extractText({ disable: true }) - ) - return config; -}; diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5b0009fd..00000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -sudo: false - -language: node_js -node_js: - - stable - -cache: - yarn: true - directories: - - node_modules - -branches: - only: - - master diff --git a/CHANGELOG.md b/CHANGELOG.md index c7e7f4f1..c8006511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,268 @@ +## [4.4.5](https://github.com/reactjs/react-transition-group/compare/v4.4.4...v4.4.5) (2022-08-01) + + +### Bug Fixes + +* 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)) + +## [4.4.4](https://github.com/reactjs/react-transition-group/compare/v4.4.3...v4.4.4) (2022-07-30) + + +### Bug Fixes + +* missing build files ([#845](https://github.com/reactjs/react-transition-group/issues/845)) ([97af789](https://github.com/reactjs/react-transition-group/commit/97af7893b0a5bbf69211bc3287aee814123ddeea)) + +## [4.4.3](https://github.com/reactjs/react-transition-group/compare/v4.4.2...v4.4.3) (2022-07-30) + + +### Bug Fixes + +* 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)) + +## [4.4.2](https://github.com/reactjs/react-transition-group/compare/v4.4.1...v4.4.2) (2021-05-29) + + +### Bug Fixes + +* `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)) + +## [4.4.1](https://github.com/reactjs/react-transition-group/compare/v4.4.0...v4.4.1) (2020-05-06) + + +### Bug Fixes + +* transition SSR ([#619](https://github.com/reactjs/react-transition-group/issues/619)) ([2722bb6](https://github.com/reactjs/react-transition-group/commit/2722bb6b755943b8292f0f2bc2fdca55df5c28f0)) + +# [4.4.0](https://github.com/reactjs/react-transition-group/compare/v4.3.0...v4.4.0) (2020-05-05) + + +### Features + +* 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)) + - 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: + + ```jsx + import React from "react" + import { CSSTransition } from "react-transition-group" + + const MyComponent = () => { + const nodeRef = React.useRef(null) + return ( + +
Fade
+
+ ) + } + ``` +### Bug Fixes + +* set the values of constants attached to `Transition` to match the exported ones ([#554](https://github.com/reactjs/react-transition-group/pull/554)) + +# [4.3.0](https://github.com/reactjs/react-transition-group/compare/v4.2.2...v4.3.0) (2019-09-05) + + +### Features + +* upgrade dom-helpers ([#549](https://github.com/reactjs/react-transition-group/issues/549)) ([b017e18](https://github.com/reactjs/react-transition-group/commit/b017e18)) + +## [4.2.2](https://github.com/reactjs/react-transition-group/compare/v4.2.1...v4.2.2) (2019-08-02) + + +### Bug Fixes + +* 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)) + +## [4.2.1](https://github.com/reactjs/react-transition-group/compare/v4.2.0...v4.2.1) (2019-07-02) + + +### Bug Fixes + +* 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)) + +# [4.2.0](https://github.com/reactjs/react-transition-group/compare/v4.1.1...v4.2.0) (2019-06-28) + + +### Features + +* add SwitchTransition component ([#470](https://github.com/reactjs/react-transition-group/issues/470)) ([c5e379d](https://github.com/reactjs/react-transition-group/commit/c5e379d)) + +## [4.1.1](https://github.com/reactjs/react-transition-group/compare/v4.1.0...v4.1.1) (2019-06-10) + + +### Bug Fixes + +* 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)) + +# [4.1.0](https://github.com/reactjs/react-transition-group/compare/v4.0.1...v4.1.0) (2019-05-30) + + +### Features + +* 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)) + +## [4.0.1](https://github.com/reactjs/react-transition-group/compare/v4.0.0...v4.0.1) (2019-05-09) + + +### Bug Fixes + +* 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)) + +# [4.0.0](https://github.com/reactjs/react-transition-group/compare/v3.0.0...v4.0.0) (2019-04-16) + + +### Features + +* 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) + + +### BREAKING CHANGES + +* in environments where esm is supported importing from commonjs requires explicitly adding the `.default` after `require()` when resolving to the esm build + +# [3.0.0](https://github.com/reactjs/react-transition-group/compare/v2.9.0...v3.0.0) (2019-04-15) + + +### Features + +* 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) + + +### BREAKING CHANGES + +* use new style react context + +```diff +// package.json +-"react": "^15.0.0", ++"react": "^16.6.0", +-"react-dom": "^15.0.0", ++"react-dom": "^16.6.0", +``` + +# [2.9.0](https://github.com/reactjs/react-transition-group/compare/v2.8.0...v2.9.0) (2019-04-06) + + +### Features + +* **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) + + +### Reverts + +* bump semantic release dependencies ([1bdcaec](https://github.com/reactjs/react-transition-group/commit/1bdcaec)) + +# [2.8.0](https://github.com/reactjs/react-transition-group/compare/v2.7.1...v2.8.0) (2019-04-02) + + +### Features + +* 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)) + +## [2.7.1](https://github.com/reactjs/react-transition-group/compare/v2.7.0...v2.7.1) (2019-03-25) + + +### Bug Fixes + +* revert tree-shaking support because it was a breaking change ([271364c](https://github.com/reactjs/react-transition-group/commit/271364c)) + +# [2.7.0](https://github.com/reactjs/react-transition-group/compare/v2.6.1...v2.7.0) (2019-03-22) + + +### Features + +* support ESM (tree-shaking) ([#455](https://github.com/reactjs/react-transition-group/issues/455)) ([ef3e357](https://github.com/reactjs/react-transition-group/commit/ef3e357)) + +## [2.6.1](https://github.com/reactjs/react-transition-group/compare/v2.6.0...v2.6.1) (2019-03-14) + + +### Bug Fixes + +* **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)) +* **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)) + +# [2.6.0](https://github.com/reactjs/react-transition-group/compare/v2.5.3...v2.6.0) (2019-02-26) + + +### Features + +* add appear timeout ([#462](https://github.com/reactjs/react-transition-group/issues/462)) ([52cdc34](https://github.com/reactjs/react-transition-group/commit/52cdc34)) + +## [2.5.3](https://github.com/reactjs/react-transition-group/compare/v2.5.2...v2.5.3) (2019-01-14) + + +### Bug Fixes + +* 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)) + +## [2.5.2](https://github.com/reactjs/react-transition-group/compare/v2.5.1...v2.5.2) (2018-12-20) + + +### Bug Fixes + +* 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) + +## [2.5.1](https://github.com/reactjs/react-transition-group/compare/v2.5.0...v2.5.1) (2018-12-10) + + +### Bug Fixes + +* 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)) + +# [2.5.0](https://github.com/reactjs/react-transition-group/compare/v2.4.0...v2.5.0) (2018-09-26) + + +### Features + +* 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)) + +# [2.4.0](https://github.com/reactjs/react-transition-group/compare/v2.3.1...v2.4.0) (2018-06-27) + + +### Features + +* remove deprecated lifecycle hooks and polyfill for older react versions ([c1ab1cf](https://github.com/reactjs/react-transition-group/commit/c1ab1cf)) + + +### Performance Improvements + +* don't reflow when there's no class to add ([d7b898d](https://github.com/reactjs/react-transition-group/commit/d7b898d)) + + +## [2.3.1](https://github.com/reactjs/react-transition-group/compare/v2.3.0...v2.3.1) (2018-04-14) + + +### Bug Fixes + +* **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)) + +## [v2.3.0] + +> 2018-03-28 + +* Added `*-done` classes to CSS Transition ([#269]) +* Reorganize docs with more interesting examples! ([#304]) +* A bunch of bug fixes + +[#269]: https://github.com/reactjs/react-transition-group/pull/269 +[#304]: https://github.com/reactjs/react-transition-group/pull/304 +[v2.3.0]: https://github.com/reactjs/react-transition-group/compare/v2.2.1...2.3.0 + ## [v2.2.1] + > 2017-09-29 -- **Patch:** Allow React v16 ([#198]) +* **Patch:** Allow React v16 ([#198]) [#198]: https://github.com/reactjs/react-transition-group/pull/198 [v2.2.1]: https://github.com/reactjs/react-transition-group/compare/v2.2.0...2.2.1 ## [v2.2.0] + > 2017-07-21 -- **Feature:** Support multiple classes in `classNames` ([#124]) -- **Docs:** fix broken link ([#127]) -- **Bugfix:** Fix Transition props pass-through ([#123]) +* **Feature:** Support multiple classes in `classNames` ([#124]) +* **Docs:** fix broken link ([#127]) +* **Bugfix:** Fix Transition props pass-through ([#123]) [#124]: https://github.com/reactjs/react-transition-group/pull/124 [#123]: https://github.com/reactjs/react-transition-group/pull/123 @@ -19,71 +270,80 @@ [v2.2.0]: https://github.com/reactjs/react-transition-group/compare/v2.1.0...2.2.0 ## [v2.1.0] + > 2017-07-06 -- **Feature:** Add back `childFactory` on `` ([#113]) -- **Bugfix:** Ensure child specified `onExited` fires in a `` ([#113]) +* **Feature:** Add back `childFactory` on `` ([#113]) +* **Bugfix:** Ensure child specified `onExited` fires in a `` ([#113]) [#113]: https://github.com/reactjs/react-transition-group/pull/113 [v2.1.0]: https://github.com/reactjs/react-transition-group/compare/v2.0.1...2.1.0 ## v2.0.2 + > 2017-07-06 -- **Fix documentation npm:** No code changes +* **Fix documentation npm:** No code changes ## v2.0.1 + > 2017-07-06 -- **Fix documentation on npm:** No code changes +* **Fix documentation on npm:** No code changes ## [v2.0.0] + > 2017-07-06 -- **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) +* **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) [#24]: https://github.com/reactjs/react-transition-group/pull/24 [v2.0.0]: https://github.com/reactjs/react-transition-group/compare/v1.2.0...v2.0.0 ## [v1.2.0] + > 2017-06-12 -- **Feature:** Dist build now includes both production and development builds ([#64]) -- **Feature:** PropTypes are now wrapped allowing for lighter weight production builds ([#69]) +* **Feature:** Dist build now includes both production and development builds ([#64]) +* **Feature:** PropTypes are now wrapped allowing for lighter weight production builds ([#69]) [#64]: https://github.com/reactjs/react-transition-group/issues/64 [#69]: https://github.com/reactjs/react-transition-group/issues/69 -[v1.1.X]: https://github.com/reactjs/react-transition-group/compare/v1.1.3...master +[v1.1.x]: https://github.com/reactjs/react-transition-group/compare/v1.1.3...master ## [v1.1.3] + > 2017-05-02 -- bonus release, no additions +* bonus release, no additions [v1.1.3]: https://github.com/reactjs/react-transition-group/compare/v1.1.2...v1.1.3 ## [v1.1.2] + > 2017-05-02 -- **Bugfix:** Fix refs on children ([#39]) +* **Bugfix:** Fix refs on children ([#39]) [v1.1.2]: https://github.com/reactjs/react-transition-group/compare/v1.1.1...v1.1.2 [#39]: https://github.com/reactjs/react-transition-group/pull/39 ## [v1.1.1] + > 2017-03-16 -- **Chore:** Add a prebuilt version of the library for jsbin and the like. +* **Chore:** Add a prebuilt version of the library for jsbin and the like. [v1.1.1]: https://github.com/reactjs/react-transition-group/compare/v1.1.0...v1.1.1 ## [v1.1.0] + > 2017-03-16 -- **Feature:** Support refs on children ([#9]) -- **Feature:** TransitionChild to passes props through ([#4]) -- **Bugfix:** Fix TransitionGroup error on quick toggle of components ([#15]) -- **Bugfix:** Fix to work enter animation with CSSTransitionGroup ([#13]) +* **Feature:** Support refs on children ([#9]) +* **Feature:** TransitionChild to passes props through ([#4]) +* **Bugfix:** Fix TransitionGroup error on quick toggle of components ([#15]) +* **Bugfix:** Fix to work enter animation with CSSTransitionGroup ([#13]) [v1.1.0]: https://github.com/reactjs/react-transition-group/compare/v1.0.0...v1.1.0 [#15]: https://github.com/reactjs/react-transition-group/pull/15 diff --git a/LICENSE b/LICENSE index 8b268b61..af586225 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2016, React Community +Copyright (c) 2018, React Community Forked from React (https://github.com/facebook/react) Copyright 2013-present, Facebook, Inc. All rights reserved. diff --git a/README.md b/README.md index 1b1b6c62..566a11a6 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # react-transition-group [![npm][npm-badge]][npm] -> **ATTENTION!** To address many issues that have come up over the years, the API in v2 is not backwards compatible with the original [`React addon (v1-stable)`](https://github.com/reactjs/react-transition-group/tree/v1-stable). +> **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). > -> **For a drop-in replacement for `react-addons-transition-group` and `react-addons-css-transition-group`, use the v1 release, which is still actively maintained. Documentation and code for that release are available on the [`v1-stable`](https://github.com/reactjs/react-transition-group/tree/v1-stable) branch.** +> **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.** > -> You can send pull requests with v1 bugfixes against the `v1-stable` branch. +> We are no longer updating the v1 codebase, please upgrade to the latest version when possible A set of components for managing component states (including mounting and unmounting) over time, specifically designed with animation in mind. @@ -13,6 +13,13 @@ A set of components for managing component states (including mounting and unmoun - [**Main documentation**](https://reactcommunity.org/react-transition-group/) - [Migration guide from v1](/Migration.md) +## TypeScript +TypeScript definitions are published via [**DefinitelyTyped**](https://github.com/DefinitelyTyped/DefinitelyTyped) and can be installed via the following command: + +``` +npm install @types/react-transition-group +``` + ## Examples Clone the repo first: diff --git a/package.json b/package.json index 6071c4d7..72a435e0 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,37 @@ { "name": "react-transition-group", - "version": "2.3.0-beta.0", + "version": "4.4.5", "description": "A react component toolset for managing animations", - "main": "lib/index.js", + "main": "lib/cjs/index.js", + "module": "lib/esm/index.js", "scripts": { "test": "npm run lint && npm run testonly", "testonly": "jest --verbose", "tdd": "jest --watch", - "build": "rimraf lib && babel src --out-dir lib && npm run build:dist && cp README.md LICENSE ./lib", - "build:dist": "rimraf lib/dist && webpack && NODE_ENV=production webpack -p", - "lint": "eslint src test", + "build": "rimraf lib && yarn build:cjs && yarn build:esm && yarn build:pick && yarn build:dist && cp README.md LICENSE ./lib", + "build:docs": "yarn --cwd www run build", + "build:cjs": "babel src --out-dir lib/cjs", + "build:esm": "cross-env BABEL_ENV=esm babel src --out-dir lib/esm", + "build:pick": "cherry-pick --cwd=lib --input-dir=../src --cjs-dir=cjs --esm-dir=esm", + "build:dist": "cross-env BABEL_ENV=esm rollup -c", + "bootstrap": "yarn && yarn --cwd www", + "fix": "run-s fix:eslint fix:prettier", + "fix:eslint": "yarn lint:eslint --fix", + "fix:prettier": "yarn lint:prettier --write", + "lint": "run-p lint:*", + "lint:eslint": "eslint .", + "lint:prettier": "prettier . --check", "release": "release", "release:next": "release --preid beta --tag next", - "deploy-docs": "npm -C www run deploy", + "deploy-docs": "yarn --cwd www run deploy", + "start": "yarn --cwd www run develop", "storybook": "start-storybook -p 6006", - "build-storybook": "build-storybook" + "build-storybook": "build-storybook", + "semantic-release": "semantic-release" }, "repository": { "type": "git", - "url": "git+https://github.com/reactjs/react-transition-group.git" + "url": "https://github.com/reactjs/react-transition-group.git" }, "keywords": [ "react", @@ -40,59 +53,87 @@ "setupFiles": [ "./test/setup.js" ], + "setupFilesAfterEnv": [ + "./test/setupAfterEnv.js" + ], "roots": [ "/test" ] }, "peerDependencies": { - "react": ">=15.0.0", - "react-dom": ">=15.0.0" + "react": ">=16.6.0", + "react-dom": ">=16.6.0" }, "dependencies": { - "chain-function": "^1.0.0", - "dom-helpers": "^3.2.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.5.8", - "warning": "^3.0.0" + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" }, "devDependencies": { - "@storybook/addon-actions": "^3.2.11", - "@storybook/react": "^3.2.11", - "babel-cli": "^6.24.0", - "babel-core": "^6.24.0", - "babel-eslint": "^7.1.1", - "babel-jest": "^20.0.3", - "babel-loader": "^6.4.1", - "babel-plugin-add-module-exports": "^0.2.1", - "babel-plugin-dev-expression": "^0.2.1", - "babel-plugin-transform-object-assign": "^6.22.0", - "babel-plugin-transform-react-remove-prop-types": "^0.4.5", - "babel-preset-latest": "^6.24.0", - "babel-preset-react": "^6.23.0", - "babel-preset-stage-2": "^6.18.0", - "enzyme": "^3.0.0", - "enzyme-adapter-react-16": "^1.0.0", - "eslint": "^3.17.1", - "eslint-config-jason": "^4.0.0", - "eslint-plugin-import": "^2.2.0", - "eslint-plugin-jsx-a11y": "^4.0.0", - "eslint-plugin-react": "^6.10.3", - "jest": "^20.0.4", - "react": "^16.0.0", - "react-dom": "^16.0.0", - "react-test-renderer": "^16.0.0", + "@babel/cli": "^7.8.4", + "@babel/core": "^7.9.0", + "@restart/hooks": "^0.3.22", + "@semantic-release/changelog": "^5.0.1", + "@semantic-release/git": "^9.0.0", + "@semantic-release/github": "^7.0.5", + "@semantic-release/npm": "^7.0.5", + "@storybook/addon-actions": "^6.3.4", + "@storybook/react": "^6.3.4", + "@testing-library/react": "alpha", + "@typescript-eslint/eslint-plugin": "^4.26.1", + "astroturf": "^0.10.4", + "babel-eslint": "^10.1.0", + "babel-loader": "^8.1.0", + "babel-plugin-transform-react-remove-prop-types": "^0.4.24", + "babel-preset-jason": "^6.2.0", + "cherry-pick": "^0.5.0", + "cross-env": "^7.0.2", + "eslint": "^7.28.0", + "eslint-config-jason": "^8.1.1", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-import": "^2.23.4", + "eslint-plugin-jsx-a11y": "^6.4.1", + "eslint-plugin-react": "^7.24.0", + "eslint-plugin-react-hooks": "^4.2.0", + "jest": "^25.3.0", + "npm-run-all": "^4.1.5", + "prettier": "^2.3.1", + "react": "^18.0.0", + "react-dom": "^18.0.0", "release-script": "^1.0.2", - "rimraf": "^2.6.1", - "sinon": "^2.1.0", - "webpack": "^3.6.0", - "webpack-atoms": "^3.0.2" + "rimraf": "^3.0.2", + "rollup": "^2.6.1", + "rollup-plugin-babel": "^4.4.0", + "rollup-plugin-commonjs": "^10.1.0", + "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-replace": "^2.2.0", + "rollup-plugin-size-snapshot": "^0.11.0", + "rollup-plugin-terser": "^5.3.0", + "semantic-release": "^17.0.6", + "semantic-release-alt-publish-dir": "^3.0.0", + "typescript": "^4.3.2", + "webpack-atoms": "14.0.0" }, - "release-script": { - "altPkgRootFolder": "lib" + "release": { + "pkgRoot": "lib", + "verifyConditions": [ + "@semantic-release/changelog", + "semantic-release-alt-publish-dir", + "@semantic-release/git", + "@semantic-release/github" + ], + "prepare": [ + "@semantic-release/changelog", + "semantic-release-alt-publish-dir", + "@semantic-release/npm", + "@semantic-release/git" + ] }, "browserify": { "transform": [ "loose-envify" ] - } + }, + "sideEffects": false } diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 00000000..e340799c --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,3 @@ +module.exports = { + singleQuote: true, +}; diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 00000000..5000c878 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,64 @@ +import nodeResolve from 'rollup-plugin-node-resolve'; +import babel from 'rollup-plugin-babel'; +import commonjs from 'rollup-plugin-commonjs'; +import replace from 'rollup-plugin-replace'; +import { sizeSnapshot } from 'rollup-plugin-size-snapshot'; +import { terser } from 'rollup-plugin-terser'; + +const input = './src/index.js'; +const name = 'ReactTransitionGroup'; +const globals = { + react: 'React', + 'react-dom': 'ReactDOM', +}; + +const babelOptions = { + exclude: /node_modules/, + runtimeHelpers: true, +}; + +const commonjsOptions = { + include: /node_modules/, + namedExports: { + 'prop-types': ['object', 'oneOfType', 'element', 'bool', 'func'], + }, +}; + +export default [ + { + input, + output: { + file: './lib/dist/react-transition-group.js', + format: 'umd', + name, + globals, + }, + external: Object.keys(globals), + plugins: [ + nodeResolve(), + babel(babelOptions), + commonjs(commonjsOptions), + replace({ 'process.env.NODE_ENV': JSON.stringify('development') }), + sizeSnapshot(), + ], + }, + + { + input, + output: { + file: './lib/dist/react-transition-group.min.js', + format: 'umd', + name, + globals, + }, + external: Object.keys(globals), + plugins: [ + nodeResolve(), + babel(babelOptions), + commonjs(commonjsOptions), + replace({ 'process.env.NODE_ENV': JSON.stringify('production') }), + sizeSnapshot(), + terser(), + ], + }, +]; diff --git a/src/CSSTransition.js b/src/CSSTransition.js index f9a30224..61592105 100644 --- a/src/CSSTransition.js +++ b/src/CSSTransition.js @@ -1,42 +1,310 @@ -import * as PropTypes from 'prop-types'; -import addOneClass from 'dom-helpers/class/addClass'; +import PropTypes from 'prop-types'; +import addOneClass from 'dom-helpers/addClass'; -import removeOneClass from 'dom-helpers/class/removeClass'; +import removeOneClass from 'dom-helpers/removeClass'; import React from 'react'; import Transition from './Transition'; import { classNamesShape } from './utils/PropTypes'; +import { forceReflow } from './utils/reflow'; -const addClass = (node, classes) => node && classes && classes.split(' ').forEach(c => addOneClass(node, c)); -const removeClass = (node, classes) => node && classes && classes.split(' ').forEach(c => removeOneClass(node, c)); +const addClass = (node, classes) => + node && classes && classes.split(' ').forEach((c) => addOneClass(node, c)); +const removeClass = (node, classes) => + node && classes && classes.split(' ').forEach((c) => removeOneClass(node, c)); -const propTypes = { +/** + * A transition component inspired by the excellent + * [ng-animate](https://docs.angularjs.org/api/ngAnimate) library, you should + * use it if you're using CSS transitions or animations. It's built upon the + * [`Transition`](https://reactcommunity.org/react-transition-group/transition) + * component, so it inherits all of its props. + * + * `CSSTransition` applies a pair of class names during the `appear`, `enter`, + * and `exit` states of the transition. The first class is applied and then a + * second `*-active` class in order to activate the CSS transition. After the + * transition, matching `*-done` class names are applied to persist the + * transition state. + * + * ```jsx + * function App() { + * const [inProp, setInProp] = useState(false); + * const nodeRef = useRef(null); + * return ( + *
+ * + *
+ * {"I'll receive my-node-* classes"} + *
+ *
+ * + *
+ * ); + * } + * ``` + * + * When the `in` prop is set to `true`, the child component will first receive + * the class `example-enter`, then the `example-enter-active` will be added in + * the next tick. `CSSTransition` [forces a + * reflow](https://github.com/reactjs/react-transition-group/blob/5007303e729a74be66a21c3e2205e4916821524b/src/CSSTransition.js#L208-L215) + * between before adding the `example-enter-active`. This is an important trick + * because it allows us to transition between `example-enter` and + * `example-enter-active` even though they were added immediately one after + * another. Most notably, this is what makes it possible for us to animate + * _appearance_. + * + * ```css + * .my-node-enter { + * opacity: 0; + * } + * .my-node-enter-active { + * opacity: 1; + * transition: opacity 200ms; + * } + * .my-node-exit { + * opacity: 1; + * } + * .my-node-exit-active { + * opacity: 0; + * transition: opacity 200ms; + * } + * ``` + * + * `*-active` classes represent which styles you want to animate **to**, so it's + * important to add `transition` declaration only to them, otherwise transitions + * might not behave as intended! This might not be obvious when the transitions + * are symmetrical, i.e. when `*-enter-active` is the same as `*-exit`, like in + * the example above (minus `transition`), but it becomes apparent in more + * complex transitions. + * + * **Note**: If you're using the + * [`appear`](http://reactcommunity.org/react-transition-group/transition#Transition-prop-appear) + * prop, make sure to define styles for `.appear-*` classes as well. + */ +class CSSTransition extends React.Component { + static defaultProps = { + classNames: '', + }; + + appliedClasses = { + appear: {}, + enter: {}, + exit: {}, + }; + + onEnter = (maybeNode, maybeAppearing) => { + const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing); + this.removeClasses(node, 'exit'); + this.addClass(node, appearing ? 'appear' : 'enter', 'base'); + + if (this.props.onEnter) { + this.props.onEnter(maybeNode, maybeAppearing); + } + }; + + onEntering = (maybeNode, maybeAppearing) => { + const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing); + const type = appearing ? 'appear' : 'enter'; + this.addClass(node, type, 'active'); + + if (this.props.onEntering) { + this.props.onEntering(maybeNode, maybeAppearing); + } + }; + + onEntered = (maybeNode, maybeAppearing) => { + const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing); + const type = appearing ? 'appear' : 'enter'; + this.removeClasses(node, type); + this.addClass(node, type, 'done'); + + if (this.props.onEntered) { + this.props.onEntered(maybeNode, maybeAppearing); + } + }; + + onExit = (maybeNode) => { + const [node] = this.resolveArguments(maybeNode); + this.removeClasses(node, 'appear'); + this.removeClasses(node, 'enter'); + this.addClass(node, 'exit', 'base'); + + if (this.props.onExit) { + this.props.onExit(maybeNode); + } + }; + + onExiting = (maybeNode) => { + const [node] = this.resolveArguments(maybeNode); + this.addClass(node, 'exit', 'active'); + + if (this.props.onExiting) { + this.props.onExiting(maybeNode); + } + }; + + onExited = (maybeNode) => { + const [node] = this.resolveArguments(maybeNode); + this.removeClasses(node, 'exit'); + this.addClass(node, 'exit', 'done'); + + if (this.props.onExited) { + this.props.onExited(maybeNode); + } + }; + + // when prop `nodeRef` is provided `node` is excluded + resolveArguments = (maybeNode, maybeAppearing) => + this.props.nodeRef + ? [this.props.nodeRef.current, maybeNode] // here `maybeNode` is actually `appearing` + : [maybeNode, maybeAppearing]; // `findDOMNode` was used + + getClassNames = (type) => { + const { classNames } = this.props; + const isStringClassNames = typeof classNames === 'string'; + const prefix = isStringClassNames && classNames ? `${classNames}-` : ''; + + let baseClassName = isStringClassNames + ? `${prefix}${type}` + : classNames[type]; + + let activeClassName = isStringClassNames + ? `${baseClassName}-active` + : classNames[`${type}Active`]; + + let doneClassName = isStringClassNames + ? `${baseClassName}-done` + : classNames[`${type}Done`]; + + return { + baseClassName, + activeClassName, + doneClassName, + }; + }; + + addClass(node, type, phase) { + let className = this.getClassNames(type)[`${phase}ClassName`]; + const { doneClassName } = this.getClassNames('enter'); + + if (type === 'appear' && phase === 'done' && doneClassName) { + className += ` ${doneClassName}`; + } + + // This is to force a repaint, + // which is necessary in order to transition styles when adding a class name. + if (phase === 'active') { + if (node) forceReflow(node); + } + + if (className) { + this.appliedClasses[type][phase] = className; + addClass(node, className); + } + } + + removeClasses(node, type) { + const { + base: baseClassName, + active: activeClassName, + done: doneClassName, + } = this.appliedClasses[type]; + + this.appliedClasses[type] = {}; + + if (baseClassName) { + removeClass(node, baseClassName); + } + if (activeClassName) { + removeClass(node, activeClassName); + } + if (doneClassName) { + removeClass(node, doneClassName); + } + } + + render() { + const { classNames: _, ...props } = this.props; + + return ( + + ); + } +} + +CSSTransition.propTypes = { ...Transition.propTypes, /** - * The animation classNames applied to the component as it enters, exits or has finished the transition. - * A single name can be provided and it will be suffixed for each stage: e.g. + * The animation classNames applied to the component as it appears, enters, + * exits or has finished the transition. A single name can be provided, which + * will be suffixed for each stage, e.g. `classNames="fade"` applies: + * + * - `fade-appear`, `fade-appear-active`, `fade-appear-done` + * - `fade-enter`, `fade-enter-active`, `fade-enter-done` + * - `fade-exit`, `fade-exit-active`, `fade-exit-done` + * + * A few details to note about how these classes are applied: + * + * 1. They are _joined_ with the ones that are already defined on the child + * component, so if you want to add some base styles, you can use + * `className` without worrying that it will be overridden. + * + * 2. If the transition component mounts with `in={false}`, no classes are + * applied yet. You might be expecting `*-exit-done`, but if you think + * about it, a component cannot finish exiting if it hasn't entered yet. + * + * 2. `fade-appear-done` and `fade-enter-done` will _both_ be applied. This + * allows you to define different behavior for when appearing is done and + * when regular entering is done, using selectors like + * `.fade-enter-done:not(.fade-appear-done)`. For example, you could apply + * an epic entrance animation when element first appears in the DOM using + * [Animate.css](https://daneden.github.io/animate.css/). Otherwise you can + * simply use `fade-enter-done` for defining both cases. * - * `classNames="fade"` applies `fade-enter`, `fade-enter-active`, `fade-enter-done`, - * `fade-exit`, `fade-exit-active`, `fade-exit-done`, `fade-appear`, and `fade-appear-active`. * Each individual classNames can also be specified independently like: * * ```js * classNames={{ * appear: 'my-appear', * appearActive: 'my-active-appear', + * appearDone: 'my-done-appear', * enter: 'my-enter', * enterActive: 'my-active-enter', - * enterDone: 'my-done-enter, + * enterDone: 'my-done-enter', * exit: 'my-exit', * exitActive: 'my-active-exit', - * exitDone: 'my-done-exit, + * exitDone: 'my-done-exit', * }} * ``` * + * If you want to set these classes using CSS Modules: + * + * ```js + * import styles from './styles.css'; + * ``` + * + * you might want to use camelCase in your CSS file, that way could simply + * spread them instead of listing them one by one: + * + * ```js + * classNames={{ ...styles }} + * ``` + * * @type {string | { * appear?: string, * appearActive?: string, + * appearDone?: string, * enter?: string, * enterActive?: string, * enterDone?: string, @@ -51,6 +319,8 @@ const propTypes = { * A `` callback fired immediately after the 'enter' or 'appear' class is * applied. * + * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. + * * @type Function(node: HtmlElement, isAppearing: bool) */ onEnter: PropTypes.func, @@ -59,6 +329,8 @@ const propTypes = { * A `` callback fired immediately after the 'enter-active' or * 'appear-active' class is applied. * + * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. + * * @type Function(node: HtmlElement, isAppearing: bool) */ onEntering: PropTypes.func, @@ -67,15 +339,18 @@ const propTypes = { * A `` callback fired immediately after the 'enter' or * 'appear' classes are **removed** and the `done` class is added to the DOM node. * + * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. + * * @type Function(node: HtmlElement, isAppearing: bool) */ onEntered: PropTypes.func, - /** * A `` callback fired immediately after the 'exit' class is * applied. * + * **Note**: when `nodeRef` prop is passed, `node` is not passed + * * @type Function(node: HtmlElement) */ onExit: PropTypes.func, @@ -83,7 +358,9 @@ const propTypes = { /** * A `` callback fired immediately after the 'exit-active' is applied. * - * @type Function(node: HtmlElement + * **Note**: when `nodeRef` prop is passed, `node` is not passed + * + * @type Function(node: HtmlElement) */ onExiting: PropTypes.func, @@ -91,148 +368,11 @@ const propTypes = { * A `` callback fired immediately after the 'exit' classes * are **removed** and the `exit-done` class is added to the DOM node. * + * **Note**: when `nodeRef` prop is passed, `node` is not passed + * * @type Function(node: HtmlElement) */ onExited: PropTypes.func, }; -/** - * A `Transition` component using CSS transitions and animations. - * It's inspired by the excellent [ng-animate](http://www.nganimate.org/) library. - * - * `CSSTransition` applies a pair of class names during the `appear`, `enter`, - * and `exit` stages of the transition. The first class is applied and then a - * second "active" class in order to activate the css animation. After the animation, - * matching `done` class names are applied to persist the animation state. - * - * When the `in` prop is toggled to `true` the Component will get - * the `example-enter` CSS class and the `example-enter-active` CSS class - * added in the next tick. This is a convention based on the `classNames` prop. - * - * - */ -class CSSTransition extends React.Component { - onEnter = (node, appearing) => { - const { className } = this.getClassNames(appearing ? 'appear' : 'enter') - - this.removeClasses(node, 'exit'); - addClass(node, className) - - if (this.props.onEnter) { - this.props.onEnter(node) - } - } - - onEntering = (node, appearing) => { - const { activeClassName } = this.getClassNames( - appearing ? 'appear' : 'enter' - ); - - this.reflowAndAddClass(node, activeClassName) - - if (this.props.onEntering) { - this.props.onEntering(node) - } - } - - onEntered = (node, appearing) => { - const { doneClassName } = this.getClassNames('enter'); - - this.removeClasses(node, appearing ? 'appear' : 'enter'); - addClass(node, doneClassName); - - if (this.props.onEntered) { - this.props.onEntered(node) - } - } - - onExit = (node) => { - const { className } = this.getClassNames('exit') - - this.removeClasses(node, 'appear'); - this.removeClasses(node, 'enter'); - addClass(node, className) - - if (this.props.onExit) { - this.props.onExit(node) - } - } - - onExiting = (node) => { - const { activeClassName } = this.getClassNames('exit') - - this.reflowAndAddClass(node, activeClassName) - - if (this.props.onExiting) { - this.props.onExiting(node) - } - } - - onExited = (node) => { - const { doneClassName } = this.getClassNames('exit'); - - this.removeClasses(node, 'exit'); - addClass(node, doneClassName); - - if (this.props.onExited) { - this.props.onExited(node) - } - } - - getClassNames = (type) => { - const { classNames } = this.props; - - let className = typeof classNames !== 'string' ? - classNames[type] : classNames + '-' + type; - - let activeClassName = typeof classNames !== 'string' ? - classNames[type + 'Active'] : className + '-active'; - - let doneClassName = typeof classNames !== 'string' ? - classNames[type + 'Done'] : className + '-done'; - - return { - className, - activeClassName, - doneClassName - }; - } - - removeClasses(node, type) { - const { className, activeClassName, doneClassName } = this.getClassNames(type) - className && removeClass(node, className); - activeClassName && removeClass(node, activeClassName); - doneClassName && removeClass(node, doneClassName); - } - - reflowAndAddClass(node, className) { - // This is for to force a repaint, - // which is necessary in order to transition styles when adding a class name. - /* eslint-disable no-unused-expressions */ - node && node.scrollTop; - /* eslint-enable no-unused-expressions */ - addClass(node, className); - } - - render() { - const props = { ...this.props }; - - delete props.classNames; - - return ( - - ); - } -} - -CSSTransition.propTypes = propTypes; - -export default CSSTransition +export default CSSTransition; diff --git a/src/ReplaceTransition.js b/src/ReplaceTransition.js index 72b4246c..7c0b9092 100644 --- a/src/ReplaceTransition.js +++ b/src/ReplaceTransition.js @@ -1,18 +1,8 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { findDOMNode } from 'react-dom' +import ReactDOM from 'react-dom'; import TransitionGroup from './TransitionGroup'; -const propTypes = { - in: PropTypes.bool.isRequired, - children(props, propName) { - if (React.Children.count(props[propName]) !== 2) - return new Error(`"${propName}" must be exactly two transition components.`) - - return null; - }, -}; - /** * The `` component is a specialized `Transition` component * that animates between two children. @@ -25,28 +15,30 @@ const propTypes = { * ``` */ class ReplaceTransition extends React.Component { - handleEnter = (...args) => this.handleLifecycle('onEnter', 0, args) - handleEntering = (...args) => this.handleLifecycle('onEntering', 0, args) - handleEntered = (...args) => this.handleLifecycle('onEntered', 0, args) + handleEnter = (...args) => this.handleLifecycle('onEnter', 0, args); + handleEntering = (...args) => this.handleLifecycle('onEntering', 0, args); + handleEntered = (...args) => this.handleLifecycle('onEntered', 0, args); - handleExit = (...args) => this.handleLifecycle('onExit', 1, args) - handleExiting = (...args) => this.handleLifecycle('onExiting', 1, args) - handleExited = (...args) => this.handleLifecycle('onExited', 1, args) + handleExit = (...args) => this.handleLifecycle('onExit', 1, args); + handleExiting = (...args) => this.handleLifecycle('onExiting', 1, args); + handleExited = (...args) => this.handleLifecycle('onExited', 1, args); handleLifecycle(handler, idx, originalArgs) { const { children } = this.props; const child = React.Children.toArray(children)[idx]; - if (child.props[handler]) child.props[handler](...originalArgs) - if (this.props[handler]) this.props[handler](findDOMNode(this)) + if (child.props[handler]) child.props[handler](...originalArgs); + if (this.props[handler]) { + const maybeNode = child.props.nodeRef + ? undefined + : ReactDOM.findDOMNode(this); + + this.props[handler](maybeNode); + } } render() { - const { - children, - in: inProp, - ...props - } = this.props; + const { children, in: inProp, ...props } = this.props; const [first, second] = React.Children.toArray(children); delete props.onEnter; @@ -58,26 +50,34 @@ class ReplaceTransition extends React.Component { return ( - {inProp ? - React.cloneElement(first, { - key: 'first', - onEnter: this.handleEnter, - onEntering: this.handleEntering, - onEntered: this.handleEntered, - - }) : - React.cloneElement(second, { - key: 'second', - onEnter: this.handleExit, - onEntering: this.handleExiting, - onEntered: this.handleExited, - }) - } + {inProp + ? React.cloneElement(first, { + key: 'first', + onEnter: this.handleEnter, + onEntering: this.handleEntering, + onEntered: this.handleEntered, + }) + : React.cloneElement(second, { + key: 'second', + onEnter: this.handleExit, + onEntering: this.handleExiting, + onEntered: this.handleExited, + })} ); } } -ReplaceTransition.propTypes = propTypes; +ReplaceTransition.propTypes = { + in: PropTypes.bool.isRequired, + children(props, propName) { + if (React.Children.count(props[propName]) !== 2) + return new Error( + `"${propName}" must be exactly two transition components.` + ); + + return null; + }, +}; export default ReplaceTransition; diff --git a/src/SwitchTransition.js b/src/SwitchTransition.js new file mode 100644 index 00000000..78dc375d --- /dev/null +++ b/src/SwitchTransition.js @@ -0,0 +1,222 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ENTERED, ENTERING, EXITING } from './Transition'; +import TransitionGroupContext from './TransitionGroupContext'; + +function areChildrenDifferent(oldChildren, newChildren) { + if (oldChildren === newChildren) return false; + if ( + React.isValidElement(oldChildren) && + React.isValidElement(newChildren) && + oldChildren.key != null && + oldChildren.key === newChildren.key + ) { + return false; + } + return true; +} + +/** + * Enum of modes for SwitchTransition component + * @enum { string } + */ +export const modes = { + out: 'out-in', + in: 'in-out', +}; + +const callHook = + (element, name, cb) => + (...args) => { + element.props[name] && element.props[name](...args); + cb(); + }; + +const leaveRenders = { + [modes.out]: ({ current, changeState }) => + React.cloneElement(current, { + in: false, + onExited: callHook(current, 'onExited', () => { + changeState(ENTERING, null); + }), + }), + [modes.in]: ({ current, changeState, children }) => [ + current, + React.cloneElement(children, { + in: true, + onEntered: callHook(children, 'onEntered', () => { + changeState(ENTERING); + }), + }), + ], +}; + +const enterRenders = { + [modes.out]: ({ children, changeState }) => + React.cloneElement(children, { + in: true, + onEntered: callHook(children, 'onEntered', () => { + changeState(ENTERED, React.cloneElement(children, { in: true })); + }), + }), + [modes.in]: ({ current, children, changeState }) => [ + React.cloneElement(current, { + in: false, + onExited: callHook(current, 'onExited', () => { + changeState(ENTERED, React.cloneElement(children, { in: true })); + }), + }), + React.cloneElement(children, { + in: true, + }), + ], +}; + +/** + * A transition component inspired by the [vue transition modes](https://vuejs.org/v2/guide/transitions.html#Transition-Modes). + * You can use it when you want to control the render between state transitions. + * 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. + * + * If the `out-in` mode is selected, the `SwitchTransition` waits until the old child leaves and then inserts a new child. + * 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. + * + * **Note**: If you want the animation to happen simultaneously + * (that is, to have the old child removed and a new child inserted **at the same time**), + * you should use + * [`TransitionGroup`](https://reactcommunity.org/react-transition-group/transition-group) + * instead. + * + * ```jsx + * function App() { + * const [state, setState] = useState(false); + * const helloRef = useRef(null); + * const goodbyeRef = useRef(null); + * const nodeRef = state ? goodbyeRef : helloRef; + * return ( + * + * node.addEventListener("transitionend", done, false)} + * classNames='fade' + * > + * + * + * + * ); + * } + * ``` + * + * ```css + * .fade-enter{ + * opacity: 0; + * } + * .fade-exit{ + * opacity: 1; + * } + * .fade-enter-active{ + * opacity: 1; + * } + * .fade-exit-active{ + * opacity: 0; + * } + * .fade-enter-active, + * .fade-exit-active{ + * transition: opacity 500ms; + * } + * ``` + */ +class SwitchTransition extends React.Component { + state = { + status: ENTERED, + current: null, + }; + + appeared = false; + + componentDidMount() { + this.appeared = true; + } + + static getDerivedStateFromProps(props, state) { + if (props.children == null) { + return { + current: null, + }; + } + + if (state.status === ENTERING && props.mode === modes.in) { + return { + status: ENTERING, + }; + } + + if (state.current && areChildrenDifferent(state.current, props.children)) { + return { + status: EXITING, + }; + } + + return { + current: React.cloneElement(props.children, { + in: true, + }), + }; + } + + changeState = (status, current = this.state.current) => { + this.setState({ + status, + current, + }); + }; + + render() { + const { + props: { children, mode }, + state: { status, current }, + } = this; + + const data = { children, current, changeState: this.changeState, status }; + let component; + switch (status) { + case ENTERING: + component = enterRenders[mode](data); + break; + case EXITING: + component = leaveRenders[mode](data); + break; + case ENTERED: + component = current; + } + + return ( + + {component} + + ); + } +} + +SwitchTransition.propTypes = { + /** + * Transition modes. + * `out-in`: Current element transitions out first, then when complete, the new element transitions in. + * `in-out`: New element transitions in first, then when complete, the current element transitions out. + * + * @type {'out-in'|'in-out'} + */ + mode: PropTypes.oneOf([modes.in, modes.out]), + /** + * Any `Transition` or `CSSTransition` component. + */ + children: PropTypes.oneOfType([PropTypes.element.isRequired]), +}; + +SwitchTransition.defaultProps = { + mode: modes.out, +}; + +export default SwitchTransition; diff --git a/src/Transition.js b/src/Transition.js index 2bb82d62..41cd1f38 100644 --- a/src/Transition.js +++ b/src/Transition.js @@ -1,8 +1,11 @@ -import * as PropTypes from 'prop-types'; +import PropTypes from 'prop-types'; import React from 'react'; import ReactDOM from 'react-dom'; +import config from './config'; import { timeoutsShape } from './utils/PropTypes'; +import TransitionGroupContext from './TransitionGroupContext'; +import { forceReflow } from './utils/reflow'; export const UNMOUNTED = 'unmounted'; export const EXITED = 'exited'; @@ -16,13 +19,25 @@ export const EXITING = 'exiting'; * it's used to animate the mounting and unmounting of a component, but can also * be used to describe in-place transition states as well. * + * --- + * + * **Note**: `Transition` is a platform-agnostic base component. If you're using + * transitions in CSS, you'll probably want to use + * [`CSSTransition`](https://reactcommunity.org/react-transition-group/css-transition) + * instead. It inherits all the features of `Transition`, but contains + * additional features necessary to play nice with CSS transitions (hence the + * name of the component). + * + * --- + * * By default the `Transition` component does not alter the behavior of the - * component it renders, it only tracks "enter" and "exit" states for the components. - * It's up to you to give meaning and effect to those states. For example we can - * add styles to a component when it enters or exits: + * component it renders, it only tracks "enter" and "exit" states for the + * components. It's up to you to give meaning and effect to those states. For + * example we can add styles to a component when it enters or exits: * * ```jsx - * import Transition from 'react-transition-group/Transition'; + * import { Transition } from 'react-transition-group'; + * import { useRef } from 'react'; * * const duration = 300; * @@ -32,90 +47,90 @@ export const EXITING = 'exiting'; * } * * const transitionStyles = { - * entering: { opacity: 0 }, + * entering: { opacity: 1 }, * entered: { opacity: 1 }, + * exiting: { opacity: 0 }, + * exited: { opacity: 0 }, * }; * - * const Fade = ({ in: inProp }) => ( - * - * {(state) => ( - *
- * I'm a fade Transition! - *
- * )} - *
- * ); + * function Fade({ in: inProp }) { + * const nodeRef = useRef(null); + * return ( + * + * {state => ( + *
+ * I'm a fade Transition! + *
+ * )} + *
+ * ); + * } * ``` * - * As noted the `Transition` component doesn't _do_ anything by itself to its child component. - * What it does do is track transition states over time so you can update the - * component (such as by adding styles or classes) when it changes states. - * * There are 4 main states a Transition can be in: - * - `ENTERING` - * - `ENTERED` - * - `EXITING` - * - `EXITED` + * - `'entering'` + * - `'entered'` + * - `'exiting'` + * - `'exited'` * - * Transition state is toggled via the `in` prop. When `true` the component begins the - * "Enter" stage. During this stage, the component will shift from its current transition state, - * to `'entering'` for the duration of the transition and then to the `'entered'` stage once - * it's complete. Let's take the following example: + * Transition state is toggled via the `in` prop. When `true` the component + * begins the "Enter" stage. During this stage, the component will shift from + * its current transition state, to `'entering'` for the duration of the + * transition and then to the `'entered'` stage once it's complete. Let's take + * the following example (we'll use the + * [useState](https://reactjs.org/docs/hooks-reference.html#usestate) hook): * * ```jsx - * state= { in: false }; + * import { Transition } from 'react-transition-group'; + * import { useState, useRef } from 'react'; * - * toggleEnterState = () => { - * this.setState({ in: true }); - * } - * - * render() { + * function App() { + * const [inProp, setInProp] = useState(false); + * const nodeRef = useRef(null); * return ( *
- * - * + * + * {state => ( + * // ... + * )} + * + * *
* ); * } * ``` * - * When the button is clicked the component will shift to the `'entering'` state and - * stay there for 500ms (the value of `timeout`) before it finally switches to `'entered'`. - * - * When `in` is `false` the same thing happens except the state moves from `'exiting'` to `'exited'`. - * - * ### Example - * - * + * When the button is clicked the component will shift to the `'entering'` state + * and stay there for 500ms (the value of `timeout`) before it finally switches + * to `'entered'`. * + * When `in` is `false` the same thing happens except the state moves from + * `'exiting'` to `'exited'`. */ class Transition extends React.Component { - static contextTypes = { - transitionGroup: PropTypes.object, - }; - static childContextTypes = { - transitionGroup: ()=>{}, - }; + static contextType = TransitionGroupContext; constructor(props, context) { super(props, context); - let parentGroup = context.transitionGroup; + let parentGroup = context; // In the context of a TransitionGroup all enters are really appears - let appear = parentGroup && !parentGroup.isMounting ? - props.enter : - props.appear; + let appear = + parentGroup && !parentGroup.isMounting ? props.enter : props.appear; let initialStatus; - this.nextStatus = null; + + this.appearStatus = null; if (props.in) { if (appear) { initialStatus = EXITED; - this.nextStatus = ENTERING; + this.appearStatus = ENTERING; } else { initialStatus = ENTERED; } @@ -132,33 +147,53 @@ class Transition extends React.Component { this.nextCallback = null; } - getChildContext() { - return { transitionGroup: null }; // allows for nested Transitions + static getDerivedStateFromProps({ in: nextIn }, prevState) { + if (nextIn && prevState.status === UNMOUNTED) { + return { status: EXITED }; + } + return null; } + // getSnapshotBeforeUpdate(prevProps) { + // let nextStatus = null + + // if (prevProps !== this.props) { + // const { status } = this.state + + // if (this.props.in) { + // if (status !== ENTERING && status !== ENTERED) { + // nextStatus = ENTERING + // } + // } else { + // if (status === ENTERING || status === ENTERED) { + // nextStatus = EXITING + // } + // } + // } + + // return { nextStatus } + // } + componentDidMount() { - this.updateStatus(true); + this.updateStatus(true, this.appearStatus); } - componentWillReceiveProps(nextProps) { - const { status } = this.pendingState || this.state; + componentDidUpdate(prevProps) { + let nextStatus = null; + if (prevProps !== this.props) { + const { status } = this.state; - if (nextProps.in) { - if (status === UNMOUNTED) { - this.setState({ status: EXITED }); - } - if (status !== ENTERING && status !== ENTERED) { - this.nextStatus = ENTERING; - } - } else { - if (status === ENTERING || status === ENTERED) { - this.nextStatus = EXITING; + if (this.props.in) { + if (status !== ENTERING && status !== ENTERED) { + nextStatus = ENTERING; + } + } else { + if (status === ENTERING || status === ENTERED) { + nextStatus = EXITING; + } } } - } - - componentDidUpdate() { - this.updateStatus(); + this.updateStatus(false, nextStatus); } componentWillUnmount() { @@ -169,87 +204,95 @@ class Transition extends React.Component { const { timeout } = this.props; let exit, enter, appear; - exit = enter = appear = timeout + exit = enter = appear = timeout; if (timeout != null && typeof timeout !== 'number') { - exit = timeout.exit - enter = timeout.enter - appear = timeout.appear + exit = timeout.exit; + enter = timeout.enter; + // TODO: remove fallback for next major + appear = timeout.appear !== undefined ? timeout.appear : enter; } - return { exit, enter, appear } + return { exit, enter, appear }; } - updateStatus(mounting = false) { - let nextStatus = this.nextStatus; - + updateStatus(mounting = false, nextStatus) { if (nextStatus !== null) { - this.nextStatus = null; // nextStatus will always be ENTERING or EXITING. this.cancelNextCallback(); - const node = ReactDOM.findDOMNode(this); if (nextStatus === ENTERING) { - this.performEnter(node, mounting); + if (this.props.unmountOnExit || this.props.mountOnEnter) { + const node = this.props.nodeRef + ? this.props.nodeRef.current + : ReactDOM.findDOMNode(this); + // https://github.com/reactjs/react-transition-group/pull/749 + // With unmountOnExit or mountOnEnter, the enter animation should happen at the transition between `exited` and `entering`. + // To make the animation happen, we have to separate each rendering and avoid being processed as batched. + if (node) forceReflow(node); + } + this.performEnter(mounting); } else { - this.performExit(node); + this.performExit(); } - } else if ( - this.props.unmountOnExit && - this.state.status === EXITED - ) { + } else if (this.props.unmountOnExit && this.state.status === EXITED) { this.setState({ status: UNMOUNTED }); } } - performEnter(node, mounting) { + performEnter(mounting) { const { enter } = this.props; - const appearing = this.context.transitionGroup ? - this.context.transitionGroup.isMounting : mounting; + const appearing = this.context ? this.context.isMounting : mounting; + const [maybeNode, maybeAppearing] = this.props.nodeRef + ? [appearing] + : [ReactDOM.findDOMNode(this), appearing]; const timeouts = this.getTimeouts(); - + const enterTimeout = appearing ? timeouts.appear : timeouts.enter; // no enter animation skip right to ENTERED // if we are mounting and running this it means appear _must_ be set - if (!mounting && !enter) { + if ((!mounting && !enter) || config.disabled) { this.safeSetState({ status: ENTERED }, () => { - this.props.onEntered(node); + this.props.onEntered(maybeNode); }); return; } - this.props.onEnter(node, appearing); + this.props.onEnter(maybeNode, maybeAppearing); this.safeSetState({ status: ENTERING }, () => { - this.props.onEntering(node, appearing); + this.props.onEntering(maybeNode, maybeAppearing); - // FIXME: appear timeout? - this.onTransitionEnd(node, timeouts.enter, () => { + this.onTransitionEnd(enterTimeout, () => { this.safeSetState({ status: ENTERED }, () => { - this.props.onEntered(node, appearing); + this.props.onEntered(maybeNode, maybeAppearing); }); }); }); } - performExit(node) { + performExit() { const { exit } = this.props; const timeouts = this.getTimeouts(); + const maybeNode = this.props.nodeRef + ? undefined + : ReactDOM.findDOMNode(this); // no exit animation skip right to EXITED - if (!exit) { + if (!exit || config.disabled) { this.safeSetState({ status: EXITED }, () => { - this.props.onExited(node); + this.props.onExited(maybeNode); }); return; } - this.props.onExit(node); + + this.props.onExit(maybeNode); this.safeSetState({ status: EXITING }, () => { - this.props.onExiting(node); + this.props.onExiting(maybeNode); - this.onTransitionEnd(node, timeouts.exit, () => { + this.onTransitionEnd(timeouts.exit, () => { this.safeSetState({ status: EXITED }, () => { - this.props.onExited(node); + this.props.onExited(maybeNode); }); }); }); @@ -263,19 +306,11 @@ class Transition extends React.Component { } safeSetState(nextState, callback) { - // We need to track pending updates for instances where a cWRP fires quickly - // after cDM and before the state flushes, which would double trigger a - // transition - this.pendingState = nextState; - // This shouldn't be necessary, but there are weird race conditions with // setState callbacks and unmounting in testing, so always make sure that // we can cancel any pending setState callbacks after we unmount. - callback = this.setNextCallback(callback) - this.setState(nextState, () => { - this.pendingState = null; - callback() - }); + callback = this.setNextCallback(callback); + this.setState(nextState, callback); } setNextCallback(callback) { @@ -297,64 +332,110 @@ class Transition extends React.Component { return this.nextCallback; } - onTransitionEnd(node, timeout, handler) { + onTransitionEnd(timeout, handler) { this.setNextCallback(handler); + const node = this.props.nodeRef + ? this.props.nodeRef.current + : ReactDOM.findDOMNode(this); - if (node) { - if (this.props.addEndListener) { - this.props.addEndListener(node, this.nextCallback) - } - if (timeout != null) { - setTimeout(this.nextCallback, timeout); - } - } else { + const doesNotHaveTimeoutOrListener = + timeout == null && !this.props.addEndListener; + if (!node || doesNotHaveTimeoutOrListener) { setTimeout(this.nextCallback, 0); + return; + } + + if (this.props.addEndListener) { + const [maybeNode, maybeNextCallback] = this.props.nodeRef + ? [this.nextCallback] + : [node, this.nextCallback]; + this.props.addEndListener(maybeNode, maybeNextCallback); + } + + if (timeout != null) { + setTimeout(this.nextCallback, timeout); } } render() { const status = this.state.status; + if (status === UNMOUNTED) { return null; } - const {children, ...childProps} = this.props; - // filter props for Transtition - delete childProps.in; - delete childProps.mountOnEnter; - delete childProps.unmountOnExit; - delete childProps.appear; - delete childProps.enter; - delete childProps.exit; - delete childProps.timeout; - delete childProps.addEndListener; - delete childProps.onEnter; - delete childProps.onEntering; - delete childProps.onEntered; - delete childProps.onExit; - delete childProps.onExiting; - delete childProps.onExited; - - if (typeof children === 'function') { - return children(status, childProps) - } - - const child = React.Children.only(children); - return React.cloneElement(child, childProps); + const { + children, + // filter props for `Transition` + in: _in, + mountOnEnter: _mountOnEnter, + unmountOnExit: _unmountOnExit, + appear: _appear, + enter: _enter, + exit: _exit, + timeout: _timeout, + addEndListener: _addEndListener, + onEnter: _onEnter, + onEntering: _onEntering, + onEntered: _onEntered, + onExit: _onExit, + onExiting: _onExiting, + onExited: _onExited, + nodeRef: _nodeRef, + ...childProps + } = this.props; + + return ( + // allows for nested Transitions + + {typeof children === 'function' + ? children(status, childProps) + : React.cloneElement(React.Children.only(children), childProps)} + + ); } } Transition.propTypes = { /** - * A `function` child can be used instead of a React element. - * This function is called with the current transition status - * ('entering', 'entered', 'exiting', 'exited', 'unmounted'), which can be used - * to apply context specific props to a component. + * A React reference to the DOM element that needs to transition: + * https://stackoverflow.com/a/51127130/4671932 + * + * - This prop is optional, but recommended in order to avoid defaulting to + * [`ReactDOM.findDOMNode`](https://reactjs.org/docs/react-dom.html#finddomnode), + * which is deprecated in `StrictMode` + * - When `nodeRef` prop is used, `node` is not passed to callback functions + * (e.g. `onEnter`) because user already has direct access to the node. + * - When changing `key` prop of `Transition` in a `TransitionGroup` a new + * `nodeRef` need to be provided to `Transition` with changed `key` prop + * (see + * [test/CSSTransition-test.js](https://github.com/reactjs/react-transition-group/blob/13435f897b3ab71f6e19d724f145596f5910581c/test/CSSTransition-test.js#L362-L437)). + */ + nodeRef: PropTypes.shape({ + current: + typeof Element === 'undefined' + ? PropTypes.any + : (propValue, key, componentName, location, propFullName, secret) => { + const value = propValue[key]; + + return PropTypes.instanceOf( + value && 'ownerDocument' in value + ? value.ownerDocument.defaultView.Element + : Element + )(propValue, key, componentName, location, propFullName, secret); + }, + }), + + /** + * A `function` child can be used instead of a React element. This function is + * called with the current transition status (`'entering'`, `'entered'`, + * `'exiting'`, `'exited'`), which can be used to apply context + * specific props to a component. * * ```jsx - * - * {(status) => ( - * + * + * {state => ( + * * )} * * ``` @@ -384,11 +465,15 @@ Transition.propTypes = { unmountOnExit: PropTypes.bool, /** - * Normally a component is not transitioned if it is shown when the `` component mounts. - * If you want to transition on the first mount set `appear` to `true`, and the - * component will transition in as soon as the `` mounts. + * By default the child component does not perform the enter transition when + * it first mounts, regardless of the value of `in`. If you want this + * behavior, set both `appear` and `in` to `true`. * - * > Note: there are no specific "appear" states. `appear` only adds an additional `enter` transition. + * > **Note**: there are no special appear states like `appearing`/`appeared`, this prop + * > only adds an additional enter transition. However, in the + * > `` component that first enter transition does result in + * > additional `.appear-*` classes, that way you can choose to style it + * > differently. */ appear: PropTypes.bool, @@ -404,30 +489,42 @@ Transition.propTypes = { /** * The duration of the transition, in milliseconds. - * Required unless `addEndListener` is provided + * Required unless `addEndListener` is provided. * - * You may specify a single timeout for all transitions like: `timeout={500}`, - * or individually like: + * You may specify a single timeout for all transitions: + * + * ```jsx + * timeout={500} + * ``` + * + * or individually: * * ```jsx * timeout={{ + * appear: 500, * enter: 300, * exit: 500, * }} * ``` * - * @type {number | { enter?: number, exit?: number }} + * - `appear` defaults to the value of `enter` + * - `enter` defaults to `0` + * - `exit` defaults to `0` + * + * @type {number | { enter?: number, exit?: number, appear?: number }} */ timeout: (props, ...args) => { - let pt = timeoutsShape - if (!props.addEndListener) pt = pt.isRequired + let pt = timeoutsShape; + if (!props.addEndListener) pt = pt.isRequired; return pt(props, ...args); }, /** * Add a custom transition end trigger. Called with the transitioning * DOM node and a `done` callback. Allows for more fine grained transition end - * logic. **Note:** Timeouts are still used as a fallback if provided. + * logic. Timeouts are still used as a fallback if provided. + * + * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `done` is being passed as the first argument. * * ```jsx * addEndListener={(node, done) => { @@ -442,6 +539,8 @@ Transition.propTypes = { * Callback fired before the "entering" status is applied. An extra parameter * `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount * + * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. + * * @type Function(node: HtmlElement, isAppearing: bool) -> void */ onEnter: PropTypes.func, @@ -450,6 +549,8 @@ Transition.propTypes = { * Callback fired after the "entering" status is applied. An extra parameter * `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount * + * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. + * * @type Function(node: HtmlElement, isAppearing: bool) */ onEntering: PropTypes.func, @@ -458,6 +559,8 @@ Transition.propTypes = { * Callback fired after the "entered" status is applied. An extra parameter * `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount * + * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. + * * @type Function(node: HtmlElement, isAppearing: bool) -> void */ onEntered: PropTypes.func, @@ -465,6 +568,8 @@ Transition.propTypes = { /** * Callback fired before the "exiting" status is applied. * + * **Note**: when `nodeRef` prop is passed, `node` is not passed. + * * @type Function(node: HtmlElement) -> void */ onExit: PropTypes.func, @@ -472,6 +577,8 @@ Transition.propTypes = { /** * Callback fired after the "exiting" status is applied. * + * **Note**: when `nodeRef` prop is passed, `node` is not passed. + * * @type Function(node: HtmlElement) -> void */ onExiting: PropTypes.func, @@ -479,6 +586,8 @@ Transition.propTypes = { /** * Callback fired after the "exited" status is applied. * + * **Note**: when `nodeRef` prop is passed, `node` is not passed + * * @type Function(node: HtmlElement) -> void */ onExited: PropTypes.func, @@ -501,13 +610,13 @@ Transition.defaultProps = { onExit: noop, onExiting: noop, - onExited: noop + onExited: noop, }; -Transition.UNMOUNTED = 0; -Transition.EXITED = 1; -Transition.ENTERING = 2; -Transition.ENTERED = 3; -Transition.EXITING = 4; +Transition.UNMOUNTED = UNMOUNTED; +Transition.EXITED = EXITED; +Transition.ENTERING = ENTERING; +Transition.ENTERED = ENTERED; +Transition.EXITING = EXITING; export default Transition; diff --git a/src/TransitionGroup.js b/src/TransitionGroup.js index 548be8fd..14783c0f 100644 --- a/src/TransitionGroup.js +++ b/src/TransitionGroup.js @@ -1,169 +1,73 @@ import PropTypes from 'prop-types'; -import React, { cloneElement, isValidElement } from 'react'; +import React from 'react'; +import TransitionGroupContext from './TransitionGroupContext'; -import { getChildMapping, mergeChildMappings } from './utils/ChildMapping'; +import { + getChildMapping, + getInitialChildMapping, + getNextChildMapping, +} from './utils/ChildMapping'; -const values = Object.values || (obj => Object.keys(obj).map(k => obj[k])); - -const propTypes = { - /** - * `` renders a `
` by default. You can change this - * behavior by providing a `component` prop. - * If you use React v16+ and would like to avoid a wrapping `
` element - * you can pass in `component={null}`. This is useful if the wrapping div - * borks your css styles. - */ - component: PropTypes.any, - /** - * A set of `` components, that are toggled `in` and out as they - * leave. the `` will inject specific transition props, so - * remember to spread them through if you are wrapping the `` as - * with our `` example. - */ - children: PropTypes.node, - - /** - * A convenience prop that enables or disables appear animations - * for all children. Note that specifying this will override any defaults set - * on individual children Transitions. - */ - appear: PropTypes.bool, - /** - * A convenience prop that enables or disables enter animations - * for all children. Note that specifying this will override any defaults set - * on individual children Transitions. - */ - enter: PropTypes.bool, - /** - * A convenience prop that enables or disables exit animations - * for all children. Note that specifying this will override any defaults set - * on individual children Transitions. - */ - exit: PropTypes.bool, - - /** - * You may need to apply reactive updates to a child as it is exiting. - * This is generally done by using `cloneElement` however in the case of an exiting - * child the element has already been removed and not accessible to the consumer. - * - * If you do need to update a child as it leaves you can provide a `childFactory` - * to wrap every child, even the ones that are leaving. - * - * @type Function(child: ReactElement) -> ReactElement - */ - childFactory: PropTypes.func, -}; +const values = Object.values || ((obj) => Object.keys(obj).map((k) => obj[k])); const defaultProps = { component: 'div', - childFactory: child => child, + childFactory: (child) => child, }; /** - * The `` component manages a set of `` components - * in a list. Like with the `` component, ``, is a - * state machine for managing the mounting and unmounting of components over - * time. - * - * Consider the example below using the `Fade` CSS transition from before. - * As items are removed or added to the TodoList the `in` prop is toggled - * automatically by the ``. You can use _any_ `` - * component in a ``, not just css. + * The `` component manages a set of transition components + * (`` and ``) in a list. Like with the transition + * components, `` is a state machine for managing the mounting + * and unmounting of components over time. * - * + * Consider the example below. As items are removed or added to the TodoList the + * `in` prop is toggled automatically by the ``. * * Note that `` does not define any animation behavior! - * Exactly _how_ a list item animates is up to the individual `` - * components. This means you can mix and match animations across different - * list items. + * Exactly _how_ a list item animates is up to the individual transition + * component. This means you can mix and match animations across different list + * items. */ class TransitionGroup extends React.Component { - static childContextTypes = { - transitionGroup: PropTypes.object.isRequired, - }; - constructor(props, context) { super(props, context); + const handleExited = this.handleExited.bind(this); + // Initial children should all be entering, dependent on appear this.state = { - children: getChildMapping(props.children, child => { - return cloneElement(child, { - onExited: this.handleExited.bind(this, child), - in: true, - appear: this.getProp(child, 'appear'), - enter: this.getProp(child, 'enter'), - exit: this.getProp(child, 'exit'), - }) - }), - }; + contextValue: { isMounting: true }, + handleExited, + firstRender: true, + }; } - getChildContext() { - return { - transitionGroup: { isMounting: !this.appeared } - } - } - // use child config unless explictly set by the Group - getProp(child, prop, props = this.props) { - return props[prop] != null ? - props[prop] : - child.props[prop]; + componentDidMount() { + this.mounted = true; + this.setState({ + contextValue: { isMounting: false }, + }); } - componentDidMount() { - this.appeared = true; + componentWillUnmount() { + this.mounted = false; } - componentWillReceiveProps(nextProps) { - let prevChildMapping = this.state.children; - let nextChildMapping = getChildMapping(nextProps.children); - - let children = mergeChildMappings(prevChildMapping, nextChildMapping); - - Object.keys(children).forEach((key) => { - let child = children[key] - - if (!isValidElement(child)) return; - - const hasPrev = key in prevChildMapping; - const hasNext = key in nextChildMapping; - - const prevChild = prevChildMapping[key]; - const isLeaving = isValidElement(prevChild) && !prevChild.props.in; - - // item is new (entering) - if (hasNext && (!hasPrev || isLeaving)) { - // console.log('entering', key) - children[key] = cloneElement(child, { - onExited: this.handleExited.bind(this, child), - in: true, - exit: this.getProp(child, 'exit', nextProps), - enter: this.getProp(child, 'enter', nextProps), - }); - } - // item is old (exiting) - else if (!hasNext && hasPrev && !isLeaving) { - // console.log('leaving', key) - children[key] = cloneElement(child, { in: false }); - } - // item hasn't changed transition states - // copy over the last transition props; - else if (hasNext && hasPrev && isValidElement(prevChild)) { - // console.log('unchanged', key) - children[key] = cloneElement(child, { - onExited: this.handleExited.bind(this, child), - in: prevChild.props.in, - exit: this.getProp(child, 'exit', nextProps), - enter: this.getProp(child, 'enter', nextProps), - }); - } - }) - - this.setState({ children }); + static getDerivedStateFromProps( + nextProps, + { children: prevChildMapping, handleExited, firstRender } + ) { + return { + children: firstRender + ? getInitialChildMapping(nextProps, handleExited) + : getNextChildMapping(nextProps, prevChildMapping, handleExited), + firstRender: false, + }; } - handleExited(child, node){ + // node is `undefined` when user provided `nodeRef` prop + handleExited(child, node) { let currentChildMapping = getChildMapping(this.props.children); if (child.key in currentChildMapping) return; @@ -172,16 +76,19 @@ class TransitionGroup extends React.Component { child.props.onExited(node); } - this.setState((state) => { - let children = { ...state.children }; + if (this.mounted) { + this.setState((state) => { + let children = { ...state.children }; - delete children[child.key]; - return { children }; - }); + delete children[child.key]; + return { children }; + }); + } } render() { const { component: Component, childFactory, ...props } = this.props; + const { contextValue } = this.state; const children = values(this.state.children).map(childFactory); delete props.appear; @@ -189,17 +96,76 @@ class TransitionGroup extends React.Component { delete props.exit; if (Component === null) { - return children; + return ( + + {children} + + ); } return ( - - {children} - + + {children} + ); } } -TransitionGroup.propTypes = propTypes; +TransitionGroup.propTypes = { + /** + * `` renders a `
` by default. You can change this + * behavior by providing a `component` prop. + * If you use React v16+ and would like to avoid a wrapping `
` element + * you can pass in `component={null}`. This is useful if the wrapping div + * borks your css styles. + */ + component: PropTypes.any, + /** + * A set of `` components, that are toggled `in` and out as they + * leave. the `` will inject specific transition props, so + * remember to spread them through if you are wrapping the `` as + * with our `` example. + * + * While this component is meant for multiple `Transition` or `CSSTransition` + * children, sometimes you may want to have a single transition child with + * content that you want to be transitioned out and in when you change it + * (e.g. routes, images etc.) In that case you can change the `key` prop of + * the transition child as you change its content, this will cause + * `TransitionGroup` to transition the child out and back in. + */ + children: PropTypes.node, + + /** + * A convenience prop that enables or disables appear animations + * for all children. Note that specifying this will override any defaults set + * on individual children Transitions. + */ + appear: PropTypes.bool, + /** + * A convenience prop that enables or disables enter animations + * for all children. Note that specifying this will override any defaults set + * on individual children Transitions. + */ + enter: PropTypes.bool, + /** + * A convenience prop that enables or disables exit animations + * for all children. Note that specifying this will override any defaults set + * on individual children Transitions. + */ + exit: PropTypes.bool, + + /** + * You may need to apply reactive updates to a child as it is exiting. + * This is generally done by using `cloneElement` however in the case of an exiting + * child the element has already been removed and not accessible to the consumer. + * + * If you do need to update a child as it leaves you can provide a `childFactory` + * to wrap every child, even the ones that are leaving. + * + * @type Function(child: ReactElement) -> ReactElement + */ + childFactory: PropTypes.func, +}; + TransitionGroup.defaultProps = defaultProps; export default TransitionGroup; diff --git a/src/TransitionGroupContext.js b/src/TransitionGroupContext.js new file mode 100644 index 00000000..51b82c7d --- /dev/null +++ b/src/TransitionGroupContext.js @@ -0,0 +1,3 @@ +import React from 'react'; + +export default React.createContext(null); diff --git a/src/config.js b/src/config.js new file mode 100644 index 00000000..fb43f3d8 --- /dev/null +++ b/src/config.js @@ -0,0 +1,3 @@ +export default { + disabled: false, +}; diff --git a/src/index.js b/src/index.js index e1ce1316..4e7160c7 100644 --- a/src/index.js +++ b/src/index.js @@ -1,12 +1,6 @@ -import CSSTransition from './CSSTransition'; -import ReplaceTransition from './ReplaceTransition'; -import TransitionGroup from './TransitionGroup'; -import Transition from './Transition'; - - -module.exports = { - Transition, - TransitionGroup, - ReplaceTransition, - CSSTransition, -}; +export { default as CSSTransition } from './CSSTransition'; +export { default as ReplaceTransition } from './ReplaceTransition'; +export { default as SwitchTransition } from './SwitchTransition'; +export { default as TransitionGroup } from './TransitionGroup'; +export { default as Transition } from './Transition'; +export { default as config } from './config'; diff --git a/src/utils/ChildMapping.js b/src/utils/ChildMapping.js index dcf4c3d7..cdd33a3e 100644 --- a/src/utils/ChildMapping.js +++ b/src/utils/ChildMapping.js @@ -1,5 +1,4 @@ -import { Children, isValidElement } from 'react'; - +import { Children, cloneElement, isValidElement } from 'react'; /** * Given `this.props.children`, return an object mapping key to child. @@ -8,11 +7,12 @@ import { Children, isValidElement } from 'react'; * @return {object} Mapping of key to child */ export function getChildMapping(children, mapFn) { - let mapper = child => mapFn && isValidElement(child ) ? mapFn(child) : child; + let mapper = (child) => + mapFn && isValidElement(child) ? mapFn(child) : child; let result = Object.create(null); if (children) - Children.map(children, c => c).forEach((child) => { + Children.map(children, (c) => c).forEach((child) => { // run the map function here instead so that the key is the computed one result[child.key] = mapper(child); }); @@ -66,9 +66,8 @@ export function mergeChildMappings(prev, next) { if (nextKeysPending[nextKey]) { for (i = 0; i < nextKeysPending[nextKey].length; i++) { let pendingNextKey = nextKeysPending[nextKey][i]; - childMapping[nextKeysPending[nextKey][i]] = getValueForKey( - pendingNextKey, - ); + childMapping[nextKeysPending[nextKey][i]] = + getValueForKey(pendingNextKey); } } childMapping[nextKey] = getValueForKey(nextKey); @@ -81,3 +80,63 @@ export function mergeChildMappings(prev, next) { return childMapping; } + +function getProp(child, prop, props) { + return props[prop] != null ? props[prop] : child.props[prop]; +} + +export function getInitialChildMapping(props, onExited) { + return getChildMapping(props.children, (child) => { + return cloneElement(child, { + onExited: onExited.bind(null, child), + in: true, + appear: getProp(child, 'appear', props), + enter: getProp(child, 'enter', props), + exit: getProp(child, 'exit', props), + }); + }); +} + +export function getNextChildMapping(nextProps, prevChildMapping, onExited) { + let nextChildMapping = getChildMapping(nextProps.children); + let children = mergeChildMappings(prevChildMapping, nextChildMapping); + + Object.keys(children).forEach((key) => { + let child = children[key]; + + if (!isValidElement(child)) return; + + const hasPrev = key in prevChildMapping; + const hasNext = key in nextChildMapping; + + const prevChild = prevChildMapping[key]; + const isLeaving = isValidElement(prevChild) && !prevChild.props.in; + + // item is new (entering) + if (hasNext && (!hasPrev || isLeaving)) { + // console.log('entering', key) + children[key] = cloneElement(child, { + onExited: onExited.bind(null, child), + in: true, + exit: getProp(child, 'exit', nextProps), + enter: getProp(child, 'enter', nextProps), + }); + } else if (!hasNext && hasPrev && !isLeaving) { + // item is old (exiting) + // console.log('leaving', key) + children[key] = cloneElement(child, { in: false }); + } else if (hasNext && hasPrev && isValidElement(prevChild)) { + // item hasn't changed transition states + // copy over the last transition props; + // console.log('unchanged', key) + children[key] = cloneElement(child, { + onExited: onExited.bind(null, child), + in: prevChild.props.in, + exit: getProp(child, 'exit', nextProps), + enter: getProp(child, 'enter', nextProps), + }); + } + }); + + return children; +} diff --git a/src/utils/PropTypes.js b/src/utils/PropTypes.js index af818dd7..5284b53a 100644 --- a/src/utils/PropTypes.js +++ b/src/utils/PropTypes.js @@ -1,53 +1,33 @@ import PropTypes from 'prop-types'; -export function transitionTimeout(transitionType) { - let timeoutPropName = 'transition' + transitionType + 'Timeout'; - let enabledPropName = 'transition' + transitionType; +export const timeoutsShape = + process.env.NODE_ENV !== 'production' + ? PropTypes.oneOfType([ + PropTypes.number, + PropTypes.shape({ + enter: PropTypes.number, + exit: PropTypes.number, + appear: PropTypes.number, + }).isRequired, + ]) + : null; - return (props) => { - // If the transition is enabled - if (props[enabledPropName]) { - // If no timeout duration is provided - if (props[timeoutPropName] == null) { - return new Error( - timeoutPropName + ' wasn\'t supplied to CSSTransitionGroup: ' + - 'this can cause unreliable animations and won\'t be supported in ' + - 'a future version of React. See ' + - 'https://fb.me/react-animation-transition-group-timeout for more ' + - 'information.', - ); - - // If the duration isn't a number - } else if (typeof props[timeoutPropName] !== 'number') { - return new Error(timeoutPropName + ' must be a number (in milliseconds)'); - } - } - - return null; - }; -} - -export const timeoutsShape = PropTypes.oneOfType([ - PropTypes.number, - PropTypes.shape({ - enter: PropTypes.number, - exit: PropTypes.number, - }).isRequired, -]); - -export const classNamesShape = PropTypes.oneOfType([ - PropTypes.string, - PropTypes.shape({ - enter: PropTypes.string, - exit: PropTypes.string, - active: PropTypes.string, - }), - PropTypes.shape({ - enter: PropTypes.string, - enterDone: PropTypes.string, - enterActive: PropTypes.string, - exit: PropTypes.string, - exitDone: PropTypes.string, - exitActive: PropTypes.string, - }), -]); +export const classNamesShape = + process.env.NODE_ENV !== 'production' + ? PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({ + enter: PropTypes.string, + exit: PropTypes.string, + active: PropTypes.string, + }), + PropTypes.shape({ + enter: PropTypes.string, + enterDone: PropTypes.string, + enterActive: PropTypes.string, + exit: PropTypes.string, + exitDone: PropTypes.string, + exitActive: PropTypes.string, + }), + ]) + : null; diff --git a/src/utils/SimpleSet.js b/src/utils/SimpleSet.js index 1a49ec91..1617ecba 100644 --- a/src/utils/SimpleSet.js +++ b/src/utils/SimpleSet.js @@ -1,18 +1,21 @@ - export default class SimpleSet { constructor() { - this.v = [] + this.v = []; + } + clear() { + this.v.length = 0; + } + has(k) { + return this.v.indexOf(k) !== -1; } - clear() { this.v.length = 0; } - has(k) { return this.v.indexOf(k) !== -1; } add(k) { if (this.has(k)) return; - this.v.push(k) + this.v.push(k); } delete(k) { const idx = this.v.indexOf(k); if (idx === -1) return false; - this.v.splice(idx, 1) + this.v.splice(idx, 1); return true; } } diff --git a/src/utils/reflow.js b/src/utils/reflow.js new file mode 100644 index 00000000..f02dd2ab --- /dev/null +++ b/src/utils/reflow.js @@ -0,0 +1 @@ +export const forceReflow = (node) => node.scrollTop; diff --git a/stories/.eslintrc b/stories/.eslintrc.yml similarity index 88% rename from stories/.eslintrc rename to stories/.eslintrc.yml index 77b9f34e..85b854c0 100644 --- a/stories/.eslintrc +++ b/stories/.eslintrc.yml @@ -1,5 +1,3 @@ -globals: - css: false rules: react/prop-types: off no-unused-vars: diff --git a/stories/CSSTransition.js b/stories/CSSTransition.js new file mode 100644 index 00000000..73379400 --- /dev/null +++ b/stories/CSSTransition.js @@ -0,0 +1,50 @@ +import React, { useState } from 'react'; +import { storiesOf } from '@storybook/react'; + +import StoryFixture from './StoryFixture'; +import Fade from './transitions/CSSFade'; + +function ToggleFixture({ defaultIn, description, children }) { + const [show, setShow] = useState(defaultIn || false); + + return ( + +
+ +
+ {React.cloneElement(children, { in: show })} +
+ ); +} + +storiesOf('CSSTransition', module) + .add('Fade', () => ( + + asaghasg asgasg + + )) + .add('Fade with appear', () => ( + + asaghasg asgasg + + )) + .add('Fade with mountOnEnter', () => { + return ( + + Fade with mountOnEnter + + ); + }) + .add('Fade with unmountOnExit', () => { + return ( + + Fade with unmountOnExit + + ); + }); diff --git a/stories/CSSTransitionGroupFixture.js b/stories/CSSTransitionGroupFixture.js index c5e0e927..091ec848 100644 --- a/stories/CSSTransitionGroupFixture.js +++ b/stories/CSSTransitionGroupFixture.js @@ -4,24 +4,20 @@ import TransitionGroup from '../src/TransitionGroup'; import StoryFixture from './StoryFixture'; class CSSTransitionGroupFixture extends React.Component { - constructor(props, context) { - super(props, context); + static defaultProps = { + items: [], + }; - let items = props.items || []; + count = this.props.items.length; + state = { + items: this.props.items, + }; - this.count = items.length; - this.state = { - items, - }; - } handleAddItem = () => { this.setState(({ items }) => ({ - items: [ - ...items, - `Item number: ${++this.count}`, - ], + items: [...items, `Item number: ${++this.count}`], })); - } + }; handleRemoveItems = () => { this.setState(({ items }) => { @@ -29,40 +25,35 @@ class CSSTransitionGroupFixture extends React.Component { items.splice(1, 3); return { items }; }); - } + }; handleRemoveItem = (item) => { this.setState(({ items }) => ({ - items: items.filter(i => i !== item), + items: items.filter((i) => i !== item), })); - } + }; render() { - const { items: _, description, children, ...props } = this.props; + const { items: _, description, children, ...rest } = this.props; + // e.g. `Fade`, see where `CSSTransitionGroupFixture` is used + const { type: TransitionType, props: transitionTypeProps } = + React.Children.only(children); return (
- - {' '} - + {' '} +
- - {this.state.items.map(item => React.cloneElement(children, { - key: item, - children: ( -
- {item} - -
- ) - }))} + + {this.state.items.map((item) => ( + + {item} + + + ))}
); diff --git a/stories/NestedTransition.js b/stories/NestedTransition.js index 3b835a02..58fe59cb 100644 --- a/stories/NestedTransition.js +++ b/stories/NestedTransition.js @@ -1,51 +1,44 @@ -import React from 'react'; -import StoryFixture from './StoryFixture'; +import React, { useState } from 'react'; -import Fade from './transitions/Fade'; +import StoryFixture from './StoryFixture'; +import Fade from './transitions/CSSFadeForTransitionGroup'; import Scale from './transitions/Scale'; -const FadeAndScale = (props) => ( - -
+function FadeAndScale(props) { + return ( +
I will fade
{/* - We also want to scale in at the same time so we pass the `in` state here as well, so it enters - at the same time as the Fade. + We also want to scale in at the same time so we pass the `in` state here as well, so it enters + at the same time as the Fade. - Note also the `appear` since the Fade will happen when the item mounts, the Scale transition - will mount at the same time as the div we want to scale, so we need to tell it to animate as - it _appears_. - */} + Note also the `appear` since the Fade will happen when the item mounts, the Scale transition + will mount at the same time as the div we want to scale, so we need to tell it to animate as + it _appears_. + */} -
I should scale
+ I should scale
-
-
-) - - -export default class Example extends React.Component { - constructor(props, context) { - super(props, context); - this.state = { showNested: false }; - } - - toggleNested = () => { - this.setState({ showNested: !this.state.showNested }) - } - - render() { - return ( - -

Nested Animations

- - -
- ); - } +
+ ); } +function Example() { + const [showNested, setShowNested] = useState(false); + + return ( + +

Nested Animations

+ + +
+ ); +} - +export default Example; diff --git a/stories/ReplaceTransition.js b/stories/ReplaceTransition.js index 58499e8d..2aec7907 100644 --- a/stories/ReplaceTransition.js +++ b/stories/ReplaceTransition.js @@ -1,4 +1,5 @@ -import React from 'react'; +import { css } from 'astroturf'; +import React, { useState } from 'react'; import { storiesOf } from '@storybook/react'; import ReplaceTransition from '../src/ReplaceTransition'; @@ -6,17 +7,18 @@ import CSSTransition from '../src/CSSTransition'; const FADE_TIMEOUT = 1000; -let styles = css` +const styles = css` .enter { opacity: 0.01; } - .enter.enter-active { + .enter.enter-active { position: absolute; - left: 0; right: 0; + left: 0; + right: 0; opacity: 1; transition: opacity ${FADE_TIMEOUT * 2}ms ease-in; - transition-delay: ${FADE_TIMEOUT}ms + transition-delay: ${FADE_TIMEOUT}ms; } .exit { @@ -37,44 +39,43 @@ let styles = css` } `; -export default class Fade extends React.Component { - static defaultProps = { - in: false, - delay: false, - timeout: FADE_TIMEOUT * 2, - }; - render() { - const { ...props } = this.props; - return ( - - ); - } -} +const defaultProps = { + in: false, + timeout: FADE_TIMEOUT * 2, +}; -class Example extends React.Component { - state = { in: false } - render() { - return ( -
- - {React.cloneElement(this.props.children, this.state)} -
- ); - } +function Fade(props) { + return ( + + ); } +Fade.defaultProps = defaultProps; -storiesOf('Replace Transition', module) - .add('Animates on all', () => ( +function Example({ children }) { + const [show, setShow] = useState(false); + + return ( +
+ + {React.cloneElement(children, { in: show })} +
+ ); +} +storiesOf('Replace Transition', module).add('Animates on all', () => { + const firstNodeRef = React.createRef(); + const secondNodeRef = React.createRef(); + return ( console.log('onEnter')} onEntering={() => console.log('onEntering')} @@ -83,8 +84,13 @@ storiesOf('Replace Transition', module) onExiting={() => console.log('onExiting')} onExited={() => console.log('onExited')} > -
in True
-
in False
+ +
in True
+
+ +
in False
+
- )) + ); +}); diff --git a/stories/StoryFixture.js b/stories/StoryFixture.js index 163310f0..cb0fb591 100644 --- a/stories/StoryFixture.js +++ b/stories/StoryFixture.js @@ -5,18 +5,14 @@ const propTypes = { description: PropTypes.string, }; -class StoryFixture extends React.Component { - render() { - const { children, description } = this.props; +function StoryFixture({ description, children }) { + return ( +
+

{description}

- return ( -
-

{description}

- - {children} -
- ); - } + {children} +
+ ); } StoryFixture.propTypes = propTypes; diff --git a/stories/Transition.js b/stories/Transition.js index db8093d1..da050927 100644 --- a/stories/Transition.js +++ b/stories/Transition.js @@ -1,48 +1,77 @@ -import React from 'react'; +import React, { useState } from 'react'; import { storiesOf } from '@storybook/react'; -import { Fade, Collapse } from './transitions/Bootstrap' import StoryFixture from './StoryFixture'; +import { + Fade, + Collapse, + FadeForwardRef, + FadeInnerRef, +} from './transitions/Bootstrap'; +function ToggleFixture({ defaultIn, description, children }) { + const [show, setShow] = useState(defaultIn); -class ToggleFixture extends React.Component { - state = { show: this.props.defaultIn } - render() { - return ( - -
- -
- {React.cloneElement(this.props.children, { - in: this.state.show - })} -
- ) - } + return ( + +
+ +
+ {React.cloneElement(children, { in: show })} +
+ ); } storiesOf('Transition', module) .add('Bootstrap Fade', () => ( - -
asaghasg asgasg
-
+ asaghasg asgasg
)) .add('Bootstrap Collapse', () => ( -
- asaghasg asgasg -
foo
-
bar
-
+ asaghasg asgasg +
foo
+
bar
- )); + )) + .add('Fade using React.forwardRef', () => { + const nodeRef = React.createRef(); + return ( + + + Fade using React.forwardRef + + + ); + }) + .add('Fade using innerRef', () => { + const nodeRef = React.createRef(); + return ( + + Fade using innerRef + + ); + }) + .add('Fade with mountOnEnter', () => { + return ( + + Fade with mountOnEnter + + ); + }) + .add('Fade with unmountOnExit', () => { + return ( + + Fade with unmountOnExit + + ); + }); diff --git a/stories/TransitionGroup.js b/stories/TransitionGroup.js index 7e8183ef..12f9d1a5 100644 --- a/stories/TransitionGroup.js +++ b/stories/TransitionGroup.js @@ -1,12 +1,12 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { storiesOf } from '@storybook/react'; import TransitionGroup from '../src/TransitionGroup'; + import CSSTransitionGroupFixture from './CSSTransitionGroupFixture'; -import NestedTransition from './NestedTransition' +import NestedTransition from './NestedTransition'; import StoryFixture from './StoryFixture'; -import Fade, { FADE_TIMEOUT } from './transitions/Fade'; - +import Fade, { FADE_TIMEOUT } from './transitions/CSSFadeForTransitionGroup'; storiesOf('Css Transition Group', module) .add('Animates on all', () => ( @@ -16,7 +16,7 @@ storiesOf('Css Transition Group', module) removed or on initial appear `} appear - items={[ 'Item number: 1' ]} + items={['Item number: 1']} > @@ -29,9 +29,9 @@ storiesOf('Css Transition Group', module) `} exit={false} timeout={{ enter: FADE_TIMEOUT }} - items={[ 'Item number: 1' ]} + items={['Item number: 1']} > - + )) .add('Animates on exit', () => ( @@ -40,11 +40,7 @@ storiesOf('Css Transition Group', module) Should animate when items are removed to the list but not when they are added or on initial appear `} - items={[ - 'Item number: 1', - 'Item number: 2', - 'Item number: 3', - ]} + items={['Item number: 1', 'Item number: 2', 'Item number: 3']} > @@ -55,11 +51,7 @@ storiesOf('Css Transition Group', module) Should animate when items first mount but not when added or removed `} appear - items={[ - 'Item number: 1', - 'Item number: 2', - 'Item number: 3', - ]} + items={['Item number: 1', 'Item number: 2', 'Item number: 3']} > @@ -76,69 +68,66 @@ storiesOf('Css Transition Group', module) .add('Re-entering while leaving', () => ( - + )) - .add('Nested Transitions', () => ( - - )) - ; + .add('Nested Transitions', () => ); class DynamicTransition extends React.Component { - state = { count: 0 } + state = { count: 0 }; handleClick = () => { - this.setState({ hide: !this.state.hide }) - } + this.setState({ hide: !this.state.hide }); + }; componentDidMount() { this.interval = setInterval(() => { - this.setState({ count: this.state.count + 1 }) + this.setState({ count: this.state.count + 1 }); }, 700); } - componentWillUnmount() { clearInterval(this.interval); } + componentWillUnmount() { + clearInterval(this.interval); + } render() { - const { hide, count } = this.state + const { hide, count } = this.state; return (
- {!hide && - -
Changing! {count}
-
- } + {!hide && Changing! {count}}
- ) + ); } } -class RenterTransition extends React.Component { - handleClick = () => { - this.setState({ hide: true }, () => { +function ReEnterTransition() { + const [hide, setHide] = useState(false); + + useEffect(() => { + if (hide) { setTimeout(() => { - console.log('re-entering!') - this.setState({ hide: false }) - }, FADE_TIMEOUT / 2); - }) - } - render() { - const { hide } = this.state || {} - return ( -
- - - {!hide && - -
I'm entering!
-
- } -
-
- ) - } + console.log('re-entering!'); + setHide(false); + }, 0.5 * FADE_TIMEOUT); + } + }, [hide]); + + return ( +
+ + + {!hide && I'm entering!} + +
+ ); } diff --git a/stories/index.js b/stories/index.js index f038fac2..535c80a5 100644 --- a/stories/index.js +++ b/stories/index.js @@ -1,3 +1,4 @@ import './Transition'; +import './CSSTransition'; import './TransitionGroup'; import './ReplaceTransition'; diff --git a/stories/transitions/Bootstrap.js b/stories/transitions/Bootstrap.js index 5b5cdd2c..8c84949b 100644 --- a/stories/transitions/Bootstrap.js +++ b/stories/transitions/Bootstrap.js @@ -1,14 +1,18 @@ -import React from 'react'; -import style from 'dom-helpers/style'; - -import Transition, { EXITED, ENTERED, ENTERING, EXITING } - from '../../src/Transition'; +import { css } from 'astroturf'; +import React, { useEffect, useRef } from 'react'; +import style from 'dom-helpers/css'; +import Transition, { + EXITED, + ENTERED, + ENTERING, + EXITING, +} from '../../src/Transition'; const styles = css` .fade { opacity: 0; - transition: opacity .15s linear; + transition: opacity 0.5s linear; } .fade.in { opacity: 1; @@ -18,82 +22,95 @@ const styles = css` display: none; } - .collapse.in { display: block; } + .collapse.in { + display: block; + } .collapsing { position: relative; height: 0; overflow: hidden; - transition: .35s ease; + transition: 0.35s ease; transition-property: height, visibility; } `; - const fadeStyles = { [ENTERING]: styles.in, [ENTERED]: styles.in, +}; + +export function Fade(props) { + const nodeRef = useRef(); + return ( + + {(status) => ( +
+ {props.children} +
+ )} +
+ ); } -export const Fade = props => ( - - {status => React.cloneElement(props.children, { - className: `${styles.fade} ${fadeStyles[status] || ''}` - })} - -) - function getHeight(elem) { let value = elem.offsetHeight; let margins = ['marginTop', 'marginBottom']; - return (value + + return ( + value + parseInt(style(elem, margins[0]), 10) + parseInt(style(elem, margins[1]), 10) ); } - const collapseStyles = { [EXITED]: styles.collapse, [EXITING]: styles.collapsing, [ENTERING]: styles.collapsing, [ENTERED]: `${styles.collapse} ${styles.in}`, -} +}; export class Collapse extends React.Component { + nodeRef = React.createRef(); + /* -- Expanding -- */ - handleEnter = (elem) => { - elem.style.height = '0'; - } + handleEnter = () => { + this.nodeRef.current.style.height = '0'; + }; - handleEntering = (elem) => { - elem.style.height = `${elem.scrollHeight}px`; - } + handleEntering = () => { + this.nodeRef.current.style.height = `${this.nodeRef.current.scrollHeight}px`; + }; - handleEntered = (elem) => { - elem.style.height = null; - } + handleEntered = () => { + this.nodeRef.current.style.height = null; + }; /* -- Collapsing -- */ - handleExit = (elem) => { - elem.style.height = getHeight(elem) + 'px'; - elem.offsetHeight; // eslint-disable-line no-unused-expressions - } + handleExit = () => { + this.nodeRef.current.style.height = getHeight(this.nodeRef.current) + 'px'; + this.nodeRef.current.offsetHeight; // eslint-disable-line no-unused-expressions + }; - handleExiting = (elem) => { - elem.style.height = '0'; - } + handleExiting = () => { + this.nodeRef.current.style.height = '0'; + }; render() { - const { children } = this.props; + const { children, ...rest } = this.props; return ( - {(state, props) => React.cloneElement(children, { - ...props, - className: collapseStyles[state] - })} + {(state, props) => ( +
+ {children} +
+ )}
); } } + +export function FadeInnerRef(props) { + const nodeRef = useMergedRef(props.innerRef); + return ( + + {(status) => ( +
+ {props.children} +
+ )} +
+ ); +} + +export const FadeForwardRef = React.forwardRef((props, ref) => { + return ; +}); + +/** + * Compose multiple refs, there may be different implementations + * This one is derived from + * e.g. https://github.com/react-restart/hooks/blob/ed37bf3dfc8fc1d9234a6d8fe0af94d69fad3b74/src/useMergedRefs.ts + * Also here are good discussion about this + * https://github.com/facebook/react/issues/13029 + * @param ref + * @returns {React.MutableRefObject} + */ +function useMergedRef(ref) { + const nodeRef = React.useRef(); + useEffect(function () { + if (ref) { + if (typeof ref === 'function') { + ref(nodeRef.current); + } else { + ref.current = nodeRef.current; + } + } + }); + return nodeRef; +} diff --git a/stories/transitions/CSSFade.js b/stories/transitions/CSSFade.js new file mode 100644 index 00000000..9ab381c8 --- /dev/null +++ b/stories/transitions/CSSFade.js @@ -0,0 +1,53 @@ +import { css } from 'astroturf'; +import React, { useRef } from 'react'; + +import CSSTransition from '../../src/CSSTransition'; + +export const FADE_TIMEOUT = 1000; + +const styles = css` + .default { + opacity: 0; + } + .enter-done { + opacity: 1; + } + + .enter, + .appear { + opacity: 0.01; + } + + .enter.enter-active, + .appear.appear-active { + opacity: 1; + transition: opacity ${FADE_TIMEOUT}ms ease-in; + } + + .exit { + opacity: 1; + } + .exit.exit-active { + opacity: 0.01; + transition: opacity ${0.8 * FADE_TIMEOUT}ms ease-in; + } +`; + +const defaultProps = { + in: false, + timeout: FADE_TIMEOUT, +}; +function Fade(props) { + const nodeRef = useRef(); + return ( + +
+ {props.children} +
+
+ ); +} + +Fade.defaultProps = defaultProps; + +export default Fade; diff --git a/stories/transitions/CSSFadeForTransitionGroup.js b/stories/transitions/CSSFadeForTransitionGroup.js new file mode 100644 index 00000000..e043f720 --- /dev/null +++ b/stories/transitions/CSSFadeForTransitionGroup.js @@ -0,0 +1,45 @@ +import { css } from 'astroturf'; +import React, { useRef } from 'react'; + +import CSSTransition from '../../src/CSSTransition'; + +export const FADE_TIMEOUT = 1000; + +const styles = css` + .enter, + .appear { + opacity: 0.01; + } + + .enter.enter-active, + .appear.appear-active { + opacity: 1; + transition: opacity ${FADE_TIMEOUT}ms ease-in; + } + + .exit { + opacity: 1; + } + .exit.exit-active { + opacity: 0.01; + transition: opacity ${0.8 * FADE_TIMEOUT}ms ease-in; + } +`; + +const defaultProps = { + in: false, + timeout: FADE_TIMEOUT, +}; + +function Fade(props) { + const nodeRef = useRef(); + return ( + +
{props.children}
+
+ ); +} + +Fade.defaultProps = defaultProps; + +export default Fade; diff --git a/stories/transitions/Fade.js b/stories/transitions/Fade.js deleted file mode 100644 index ce90acdd..00000000 --- a/stories/transitions/Fade.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; - -import CSSTransition from '../../src/CSSTransition'; - -export const FADE_TIMEOUT = 3000; - -let styles = css` - .enter, - .appear { - opacity: 0.01; - } - - .enter.enter-active, - .appear.appear-active { - opacity: 1; - transition: opacity 1000ms ease-in; - } - - .exit { - opacity: 1; - } - .exit.exit-active { - opacity: 0.01; - transition: opacity 800ms ease-in; - } -`; - -export default class Fade extends React.Component { - static defaultProps = { - in: false, - timeout: FADE_TIMEOUT, - }; - render() { - return ( - - ); - } -} diff --git a/stories/transitions/Scale.js b/stories/transitions/Scale.js index 8189f758..dc30012f 100644 --- a/stories/transitions/Scale.js +++ b/stories/transitions/Scale.js @@ -1,10 +1,11 @@ -import React from 'react'; +import { css } from 'astroturf'; +import React, { useRef } from 'react'; import CSSTransition from '../../src/CSSTransition'; export const SCALE_TIMEOUT = 1000; -let styles = css` +const styles = css` .enter, .appear { transform: scale(0); @@ -25,17 +26,20 @@ let styles = css` } `; -export default class Scale extends React.Component { - static defaultProps = { - in: false, - timeout: SCALE_TIMEOUT, - }; - render() { - return ( - - ); - } +const defaultProps = { + in: false, + timeout: SCALE_TIMEOUT, +}; + +function Scale(props) { + const nodeRef = useRef(); + return ( + +
{props.children}
+
+ ); } + +Scale.defaultProps = defaultProps; + +export default Scale; diff --git a/test/.eslintrc b/test/.eslintrc.yml similarity index 95% rename from test/.eslintrc rename to test/.eslintrc.yml index 2cfb7601..3c0a3a9e 100644 --- a/test/.eslintrc +++ b/test/.eslintrc.yml @@ -1,7 +1,6 @@ - env: jest: true - jasmine: true + es6: true rules: no-require: off global-require: off diff --git a/test/CSSTransition-test.js b/test/CSSTransition-test.js index 6d4a1e40..9ec4672a 100644 --- a/test/CSSTransition-test.js +++ b/test/CSSTransition-test.js @@ -1,162 +1,346 @@ import React from 'react'; -import { mount } from 'enzyme'; +import { render, waitFor } from './utils'; import CSSTransition from '../src/CSSTransition'; - -jasmine.addMatchers({ - toExist: () => ({ - compare: actual => ({ - pass: actual != null, - }) - }) -}); +import TransitionGroup from '../src/TransitionGroup'; describe('CSSTransition', () => { - it('should flush new props to the DOM before initiating a transition', (done) => { - mount( + const nodeRef = React.createRef(); + const { setProps } = render( { - expect(node.classList.contains('test-class')).toEqual(true) - expect(node.classList.contains('test-entering')).toEqual(false) - done() + onEnter={() => { + expect(nodeRef.current.classList.contains('test-class')).toEqual( + true + ); + expect(nodeRef.current.classList.contains('test-entering')).toEqual( + false + ); + done(); }} > -
+
- ) - .tap(inst => { + ); + + expect(nodeRef.current.classList.contains('test-class')).toEqual(false); - expect(inst.getDOMNode().classList.contains('test-class')).toEqual(false) - }) - .setProps({ + setProps({ in: true, - className: 'test-class' - }) + className: 'test-class', + }); }); describe('entering', () => { - let instance; - - beforeEach(() => { - instance = mount( - -
- - ) - }); - - it('should apply classes at each transition state', done => { + it('should apply classes at each transition state', async () => { let count = 0; + let done = false; + const nodeRef = React.createRef(); + const { setProps } = render( + +
+ + ); - instance.setProps({ + setProps({ in: true, - onEnter(node) { + onEnter() { count++; - expect(node.className).toEqual('test-enter'); + expect(nodeRef.current.className).toEqual('test-enter'); }, - onEntering(node){ + onEntering() { count++; - expect(node.className).toEqual('test-enter test-enter-active'); + expect(nodeRef.current.className).toEqual( + 'test-enter test-enter-active' + ); }, - onEntered(node){ - expect(node.className).toEqual('test-enter-done'); + onEntered() { + expect(nodeRef.current.className).toEqual('test-enter-done'); expect(count).toEqual(2); - done(); - } + done = true; + }, + }); + + await waitFor(() => { + expect(done).toBe(true); }); }); - it('should apply custom classNames names', done => { + it('should apply custom classNames names', async () => { let count = 0; - instance = mount( + const nodeRef = React.createRef(); + const { setProps } = render( -
+
); - instance.setProps({ + setProps({ in: true, - onEnter(node){ + onEnter() { count++; - expect(node.className).toEqual('custom'); + expect(nodeRef.current.className).toEqual('custom'); }, - onEntering(node){ + onEntering() { count++; - expect(node.className).toEqual('custom custom-super-active'); + expect(nodeRef.current.className).toEqual( + 'custom custom-super-active' + ); }, - onEntered(node){ - expect(node.className).toEqual('custom-super-done'); - expect(count).toEqual(2); - done(); - } + onEntered() { + expect(nodeRef.current.className).toEqual('custom-super-done'); + }, + }); + + await waitFor(() => { + expect(count).toEqual(2); }); }); }); - describe('exiting', ()=> { - let instance; + describe('appearing', () => { + it('should apply appear classes at each transition state', async () => { + let count = 0; + const nodeRef = React.createRef(); + render( + { + count++; + expect(isAppearing).toEqual(true); + expect(nodeRef.current.className).toEqual('appear-test-appear'); + }} + onEntering={(isAppearing) => { + count++; + expect(isAppearing).toEqual(true); + expect(nodeRef.current.className).toEqual( + 'appear-test-appear appear-test-appear-active' + ); + }} + onEntered={(isAppearing) => { + expect(isAppearing).toEqual(true); + expect(nodeRef.current.className).toEqual( + 'appear-test-appear-done appear-test-enter-done' + ); + }} + > +
+ + ); + + await waitFor(() => { + expect(count).toEqual(2); + }); + }); + + it('should lose the "*-appear-done" class after leaving and entering again', async () => { + const nodeRef = React.createRef(); + let entered = false; + let exited = false; + const { setProps } = render( + { + entered = true; + }} + > +
+ + ); + + await waitFor(() => { + expect(entered).toEqual(true); + }); + setProps({ + in: false, + onEntered: () => {}, + onExited: () => { + exited = true; + }, + }); + + await waitFor(() => { + expect(exited).toEqual(true); + }); + expect(nodeRef.current.className).toBe('appear-test-exit-done'); + entered = false; + setProps({ + in: true, + onEntered: () => { + entered = true; + }, + }); + + await waitFor(() => { + expect(entered).toEqual(true); + }); + expect(nodeRef.current.className).toBe('appear-test-enter-done'); + }); + + it('should not add undefined when appearDone is not defined', async () => { + const nodeRef = React.createRef(); + let done = false; + render( + { + expect(isAppearing).toEqual(true); + expect(nodeRef.current.className).toEqual('appear-test'); + }} + onEntered={(isAppearing) => { + expect(isAppearing).toEqual(true); + expect(nodeRef.current.className).toEqual(''); + done = true; + }} + > +
+ + ); - beforeEach(() => { - instance = mount( + await waitFor(() => { + expect(done).toEqual(true); + }); + }); + + it('should not be appearing in normal enter mode', async () => { + let count = 0; + const nodeRef = React.createRef(); + render( + +
+ + ).setProps({ + in: true, + + onEnter(isAppearing) { + count++; + expect(isAppearing).toEqual(false); + expect(nodeRef.current.className).toEqual('not-appear-test-enter'); + }, + + onEntering(isAppearing) { + count++; + expect(isAppearing).toEqual(false); + expect(nodeRef.current.className).toEqual( + 'not-appear-test-enter not-appear-test-enter-active' + ); + }, + + onEntered(isAppearing) { + expect(isAppearing).toEqual(false); + expect(nodeRef.current.className).toEqual( + 'not-appear-test-enter-done' + ); + }, + }); + + await waitFor(() => { + expect(count).toEqual(2); + }); + }); + + it('should not enter the transition states when appear=false', () => { + const nodeRef = React.createRef(); + render( { + throw Error('Enter called!'); + }} + onEntering={() => { + throw Error('Entring called!'); + }} + onEntered={() => { + throw Error('Entred called!'); + }} > -
+
- ) + ); }); + }); - it('should apply classes at each transition state', done => { + describe('exiting', () => { + it('should apply classes at each transition state', async () => { let count = 0; + const nodeRef = React.createRef(); + const { setProps } = render( + +
+ + ); - instance.setProps({ + setProps({ in: false, - onExit(node){ + onExit() { count++; - expect(node.className).toEqual('test-exit'); + expect(nodeRef.current.className).toEqual('test-exit'); }, - onExiting(node){ + onExiting() { count++; - expect(node.className).toEqual('test-exit test-exit-active'); + expect(nodeRef.current.className).toEqual( + 'test-exit test-exit-active' + ); }, - onExited(node){ - expect(node.className).toEqual('test-exit-done'); - expect(count).toEqual(2); - done(); - } + onExited() { + expect(nodeRef.current.className).toEqual('test-exit-done'); + }, + }); + + await waitFor(() => { + expect(count).toEqual(2); }); }); - it('should apply custom classNames names', done => { + it('should apply custom classNames names', async () => { let count = 0; - instance = mount( + const nodeRef = React.createRef(); + const { setProps } = render( { exitDone: 'custom-super-done', }} > -
+
); - instance.setProps({ + setProps({ in: false, - onExit(node){ + onExit() { count++; - expect(node.className).toEqual('custom'); + expect(nodeRef.current.className).toEqual('custom'); }, - onExiting(node){ + onExiting() { count++; - expect(node.className).toEqual('custom custom-super-active'); + expect(nodeRef.current.className).toEqual( + 'custom custom-super-active' + ); }, - onExited(node){ - expect(node.className).toEqual('custom-super-done'); - expect(count).toEqual(2); - done(); + onExited() { + expect(nodeRef.current.className).toEqual('custom-super-done'); + }, + }); + + await waitFor(() => { + expect(count).toEqual(2); + }); + }); + + it('should support empty prefix', async () => { + let count = 0; + + const nodeRef = React.createRef(); + const { setProps } = render( + +
+ + ); + + setProps({ + in: false, + + onExit() { + count++; + expect(nodeRef.current.className).toEqual('exit'); + }, + + onExiting() { + count++; + expect(nodeRef.current.className).toEqual('exit exit-active'); + }, + + onExited() { + expect(nodeRef.current.className).toEqual('exit-done'); + }, + }); + + await waitFor(() => { + expect(count).toEqual(2); + }); + }); + }); + + describe('reentering', () => { + it('should remove dynamically applied classes', async () => { + let count = 0; + class Test extends React.Component { + render() { + const { direction, text, nodeRef, ...props } = this.props; + + return ( + + React.cloneElement(child, { + classNames: direction, + }) + } + > + + {text} + + + ); } + } + + const nodeRef = { + foo: React.createRef(), + bar: React.createRef(), + }; + + const { setProps } = render( + + ); + + setProps({ + direction: 'up', + text: 'bar', + nodeRef: nodeRef.bar, + + onEnter() { + count++; + expect(nodeRef.bar.current.className).toEqual('up-enter'); + }, + onEntering() { + count++; + expect(nodeRef.bar.current.className).toEqual( + 'up-enter up-enter-active' + ); + }, + }); + + await waitFor(() => { + expect(count).toEqual(2); + }); + + setProps({ + direction: 'down', + text: 'foo', + nodeRef: nodeRef.foo, + + onEntering() { + count++; + expect(nodeRef.foo.current.className).toEqual( + 'down-enter down-enter-active' + ); + }, + onEntered() { + count++; + expect(nodeRef.foo.current.className).toEqual('down-enter-done'); + }, + }); + + await waitFor(() => { + expect(count).toEqual(4); }); }); }); diff --git a/test/CSSTransitionGroup-test.js b/test/CSSTransitionGroup-test.js index 472d8385..3f37106c 100644 --- a/test/CSSTransitionGroup-test.js +++ b/test/CSSTransitionGroup-test.js @@ -1,21 +1,25 @@ -import hasClass from 'dom-helpers/class/hasClass'; +import hasClass from 'dom-helpers/hasClass'; import CSSTransition from '../src/CSSTransition'; let React; let ReactDOM; let TransitionGroup; +let act; +let render; // Most of the real functionality is covered in other unit tests, this just // makes sure we're wired up correctly. describe('CSSTransitionGroup', () => { let container; + let consoleErrorSpy; function YoloTransition({ id, ...props }) { + const nodeRef = React.useRef(); return ( - - + + - ) + ); } beforeEach(() => { @@ -24,174 +28,187 @@ describe('CSSTransitionGroup', () => { React = require('react'); ReactDOM = require('react-dom'); + const testUtils = require('./utils'); + act = testUtils.act; + const baseRender = testUtils.render; + + render = (element, container) => + baseRender({element}, { container }); TransitionGroup = require('../src/TransitionGroup'); container = document.createElement('div'); - spyOn(console, 'error'); + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); }); + afterEach(() => { + consoleErrorSpy.mockRestore(); + jest.useRealTimers(); + }); it('should clean-up silently after the timeout elapses', () => { - let a = ReactDOM.render( + render( - + , - container, + container ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1); + const transitionGroupDiv = container.childNodes[0]; + + expect(transitionGroupDiv.childNodes.length).toBe(1); - ReactDOM.render( + render( - + , - container, + container ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(2); - expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('two'); - expect(ReactDOM.findDOMNode(a).childNodes[1].id).toBe('one'); - jest.runAllTimers(); + expect(transitionGroupDiv.childNodes.length).toBe(2); + expect(transitionGroupDiv.childNodes[0].id).toBe('two'); + expect(transitionGroupDiv.childNodes[1].id).toBe('one'); + + act(() => { + jest.runAllTimers(); + }); // No warnings - expect(console.error.calls.count()).toBe(0); + expect(consoleErrorSpy).not.toHaveBeenCalled(); // The leaving child has been removed - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1); - expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('two'); + expect(transitionGroupDiv.childNodes.length).toBe(1); + expect(transitionGroupDiv.childNodes[0].id).toBe('two'); }); it('should keep both sets of DOM nodes around', () => { - let a = ReactDOM.render( + render( , - container, + container ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1); - ReactDOM.render( + + const transitionGroupDiv = container.childNodes[0]; + + expect(transitionGroupDiv.childNodes.length).toBe(1); + + render( , - container, + container ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(2); - expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('two'); - expect(ReactDOM.findDOMNode(a).childNodes[1].id).toBe('one'); + + expect(transitionGroupDiv.childNodes.length).toBe(2); + expect(transitionGroupDiv.childNodes[0].id).toBe('two'); + expect(transitionGroupDiv.childNodes[1].id).toBe('one'); }); it('should switch transitionLeave from false to true', () => { - let a = ReactDOM.render( + render( , - container, + container ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1); - ReactDOM.render( + + const transitionGroupDiv = container.childNodes[0]; + + expect(transitionGroupDiv.childNodes.length).toBe(1); + + render( , - container, + container ); - jest.runAllTimers(); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1); - ReactDOM.render( + act(() => { + jest.runAllTimers(); + }); + + expect(transitionGroupDiv.childNodes.length).toBe(1); + + render( , - container, + container ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(2); - expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('three'); - expect(ReactDOM.findDOMNode(a).childNodes[1].id).toBe('two'); - }); + expect(transitionGroupDiv.childNodes.length).toBe(2); + expect(transitionGroupDiv.childNodes[0].id).toBe('three'); + expect(transitionGroupDiv.childNodes[1].id).toBe('two'); + }); it('should work with a null child', () => { - ReactDOM.render( - - {[null]} - , - container, - ); + render({[null]}, container); }); it('should work with a child which renders as null', () => { const NullComponent = () => null; // Testing the whole lifecycle of entering and exiting, // because those lifecycle methods used to fail when the DOM node was null. - ReactDOM.render( - , - container, - ); - ReactDOM.render( + render(, container); + render( - + , - container, - ); - ReactDOM.render( - , - container, + container ); + render(, container); }); it('should transition from one to null', () => { - let a = ReactDOM.render( + render( , - container, - ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1); - ReactDOM.render( - - {null} - , - container, + container ); + + const transitionGroupDiv = container.childNodes[0]; + + expect(transitionGroupDiv.childNodes.length).toBe(1); + + render({null}, container); + // (Here, we expect the original child to stick around but test that no // exception is thrown) - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1); - expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('one'); + expect(transitionGroupDiv.childNodes.length).toBe(1); + expect(transitionGroupDiv.childNodes[0].id).toBe('one'); }); it('should transition from false to one', () => { - let a = ReactDOM.render( - - {false} - , - container, - ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(0); - ReactDOM.render( + render({false}, container); + + const transitionGroupDiv = container.childNodes[0]; + + expect(transitionGroupDiv.childNodes.length).toBe(0); + + render( , - container, + container ); - expect(ReactDOM.findDOMNode(a).childNodes.length).toBe(1); - expect(ReactDOM.findDOMNode(a).childNodes[0].id).toBe('one'); + + expect(transitionGroupDiv.childNodes.length).toBe(1); + expect(transitionGroupDiv.childNodes[0].id).toBe('one'); }); it('should clear transition timeouts when unmounted', () => { class Component extends React.Component { render() { - return ( - - {this.props.children} - - ); + return {this.props.children}; } } - ReactDOM.render(, container); - ReactDOM.render( + render(, container); + render( , @@ -201,7 +218,9 @@ describe('CSSTransitionGroup', () => { ReactDOM.unmountComponentAtNode(container); // Testing that no exception is thrown here, as the timeout has been cleared. - jest.runAllTimers(); + act(() => { + jest.runAllTimers(); + }); }); it('should handle unmounted elements properly', () => { @@ -228,10 +247,12 @@ describe('CSSTransitionGroup', () => { } } - ReactDOM.render(, container); + render(, container); // Testing that no exception is thrown here, as the timeout has been cleared. - jest.runAllTimers(); + act(() => { + jest.runAllTimers(); + }); }); it('should work with custom component wrapper cloning children', () => { @@ -240,10 +261,9 @@ describe('CSSTransitionGroup', () => { render() { return (
- { - React.Children.map(this.props.children, - child => React.cloneElement(child, { className: extraClassNameProp })) - } + {React.Children.map(this.props.children, (child) => + React.cloneElement(child, { className: extraClassNameProp }) + )}
); } @@ -258,20 +278,22 @@ describe('CSSTransitionGroup', () => { class Component extends React.Component { render() { return ( - + ); } } - const a = ReactDOM.render(, container); - const child = ReactDOM.findDOMNode(a).childNodes[0]; - expect(hasClass(child, extraClassNameProp)).toBe(true); + render(, container); + const transitionGroupDiv = container.childNodes[0]; + transitionGroupDiv.childNodes.forEach((child) => { + expect(hasClass(child, extraClassNameProp)).toBe(true); + }); // Testing that no exception is thrown here, as the timeout has been cleared. - jest.runAllTimers(); + act(() => { + jest.runAllTimers(); + }); }); }); diff --git a/test/ChildMapping-test.js b/test/ChildMapping-test.js index 554b8e0b..7afb2574 100644 --- a/test/ChildMapping-test.js +++ b/test/ChildMapping-test.js @@ -10,9 +10,19 @@ describe('ChildMapping', () => { it('should support getChildMapping', () => { let oneone =
; let onetwo =
; - let one =
{oneone}{onetwo}
; + let one = ( +
+ {oneone} + {onetwo} +
+ ); let two =
foo
; - let component =
{one}{two}
; + let component = ( +
+ {one} + {two} +
+ ); let mapping = ChildMapping.getChildMapping(component.props.children); diff --git a/test/SSR-test.js b/test/SSR-test.js new file mode 100644 index 00000000..629acacd --- /dev/null +++ b/test/SSR-test.js @@ -0,0 +1,10 @@ +/** + * @jest-environment node + */ + +// test that import does not crash +import * as ReactTransitionGroup from '../src'; // eslint-disable-line no-unused-vars + +describe('SSR', () => { + it('should import react-transition-group in node env', () => {}); +}); diff --git a/test/SwitchTransition-test.js b/test/SwitchTransition-test.js new file mode 100644 index 00000000..9250a586 --- /dev/null +++ b/test/SwitchTransition-test.js @@ -0,0 +1,160 @@ +import React from 'react'; + +import { act, render } from './utils'; + +import Transition, { ENTERED } from '../src/Transition'; +import SwitchTransition from '../src/SwitchTransition'; + +describe('SwitchTransition', () => { + let log, Parent; + beforeEach(() => { + log = []; + let events = { + onEnter: (m) => log.push(m ? 'appear' : 'enter'), + onEntering: (m) => log.push(m ? 'appearing' : 'entering'), + onEntered: (m) => log.push(m ? 'appeared' : 'entered'), + onExit: () => log.push('exit'), + onExiting: () => log.push('exiting'), + onExited: () => log.push('exited'), + }; + + const nodeRef = React.createRef(); + Parent = function Parent({ on, rendered = true }) { + return ( + + {rendered ? ( + + {on ? 'first' : 'second'} + + ) : null} + + ); + }; + + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should have default status ENTERED', () => { + const nodeRef = React.createRef(); + render( + + + {(status) => { + return status: {status}; + }} + + + ); + + expect(nodeRef.current.textContent).toBe(`status: ${ENTERED}`); + }); + + it('should have default mode: out-in', () => { + const firstNodeRef = React.createRef(); + const secondNodeRef = React.createRef(); + const { rerender } = render( + + + {(status) => { + return first status: {status}; + }} + + + ); + rerender( + + + {(status) => { + return second status: {status}; + }} + + + ); + + expect(firstNodeRef.current.textContent).toBe('first status: exiting'); + expect(secondNodeRef.current).toBe(null); + }); + + it('should work without childs', () => { + const nodeRef = React.createRef(); + expect(() => { + render( + + + + + + ); + }).not.toThrow(); + }); + + it('should switch between components on change state', () => { + const { container, setProps } = render(); + + expect(container.textContent).toBe('first'); + setProps({ on: false }); + expect(log).toEqual(['exit', 'exiting']); + act(() => { + jest.runAllTimers(); + }); + act(() => { + jest.runAllTimers(); + }); + expect(log).toEqual([ + 'exit', + 'exiting', + 'exited', + 'enter', + 'entering', + 'entered', + ]); + expect(container.textContent).toBe('second'); + }); + + it('should switch between null and component', () => { + const { container, setProps } = render( + + ); + + expect(container.textContent).toBe(''); + + jest.useFakeTimers(); + + setProps({ rendered: true }); + act(() => { + jest.runAllTimers(); + }); + expect(log).toEqual(['enter', 'entering', 'entered']); + expect(container.textContent).toBe('first'); + + setProps({ on: false, rendered: true }); + act(() => { + jest.runAllTimers(); + }); + act(() => { + jest.runAllTimers(); + }); + expect(log).toEqual([ + 'enter', + 'entering', + 'entered', + 'exit', + 'exiting', + 'exited', + 'enter', + 'entering', + 'entered', + ]); + + expect(container.textContent).toBe('second'); + }); +}); diff --git a/test/Transition-test.js b/test/Transition-test.js index 3db30d0e..e9124e14 100644 --- a/test/Transition-test.js +++ b/test/Transition-test.js @@ -1,54 +1,74 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { mount } from 'enzyme'; -import sinon from 'sinon'; - -import Transition, { UNMOUNTED, EXITED, ENTERING, ENTERED, EXITING } - from '../src/Transition'; - -jasmine.addMatchers({ - toExist: () => ({ - compare: actual => ({ - pass: actual != null, - }) - }) +import { render, waitFor } from './utils'; + +import Transition, { + UNMOUNTED, + EXITED, + ENTERING, + ENTERED, + EXITING, +} from '../src/Transition'; + +expect.extend({ + toExist(received) { + const pass = received != null; + return pass + ? { + message: () => `expected ${received} to be null or undefined`, + pass: true, + } + : { + message: () => `expected ${received} not to be null or undefined`, + pass: false, + }; + }, }); describe('Transition', () => { it('should not transition on mount', () => { - let wrapper = mount( + const nodeRef = React.createRef(); + render( { throw new Error('should not Enter'); }} + onEnter={() => { + throw new Error('should not Enter'); + }} > -
+ {(status) =>
status: {status}
}
- ) + ); - expect(wrapper.state('status')).toEqual(ENTERED); + expect(nodeRef.current.textContent).toEqual(`status: ${ENTERED}`); }); - it('should transition on mount with `appear`', done => { - mount( + it('should transition on mount with `appear`', (done) => { + const nodeRef = React.createRef(); + render( { throw Error('Animated!') }} + onEnter={() => { + throw Error('Animated!'); + }} > -
+
); - mount( + render( done()} + onEnter={() => done()} > -
+
); }); @@ -56,14 +76,20 @@ describe('Transition', () => { it('should pass filtered props to children', () => { class Child extends React.Component { render() { - return
child
; + return ( +
+ foo: {this.props.foo}, bar: {this.props.bar} +
+ ); } } - const child = mount( + const nodeRef = React.createRef(); + render( { onExiting={() => {}} onExited={() => {}} > - + + + ); + + expect(nodeRef.current.textContent).toBe('foo: foo, bar: bar'); + }); + + it('should allow addEndListener instead of timeouts', async () => { + let listener = jest.fn((end) => setTimeout(end, 0)); + let done = false; + + const nodeRef = React.createRef(); + const { setProps } = render( + { + expect(listener).toHaveBeenCalledTimes(1); + done = true; + }} + > +
+ + ); + + setProps({ in: true }); + + await waitFor(() => { + expect(done).toEqual(true); + }); + }); + + it('should fallback to timeouts with addEndListener', async () => { + let calledEnd = false; + let done = false; + let listener = (end) => + setTimeout(() => { + calledEnd = true; + end(); + }, 100); + + const nodeRef = React.createRef(); + const { setProps } = render( + { + expect(calledEnd).toEqual(false); + done = true; + }} + > +
+ + ); + + setProps({ in: true }); + + await waitFor(() => { + expect(done).toEqual(true); + }); + }); + + it('should mount/unmount immediately if not have enter/exit timeout', async () => { + const nodeRef = React.createRef(); + let done = false; + const { setProps } = render( + + {(status) =>
status: {status}
}
- ) - .find(Child); + ); + + expect(nodeRef.current.textContent).toEqual(`status: ${ENTERED}`); + let calledAfterTimeout = false; + setTimeout(() => { + calledAfterTimeout = true; + }, 10); + setProps({ + in: false, + onExited() { + expect(nodeRef.current.textContent).toEqual(`status: ${EXITED}`); + if (calledAfterTimeout) { + throw new Error('wrong timeout'); + } + done = true; + }, + }); - expect(child.props()).toEqual({foo: 'foo', bar: 'bar'}); + await waitFor(() => { + expect(done).toEqual(true); + }); }); - it('should allow addEndListener instead of timeouts', done => { - let listener = sinon.spy((node, end) => setTimeout(end, 0)); + it('should use `React.findDOMNode` when `nodeRef` is not provided', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); + const findDOMNodeSpy = jest.spyOn(ReactDOM, 'findDOMNode'); + + render( + +
+ + ); + + expect(findDOMNodeSpy).toHaveBeenCalled(); + findDOMNodeSpy.mockRestore(); + consoleSpy.mockRestore(); + }); + + it('should not use `React.findDOMNode` when `nodeRef` is provided', () => { + const findDOMNodeSpy = jest.spyOn(ReactDOM, 'findDOMNode'); + + const nodeRef = React.createRef(); + render( + +
+ + ); + + expect(findDOMNodeSpy).not.toHaveBeenCalled(); + findDOMNodeSpy.mockRestore(); + }); - let inst = mount( + describe('appearing timeout', () => { + it('should use enter timeout if appear not set', async () => { + let calledBeforeEntered = false; + let done = false; + setTimeout(() => { + calledBeforeEntered = true; + }, 10); + const nodeRef = React.createRef(); + const { setProps } = render( { - expect(listener.callCount).toEqual(1); - done(); - }} + nodeRef={nodeRef} + in={true} + timeout={{ enter: 20, exit: 10 }} + appear > -
+
- ) + ); - inst.setProps({ 'in': true }); - }) + setProps({ + onEntered() { + if (calledBeforeEntered) { + done = true; + } else { + throw new Error('wrong timeout'); + } + }, + }); - it('should fallback to timeous with addEndListener ', done => { - let calledEnd = false - let listener = (node, end) => setTimeout(() => { - calledEnd = true; - end() - }, 100); + await waitFor(() => { + expect(done).toEqual(true); + }); + }); - let inst = mount( + it('should use appear timeout if appear is set', async () => { + let done = false; + const nodeRef = React.createRef(); + const { setProps } = render( { - expect(calledEnd).toEqual(false); - done(); - }} + nodeRef={nodeRef} + in={true} + timeout={{ enter: 20, exit: 10, appear: 5 }} + appear > -
+
); - inst.setProps({ in: true }); - }) + let isCausedLate = false; + setTimeout(() => { + isCausedLate = true; + }, 15); - describe('entering', () => { - let wrapper; + setProps({ + onEntered() { + if (isCausedLate) { + throw new Error('wrong timeout'); + } else { + done = true; + } + }, + }); - beforeEach(() => { - wrapper = mount( - -
- - ); + await waitFor(() => { + expect(done).toEqual(true); + }); }); + }); - it('should fire callbacks', done => { - let onEnter = sinon.spy(); - let onEntering = sinon.spy(); + describe('entering', () => { + it('should fire callbacks', async () => { + let callOrder = []; + let done = false; + let onEnter = jest.fn(() => callOrder.push('onEnter')); + let onEntering = jest.fn(() => callOrder.push('onEntering')); + const nodeRef = React.createRef(); + const { setProps } = render( + + {(status) =>
status: {status}
} +
+ ); - expect(wrapper.state('status')).toEqual(EXITED); + expect(nodeRef.current.textContent).toEqual(`status: ${EXITED}`); - wrapper.setProps({ + setProps({ in: true, onEnter, @@ -152,107 +318,129 @@ describe('Transition', () => { onEntering, onEntered() { - expect(onEnter.calledOnce).toEqual(true); - expect(onEntering.calledOnce).toEqual(true); - expect(onEnter.calledBefore(onEntering)).toEqual(true); - done(); - } + expect(onEnter).toHaveBeenCalledTimes(1); + expect(onEntering).toHaveBeenCalledTimes(1); + expect(callOrder).toEqual(['onEnter', 'onEntering']); + done = true; + }, + }); + + await waitFor(() => { + expect(done).toEqual(true); }); }); - it('should move to each transition state', done => { + it('should move to each transition state', async () => { let count = 0; + const nodeRef = React.createRef(); + const { setProps } = render( + + {(status) =>
status: {status}
} +
+ ); - expect(wrapper.state('status')).toEqual(EXITED); + expect(nodeRef.current.textContent).toEqual(`status: ${EXITED}`); - wrapper.setProps({ + setProps({ in: true, - onEnter(){ + onEnter() { count++; - expect(wrapper.state('status')).toEqual(EXITED); + expect(nodeRef.current.textContent).toEqual(`status: ${EXITED}`); }, - onEntering(){ + onEntering() { count++; - expect(wrapper.state('status')).toEqual(ENTERING); + expect(nodeRef.current.textContent).toEqual(`status: ${ENTERING}`); }, - onEntered(){ - expect(wrapper.state('status')).toEqual(ENTERED); - expect(count).toEqual(2); - done(); - } + onEntered() { + expect(nodeRef.current.textContent).toEqual(`status: ${ENTERED}`); + }, + }); + + await waitFor(() => { + expect(count).toEqual(2); }); }); }); - describe('exiting', ()=> { - let wrapper; - - beforeEach(() => { - wrapper = mount( - -
+ describe('exiting', () => { + it('should fire callbacks', async () => { + let callOrder = []; + let done = false; + let onExit = jest.fn(() => callOrder.push('onExit')); + let onExiting = jest.fn(() => callOrder.push('onExiting')); + const nodeRef = React.createRef(); + const { setProps } = render( + + {(status) =>
status: {status}
}
); - }); - it('should fire callbacks', done => { - let onExit = sinon.spy(); - let onExiting = sinon.spy(); + expect(nodeRef.current.textContent).toEqual(`status: ${ENTERED}`); - expect(wrapper.state('status')).toEqual(ENTERED); - - wrapper.setProps({ + setProps({ in: false, onExit, onExiting, - onExited(){ - expect(onExit.calledOnce).toEqual(true); - expect(onExiting.calledOnce).toEqual(true); - expect(onExit.calledBefore(onExiting)).toEqual(true); - done(); - } + onExited() { + expect(onExit).toHaveBeenCalledTimes(1); + expect(onExiting).toHaveBeenCalledTimes(1); + expect(callOrder).toEqual(['onExit', 'onExiting']); + done = true; + }, + }); + + await waitFor(() => { + expect(done).toEqual(true); }); }); - it('should move to each transition state', done => { + it('should move to each transition state', async () => { let count = 0; + let done = false; + const nodeRef = React.createRef(); + const { setProps } = render( + + {(status) =>
status: {status}
} +
+ ); - expect(wrapper.state('status')).toEqual(ENTERED); + expect(nodeRef.current.textContent).toEqual(`status: ${ENTERED}`); - wrapper.setProps({ + setProps({ in: false, - onExit(){ + onExit() { count++; - expect(wrapper.state('status')).toEqual(ENTERED); + expect(nodeRef.current.textContent).toEqual(`status: ${ENTERED}`); }, - onExiting(){ + onExiting() { count++; - expect(wrapper.state('status')).toEqual(EXITING); + expect(nodeRef.current.textContent).toEqual(`status: ${EXITING}`); }, - onExited(){ - expect(wrapper.state('status')).toEqual(EXITED); + onExited() { + expect(nodeRef.current.textContent).toEqual(`status: ${EXITED}`); expect(count).toEqual(2); - done(); - } + done = true; + }, + }); + + await waitFor(() => { + expect(done).toEqual(true); }); }); }); describe('mountOnEnter', () => { class MountTransition extends React.Component { - constructor(props) { - super(props); - this.state = {in: props.initialIn}; - } + nodeRef = React.createRef(); render() { const { ...props } = this.props; @@ -260,72 +448,76 @@ describe('Transition', () => { return ( + (this.transition = this.transition || transition) + } + nodeRef={this.nodeRef} mountOnEnter - in={this.state.in} + in={this.props.in} timeout={10} {...props} > -
+ {(status) =>
status: {status}
} ); } getStatus = () => { - return this.refs.transition.state.status; - } + return this.transition.state.status; + }; } - it('should mount when entering', done => { - const wrapper = mount( + it('should mount when entering', (done) => { + const { container, setProps } = render( { - expect(wrapper.instance().getStatus()).toEqual(EXITED); - expect(wrapper.getDOMNode()).toExist(); + expect(container.textContent).toEqual(`status: ${EXITED}`); done(); }} /> ); - expect(wrapper.instance().getStatus()).toEqual(UNMOUNTED); + expect(container.textContent).toEqual(''); - expect(wrapper.getDOMNode()).not.toExist(); - - wrapper.setProps({ in: true }); + setProps({ in: true }); }); - it('should stay mounted after exiting', done => { - const wrapper = mount( + it('should stay mounted after exiting', async () => { + let entered = false; + let exited = false; + const { container, setProps } = render( { - expect(wrapper.instance().getStatus()).toEqual(ENTERED); - expect(wrapper.getDOMNode()).toExist(); - - wrapper.setState({ in: false }); + entered = true; }} onExited={() => { - expect(wrapper.instance().getStatus()).toEqual(EXITED); - expect(wrapper.getDOMNode()).toExist(); - - done(); + exited = true; }} /> ); - expect(wrapper.getDOMNode()).not.toExist(); - wrapper.setState({ in: true }); + expect(container.textContent).toEqual(''); + setProps({ in: true }); + + await waitFor(() => { + expect(entered).toEqual(true); + }); + expect(container.textContent).toEqual(`status: ${ENTERED}`); + + setProps({ in: false }); + + await waitFor(() => { + expect(exited).toEqual(true); + }); + expect(container.textContent).toEqual(`status: ${EXITED}`); }); - }) + }); describe('unmountOnExit', () => { class UnmountTransition extends React.Component { - constructor(props) { - super(props); - - this.state = {in: props.initialIn}; - } + nodeRef = React.createRef(); render() { const { ...props } = this.props; @@ -333,61 +525,77 @@ describe('Transition', () => { return ( + (this.transition = this.transition || transition) + } + nodeRef={this.nodeRef} unmountOnExit - in={this.state.in} + in={this.props.in} timeout={10} {...props} > -
+
); } getStatus = () => { - return this.refs.transition.state.status; - } + return this.transition.state.status; + }; } - it('should mount when entering', done => { - const wrapper = mount( + it('should mount when entering', async () => { + let done = false; + const instanceRef = React.createRef(); + const { setProps } = render( { - expect(wrapper.getStatus()).toEqual(EXITED); - expect(ReactDOM.findDOMNode(wrapper)).toExist(); + expect(instanceRef.current.getStatus()).toEqual(EXITED); + expect(instanceRef.current.nodeRef.current).toExist(); - done(); + done = true; }} /> - ) - .instance() + ); + + expect(instanceRef.current.getStatus()).toEqual(UNMOUNTED); + expect(instanceRef.current.nodeRef.current).toBeNull(); - expect(wrapper.getStatus()).toEqual(UNMOUNTED); - expect(ReactDOM.findDOMNode(wrapper)).toBeNull(); + setProps({ in: true }); - wrapper.setState({in: true}); + await waitFor(() => { + expect(done).toEqual(true); + }); }); - it('should unmount after exiting', done => { - const wrapper = mount( + it('should unmount after exiting', async () => { + let exited = false; + const instanceRef = React.createRef(); + const { setProps } = render( { setTimeout(() => { - expect(wrapper.getStatus()).toEqual(UNMOUNTED); - expect(ReactDOM.findDOMNode(wrapper)).not.toExist(); - done(); - }) + exited = true; + }); }} /> - ) - .instance(); + ); - expect(wrapper.getStatus()).toEqual(ENTERED); - expect(ReactDOM.findDOMNode(wrapper)).toExist(); + expect(instanceRef.current.getStatus()).toEqual(ENTERED); + expect(instanceRef.current.nodeRef.current).toExist(); + + setProps({ in: false }); + + await waitFor(() => { + expect(exited).toEqual(true); + }); - wrapper.setState({in: false}); + expect(instanceRef.current.getStatus()).toEqual(UNMOUNTED); + expect(instanceRef.current.nodeRef.current).not.toExist(); }); }); }); diff --git a/test/TransitionGroup-test.js b/test/TransitionGroup-test.js index c3a88e88..da07db44 100644 --- a/test/TransitionGroup-test.js +++ b/test/TransitionGroup-test.js @@ -1,36 +1,43 @@ -import { mount } from 'enzyme'; - let React; -let ReactDOM; let TransitionGroup; let Transition; // Most of the real functionality is covered in other unit tests, this just // makes sure we're wired up correctly. describe('TransitionGroup', () => { - let container, log, Child; + let act, container, log, Child, renderStrict, render; beforeEach(() => { React = require('react'); - ReactDOM = require('react-dom'); Transition = require('../src/Transition').default; TransitionGroup = require('../src/TransitionGroup'); + const testUtils = require('./utils'); + act = testUtils.act; + render = testUtils.render; + + renderStrict = (element, container) => + render({element}, { container }); container = document.createElement('div'); log = []; let events = { - onEnter: (_, m) => log.push(m ? 'appear' : 'enter'), - onEntering: (_, m) => log.push(m ? 'appearing' : 'entering'), - onEntered: (_, m) => log.push(m ? 'appeared' : 'entered'), + onEnter: (m) => log.push(m ? 'appear' : 'enter'), + onEntering: (m) => log.push(m ? 'appearing' : 'entering'), + onEntered: (m) => log.push(m ? 'appeared' : 'entered'), onExit: () => log.push('exit'), onExiting: () => log.push('exiting'), onExited: () => log.push('exited'), }; + const nodeRef = React.createRef(); Child = function Child(props) { - return ; - } + return ( + + + + ); + }; }); it('should allow null components', () => { @@ -39,11 +46,11 @@ describe('TransitionGroup', () => { return childrenArray[0] || null; } - mount( + render( - , - ) + + ); }); it('should allow callback refs', () => { @@ -55,124 +62,60 @@ describe('TransitionGroup', () => { } } - mount( + render( - , - ) + + ); expect(ref).toHaveBeenCalled(); }); - it('should work with no children', () => { - ReactDOM.render( - , - container, - ); + renderStrict(, container); }); it('should handle transitioning correctly', () => { function Parent({ count = 1 }) { let children = []; for (let i = 0; i < count; i++) children.push(); - return {children}; + return ( + + {children} + + ); } jest.useFakeTimers(); - ReactDOM.render(, container); - - jest.runAllTimers() - expect(log).toEqual(['appear', 'appearing', 'appeared']); + renderStrict(, container); + + act(() => { + jest.runAllTimers(); + }); + expect(log).toEqual( + // React 18 StrictEffects will call `componentDidMount` twice causing two `onEnter` calls. + React.useTransition !== undefined + ? ['appear', 'appear', 'appearing', 'appeared'] + : ['appear', 'appearing', 'appeared'] + ); log = []; - ReactDOM.render(, container) - jest.runAllTimers() - expect(log).toEqual(['enter', 'entering', 'entered']); + renderStrict(, container); + act(() => { + jest.runAllTimers(); + }); + expect(log).toEqual( + // React 18 StrictEffects will call `componentDidMount` twice causing two `onEnter` calls. + React.useTransition !== undefined + ? ['enter', 'enter', 'entering', 'entered'] + : ['enter', 'entering', 'entered'] + ); log = []; - ReactDOM.render(, container) - jest.runAllTimers() + renderStrict(, container); + act(() => { + jest.runAllTimers(); + }); expect(log).toEqual(['exit', 'exiting', 'exited']); }); - - it('should not throw when enter callback is called and is now leaving', () => { - class Child extends React.Component { - componentWillReceiveProps() { - if (this.callback) { - this.callback(); - } - } - - componentWillEnter(callback) { - this.callback = callback; - } - - render() { - return (); - } - } - - class Component extends React.Component { - render() { - return ( - - {this.props.children} - - ); - } - } - - // render the base component - ReactDOM.render(, container); - // now make the child enter - ReactDOM.render( - , - container, - ); - // rendering the child leaving will call 'componentWillProps' which will trigger the - // callback. This would throw an error previously. - expect(ReactDOM.render.bind(this, , container)).not.toThrow(); - }) - - it('should not throw when leave callback is called and is now entering', () => { - class Child extends React.Component { - componentWillReceiveProps() { - if (this.callback) { - this.callback(); - } - } - - componentWillLeave(callback) { - this.callback = callback; - } - - render() { - return (); - } - } - - class Component extends React.Component { - render() { - return ( - - {this.props.children} - - ); - } - } - - // render the base component - ReactDOM.render(, container); - // now make the child enter - ReactDOM.render( - , - container, - ); - // make the child leave - ReactDOM.render(, container); - // rendering the child entering again will call 'componentWillProps' which will trigger the - // callback. This would throw an error previously. - expect(ReactDOM.render.bind(this, , container)).not.toThrow(); - }) }); diff --git a/test/setup.js b/test/setup.js index 4b7566f9..85c00e4d 100644 --- a/test/setup.js +++ b/test/setup.js @@ -1,9 +1,3 @@ - -global.requestAnimationFrame = function(callback) { +global.requestAnimationFrame = function (callback) { setTimeout(callback, 0); }; - -const Enzyme = require('enzyme'); -const Adapter = require('enzyme-adapter-react-16'); - -Enzyme.configure({ adapter: new Adapter() }) diff --git a/test/setupAfterEnv.js b/test/setupAfterEnv.js new file mode 100644 index 00000000..aa245bf1 --- /dev/null +++ b/test/setupAfterEnv.js @@ -0,0 +1,5 @@ +import { cleanup } from '@testing-library/react/pure'; + +afterEach(() => { + cleanup(); +}); diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 00000000..e42aae60 --- /dev/null +++ b/test/utils.js @@ -0,0 +1,14 @@ +import { render as baseRender } from '@testing-library/react/pure'; +import React from 'react'; + +export * from '@testing-library/react'; +export function render(element, options) { + const result = baseRender(element, options); + + return { + ...result, + setProps(props) { + result.rerender(React.cloneElement(element, props)); + }, + }; +} diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index 8609bb9f..00000000 --- a/webpack.config.js +++ /dev/null @@ -1,35 +0,0 @@ -const path = require('path'); - -module.exports = { - entry: './src/index.js', - output: { - filename: process.env.NODE_ENV === 'production' - ? 'react-transition-group.min.js' - : 'react-transition-group.js', - path: path.join(__dirname, 'lib/dist'), - library: 'ReactTransitionGroup', - libraryTarget: 'umd', - }, - externals: { - react: { - root: 'React', - commonjs2: 'react', - commonjs: 'react', - amd: 'react', - }, - 'react-dom': { - root: 'ReactDOM', - commonjs2: 'react-dom', - commonjs: 'react-dom', - amd: 'react-dom', - }, - }, - module: { - rules: [ - { - test: /\.js$/, - loader: 'babel-loader', - }, - ], - }, -}; diff --git a/www/.babelrc.js b/www/.babelrc.js new file mode 100644 index 00000000..760cc86e --- /dev/null +++ b/www/.babelrc.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['babel-preset-gatsby'], +}; diff --git a/www/.npmrc b/www/.npmrc new file mode 100644 index 00000000..b6f27f13 --- /dev/null +++ b/www/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/www/gatsby-config.js b/www/gatsby-config.js index d8bfd263..200b3664 100644 --- a/www/gatsby-config.js +++ b/www/gatsby-config.js @@ -5,8 +5,31 @@ module.exports = { siteMetadata: { title: 'React Transition Group Documentation', author: 'Jason Quense', + componentPages: [ + { + path: '/transition', + displayName: 'Transition', + codeSandboxId: null, + }, + { + path: '/css-transition', + displayName: 'CSSTransition', + codeSandboxId: 'm77l2vp00x', + }, + { + path: '/switch-transition', + displayName: 'SwitchTransition', + codeSandboxId: 'switchtransition-component-iqm0d', + }, + { + path: '/transition-group', + displayName: 'TransitionGroup', + codeSandboxId: '00rqyo26kn', + }, + ], }, plugins: [ + 'gatsby-plugin-react-helmet', { resolve: 'gatsby-source-filesystem', options: { @@ -24,12 +47,10 @@ module.exports = { { resolve: 'gatsby-transformer-remark', options: { - plugins: [ - 'gatsby-remark-prismjs', - ], + plugins: ['gatsby-remark-prismjs'], }, }, 'gatsby-transformer-react-docgen', - 'gatsby-plugin-sass' + 'gatsby-plugin-sass', ], -} +}; diff --git a/www/gatsby-node.js b/www/gatsby-node.js new file mode 100644 index 00000000..28b70c5a --- /dev/null +++ b/www/gatsby-node.js @@ -0,0 +1,47 @@ +const path = require('path'); +const config = require('./gatsby-config'); + +exports.createPages = ({ actions, graphql }) => { + const { createPage } = actions; + const componentTemplate = path.join( + __dirname, + 'src', + 'templates', + 'component.js' + ); + return new Promise((resolve, reject) => { + resolve( + graphql(` + { + allComponentMetadata { + edges { + node { + displayName + } + } + } + } + `).then((result) => { + if (result.errors) { + reject(result.errors); + } + const { componentPages } = config.siteMetadata; + result.data.allComponentMetadata.edges + .filter(({ node: { displayName } }) => + componentPages.some((page) => page.displayName === displayName) + ) + .forEach(({ node: { displayName } }) => { + createPage({ + path: componentPages.find( + (page) => page.displayName === displayName + ).path, + component: componentTemplate, + context: { + displayName, + }, + }); + }); + }) + ); + }); +}; diff --git a/www/package.json b/www/package.json index 450c2e07..7bea35a4 100644 --- a/www/package.json +++ b/www/package.json @@ -1,4 +1,5 @@ { + "private": true, "name": "react-transition-group-docs", "version": "1.0.0", "description": "", @@ -11,18 +12,29 @@ }, "author": "", "license": "MIT", + "engines": { + "node": "<=16" + }, "dependencies": { - "bootstrap": "^4.0.0-alpha.6", - "gatsby": "^1.0.0", - "gatsby-link": "^1.0.0", - "gatsby-plugin-sass": "^1.0.0", - "gatsby-remark-prismjs": "^1.0.0", - "gatsby-source-filesystem": "^1.0.0", - "gatsby-transformer-react-docgen": "^1.0.0", - "gatsby-transformer-remark": "^1.0.0", - "lodash": "^4.17.4" + "@babel/core": "^7.3.4", + "babel-preset-gatsby": "^2.7.0", + "bootstrap": "^4.3.1", + "gatsby": "^2.1.22", + "gatsby-plugin-react-helmet": "^3.0.10", + "gatsby-plugin-sass": "^2.0.10", + "gatsby-remark-prismjs": "^3.2.4", + "gatsby-source-filesystem": "^2.0.23", + "gatsby-transformer-react-docgen": "^3.0.5", + "gatsby-transformer-remark": "^2.3.0", + "lodash": "^4.17.19", + "prismjs": "^1.25.0", + "react": "^16.8.3", + "react-bootstrap": "^1.0.0-beta.5", + "react-dom": "^16.8.3", + "react-helmet": "^5.2.0", + "sass": "^1.49.7" }, "devDependencies": { - "gh-pages": "^1.0.0" + "gh-pages": "^2.0.1" } } diff --git a/www/src/components/ComponentPage.js b/www/src/components/ComponentPage.js deleted file mode 100644 index 522fa1a1..00000000 --- a/www/src/components/ComponentPage.js +++ /dev/null @@ -1,225 +0,0 @@ - -import React from 'react'; -import transform from 'lodash/transform'; - - -function displayObj(obj){ - return JSON.stringify(obj, null, 2).replace(/"|'/g, '') -} - -let cleanDocletValue = str => str.trim().replace(/^\{/, '').replace(/\}$/, ''); - -const extractMarkdown = ({ description }) => ( - description && - description.childMarkdownRemark && - description.childMarkdownRemark.html -); - -class ComponentPage extends React.Component { - render() { - const { metadata, ...props } = this.props; - - return ( -
-

- {metadata.displayName} -

-

- -

-
Props
- {metadata.composes && ( - - Accepts all props - from {metadata.composes.map(p => `<${p.replace('./', '')}>`).join(', ')} unless otherwise noted. - - )} -

- - {metadata.props.map(p => - this.renderProp(p, metadata.displayName - ))} -
- ) - } - - renderProp = (prop, componentName) => { - const { defaultValue, name, required } = prop - let typeInfo = this.renderType(prop); - let id = `${componentName}-prop-${name}`; - - return ( -
-

- {name} - -

-
- -
-
- {'type: '} - { typeInfo && typeInfo.type === 'pre' ? typeInfo : {typeInfo} } -
- {required && ( -
required
- )} - {defaultValue && -
default: {defaultValue.value.trim()}
- } - -
-
- ) - } - - renderType(prop) { - let type = prop.type || {}; - let name = getDisplayTypeName(type.name); - let doclets = prop.doclets || {}; - - switch (name) { - case 'node': - return 'any'; - case 'function': - return 'Function'; - case 'elementType': - return 'ReactClass'; - case 'dateFormat': - return 'string | (date: Date, culture: ?string, localizer: Localizer) => string'; - case 'dateRangeFormat': - return '(range: { start: Date, end: Date }, culture: ?string, localizer: Localizer) => string'; - case 'object': - case 'Object': - if (type.value) - return ( -
-              {displayObj(renderObject(type.value))}
-            
- ) - - return name; - case 'union': - return type.value.reduce((current, val, i, list) => { - val = typeof val === 'string' ? { name: val } : val; - let item = this.renderType({ type: val }); - - if (React.isValidElement(item)) { - item = React.cloneElement(item, {key: i}); - } - - current = current.concat(item); - - return i === (list.length - 1) ? current : current.concat(' | '); - }, []); - case 'array': - case 'Array': { - let child = this.renderType({ type: type.value }); - - return {'Array<'}{ child }{'>'}; - } - case 'enum': - return this.renderEnum(type); - case 'custom': - return cleanDocletValue(doclets.type || name); - default: - return name; - } - } - - renderEnum(enumType) { - const enumValues = enumType.value || []; - return {enumValues.join(' | ')}; - } -} - -function getDisplayTypeName(typeName) { - if (typeName === 'func') { - return 'function'; - } else if (typeName === 'bool') { - return 'boolean'; - } else if (typeName === 'object') { - return 'Object'; - } - - return typeName; -} - -function renderObject(props){ - return transform(props, (obj, val, key) => { - obj[val.required ? key : key + '?'] = simpleType(val) - - }, {}) -} - -function simpleType(prop) { - let type = prop.type || {}; - let name = getDisplayTypeName(type.name); - let doclets = prop.doclets || {}; - - switch (name) { - case 'node': - return 'any'; - case 'function': - return 'Function'; - case 'elementType': - return 'ReactClass'; - case 'object': - case 'Object': - if (type.value) - return renderObject(type.value) - return name; - case 'array': - case 'Array': { - let child = simpleType({ type: type.value }); - - return 'Array<' + child + '>'; - } - case 'custom': - return cleanDocletValue(doclets.type || name); - default: - return name; - } -} -export default ComponentPage; - -export const descFragment = graphql` - fragment ComponentPage_desc on ComponentDescription { - childMarkdownRemark { - html - } - } -`; - -export const propsFragment = graphql` - fragment ComponentPage_prop on ComponentProp { - name - required - type { - name - value - raw - } - defaultValue { - value - computed - } - description { - ...ComponentPage_desc - } - doclets { type } - } -`; - -export const query = graphql` - fragment ComponentPage_metadata on ComponentMetadata { - displayName - composes - description { - ...ComponentPage_desc - } - props { - ...ComponentPage_prop - } - } -`; diff --git a/www/src/components/Example.js b/www/src/components/Example.js new file mode 100644 index 00000000..a8380da2 --- /dev/null +++ b/www/src/components/Example.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Container } from 'react-bootstrap'; + +const propTypes = { + codeSandbox: PropTypes.shape({ + title: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + }).isRequired, +}; + +const Example = ({ codeSandbox }) => ( +
+ +

Example

+
+