Transition.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
  2. import _inheritsLoose from "@babel/runtime/helpers/esm/inheritsLoose";
  3. import PropTypes from 'prop-types';
  4. import React from 'react';
  5. import ReactDOM from 'react-dom';
  6. import config from './config';
  7. import { timeoutsShape } from './utils/PropTypes';
  8. import TransitionGroupContext from './TransitionGroupContext';
  9. import { forceReflow } from './utils/reflow';
  10. export var UNMOUNTED = 'unmounted';
  11. export var EXITED = 'exited';
  12. export var ENTERING = 'entering';
  13. export var ENTERED = 'entered';
  14. export var EXITING = 'exiting';
  15. /**
  16. * The Transition component lets you describe a transition from one component
  17. * state to another _over time_ with a simple declarative API. Most commonly
  18. * it's used to animate the mounting and unmounting of a component, but can also
  19. * be used to describe in-place transition states as well.
  20. *
  21. * ---
  22. *
  23. * **Note**: `Transition` is a platform-agnostic base component. If you're using
  24. * transitions in CSS, you'll probably want to use
  25. * [`CSSTransition`](https://reactcommunity.org/react-transition-group/css-transition)
  26. * instead. It inherits all the features of `Transition`, but contains
  27. * additional features necessary to play nice with CSS transitions (hence the
  28. * name of the component).
  29. *
  30. * ---
  31. *
  32. * By default the `Transition` component does not alter the behavior of the
  33. * component it renders, it only tracks "enter" and "exit" states for the
  34. * components. It's up to you to give meaning and effect to those states. For
  35. * example we can add styles to a component when it enters or exits:
  36. *
  37. * ```jsx
  38. * import { Transition } from 'react-transition-group';
  39. *
  40. * const duration = 300;
  41. *
  42. * const defaultStyle = {
  43. * transition: `opacity ${duration}ms ease-in-out`,
  44. * opacity: 0,
  45. * }
  46. *
  47. * const transitionStyles = {
  48. * entering: { opacity: 1 },
  49. * entered: { opacity: 1 },
  50. * exiting: { opacity: 0 },
  51. * exited: { opacity: 0 },
  52. * };
  53. *
  54. * const Fade = ({ in: inProp }) => (
  55. * <Transition in={inProp} timeout={duration}>
  56. * {state => (
  57. * <div style={{
  58. * ...defaultStyle,
  59. * ...transitionStyles[state]
  60. * }}>
  61. * I'm a fade Transition!
  62. * </div>
  63. * )}
  64. * </Transition>
  65. * );
  66. * ```
  67. *
  68. * There are 4 main states a Transition can be in:
  69. * - `'entering'`
  70. * - `'entered'`
  71. * - `'exiting'`
  72. * - `'exited'`
  73. *
  74. * Transition state is toggled via the `in` prop. When `true` the component
  75. * begins the "Enter" stage. During this stage, the component will shift from
  76. * its current transition state, to `'entering'` for the duration of the
  77. * transition and then to the `'entered'` stage once it's complete. Let's take
  78. * the following example (we'll use the
  79. * [useState](https://reactjs.org/docs/hooks-reference.html#usestate) hook):
  80. *
  81. * ```jsx
  82. * function App() {
  83. * const [inProp, setInProp] = useState(false);
  84. * return (
  85. * <div>
  86. * <Transition in={inProp} timeout={500}>
  87. * {state => (
  88. * // ...
  89. * )}
  90. * </Transition>
  91. * <button onClick={() => setInProp(true)}>
  92. * Click to Enter
  93. * </button>
  94. * </div>
  95. * );
  96. * }
  97. * ```
  98. *
  99. * When the button is clicked the component will shift to the `'entering'` state
  100. * and stay there for 500ms (the value of `timeout`) before it finally switches
  101. * to `'entered'`.
  102. *
  103. * When `in` is `false` the same thing happens except the state moves from
  104. * `'exiting'` to `'exited'`.
  105. */
  106. var Transition = /*#__PURE__*/function (_React$Component) {
  107. _inheritsLoose(Transition, _React$Component);
  108. function Transition(props, context) {
  109. var _this;
  110. _this = _React$Component.call(this, props, context) || this;
  111. var parentGroup = context; // In the context of a TransitionGroup all enters are really appears
  112. var appear = parentGroup && !parentGroup.isMounting ? props.enter : props.appear;
  113. var initialStatus;
  114. _this.appearStatus = null;
  115. if (props.in) {
  116. if (appear) {
  117. initialStatus = EXITED;
  118. _this.appearStatus = ENTERING;
  119. } else {
  120. initialStatus = ENTERED;
  121. }
  122. } else {
  123. if (props.unmountOnExit || props.mountOnEnter) {
  124. initialStatus = UNMOUNTED;
  125. } else {
  126. initialStatus = EXITED;
  127. }
  128. }
  129. _this.state = {
  130. status: initialStatus
  131. };
  132. _this.nextCallback = null;
  133. return _this;
  134. }
  135. Transition.getDerivedStateFromProps = function getDerivedStateFromProps(_ref, prevState) {
  136. var nextIn = _ref.in;
  137. if (nextIn && prevState.status === UNMOUNTED) {
  138. return {
  139. status: EXITED
  140. };
  141. }
  142. return null;
  143. } // getSnapshotBeforeUpdate(prevProps) {
  144. // let nextStatus = null
  145. // if (prevProps !== this.props) {
  146. // const { status } = this.state
  147. // if (this.props.in) {
  148. // if (status !== ENTERING && status !== ENTERED) {
  149. // nextStatus = ENTERING
  150. // }
  151. // } else {
  152. // if (status === ENTERING || status === ENTERED) {
  153. // nextStatus = EXITING
  154. // }
  155. // }
  156. // }
  157. // return { nextStatus }
  158. // }
  159. ;
  160. var _proto = Transition.prototype;
  161. _proto.componentDidMount = function componentDidMount() {
  162. this.updateStatus(true, this.appearStatus);
  163. };
  164. _proto.componentDidUpdate = function componentDidUpdate(prevProps) {
  165. var nextStatus = null;
  166. if (prevProps !== this.props) {
  167. var status = this.state.status;
  168. if (this.props.in) {
  169. if (status !== ENTERING && status !== ENTERED) {
  170. nextStatus = ENTERING;
  171. }
  172. } else {
  173. if (status === ENTERING || status === ENTERED) {
  174. nextStatus = EXITING;
  175. }
  176. }
  177. }
  178. this.updateStatus(false, nextStatus);
  179. };
  180. _proto.componentWillUnmount = function componentWillUnmount() {
  181. this.cancelNextCallback();
  182. };
  183. _proto.getTimeouts = function getTimeouts() {
  184. var timeout = this.props.timeout;
  185. var exit, enter, appear;
  186. exit = enter = appear = timeout;
  187. if (timeout != null && typeof timeout !== 'number') {
  188. exit = timeout.exit;
  189. enter = timeout.enter; // TODO: remove fallback for next major
  190. appear = timeout.appear !== undefined ? timeout.appear : enter;
  191. }
  192. return {
  193. exit: exit,
  194. enter: enter,
  195. appear: appear
  196. };
  197. };
  198. _proto.updateStatus = function updateStatus(mounting, nextStatus) {
  199. if (mounting === void 0) {
  200. mounting = false;
  201. }
  202. if (nextStatus !== null) {
  203. // nextStatus will always be ENTERING or EXITING.
  204. this.cancelNextCallback();
  205. if (nextStatus === ENTERING) {
  206. if (this.props.unmountOnExit || this.props.mountOnEnter) {
  207. var node = this.props.nodeRef ? this.props.nodeRef.current : ReactDOM.findDOMNode(this); // https://github.com/reactjs/react-transition-group/pull/749
  208. // With unmountOnExit or mountOnEnter, the enter animation should happen at the transition between `exited` and `entering`.
  209. // To make the animation happen, we have to separate each rendering and avoid being processed as batched.
  210. if (node) forceReflow(node);
  211. }
  212. this.performEnter(mounting);
  213. } else {
  214. this.performExit();
  215. }
  216. } else if (this.props.unmountOnExit && this.state.status === EXITED) {
  217. this.setState({
  218. status: UNMOUNTED
  219. });
  220. }
  221. };
  222. _proto.performEnter = function performEnter(mounting) {
  223. var _this2 = this;
  224. var enter = this.props.enter;
  225. var appearing = this.context ? this.context.isMounting : mounting;
  226. var _ref2 = this.props.nodeRef ? [appearing] : [ReactDOM.findDOMNode(this), appearing],
  227. maybeNode = _ref2[0],
  228. maybeAppearing = _ref2[1];
  229. var timeouts = this.getTimeouts();
  230. var enterTimeout = appearing ? timeouts.appear : timeouts.enter; // no enter animation skip right to ENTERED
  231. // if we are mounting and running this it means appear _must_ be set
  232. if (!mounting && !enter || config.disabled) {
  233. this.safeSetState({
  234. status: ENTERED
  235. }, function () {
  236. _this2.props.onEntered(maybeNode);
  237. });
  238. return;
  239. }
  240. this.props.onEnter(maybeNode, maybeAppearing);
  241. this.safeSetState({
  242. status: ENTERING
  243. }, function () {
  244. _this2.props.onEntering(maybeNode, maybeAppearing);
  245. _this2.onTransitionEnd(enterTimeout, function () {
  246. _this2.safeSetState({
  247. status: ENTERED
  248. }, function () {
  249. _this2.props.onEntered(maybeNode, maybeAppearing);
  250. });
  251. });
  252. });
  253. };
  254. _proto.performExit = function performExit() {
  255. var _this3 = this;
  256. var exit = this.props.exit;
  257. var timeouts = this.getTimeouts();
  258. var maybeNode = this.props.nodeRef ? undefined : ReactDOM.findDOMNode(this); // no exit animation skip right to EXITED
  259. if (!exit || config.disabled) {
  260. this.safeSetState({
  261. status: EXITED
  262. }, function () {
  263. _this3.props.onExited(maybeNode);
  264. });
  265. return;
  266. }
  267. this.props.onExit(maybeNode);
  268. this.safeSetState({
  269. status: EXITING
  270. }, function () {
  271. _this3.props.onExiting(maybeNode);
  272. _this3.onTransitionEnd(timeouts.exit, function () {
  273. _this3.safeSetState({
  274. status: EXITED
  275. }, function () {
  276. _this3.props.onExited(maybeNode);
  277. });
  278. });
  279. });
  280. };
  281. _proto.cancelNextCallback = function cancelNextCallback() {
  282. if (this.nextCallback !== null) {
  283. this.nextCallback.cancel();
  284. this.nextCallback = null;
  285. }
  286. };
  287. _proto.safeSetState = function safeSetState(nextState, callback) {
  288. // This shouldn't be necessary, but there are weird race conditions with
  289. // setState callbacks and unmounting in testing, so always make sure that
  290. // we can cancel any pending setState callbacks after we unmount.
  291. callback = this.setNextCallback(callback);
  292. this.setState(nextState, callback);
  293. };
  294. _proto.setNextCallback = function setNextCallback(callback) {
  295. var _this4 = this;
  296. var active = true;
  297. this.nextCallback = function (event) {
  298. if (active) {
  299. active = false;
  300. _this4.nextCallback = null;
  301. callback(event);
  302. }
  303. };
  304. this.nextCallback.cancel = function () {
  305. active = false;
  306. };
  307. return this.nextCallback;
  308. };
  309. _proto.onTransitionEnd = function onTransitionEnd(timeout, handler) {
  310. this.setNextCallback(handler);
  311. var node = this.props.nodeRef ? this.props.nodeRef.current : ReactDOM.findDOMNode(this);
  312. var doesNotHaveTimeoutOrListener = timeout == null && !this.props.addEndListener;
  313. if (!node || doesNotHaveTimeoutOrListener) {
  314. setTimeout(this.nextCallback, 0);
  315. return;
  316. }
  317. if (this.props.addEndListener) {
  318. var _ref3 = this.props.nodeRef ? [this.nextCallback] : [node, this.nextCallback],
  319. maybeNode = _ref3[0],
  320. maybeNextCallback = _ref3[1];
  321. this.props.addEndListener(maybeNode, maybeNextCallback);
  322. }
  323. if (timeout != null) {
  324. setTimeout(this.nextCallback, timeout);
  325. }
  326. };
  327. _proto.render = function render() {
  328. var status = this.state.status;
  329. if (status === UNMOUNTED) {
  330. return null;
  331. }
  332. var _this$props = this.props,
  333. children = _this$props.children,
  334. _in = _this$props.in,
  335. _mountOnEnter = _this$props.mountOnEnter,
  336. _unmountOnExit = _this$props.unmountOnExit,
  337. _appear = _this$props.appear,
  338. _enter = _this$props.enter,
  339. _exit = _this$props.exit,
  340. _timeout = _this$props.timeout,
  341. _addEndListener = _this$props.addEndListener,
  342. _onEnter = _this$props.onEnter,
  343. _onEntering = _this$props.onEntering,
  344. _onEntered = _this$props.onEntered,
  345. _onExit = _this$props.onExit,
  346. _onExiting = _this$props.onExiting,
  347. _onExited = _this$props.onExited,
  348. _nodeRef = _this$props.nodeRef,
  349. childProps = _objectWithoutPropertiesLoose(_this$props, ["children", "in", "mountOnEnter", "unmountOnExit", "appear", "enter", "exit", "timeout", "addEndListener", "onEnter", "onEntering", "onEntered", "onExit", "onExiting", "onExited", "nodeRef"]);
  350. return (
  351. /*#__PURE__*/
  352. // allows for nested Transitions
  353. React.createElement(TransitionGroupContext.Provider, {
  354. value: null
  355. }, typeof children === 'function' ? children(status, childProps) : React.cloneElement(React.Children.only(children), childProps))
  356. );
  357. };
  358. return Transition;
  359. }(React.Component);
  360. Transition.contextType = TransitionGroupContext;
  361. Transition.propTypes = process.env.NODE_ENV !== "production" ? {
  362. /**
  363. * A React reference to DOM element that need to transition:
  364. * https://stackoverflow.com/a/51127130/4671932
  365. *
  366. * - When `nodeRef` prop is used, `node` is not passed to callback functions
  367. * (e.g. `onEnter`) because user already has direct access to the node.
  368. * - When changing `key` prop of `Transition` in a `TransitionGroup` a new
  369. * `nodeRef` need to be provided to `Transition` with changed `key` prop
  370. * (see
  371. * [test/CSSTransition-test.js](https://github.com/reactjs/react-transition-group/blob/13435f897b3ab71f6e19d724f145596f5910581c/test/CSSTransition-test.js#L362-L437)).
  372. */
  373. nodeRef: PropTypes.shape({
  374. current: typeof Element === 'undefined' ? PropTypes.any : function (propValue, key, componentName, location, propFullName, secret) {
  375. var value = propValue[key];
  376. return PropTypes.instanceOf(value && 'ownerDocument' in value ? value.ownerDocument.defaultView.Element : Element)(propValue, key, componentName, location, propFullName, secret);
  377. }
  378. }),
  379. /**
  380. * A `function` child can be used instead of a React element. This function is
  381. * called with the current transition status (`'entering'`, `'entered'`,
  382. * `'exiting'`, `'exited'`), which can be used to apply context
  383. * specific props to a component.
  384. *
  385. * ```jsx
  386. * <Transition in={this.state.in} timeout={150}>
  387. * {state => (
  388. * <MyComponent className={`fade fade-${state}`} />
  389. * )}
  390. * </Transition>
  391. * ```
  392. */
  393. children: PropTypes.oneOfType([PropTypes.func.isRequired, PropTypes.element.isRequired]).isRequired,
  394. /**
  395. * Show the component; triggers the enter or exit states
  396. */
  397. in: PropTypes.bool,
  398. /**
  399. * By default the child component is mounted immediately along with
  400. * the parent `Transition` component. If you want to "lazy mount" the component on the
  401. * first `in={true}` you can set `mountOnEnter`. After the first enter transition the component will stay
  402. * mounted, even on "exited", unless you also specify `unmountOnExit`.
  403. */
  404. mountOnEnter: PropTypes.bool,
  405. /**
  406. * By default the child component stays mounted after it reaches the `'exited'` state.
  407. * Set `unmountOnExit` if you'd prefer to unmount the component after it finishes exiting.
  408. */
  409. unmountOnExit: PropTypes.bool,
  410. /**
  411. * By default the child component does not perform the enter transition when
  412. * it first mounts, regardless of the value of `in`. If you want this
  413. * behavior, set both `appear` and `in` to `true`.
  414. *
  415. * > **Note**: there are no special appear states like `appearing`/`appeared`, this prop
  416. * > only adds an additional enter transition. However, in the
  417. * > `<CSSTransition>` component that first enter transition does result in
  418. * > additional `.appear-*` classes, that way you can choose to style it
  419. * > differently.
  420. */
  421. appear: PropTypes.bool,
  422. /**
  423. * Enable or disable enter transitions.
  424. */
  425. enter: PropTypes.bool,
  426. /**
  427. * Enable or disable exit transitions.
  428. */
  429. exit: PropTypes.bool,
  430. /**
  431. * The duration of the transition, in milliseconds.
  432. * Required unless `addEndListener` is provided.
  433. *
  434. * You may specify a single timeout for all transitions:
  435. *
  436. * ```jsx
  437. * timeout={500}
  438. * ```
  439. *
  440. * or individually:
  441. *
  442. * ```jsx
  443. * timeout={{
  444. * appear: 500,
  445. * enter: 300,
  446. * exit: 500,
  447. * }}
  448. * ```
  449. *
  450. * - `appear` defaults to the value of `enter`
  451. * - `enter` defaults to `0`
  452. * - `exit` defaults to `0`
  453. *
  454. * @type {number | { enter?: number, exit?: number, appear?: number }}
  455. */
  456. timeout: function timeout(props) {
  457. var pt = timeoutsShape;
  458. if (!props.addEndListener) pt = pt.isRequired;
  459. for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
  460. args[_key - 1] = arguments[_key];
  461. }
  462. return pt.apply(void 0, [props].concat(args));
  463. },
  464. /**
  465. * Add a custom transition end trigger. Called with the transitioning
  466. * DOM node and a `done` callback. Allows for more fine grained transition end
  467. * logic. Timeouts are still used as a fallback if provided.
  468. *
  469. * **Note**: when `nodeRef` prop is passed, `node` is not passed.
  470. *
  471. * ```jsx
  472. * addEndListener={(node, done) => {
  473. * // use the css transitionend event to mark the finish of a transition
  474. * node.addEventListener('transitionend', done, false);
  475. * }}
  476. * ```
  477. */
  478. addEndListener: PropTypes.func,
  479. /**
  480. * Callback fired before the "entering" status is applied. An extra parameter
  481. * `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount
  482. *
  483. * **Note**: when `nodeRef` prop is passed, `node` is not passed.
  484. *
  485. * @type Function(node: HtmlElement, isAppearing: bool) -> void
  486. */
  487. onEnter: PropTypes.func,
  488. /**
  489. * Callback fired after the "entering" status is applied. An extra parameter
  490. * `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount
  491. *
  492. * **Note**: when `nodeRef` prop is passed, `node` is not passed.
  493. *
  494. * @type Function(node: HtmlElement, isAppearing: bool)
  495. */
  496. onEntering: PropTypes.func,
  497. /**
  498. * Callback fired after the "entered" status is applied. An extra parameter
  499. * `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount
  500. *
  501. * **Note**: when `nodeRef` prop is passed, `node` is not passed.
  502. *
  503. * @type Function(node: HtmlElement, isAppearing: bool) -> void
  504. */
  505. onEntered: PropTypes.func,
  506. /**
  507. * Callback fired before the "exiting" status is applied.
  508. *
  509. * **Note**: when `nodeRef` prop is passed, `node` is not passed.
  510. *
  511. * @type Function(node: HtmlElement) -> void
  512. */
  513. onExit: PropTypes.func,
  514. /**
  515. * Callback fired after the "exiting" status is applied.
  516. *
  517. * **Note**: when `nodeRef` prop is passed, `node` is not passed.
  518. *
  519. * @type Function(node: HtmlElement) -> void
  520. */
  521. onExiting: PropTypes.func,
  522. /**
  523. * Callback fired after the "exited" status is applied.
  524. *
  525. * **Note**: when `nodeRef` prop is passed, `node` is not passed
  526. *
  527. * @type Function(node: HtmlElement) -> void
  528. */
  529. onExited: PropTypes.func
  530. } : {}; // Name the function so it is clearer in the documentation
  531. function noop() {}
  532. Transition.defaultProps = {
  533. in: false,
  534. mountOnEnter: false,
  535. unmountOnExit: false,
  536. appear: false,
  537. enter: true,
  538. exit: true,
  539. onEnter: noop,
  540. onEntering: noop,
  541. onEntered: noop,
  542. onExit: noop,
  543. onExiting: noop,
  544. onExited: noop
  545. };
  546. Transition.UNMOUNTED = UNMOUNTED;
  547. Transition.EXITED = EXITED;
  548. Transition.ENTERING = ENTERING;
  549. Transition.ENTERED = ENTERED;
  550. Transition.EXITING = EXITING;
  551. export default Transition;