Skip to content

Commit 691ecfc

Browse files
authored
refactor: Prepare should always run (#42)
* chore: prepare note * chore: rm motion block code * feat: prepare always * test: add test case
1 parent db9adc8 commit 691ecfc

File tree

7 files changed

+145
-46
lines changed

7 files changed

+145
-46
lines changed

docs/demo/provider.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: provider
3+
nav:
4+
title: Demo
5+
path: /demo
6+
---
7+
8+
<code src="../examples/provider.tsx"></code>

docs/examples/provider.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import classNames from 'classnames';
2+
import CSSMotion, { Provider } from 'rc-motion';
3+
import React from 'react';
4+
import './basic.less';
5+
6+
export default () => {
7+
const [show, setShow] = React.useState(true);
8+
const [motion, setMotion] = React.useState(false);
9+
10+
const onPrepare = (node: HTMLElement) => {
11+
console.log('🔥 prepare', node);
12+
13+
return new Promise(resolve => {
14+
setTimeout(resolve, 500);
15+
});
16+
};
17+
18+
return (
19+
<Provider motion={motion}>
20+
<button onClick={() => setShow(v => !v)}>show: {String(show)}</button>
21+
<button onClick={() => setMotion(v => !v)}>
22+
motion: {String(motion)}
23+
</button>
24+
25+
<CSSMotion
26+
visible={show}
27+
motionName={'transition'}
28+
leavedClassName="hidden"
29+
motionAppear
30+
onAppearPrepare={onPrepare}
31+
onEnterPrepare={onPrepare}
32+
onLeavePrepare={onPrepare}
33+
onVisibleChanged={visible => {
34+
console.log('Visible Changed:', visible);
35+
}}
36+
>
37+
{({ style, className }, ref) => (
38+
<>
39+
<div
40+
ref={ref}
41+
className={classNames('demo-block', className)}
42+
style={style}
43+
/>
44+
<ul>
45+
<li>ClassName: {JSON.stringify(className)}</li>
46+
<li>Style: {JSON.stringify(style)}</li>
47+
</ul>
48+
</>
49+
)}
50+
</CSSMotion>
51+
</Provider>
52+
);
53+
};

src/CSSMotion.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
/* eslint-disable react/default-props-match-prop-types, react/no-multi-comp, react/prop-types */
2-
import * as React from 'react';
3-
import { useRef } from 'react';
2+
import classNames from 'classnames';
43
import findDOMNode from 'rc-util/lib/Dom/findDOMNode';
54
import { fillRef, supportRef } from 'rc-util/lib/ref';
6-
import classNames from 'classnames';
7-
import { getTransitionName, supportTransition } from './util/motion';
5+
import * as React from 'react';
6+
import { useRef } from 'react';
7+
import { Context } from './context';
8+
import DomWrapper from './DomWrapper';
9+
import useStatus from './hooks/useStatus';
10+
import { isActive } from './hooks/useStepQueue';
811
import type {
9-
MotionStatus,
10-
MotionEventHandler,
1112
MotionEndEventHandler,
13+
MotionEventHandler,
1214
MotionPrepareEventHandler,
15+
MotionStatus,
1316
} from './interface';
1417
import { STATUS_NONE, STEP_PREPARE, STEP_START } from './interface';
15-
import useStatus from './hooks/useStatus';
16-
import DomWrapper from './DomWrapper';
17-
import { isActive } from './hooks/useStepQueue';
18-
import { Context } from './context';
18+
import { getTransitionName, supportTransition } from './util/motion';
1919

2020
export type CSSMotionConfig =
2121
| boolean
@@ -58,8 +58,11 @@ export interface CSSMotionProps {
5858
eventProps?: object;
5959

6060
// Prepare groups
61+
/** Prepare phase is used for measure element info. It will always trigger even motion is off */
6162
onAppearPrepare?: MotionPrepareEventHandler;
63+
/** Prepare phase is used for measure element info. It will always trigger even motion is off */
6264
onEnterPrepare?: MotionPrepareEventHandler;
65+
/** Prepare phase is used for measure element info. It will always trigger even motion is off */
6366
onLeavePrepare?: MotionPrepareEventHandler;
6467

6568
// Normal motion groups
@@ -184,10 +187,7 @@ export function genCSSMotion(
184187
if (!children) {
185188
// No children
186189
motionChildren = null;
187-
} else if (
188-
status === STATUS_NONE ||
189-
!isSupportTransition(props, contextMotion)
190-
) {
190+
} else if (status === STATUS_NONE) {
191191
// Stable children
192192
if (mergedVisible) {
193193
motionChildren = children({ ...mergedProps }, setNodeRef);

src/hooks/useStatus.ts

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
1-
import * as React from 'react';
2-
import { useRef, useEffect } from 'react';
31
import useState from 'rc-util/lib/hooks/useState';
2+
import * as React from 'react';
3+
import { useEffect, useRef } from 'react';
4+
import type { CSSMotionProps } from '../CSSMotion';
5+
import type {
6+
MotionEvent,
7+
MotionEventHandler,
8+
MotionPrepareEventHandler,
9+
MotionStatus,
10+
StepStatus,
11+
} from '../interface';
412
import {
513
STATUS_APPEAR,
6-
STATUS_NONE,
7-
STATUS_LEAVE,
814
STATUS_ENTER,
15+
STATUS_LEAVE,
16+
STATUS_NONE,
17+
STEP_ACTIVE,
918
STEP_PREPARE,
19+
STEP_PREPARED,
1020
STEP_START,
11-
STEP_ACTIVE,
12-
} from '../interface';
13-
import type {
14-
MotionStatus,
15-
MotionEventHandler,
16-
MotionEvent,
17-
MotionPrepareEventHandler,
18-
StepStatus,
1921
} from '../interface';
20-
import type { CSSMotionProps } from '../CSSMotion';
21-
import useStepQueue, { DoStep, SkipStep, isActive } from './useStepQueue';
2222
import useDomMotionEvents from './useDomMotionEvents';
2323
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
24+
import useStepQueue, { DoStep, isActive, SkipStep } from './useStepQueue';
2425

2526
export default function useStatus(
2627
supportMotion: boolean,
@@ -63,6 +64,14 @@ export default function useStatus(
6364
// ========================== Motion End ==========================
6465
const activeRef = useRef(false);
6566

67+
/**
68+
* Clean up status & style
69+
*/
70+
function updateMotionEndStatus() {
71+
setStatus(STATUS_NONE, true);
72+
setStyle(null, true);
73+
}
74+
6675
function onInternalMotionEnd(event: MotionEvent) {
6776
const element = getDomElement();
6877
if (event && !event.deadline && event.target !== element) {
@@ -85,8 +94,7 @@ export default function useStatus(
8594

8695
// Only update status when `canEnd` and not destroyed
8796
if (status !== STATUS_NONE && currentActive && canEnd !== false) {
88-
setStatus(STATUS_NONE, true);
89-
setStyle(null, true);
97+
updateMotionEndStatus();
9098
}
9199
}
92100

@@ -125,7 +133,7 @@ export default function useStatus(
125133
}
126134
}, [status]);
127135

128-
const [startStep, step] = useStepQueue(status, newStep => {
136+
const [startStep, step] = useStepQueue(status, !supportMotion, newStep => {
129137
// Only prepare step can be skip
130138
if (newStep === STEP_PREPARE) {
131139
const onPrepare = eventHandlers[STEP_PREPARE];
@@ -155,6 +163,10 @@ export default function useStatus(
155163
}
156164
}
157165

166+
if (step === STEP_PREPARED) {
167+
updateMotionEndStatus();
168+
}
169+
158170
return DoStep;
159171
});
160172

@@ -169,9 +181,9 @@ export default function useStatus(
169181
const isMounted = mountedRef.current;
170182
mountedRef.current = true;
171183

172-
if (!supportMotion) {
173-
return;
174-
}
184+
// if (!supportMotion) {
185+
// return;
186+
// }
175187

176188
let nextStatus: MotionStatus;
177189

src/hooks/useStepQueue.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
1-
import * as React from 'react';
21
import useState from 'rc-util/lib/hooks/useState';
3-
import type { StepStatus, MotionStatus } from '../interface';
2+
import * as React from 'react';
3+
import type { MotionStatus, StepStatus } from '../interface';
44
import {
5-
STEP_PREPARE,
6-
STEP_ACTIVE,
7-
STEP_START,
85
STEP_ACTIVATED,
6+
STEP_ACTIVE,
97
STEP_NONE,
8+
STEP_PREPARE,
9+
STEP_PREPARED,
10+
STEP_START,
1011
} from '../interface';
11-
import useNextFrame from './useNextFrame';
1212
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
13+
import useNextFrame from './useNextFrame';
1314

14-
const STEP_QUEUE: StepStatus[] = [
15+
const FULL_STEP_QUEUE: StepStatus[] = [
1516
STEP_PREPARE,
1617
STEP_START,
1718
STEP_ACTIVE,
1819
STEP_ACTIVATED,
1920
];
2021

22+
const SIMPLE_STEP_QUEUE: StepStatus[] = [STEP_PREPARE, STEP_PREPARED];
23+
2124
/** Skip current step */
2225
export const SkipStep = false as const;
2326
/** Current step should be update in */
@@ -29,6 +32,7 @@ export function isActive(step: StepStatus) {
2932

3033
export default (
3134
status: MotionStatus,
35+
prepareOnly: boolean,
3236
callback: (
3337
step: StepStatus,
3438
) => Promise<void> | void | typeof SkipStep | typeof DoStep,
@@ -41,6 +45,8 @@ export default (
4145
setStep(STEP_PREPARE, true);
4246
}
4347

48+
const STEP_QUEUE = prepareOnly ? SIMPLE_STEP_QUEUE : FULL_STEP_QUEUE;
49+
4450
useIsomorphicLayoutEffect(() => {
4551
if (step !== STEP_NONE && step !== STEP_ACTIVATED) {
4652
const index = STEP_QUEUE.indexOf(step);
@@ -51,7 +57,7 @@ export default (
5157
if (result === SkipStep) {
5258
// Skip when no needed
5359
setStep(nextStep, true);
54-
} else {
60+
} else if (nextStep) {
5561
// Do as frame for step update
5662
nextFrame(info => {
5763
function doNext() {

src/interface.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,20 @@ export const STEP_PREPARE = 'prepare' as const;
1414
export const STEP_START = 'start' as const;
1515
export const STEP_ACTIVE = 'active' as const;
1616
export const STEP_ACTIVATED = 'end' as const;
17+
/**
18+
* Used for disabled motion case.
19+
* Prepare stage will still work but start & active will be skipped.
20+
*/
21+
export const STEP_PREPARED = 'prepared' as const;
1722

1823
export type StepStatus =
1924
| typeof STEP_NONE
2025
| typeof STEP_PREPARE
2126
| typeof STEP_START
2227
| typeof STEP_ACTIVE
23-
| typeof STEP_ACTIVATED;
28+
| typeof STEP_ACTIVATED
29+
// Skip motion only
30+
| typeof STEP_PREPARED;
2431

2532
export type MotionEvent = (TransitionEvent | AnimationEvent) & {
2633
deadline?: boolean;

tests/CSSMotion.spec.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
react/no-render-return-value, max-classes-per-file,
33
react/prefer-stateless-function, react/no-multi-comp
44
*/
5+
import { fireEvent, render } from '@testing-library/react';
6+
import classNames from 'classnames';
57
import React from 'react';
8+
import ReactDOM from 'react-dom';
69
import { act } from 'react-dom/test-utils';
7-
import classNames from 'classnames';
8-
import { render, fireEvent } from '@testing-library/react';
910
import type { CSSMotionProps } from '../src';
1011
import { Provider } from '../src';
1112
import RefCSSMotion, { genCSSMotion } from '../src/CSSMotion';
12-
import ReactDOM from 'react-dom';
1313

1414
describe('CSSMotion', () => {
1515
const CSSMotion = genCSSMotion({
@@ -482,6 +482,9 @@ describe('CSSMotion', () => {
482482
});
483483

484484
it('MotionProvider to disable motion', () => {
485+
const onAppearPrepare = jest.fn();
486+
const onAppearStart = jest.fn();
487+
485488
const Demo = ({
486489
motion,
487490
visible,
@@ -495,6 +498,9 @@ describe('CSSMotion', () => {
495498
visible={visible}
496499
removeOnLeave={false}
497500
leavedClassName="hidden"
501+
motionAppear
502+
onAppearPrepare={onAppearPrepare}
503+
onAppearStart={onAppearStart}
498504
>
499505
{({ style, className }) => (
500506
<div
@@ -509,6 +515,13 @@ describe('CSSMotion', () => {
509515
const { container, rerender } = render(<Demo motion={false} visible />);
510516
expect(container.querySelector('.motion-box')).toBeTruthy();
511517

518+
act(() => {
519+
jest.runAllTimers();
520+
});
521+
522+
expect(onAppearPrepare).toHaveBeenCalled();
523+
expect(onAppearStart).not.toHaveBeenCalled();
524+
512525
// hide immediately since motion is disabled
513526
rerender(<Demo motion={false} visible={false} />);
514527
expect(container.querySelector('.hidden')).toBeTruthy();

0 commit comments

Comments
 (0)