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/.travis.yml b/.travis.yml deleted file mode 100644 index 72a0e183..00000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -sudo: false -language: node_js -node_js: - - 14 -env: - - REACT_DIST=16 - - REACT_DIST=17 -cache: yarn -install: - - yarn - - yarn add react@$REACT_DIST react-dom@$REACT_DIST - - yarn --cwd www -script: - - yarn test - -after_success: - - npm run build - - npm run semantic-release -branches: - only: - - master diff --git a/CHANGELOG.md b/CHANGELOG.md index c6aa197c..c8006511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## [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) diff --git a/package.json b/package.json index b17cae7f..72a435e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-transition-group", - "version": "4.4.2", + "version": "4.4.5", "description": "A react component toolset for managing animations", "main": "lib/cjs/index.js", "module": "lib/esm/index.js", @@ -80,7 +80,7 @@ "@semantic-release/npm": "^7.0.5", "@storybook/addon-actions": "^6.3.4", "@storybook/react": "^6.3.4", - "@testing-library/react": "^12.1.2", + "@testing-library/react": "alpha", "@typescript-eslint/eslint-plugin": "^4.26.1", "astroturf": "^0.10.4", "babel-eslint": "^10.1.0", @@ -99,8 +99,8 @@ "jest": "^25.3.0", "npm-run-all": "^4.1.5", "prettier": "^2.3.1", - "react": "^17.0.1", - "react-dom": "^17.0.1", + "react": "^18.0.0", + "react-dom": "^18.0.0", "release-script": "^1.0.2", "rimraf": "^3.0.2", "rollup": "^2.6.1", diff --git a/src/CSSTransition.js b/src/CSSTransition.js index ef97b825..61592105 100644 --- a/src/CSSTransition.js +++ b/src/CSSTransition.js @@ -6,6 +6,7 @@ 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)); @@ -28,10 +29,11 @@ const removeClass = (node, classes) => * ```jsx * function App() { * const [inProp, setInProp] = useState(false); + * const nodeRef = useRef(null); * return ( *
- * - *
+ * + *
* {"I'll receive my-node-* classes"} *
*
@@ -194,8 +196,7 @@ class CSSTransition extends React.Component { // This is to force a repaint, // which is necessary in order to transition styles when adding a class name. if (phase === 'active') { - /* eslint-disable no-unused-expressions */ - node && node.scrollTop; + if (node) forceReflow(node); } if (className) { @@ -318,7 +319,7 @@ CSSTransition.propTypes = { * A `` callback fired immediately after the 'enter' or 'appear' class is * applied. * - * **Note**: when `nodeRef` prop is passed, `node` is not passed. + * **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) */ @@ -328,7 +329,7 @@ CSSTransition.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. + * **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) */ @@ -338,7 +339,7 @@ CSSTransition.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. + * **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) */ diff --git a/src/SwitchTransition.js b/src/SwitchTransition.js index cb3a45b3..78dc375d 100644 --- a/src/SwitchTransition.js +++ b/src/SwitchTransition.js @@ -89,14 +89,18 @@ const enterRenders = { * ```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' * > - * * diff --git a/src/Transition.js b/src/Transition.js index 04e845db..41cd1f38 100644 --- a/src/Transition.js +++ b/src/Transition.js @@ -5,6 +5,7 @@ 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'; @@ -36,6 +37,7 @@ export const EXITING = 'exiting'; * * ```jsx * import { Transition } from 'react-transition-group'; + * import { useRef } from 'react'; * * const duration = 300; * @@ -51,18 +53,21 @@ export const EXITING = 'exiting'; * 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! + *
+ * )} + *
+ * ); + * } * ``` * * There are 4 main states a Transition can be in: @@ -79,11 +84,15 @@ export const EXITING = 'exiting'; * [useState](https://reactjs.org/docs/hooks-reference.html#usestate) hook): * * ```jsx + * import { Transition } from 'react-transition-group'; + * import { useState, useRef } from 'react'; + * * function App() { * const [inProp, setInProp] = useState(false); + * const nodeRef = useRef(null); * return ( *
- * + * * {state => ( * // ... * )} @@ -212,6 +221,15 @@ class Transition extends React.Component { this.cancelNextCallback(); if (nextStatus === ENTERING) { + 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(); @@ -380,9 +398,12 @@ class Transition extends React.Component { Transition.propTypes = { /** - * A React reference to DOM element that need to transition: + * 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 @@ -412,9 +433,9 @@ Transition.propTypes = { * specific props to a component. * * ```jsx - * + * * {state => ( - * + * * )} * * ``` @@ -503,7 +524,7 @@ Transition.propTypes = { * DOM node and a `done` callback. Allows for more fine grained transition end * logic. Timeouts are still used as a fallback if provided. * - * **Note**: when `nodeRef` prop is passed, `node` is not passed. + * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `done` is being passed as the first argument. * * ```jsx * addEndListener={(node, done) => { @@ -518,7 +539,7 @@ 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. + * **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 */ @@ -528,7 +549,7 @@ 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. + * **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) */ @@ -538,7 +559,7 @@ 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. + * **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 */ 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/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/NestedTransition.js b/stories/NestedTransition.js index ab2bd15c..58fe59cb 100644 --- a/stories/NestedTransition.js +++ b/stories/NestedTransition.js @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import StoryFixture from './StoryFixture'; -import Fade from './transitions/Fade'; +import Fade from './transitions/CSSFadeForTransitionGroup'; import Scale from './transitions/Scale'; function FadeAndScale(props) { @@ -27,7 +27,7 @@ function Example() { const [showNested, setShowNested] = useState(false); return ( - +

Nested Animations