Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add nodeRef alternative instead of internal findDOMNode #559

Merged
merged 7 commits into from May 5, 2020
7 changes: 6 additions & 1 deletion .storybook/config.js
@@ -1,4 +1,9 @@
import { configure } from '@storybook/react';
import { configure, addDecorator } from '@storybook/react';
import React from 'react';

addDecorator(
storyFn => <React.StrictMode>{storyFn()}</React.StrictMode>,
)

function loadStories() {
require('../stories');
Expand Down
41 changes: 29 additions & 12 deletions src/CSSTransition.js
Expand Up @@ -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';
Expand Down Expand Up @@ -305,6 +316,7 @@ CSSTransition.propTypes = {
/**
* A `<Transition>` 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)
*/
Expand All @@ -313,6 +325,7 @@ CSSTransition.propTypes = {
/**
* A `<Transition>` 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)
*/
Expand All @@ -321,6 +334,7 @@ CSSTransition.propTypes = {
/**
* A `<Transition>` 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)
*/
Expand All @@ -329,13 +343,15 @@ CSSTransition.propTypes = {
/**
* A `<Transition>` 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,

/**
* A `<Transition>` callback fired immediately after the 'exit-active' is applied.
* Note: when `nodeRef` prop is passed, `node` is not passed
*
* @type Function(node: HtmlElement)
*/
Expand All @@ -344,6 +360,7 @@ CSSTransition.propTypes = {
/**
* A `<Transition>` 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)
*/
Expand Down
8 changes: 7 additions & 1 deletion src/ReplaceTransition.js
Expand Up @@ -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() {
Expand Down
66 changes: 49 additions & 17 deletions src/Transition.js
Expand Up @@ -210,65 +210,71 @@ 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
// no enter animation skip right to ENTERED
// 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)
})
})
})
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -370,6 +383,18 @@ class Transition extends React.Component {
}

Transition.propTypes = {
/**
* A react reference to DOM element that need to transition
* https://stackoverflow.com/a/51127130/4671932
* Note: When `nodeRef` prop is used, `node` is not passed to callback functions (e.g. `onEnter`)
* because user already has direct access to the node
* Note: 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/master/test/CSSTransition-test.js))
* CSSTransition > reentering > should remove dynamically applied classes
*/
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'`,
Expand Down Expand Up @@ -467,6 +492,7 @@ 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.
* Note: when `nodeRef` prop is passed, `node` is not passed
*
* ```jsx
* addEndListener={(node, done) => {
Expand All @@ -480,6 +506,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
*
* @type Function(node: HtmlElement, isAppearing: bool) -> void
*/
Expand All @@ -488,6 +515,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
*
* @type Function(node: HtmlElement, isAppearing: bool)
*/
Expand All @@ -496,27 +524,31 @@ 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,

/**
* 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,

/**
* 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,

/**
* Callback fired after the "exited" status is applied.
* Note: when `nodeRef` prop is passed, `node` is not passed
*
* @type Function(node: HtmlElement) -> void
*/
Expand Down
1 change: 1 addition & 0 deletions src/TransitionGroup.js
Expand Up @@ -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)

Expand Down