-
Notifications
You must be signed in to change notification settings - Fork 651
/
Copy pathTransition.js
622 lines (550 loc) · 18 KB
/
Transition.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
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';
export const ENTERING = 'entering';
export const ENTERED = 'entered';
export const EXITING = 'exiting';
/**
* The Transition component lets you describe a transition from one component
* state to another _over time_ with a simple declarative API. Most commonly
* 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:
*
* ```jsx
* import { Transition } from 'react-transition-group';
* import { useRef } from 'react';
*
* const duration = 300;
*
* const defaultStyle = {
* transition: `opacity ${duration}ms ease-in-out`,
* opacity: 0,
* }
*
* const transitionStyles = {
* entering: { opacity: 1 },
* entered: { opacity: 1 },
* exiting: { opacity: 0 },
* exited: { opacity: 0 },
* };
*
* function Fade({ in: inProp }) {
* const nodeRef = useRef(null);
* return (
* <Transition nodeRef={nodeRef} in={inProp} timeout={duration}>
* {state => (
* <div ref={nodeRef} style={{
* ...defaultStyle,
* ...transitionStyles[state]
* }}>
* I'm a fade Transition!
* </div>
* )}
* </Transition>
* );
* }
* ```
*
* There are 4 main states a Transition can be in:
* - `'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 (we'll use the
* [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 (
* <div>
* <Transition nodeRef={nodeRef} in={inProp} timeout={500}>
* {state => (
* // ...
* )}
* </Transition>
* <button onClick={() => setInProp(true)}>
* Click to Enter
* </button>
* </div>
* );
* }
* ```
*
* 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 contextType = TransitionGroupContext;
constructor(props, context) {
super(props, context);
let parentGroup = context;
// In the context of a TransitionGroup all enters are really appears
let appear =
parentGroup && !parentGroup.isMounting ? props.enter : props.appear;
let initialStatus;
this.appearStatus = null;
if (props.in) {
if (appear) {
initialStatus = EXITED;
this.appearStatus = ENTERING;
} else {
initialStatus = ENTERED;
}
} else {
if (props.unmountOnExit || props.mountOnEnter) {
initialStatus = UNMOUNTED;
} else {
initialStatus = EXITED;
}
}
this.state = { status: initialStatus };
this.nextCallback = null;
}
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.appearStatus);
}
componentDidUpdate(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;
}
}
}
this.updateStatus(false, nextStatus);
}
componentWillUnmount() {
this.cancelNextCallback();
}
getTimeouts() {
const { timeout } = this.props;
let exit, enter, appear;
exit = enter = appear = timeout;
if (timeout != null && typeof timeout !== 'number') {
exit = timeout.exit;
enter = timeout.enter;
// TODO: remove fallback for next major
appear = timeout.appear !== undefined ? timeout.appear : enter;
}
return { exit, enter, appear };
}
updateStatus(mounting = false, nextStatus) {
if (nextStatus !== null) {
// nextStatus will always be ENTERING or EXITING.
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();
}
} else if (this.props.unmountOnExit && this.state.status === EXITED) {
this.setState({ status: UNMOUNTED });
}
}
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(maybeNode);
});
return;
}
this.props.onEnter(maybeNode, maybeAppearing);
this.safeSetState({ status: ENTERING }, () => {
this.props.onEntering(maybeNode, maybeAppearing);
this.onTransitionEnd(enterTimeout, () => {
this.safeSetState({ status: ENTERED }, () => {
this.props.onEntered(maybeNode, maybeAppearing);
});
});
});
}
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(maybeNode);
});
return;
}
this.props.onExit(maybeNode);
this.safeSetState({ status: EXITING }, () => {
this.props.onExiting(maybeNode);
this.onTransitionEnd(timeouts.exit, () => {
this.safeSetState({ status: EXITED }, () => {
this.props.onExited(maybeNode);
});
});
});
}
cancelNextCallback() {
if (this.nextCallback !== null) {
this.nextCallback.cancel();
this.nextCallback = null;
}
}
safeSetState(nextState, callback) {
// 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, callback);
}
setNextCallback(callback) {
let active = true;
this.nextCallback = (event) => {
if (active) {
active = false;
this.nextCallback = null;
callback(event);
}
};
this.nextCallback.cancel = () => {
active = false;
};
return this.nextCallback;
}
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;
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,
// 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
<TransitionGroupContext.Provider value={null}>
{typeof children === 'function'
? children(status, childProps)
: React.cloneElement(React.Children.only(children), childProps)}
</TransitionGroupContext.Provider>
);
}
}
Transition.propTypes = {
/**
* 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
* <Transition nodeRef={nodeRef} in={this.state.in} timeout={150}>
* {state => (
* <MyComponent ref={nodeRef} className={`fade fade-${state}`} />
* )}
* </Transition>
* ```
*/
children: PropTypes.oneOfType([
PropTypes.func.isRequired,
PropTypes.element.isRequired,
]).isRequired,
/**
* Show the component; triggers the enter or exit states
*/
in: PropTypes.bool,
/**
* By default the child component is mounted immediately along with
* the parent `Transition` component. If you want to "lazy mount" the component on the
* first `in={true}` you can set `mountOnEnter`. After the first enter transition the component will stay
* mounted, even on "exited", unless you also specify `unmountOnExit`.
*/
mountOnEnter: PropTypes.bool,
/**
* By default the child component stays mounted after it reaches the `'exited'` state.
* Set `unmountOnExit` if you'd prefer to unmount the component after it finishes exiting.
*/
unmountOnExit: PropTypes.bool,
/**
* 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 special appear states like `appearing`/`appeared`, this prop
* > only adds an additional enter transition. However, in the
* > `<CSSTransition>` component that first enter transition does result in
* > additional `.appear-*` classes, that way you can choose to style it
* > differently.
*/
appear: PropTypes.bool,
/**
* Enable or disable enter transitions.
*/
enter: PropTypes.bool,
/**
* Enable or disable exit transitions.
*/
exit: PropTypes.bool,
/**
* The duration of the transition, in milliseconds.
* Required unless `addEndListener` is provided.
*
* You may specify a single timeout for all transitions:
*
* ```jsx
* timeout={500}
* ```
*
* or individually:
*
* ```jsx
* timeout={{
* appear: 500,
* enter: 300,
* exit: 500,
* }}
* ```
*
* - `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;
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. 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) => {
* // use the css transitionend event to mark the finish of a transition
* node.addEventListener('transitionend', done, false);
* }}
* ```
*/
addEndListener: PropTypes.func,
/**
* 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,
/**
* 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,
/**
* 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,
/**
* 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
*/
onExited: PropTypes.func,
};
// Name the function so it is clearer in the documentation
function noop() {}
Transition.defaultProps = {
in: false,
mountOnEnter: false,
unmountOnExit: false,
appear: false,
enter: true,
exit: true,
onEnter: noop,
onEntering: noop,
onEntered: noop,
onExit: noop,
onExiting: noop,
onExited: noop,
};
Transition.UNMOUNTED = UNMOUNTED;
Transition.EXITED = EXITED;
Transition.ENTERING = ENTERING;
Transition.ENTERED = ENTERED;
Transition.EXITING = EXITING;
export default Transition;