From 85016bfddd3831e6d7bb27926f9f178d25502913 Mon Sep 17 00:00:00 2001 From: Andrew Luca Date: Tue, 5 May 2020 21:52:11 +0300 Subject: [PATCH] feat: add `nodeRef` alternative instead of internal `findDOMNode` (#559) `ReactDOM.findDOMNode` is deprecated and causes warnings in `StrictMode`, so we're offering users an option to pass the `nodeRef` prop, a ref object which should point to the child node being transitioned. --- .storybook/config.js | 7 +- src/CSSTransition.js | 47 +++-- src/ReplaceTransition.js | 8 +- src/Transition.js | 76 ++++++-- src/TransitionGroup.js | 1 + stories/CSSTransitionGroupFixture.js | 39 ++-- stories/NestedTransition.js | 14 +- stories/ReplaceTransition.js | 44 +++-- stories/Transition.js | 37 ++-- stories/TransitionGroup.js | 12 +- stories/transitions/Bootstrap.js | 115 ++++++++---- stories/transitions/Fade.js | 9 +- stories/transitions/Scale.js | 9 +- test/CSSTransition-test.js | 262 +++++++++++++++------------ test/CSSTransitionGroup-test.js | 5 +- test/SwitchTransition-test.js | 26 +-- test/Transition-test.js | 147 +++++++++------ test/TransitionGroup-test.js | 11 +- test/setup.js | 6 +- 19 files changed, 549 insertions(+), 326 deletions(-) diff --git a/.storybook/config.js b/.storybook/config.js index 9154670a..6fcf7bfa 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -1,4 +1,9 @@ -import { configure } from '@storybook/react'; +import { configure, addDecorator } from '@storybook/react'; +import React from 'react'; + +addDecorator( + storyFn => {storyFn()}, +) function loadStories() { require('../stories'); diff --git a/src/CSSTransition.js b/src/CSSTransition.js index 58de70ad..d0240b62 100644 --- a/src/CSSTransition.js +++ b/src/CSSTransition.js @@ -90,61 +90,72 @@ class CSSTransition extends React.Component { exit: {}, } - onEnter = (node, appearing) => { + 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(node, appearing) + this.props.onEnter(maybeNode, maybeAppearing) } } - onEntering = (node, appearing) => { + 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(node, appearing) + this.props.onEntering(maybeNode, maybeAppearing) } } - onEntered = (node, appearing) => { + 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(node, appearing) + this.props.onEntered(maybeNode, maybeAppearing) } } - onExit = (node) => { + 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(node) + this.props.onExit(maybeNode) } } - onExiting = (node) => { + onExiting = (maybeNode) => { + const [node] = this.resolveArguments(maybeNode) this.addClass(node, 'exit', 'active') if (this.props.onExiting) { - this.props.onExiting(node) + this.props.onExiting(maybeNode) } } - onExited = (node) => { + onExited = (maybeNode) => { + const [node] = this.resolveArguments(maybeNode) this.removeClasses(node, 'exit'); this.addClass(node, 'exit', 'done'); if (this.props.onExited) { - this.props.onExited(node) + 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'; @@ -306,6 +317,8 @@ CSSTransition.propTypes = { * A `` callback fired immediately after the 'enter' or 'appear' class is * applied. * + * **Note**: when `nodeRef` prop is passed, `node` is not passed. + * * @type Function(node: HtmlElement, isAppearing: bool) */ onEnter: PropTypes.func, @@ -314,6 +327,8 @@ 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. + * * @type Function(node: HtmlElement, isAppearing: bool) */ onEntering: PropTypes.func, @@ -322,6 +337,8 @@ 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. + * * @type Function(node: HtmlElement, isAppearing: bool) */ onEntered: PropTypes.func, @@ -330,6 +347,8 @@ CSSTransition.propTypes = { * 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, @@ -337,6 +356,8 @@ CSSTransition.propTypes = { /** * A `` callback fired immediately after the 'exit-active' is applied. * + * **Note**: when `nodeRef` prop is passed, `node` is not passed + * * @type Function(node: HtmlElement) */ onExiting: PropTypes.func, @@ -345,6 +366,8 @@ CSSTransition.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, diff --git a/src/ReplaceTransition.js b/src/ReplaceTransition.js index 1eea0259..746ed8d7 100644 --- a/src/ReplaceTransition.js +++ b/src/ReplaceTransition.js @@ -28,7 +28,13 @@ class ReplaceTransition extends React.Component { const child = React.Children.toArray(children)[idx]; if (child.props[handler]) child.props[handler](...originalArgs) - if (this.props[handler]) this.props[handler](ReactDOM.findDOMNode(this)) + if (this.props[handler]) { + const maybeNode = child.props.nodeRef + ? undefined + : ReactDOM.findDOMNode(this) + + this.props[handler](maybeNode) + } } render() { diff --git a/src/Transition.js b/src/Transition.js index d3c7a87e..116852e3 100644 --- a/src/Transition.js +++ b/src/Transition.js @@ -210,21 +210,23 @@ class Transition extends React.Component { if (nextStatus !== null) { // nextStatus will always be ENTERING or EXITING. this.cancelNextCallback() - const node = ReactDOM.findDOMNode(this) if (nextStatus === ENTERING) { - this.performEnter(node, mounting) + this.performEnter(mounting) } else { - this.performExit(node) + this.performExit() } } 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 ? 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 @@ -232,43 +234,47 @@ class Transition extends React.Component { // if we are mounting and running this it means appear _must_ be set 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) - this.onTransitionEnd(node, enterTimeout, () => { + 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 || 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) }) }) }) @@ -308,8 +314,11 @@ 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) const doesNotHaveTimeoutOrListener = timeout == null && !this.props.addEndListener @@ -319,7 +328,10 @@ class Transition extends React.Component { } if (this.props.addEndListener) { - this.props.addEndListener(node, this.nextCallback) + const [maybeNode, maybeNextCallback] = this.props.nodeRef + ? [this.nextCallback] + : [node, this.nextCallback] + this.props.addEndListener(maybeNode, maybeNextCallback) } if (timeout != null) { @@ -349,6 +361,7 @@ class Transition extends React.Component { delete childProps.onExit delete childProps.onExiting delete childProps.onExited + delete childProps.nodeRef if (typeof children === 'function') { // allows for nested Transitions @@ -370,6 +383,19 @@ class Transition extends React.Component { } Transition.propTypes = { + /** + * A React reference to DOM element that need to transition: + * https://stackoverflow.com/a/51127130/4671932 + * + * - 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: PropTypes.instanceOf(Element) }), + /** * A `function` child can be used instead of a React element. This function is * called with the current transition status (`'entering'`, `'entered'`, @@ -466,7 +492,9 @@ Transition.propTypes = { /** * 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. * * ```jsx * addEndListener={(node, done) => { @@ -481,6 +509,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. + * * @type Function(node: HtmlElement, isAppearing: bool) -> void */ onEnter: PropTypes.func, @@ -489,6 +519,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. + * * @type Function(node: HtmlElement, isAppearing: bool) */ onEntering: PropTypes.func, @@ -497,6 +529,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. + * * @type Function(node: HtmlElement, isAppearing: bool) -> void */ onEntered: PropTypes.func, @@ -504,6 +538,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, @@ -511,6 +547,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, @@ -518,6 +556,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, diff --git a/src/TransitionGroup.js b/src/TransitionGroup.js index eec05197..bda2f82b 100644 --- a/src/TransitionGroup.js +++ b/src/TransitionGroup.js @@ -66,6 +66,7 @@ class TransitionGroup extends React.Component { } } + // node is `undefined` when user provided `nodeRef` prop handleExited(child, node) { let currentChildMapping = getChildMapping(this.props.children) diff --git a/stories/CSSTransitionGroupFixture.js b/stories/CSSTransitionGroupFixture.js index 2b3ac0c0..cc2e503f 100644 --- a/stories/CSSTransitionGroupFixture.js +++ b/stories/CSSTransitionGroupFixture.js @@ -1,18 +1,16 @@ -import React from 'react'; +import React from 'react' import TransitionGroup from '../src/TransitionGroup'; import StoryFixture from './StoryFixture'; class CSSTransitionGroupFixture extends React.Component { - constructor(props, context) { - super(props, context); - - let items = props.items || []; + static defaultProps = { + items: [] + } - this.count = items.length; - this.state = { - items, - }; + count = this.props.items.length + state = { + items: this.props.items } handleAddItem = () => { @@ -39,7 +37,9 @@ class CSSTransitionGroupFixture extends React.Component { } 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 ( @@ -52,18 +52,13 @@ class CSSTransitionGroupFixture extends React.Component { Remove a few - - {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 7e6f2905..34e987ea 100644 --- a/stories/NestedTransition.js +++ b/stories/NestedTransition.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react' import StoryFixture from './StoryFixture'; import Fade from './transitions/Fade'; @@ -7,9 +7,8 @@ import Scale from './transitions/Scale'; function FadeAndScale(props) { return ( -
-
I will fade
- {/* +
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. @@ -17,10 +16,9 @@ function FadeAndScale(props) { 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 +
); } diff --git a/stories/ReplaceTransition.js b/stories/ReplaceTransition.js index 22e397c6..9f6a0399 100644 --- a/stories/ReplaceTransition.js +++ b/stories/ReplaceTransition.js @@ -40,7 +40,6 @@ const styles = css` const defaultProps = { in: false, - delay: false, timeout: FADE_TIMEOUT * 2, } @@ -56,8 +55,6 @@ function Fade(props) { Fade.defaultProps = defaultProps; -export default Fade; - function Example({ children }) { const [show, setShow] = useState(false); @@ -77,20 +74,27 @@ function Example({ children }) { storiesOf('Replace Transition', module) - .add('Animates on all', () => ( - - - console.log('onEnter')} - onEntering={() => console.log('onEntering')} - onEntered={() => console.log('onEntered')} - onExit={() => console.log('onExit')} - onExiting={() => console.log('onExiting')} - onExited={() => console.log('onExited')} - > -
in True
-
in False
-
-
- )) + .add('Animates on all', () => { + const firstNodeRef = React.createRef() + const secondNodeRef = React.createRef() + return ( + + console.log('onEnter')} + onEntering={() => console.log('onEntering')} + onEntered={() => console.log('onEntered')} + onExit={() => console.log('onExit')} + onExiting={() => console.log('onExiting')} + onExited={() => console.log('onExited')}> + +
in True
+
+ +
in False
+
+
+
+ ) + }) diff --git a/stories/Transition.js b/stories/Transition.js index e12d0b60..21df6adf 100644 --- a/stories/Transition.js +++ b/stories/Transition.js @@ -2,17 +2,17 @@ import React, { useState } from 'react' import { storiesOf } from '@storybook/react' import StoryFixture from './StoryFixture' -import { Fade, Collapse } from './transitions/Bootstrap' +import { Fade, Collapse, FadeForwardRef, FadeInnerRef } from './transitions/Bootstrap' function ToggleFixture({ defaultIn, description, children }) { - const [show, setShow] = useState(defaultIn); + const [show, setShow] = useState(defaultIn) 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 + + ) + }) + diff --git a/stories/TransitionGroup.js b/stories/TransitionGroup.js index 17bba6e6..d7447597 100644 --- a/stories/TransitionGroup.js +++ b/stories/TransitionGroup.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react' import { storiesOf } from '@storybook/react'; import TransitionGroup from '../src/TransitionGroup'; @@ -106,11 +106,7 @@ class DynamicTransition extends React.Component {
- {!hide && - -
Changing! {count}
-
- } + {!hide && Changing! {count}}
) @@ -140,9 +136,7 @@ function ReEnterTransition() { {!hide && ( - -
I'm entering!
-
+ I'm entering! )}
diff --git a/stories/transitions/Bootstrap.js b/stories/transitions/Bootstrap.js index ae98f597..78232e3a 100644 --- a/stories/transitions/Bootstrap.js +++ b/stories/transitions/Bootstrap.js @@ -1,5 +1,5 @@ import { css } from 'astroturf'; -import React from 'react'; +import React, { useEffect, useRef } from 'react' import style from 'dom-helpers/css'; import Transition, { EXITED, ENTERED, ENTERING, EXITING } @@ -35,17 +35,23 @@ const fadeStyles = { [ENTERED]: styles.in, } -export const Fade = props => ( - - {status => React.cloneElement(props.children, { - className: `${styles.fade} ${fadeStyles[status] || ''}` - })} - -) +export function Fade(props) { + const nodeRef = useRef() + return ( + + {status => ( +
+ {props.children} +
+ )} +
+ ) +} function getHeight(elem) { let value = elem.offsetHeight; @@ -66,46 +72,93 @@ const collapseStyles = { } 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] - })} + onExiting={this.handleExiting}> + {(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/Fade.js b/stories/transitions/Fade.js index d8e9c360..4a3937f8 100644 --- a/stories/transitions/Fade.js +++ b/stories/transitions/Fade.js @@ -1,5 +1,5 @@ import { css } from 'astroturf'; -import React from 'react'; +import React, { useRef } from 'react' import CSSTransition from '../../src/CSSTransition'; @@ -32,7 +32,12 @@ const defaultProps = { }; function Fade(props) { - return ; + const nodeRef = useRef() + return ( + +
{props.children}
+
+ ); } Fade.defaultProps = defaultProps; diff --git a/stories/transitions/Scale.js b/stories/transitions/Scale.js index cd46f361..7a8fffd7 100644 --- a/stories/transitions/Scale.js +++ b/stories/transitions/Scale.js @@ -1,5 +1,5 @@ import { css } from 'astroturf'; -import React from 'react'; +import React, { useRef } from 'react' import CSSTransition from '../../src/CSSTransition'; @@ -32,7 +32,12 @@ const defaultProps = { }; function Scale(props) { - return ; + const nodeRef = useRef() + return ( + +
{props.children}
+
+ ); } Scale.defaultProps = defaultProps; diff --git a/test/CSSTransition-test.js b/test/CSSTransition-test.js index 3e5f9761..59fa557c 100644 --- a/test/CSSTransition-test.js +++ b/test/CSSTransition-test.js @@ -7,40 +7,43 @@ 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 wrapper = mount( { - expect(node.classList.contains('test-class')).toEqual(true) - expect(node.classList.contains('test-entering')).toEqual(false) + onEnter={() => { + expect(nodeRef.current.classList.contains('test-class')).toEqual(true) + expect(nodeRef.current.classList.contains('test-entering')).toEqual(false) done() }} > -
+
) - .tap(inst => { - expect(inst.getDOMNode().classList.contains('test-class')).toEqual(false) - }) - .setProps({ + expect(nodeRef.current.classList.contains('test-class')).toEqual(false) + + wrapper.setProps({ in: true, className: 'test-class' }) }); describe('entering', () => { - let instance; + let wrapper, nodeRef; beforeEach(() => { - instance = mount( + nodeRef = React.createRef() + wrapper = mount( -
+
) }); @@ -48,21 +51,21 @@ describe('CSSTransition', () => { it('should apply classes at each transition state', done => { let count = 0; - instance.setProps({ + wrapper.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(); } @@ -71,34 +74,36 @@ describe('CSSTransition', () => { it('should apply custom classNames names', done => { let count = 0; - instance = mount( + const nodeRef = React.createRef() + wrapper = mount( -
+
); - instance.setProps({ + wrapper.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'); + onEntered(){ + expect(nodeRef.current.className).toEqual('custom-super-done'); expect(count).toEqual(2); done(); } @@ -109,39 +114,43 @@ describe('CSSTransition', () => { describe('appearing', () => { it('should apply appear classes at each transition state', done => { let count = 0; + const nodeRef = React.createRef() mount( { + onEnter={(isAppearing) => { count++; expect(isAppearing).toEqual(true); - expect(node.className).toEqual('appear-test-appear'); + expect(nodeRef.current.className).toEqual('appear-test-appear'); }} - onEntering={(node, isAppearing) => { + onEntering={(isAppearing) => { count++; expect(isAppearing).toEqual(true); - expect(node.className).toEqual('appear-test-appear appear-test-appear-active'); + expect(nodeRef.current.className).toEqual('appear-test-appear appear-test-appear-active'); }} - onEntered={(node, isAppearing) => { + onEntered={(isAppearing) => { expect(isAppearing).toEqual(true); - expect(node.className).toEqual('appear-test-appear-done appear-test-enter-done'); + expect(nodeRef.current.className).toEqual('appear-test-appear-done appear-test-enter-done'); expect(count).toEqual(2); done(); }} > -
+
); }); it('should lose the "*-appear-done" class after leaving and entering again', (done) => { + const nodeRef = React.createRef() const wrapper = mount( { wrapper.setProps({ in: false, onEntered: () => {}, - onExited: (node) => { - expect(node.className).toBe('appear-test-exit-done') + onExited: () => { + expect(nodeRef.current.className).toBe('appear-test-exit-done') wrapper.setProps({ in: true, onEntered: () => { - expect(node.className).toBe('appear-test-enter-done') + expect(nodeRef.current.className).toBe('appear-test-enter-done') done() } }) @@ -162,61 +171,65 @@ describe('CSSTransition', () => { }) }} > -
+
) }); it('should not add undefined when appearDone is not defined', done => { + const nodeRef = React.createRef() mount( { + onEnter={(isAppearing) => { expect(isAppearing).toEqual(true); - expect(node.className).toEqual('appear-test'); + expect(nodeRef.current.className).toEqual('appear-test'); }} - onEntered={(node, isAppearing) => { + onEntered={(isAppearing) => { expect(isAppearing).toEqual(true); - expect(node.className).toEqual(''); + expect(nodeRef.current.className).toEqual(''); done(); }} > -
+
); }); it('should not be appearing in normal enter mode', done => { let count = 0; + const nodeRef = React.createRef() mount( -
+
).setProps({ in: true, - onEnter(node, isAppearing){ + onEnter(isAppearing){ count++; expect(isAppearing).toEqual(false); - expect(node.className).toEqual('not-appear-test-enter'); + expect(nodeRef.current.className).toEqual('not-appear-test-enter'); }, - onEntering(node, isAppearing){ + onEntering(isAppearing){ count++; expect(isAppearing).toEqual(false); - expect(node.className).toEqual('not-appear-test-enter not-appear-test-enter-active'); + expect(nodeRef.current.className).toEqual('not-appear-test-enter not-appear-test-enter-active'); }, - onEntered(node, isAppearing){ + onEntered(isAppearing){ expect(isAppearing).toEqual(false); - expect(node.className).toEqual('not-appear-test-enter-done'); + expect(nodeRef.current.className).toEqual('not-appear-test-enter-done'); expect(count).toEqual(2); done(); } @@ -224,9 +237,11 @@ describe('CSSTransition', () => { }); it('should not enter the transition states when appear=false', () => { + const nodeRef = React.createRef() mount( { throw Error('Entred called!') }} > -
+
); }); @@ -249,16 +264,18 @@ describe('CSSTransition', () => { }); describe('exiting', ()=> { - let instance; + let wrapper, nodeRef; beforeEach(() => { - instance = mount( + nodeRef = React.createRef() + wrapper = mount( -
+
) }); @@ -266,21 +283,21 @@ describe('CSSTransition', () => { it('should apply classes at each transition state', done => { let count = 0; - instance.setProps({ + wrapper.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'); + onExited(){ + expect(nodeRef.current.className).toEqual('test-exit-done'); expect(count).toEqual(2); done(); } @@ -289,9 +306,11 @@ describe('CSSTransition', () => { it('should apply custom classNames names', done => { let count = 0; - instance = mount( + const nodeRef = React.createRef() + wrapper = mount( { exitDone: 'custom-super-done', }} > -
+
); - instance.setProps({ + wrapper.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'); + onExited() { + expect(nodeRef.current.className).toEqual('custom-super-done'); expect(count).toEqual(2); done(); } @@ -327,30 +346,32 @@ describe('CSSTransition', () => { it('should support empty prefix', done => { let count = 0; - const instance = mount( + const nodeRef = React.createRef() + const wrapper = mount( -
+
) - instance.setProps({ + wrapper.setProps({ in: false, - onExit(node){ + onExit() { count++; - expect(node.className).toEqual('exit'); + expect(nodeRef.current.className).toEqual('exit'); }, - onExiting(node){ + onExiting() { count++; - expect(node.className).toEqual('exit exit-active'); + expect(nodeRef.current.className).toEqual('exit exit-active'); }, - onExited(node){ - expect(node.className).toEqual('exit-done'); + onExited() { + expect(nodeRef.current.className).toEqual('exit-done'); expect(count).toEqual(2); done(); } @@ -359,11 +380,11 @@ describe('CSSTransition', () => { }); describe('reentering', () => { - it('should remove dynamically applied classes', done => { + it('should remove dynamically applied classes', async () => { let count = 0; class Test extends React.Component { render() { - const { direction, text, ...props } = this.props; + const { direction, text, nodeRef, ...props } = this.props; return ( { - {text} + {text} ) } } - const instance = mount() + const nodeRef = { + foo: React.createRef(), + bar: React.createRef(), + } + + const wrapper = mount() const rerender = getProps => new Promise(resolve => - instance.setProps({ + wrapper.setProps({ onEnter: undefined, onEntering: undefined, onEntered: undefined, @@ -400,40 +427,39 @@ describe('CSSTransition', () => { }) ); - Promise.resolve().then(() => - rerender(resolve => ({ - direction: 'up', - text: 'bar', + await rerender(resolve => ({ + direction: 'up', + text: 'bar', + nodeRef: nodeRef.bar, - onEnter(node) { - count++; - expect(node.className).toEqual('up-enter'); - }, - onEntering(node) { - count++; - expect(node.className).toEqual('up-enter up-enter-active'); - resolve() - } - })) - ).then(() => { - return rerender(resolve => ({ - direction: 'down', - text: 'foo', - - onEntering(node) { - count++; - expect(node.className).toEqual('down-enter down-enter-active'); - }, - onEntered(node) { - count++; - expect(node.className).toEqual('down-enter-done'); - resolve(); - } - })) - }).then(() => { - expect(count).toEqual(4); - done(); - }); + onEnter() { + count++; + expect(nodeRef.bar.current.className).toEqual('up-enter'); + }, + onEntering() { + count++; + expect(nodeRef.bar.current.className).toEqual('up-enter up-enter-active'); + resolve() + } + })) + + await rerender(resolve => ({ + 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'); + resolve(); + } + })) + + expect(count).toEqual(4); }); }); }); diff --git a/test/CSSTransitionGroup-test.js b/test/CSSTransitionGroup-test.js index 4d0decbf..2776a8e0 100644 --- a/test/CSSTransitionGroup-test.js +++ b/test/CSSTransitionGroup-test.js @@ -12,9 +12,10 @@ describe('CSSTransitionGroup', () => { let consoleErrorSpy; function YoloTransition({ id, ...props }) { + const nodeRef = React.useRef() return ( - - + + ) } diff --git a/test/SwitchTransition-test.js b/test/SwitchTransition-test.js index 744e2967..76d2a527 100644 --- a/test/SwitchTransition-test.js +++ b/test/SwitchTransition-test.js @@ -10,20 +10,21 @@ describe('SwitchTransition', () => { 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'), + 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 ? ( - - + + ) : null} @@ -32,10 +33,11 @@ describe('SwitchTransition', () => { }); it('should have default status ENTERED', () => { + const nodeRef = React.createRef() const wrapper = mount( - - + + ); @@ -44,10 +46,11 @@ describe('SwitchTransition', () => { }); it('should have default mode: out-in', () => { + const nodeRef = React.createRef() const wrapper = mount( - - + + ); @@ -56,11 +59,12 @@ describe('SwitchTransition', () => { }); it('should work without childs', () => { + const nodeRef = React.createRef() expect(() => { mount( - - + + ); diff --git a/test/Transition-test.js b/test/Transition-test.js index bc31c926..c3690fa3 100644 --- a/test/Transition-test.js +++ b/test/Transition-test.js @@ -1,4 +1,5 @@ import React from 'react' +import ReactDOM from 'react-dom' import { mount } from 'enzyme' @@ -25,15 +26,17 @@ expect.extend({ describe('Transition', () => { it('should not transition on mount', () => { + const nodeRef = React.createRef() let wrapper = mount( { throw new Error('should not Enter') }} > -
+
) @@ -41,21 +44,23 @@ describe('Transition', () => { }) it('should transition on mount with `appear`', done => { + const nodeRef = React.createRef() mount( { throw Error('Animated!') }} > -
+
) mount( - done()}> -
+ done()}> +
) }) @@ -63,14 +68,16 @@ describe('Transition', () => { it('should pass filtered props to children', () => { class Child extends React.Component { render() { - return
child
+ return
child
} } + const nodeRef = React.createRef() const child = mount( { onExiting={() => {}} onExited={() => {}} > - + ).find(Child) - expect(child.props()).toEqual({ foo: 'foo', bar: 'bar' }) + expect(child.props()).toEqual({ foo: 'foo', bar: 'bar', nodeRef }) }) it('should allow addEndListener instead of timeouts', done => { - let listener = jest.fn((node, end) => setTimeout(end, 0)) + let listener = jest.fn(end => setTimeout(end, 0)) - let inst = mount( + const nodeRef = React.createRef() + let wrapper = mount( { expect(listener).toHaveBeenCalledTimes(1) done() }} > -
+
) - inst.setProps({ in: true }) + wrapper.setProps({ in: true }) }) it('should fallback to timeouts with addEndListener', done => { let calledEnd = false - let listener = (node, end) => + let listener = (end) => setTimeout(() => { calledEnd = true end() }, 100) - let inst = mount( + const nodeRef = React.createRef() + let wrapper = mount( { expect(calledEnd).toEqual(false) done() }} > -
+
) - inst.setProps({ in: true }) + wrapper.setProps({ in: true }) }) it('should mount/unmount immediately if not have enter/exit timeout', (done) => { + const nodeRef = React.createRef() const wrapper = mount( - -
+ +
) @@ -158,15 +170,45 @@ describe('Transition', () => { }) }) + it('should use `React.findDOMNode` when `nodeRef` is not provided', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation() + const findDOMNodeSpy = jest.spyOn(ReactDOM, 'findDOMNode') + + mount( + +
+ + ) + + 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() + mount( + +
+ + ) + + expect(findDOMNodeSpy).not.toHaveBeenCalled(); + findDOMNodeSpy.mockRestore() + }) + describe('appearing timeout', () => { it('should use enter timeout if appear not set', done => { let calledBeforeEntered = false setTimeout(() => { calledBeforeEntered = true }, 10) + const nodeRef = React.createRef() const wrapper = mount( - -
+ +
) @@ -182,9 +224,10 @@ describe('Transition', () => { }) it('should use appear timeout if appear is set', done => { + const nodeRef = React.createRef() const wrapper = mount( - -
+ +
) @@ -206,12 +249,13 @@ describe('Transition', () => { }) describe('entering', () => { - let wrapper + let wrapper, nodeRef; beforeEach(() => { + nodeRef = React.createRef() wrapper = mount( - -
+ +
) }) @@ -267,12 +311,13 @@ describe('Transition', () => { }) describe('exiting', () => { - let wrapper + let wrapper, nodeRef; beforeEach(() => { + nodeRef = React.createRef() wrapper = mount( - -
+ +
) }) @@ -329,10 +374,8 @@ describe('Transition', () => { describe('mountOnEnter', () => { class MountTransition extends React.Component { - constructor(props) { - super(props) - this.state = { in: props.initialIn } - } + nodeRef = React.createRef() + state = { in: this.props.initialIn } render() { const { ...props } = this.props @@ -341,12 +384,13 @@ describe('Transition', () => { return ( this.transition = this.transition || transition} + nodeRef={this.nodeRef} mountOnEnter in={this.state.in} timeout={10} {...props} > -
+
) } @@ -362,7 +406,7 @@ describe('Transition', () => { initialIn={false} onEnter={() => { expect(wrapper.instance().getStatus()).toEqual(EXITED) - expect(wrapper.getDOMNode()).toExist() + expect(wrapper.instance().nodeRef.current).toExist() done() }} /> @@ -370,7 +414,7 @@ describe('Transition', () => { expect(wrapper.instance().getStatus()).toEqual(UNMOUNTED) - expect(wrapper.getDOMNode()).not.toExist() + expect(wrapper.instance().nodeRef.current).not.toExist() wrapper.setProps({ in: true }) }) @@ -381,27 +425,27 @@ describe('Transition', () => { initialIn={false} onEntered={() => { expect(wrapper.instance().getStatus()).toEqual(ENTERED) - expect(wrapper.getDOMNode()).toExist() + expect(wrapper.instance().nodeRef.current).toExist() wrapper.setState({ in: false }) }} onExited={() => { expect(wrapper.instance().getStatus()).toEqual(EXITED) - expect(wrapper.getDOMNode()).toExist() + expect(wrapper.instance().nodeRef.current).toExist() done() }} /> ) - expect(wrapper.getDOMNode()).not.toExist() + expect(wrapper.instance().nodeRef.current).not.toExist() wrapper.setState({ in: true }) }) }) describe('unmountOnExit', () => { class UnmountTransition extends React.Component { - divRef = React.createRef() + nodeRef = React.createRef() state = { in: this.props.initialIn } render() { @@ -411,12 +455,13 @@ describe('Transition', () => { return ( this.transition = this.transition || transition} + nodeRef={this.nodeRef} unmountOnExit in={this.state.in} timeout={10} {...props} > -
+
) } @@ -427,42 +472,42 @@ describe('Transition', () => { } it('should mount when entering', done => { - const wrapper = mount( + const instance = mount( { - expect(wrapper.getStatus()).toEqual(EXITED) - expect(wrapper.divRef.current).toExist() + expect(instance.getStatus()).toEqual(EXITED) + expect(instance.nodeRef.current).toExist() done() }} /> ).instance() - expect(wrapper.getStatus()).toEqual(UNMOUNTED) - expect(wrapper.divRef.current).toBeNull() + expect(instance.getStatus()).toEqual(UNMOUNTED) + expect(instance.nodeRef.current).toBeNull() - wrapper.setState({ in: true }) + instance.setState({ in: true }) }) it('should unmount after exiting', done => { - const wrapper = mount( + const instance = mount( { setTimeout(() => { - expect(wrapper.getStatus()).toEqual(UNMOUNTED) - expect(wrapper.divRef.current).not.toExist() + expect(instance.getStatus()).toEqual(UNMOUNTED) + expect(instance.nodeRef.current).not.toExist() done() }) }} /> ).instance() - expect(wrapper.getStatus()).toEqual(ENTERED) - expect(wrapper.divRef.current).toExist() + expect(instance.getStatus()).toEqual(ENTERED) + expect(instance.nodeRef.current).toExist() - wrapper.setState({ in: false }) + instance.setState({ in: false }) }) }) }) diff --git a/test/TransitionGroup-test.js b/test/TransitionGroup-test.js index 91ed167d..f921ae40 100644 --- a/test/TransitionGroup-test.js +++ b/test/TransitionGroup-test.js @@ -20,18 +20,19 @@ describe('TransitionGroup', () => { 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 ( - - + + ) } diff --git a/test/setup.js b/test/setup.js index 4b7566f9..f053547a 100644 --- a/test/setup.js +++ b/test/setup.js @@ -1,3 +1,4 @@ +import React from 'react' global.requestAnimationFrame = function(callback) { setTimeout(callback, 0); @@ -6,4 +7,7 @@ global.requestAnimationFrame = function(callback) { const Enzyme = require('enzyme'); const Adapter = require('enzyme-adapter-react-16'); -Enzyme.configure({ adapter: new Adapter() }) +Enzyme.configure({ + adapter: new Adapter(), + wrappingComponent: props => +})