Skip to content

Commit

Permalink
refactor: Prepare should always run (#42)
Browse files Browse the repository at this point in the history
* chore: prepare note

* chore: rm motion block code

* feat: prepare always

* test: add test case
  • Loading branch information
zombieJ authored Apr 18, 2023
1 parent db9adc8 commit 691ecfc
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 46 deletions.
8 changes: 8 additions & 0 deletions docs/demo/provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: provider
nav:
title: Demo
path: /demo
---

<code src="../examples/provider.tsx"></code>
53 changes: 53 additions & 0 deletions docs/examples/provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import classNames from 'classnames';
import CSSMotion, { Provider } from 'rc-motion';
import React from 'react';
import './basic.less';

export default () => {
const [show, setShow] = React.useState(true);
const [motion, setMotion] = React.useState(false);

const onPrepare = (node: HTMLElement) => {
console.log('🔥 prepare', node);

return new Promise(resolve => {
setTimeout(resolve, 500);
});
};

return (
<Provider motion={motion}>
<button onClick={() => setShow(v => !v)}>show: {String(show)}</button>
<button onClick={() => setMotion(v => !v)}>
motion: {String(motion)}
</button>

<CSSMotion
visible={show}
motionName={'transition'}
leavedClassName="hidden"
motionAppear
onAppearPrepare={onPrepare}
onEnterPrepare={onPrepare}
onLeavePrepare={onPrepare}
onVisibleChanged={visible => {
console.log('Visible Changed:', visible);
}}
>
{({ style, className }, ref) => (
<>
<div
ref={ref}
className={classNames('demo-block', className)}
style={style}
/>
<ul>
<li>ClassName: {JSON.stringify(className)}</li>
<li>Style: {JSON.stringify(style)}</li>
</ul>
</>
)}
</CSSMotion>
</Provider>
);
};
28 changes: 14 additions & 14 deletions src/CSSMotion.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
/* eslint-disable react/default-props-match-prop-types, react/no-multi-comp, react/prop-types */
import * as React from 'react';
import { useRef } from 'react';
import classNames from 'classnames';
import findDOMNode from 'rc-util/lib/Dom/findDOMNode';
import { fillRef, supportRef } from 'rc-util/lib/ref';
import classNames from 'classnames';
import { getTransitionName, supportTransition } from './util/motion';
import * as React from 'react';
import { useRef } from 'react';
import { Context } from './context';
import DomWrapper from './DomWrapper';
import useStatus from './hooks/useStatus';
import { isActive } from './hooks/useStepQueue';
import type {
MotionStatus,
MotionEventHandler,
MotionEndEventHandler,
MotionEventHandler,
MotionPrepareEventHandler,
MotionStatus,
} from './interface';
import { STATUS_NONE, STEP_PREPARE, STEP_START } from './interface';
import useStatus from './hooks/useStatus';
import DomWrapper from './DomWrapper';
import { isActive } from './hooks/useStepQueue';
import { Context } from './context';
import { getTransitionName, supportTransition } from './util/motion';

export type CSSMotionConfig =
| boolean
Expand Down Expand Up @@ -58,8 +58,11 @@ export interface CSSMotionProps {
eventProps?: object;

// Prepare groups
/** Prepare phase is used for measure element info. It will always trigger even motion is off */
onAppearPrepare?: MotionPrepareEventHandler;
/** Prepare phase is used for measure element info. It will always trigger even motion is off */
onEnterPrepare?: MotionPrepareEventHandler;
/** Prepare phase is used for measure element info. It will always trigger even motion is off */
onLeavePrepare?: MotionPrepareEventHandler;

// Normal motion groups
Expand Down Expand Up @@ -184,10 +187,7 @@ export function genCSSMotion(
if (!children) {
// No children
motionChildren = null;
} else if (
status === STATUS_NONE ||
!isSupportTransition(props, contextMotion)
) {
} else if (status === STATUS_NONE) {
// Stable children
if (mergedVisible) {
motionChildren = children({ ...mergedProps }, setNodeRef);
Expand Down
52 changes: 32 additions & 20 deletions src/hooks/useStatus.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import * as React from 'react';
import { useRef, useEffect } from 'react';
import useState from 'rc-util/lib/hooks/useState';
import * as React from 'react';
import { useEffect, useRef } from 'react';
import type { CSSMotionProps } from '../CSSMotion';
import type {
MotionEvent,
MotionEventHandler,
MotionPrepareEventHandler,
MotionStatus,
StepStatus,
} from '../interface';
import {
STATUS_APPEAR,
STATUS_NONE,
STATUS_LEAVE,
STATUS_ENTER,
STATUS_LEAVE,
STATUS_NONE,
STEP_ACTIVE,
STEP_PREPARE,
STEP_PREPARED,
STEP_START,
STEP_ACTIVE,
} from '../interface';
import type {
MotionStatus,
MotionEventHandler,
MotionEvent,
MotionPrepareEventHandler,
StepStatus,
} from '../interface';
import type { CSSMotionProps } from '../CSSMotion';
import useStepQueue, { DoStep, SkipStep, isActive } from './useStepQueue';
import useDomMotionEvents from './useDomMotionEvents';
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
import useStepQueue, { DoStep, isActive, SkipStep } from './useStepQueue';

export default function useStatus(
supportMotion: boolean,
Expand Down Expand Up @@ -63,6 +64,14 @@ export default function useStatus(
// ========================== Motion End ==========================
const activeRef = useRef(false);

/**
* Clean up status & style
*/
function updateMotionEndStatus() {
setStatus(STATUS_NONE, true);
setStyle(null, true);
}

function onInternalMotionEnd(event: MotionEvent) {
const element = getDomElement();
if (event && !event.deadline && event.target !== element) {
Expand All @@ -85,8 +94,7 @@ export default function useStatus(

// Only update status when `canEnd` and not destroyed
if (status !== STATUS_NONE && currentActive && canEnd !== false) {
setStatus(STATUS_NONE, true);
setStyle(null, true);
updateMotionEndStatus();
}
}

Expand Down Expand Up @@ -125,7 +133,7 @@ export default function useStatus(
}
}, [status]);

const [startStep, step] = useStepQueue(status, newStep => {
const [startStep, step] = useStepQueue(status, !supportMotion, newStep => {
// Only prepare step can be skip
if (newStep === STEP_PREPARE) {
const onPrepare = eventHandlers[STEP_PREPARE];
Expand Down Expand Up @@ -155,6 +163,10 @@ export default function useStatus(
}
}

if (step === STEP_PREPARED) {
updateMotionEndStatus();
}

return DoStep;
});

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

if (!supportMotion) {
return;
}
// if (!supportMotion) {
// return;
// }

let nextStatus: MotionStatus;

Expand Down
22 changes: 14 additions & 8 deletions src/hooks/useStepQueue.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import * as React from 'react';
import useState from 'rc-util/lib/hooks/useState';
import type { StepStatus, MotionStatus } from '../interface';
import * as React from 'react';
import type { MotionStatus, StepStatus } from '../interface';
import {
STEP_PREPARE,
STEP_ACTIVE,
STEP_START,
STEP_ACTIVATED,
STEP_ACTIVE,
STEP_NONE,
STEP_PREPARE,
STEP_PREPARED,
STEP_START,
} from '../interface';
import useNextFrame from './useNextFrame';
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';
import useNextFrame from './useNextFrame';

const STEP_QUEUE: StepStatus[] = [
const FULL_STEP_QUEUE: StepStatus[] = [
STEP_PREPARE,
STEP_START,
STEP_ACTIVE,
STEP_ACTIVATED,
];

const SIMPLE_STEP_QUEUE: StepStatus[] = [STEP_PREPARE, STEP_PREPARED];

/** Skip current step */
export const SkipStep = false as const;
/** Current step should be update in */
Expand All @@ -29,6 +32,7 @@ export function isActive(step: StepStatus) {

export default (
status: MotionStatus,
prepareOnly: boolean,
callback: (
step: StepStatus,
) => Promise<void> | void | typeof SkipStep | typeof DoStep,
Expand All @@ -41,6 +45,8 @@ export default (
setStep(STEP_PREPARE, true);
}

const STEP_QUEUE = prepareOnly ? SIMPLE_STEP_QUEUE : FULL_STEP_QUEUE;

useIsomorphicLayoutEffect(() => {
if (step !== STEP_NONE && step !== STEP_ACTIVATED) {
const index = STEP_QUEUE.indexOf(step);
Expand All @@ -51,7 +57,7 @@ export default (
if (result === SkipStep) {
// Skip when no needed
setStep(nextStep, true);
} else {
} else if (nextStep) {
// Do as frame for step update
nextFrame(info => {
function doNext() {
Expand Down
9 changes: 8 additions & 1 deletion src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@ export const STEP_PREPARE = 'prepare' as const;
export const STEP_START = 'start' as const;
export const STEP_ACTIVE = 'active' as const;
export const STEP_ACTIVATED = 'end' as const;
/**
* Used for disabled motion case.
* Prepare stage will still work but start & active will be skipped.
*/
export const STEP_PREPARED = 'prepared' as const;

export type StepStatus =
| typeof STEP_NONE
| typeof STEP_PREPARE
| typeof STEP_START
| typeof STEP_ACTIVE
| typeof STEP_ACTIVATED;
| typeof STEP_ACTIVATED
// Skip motion only
| typeof STEP_PREPARED;

export type MotionEvent = (TransitionEvent | AnimationEvent) & {
deadline?: boolean;
Expand Down
19 changes: 16 additions & 3 deletions tests/CSSMotion.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
react/no-render-return-value, max-classes-per-file,
react/prefer-stateless-function, react/no-multi-comp
*/
import { fireEvent, render } from '@testing-library/react';
import classNames from 'classnames';
import React from 'react';
import ReactDOM from 'react-dom';
import { act } from 'react-dom/test-utils';
import classNames from 'classnames';
import { render, fireEvent } from '@testing-library/react';
import type { CSSMotionProps } from '../src';
import { Provider } from '../src';
import RefCSSMotion, { genCSSMotion } from '../src/CSSMotion';
import ReactDOM from 'react-dom';

describe('CSSMotion', () => {
const CSSMotion = genCSSMotion({
Expand Down Expand Up @@ -482,6 +482,9 @@ describe('CSSMotion', () => {
});

it('MotionProvider to disable motion', () => {
const onAppearPrepare = jest.fn();
const onAppearStart = jest.fn();

const Demo = ({
motion,
visible,
Expand All @@ -495,6 +498,9 @@ describe('CSSMotion', () => {
visible={visible}
removeOnLeave={false}
leavedClassName="hidden"
motionAppear
onAppearPrepare={onAppearPrepare}
onAppearStart={onAppearStart}
>
{({ style, className }) => (
<div
Expand All @@ -509,6 +515,13 @@ describe('CSSMotion', () => {
const { container, rerender } = render(<Demo motion={false} visible />);
expect(container.querySelector('.motion-box')).toBeTruthy();

act(() => {
jest.runAllTimers();
});

expect(onAppearPrepare).toHaveBeenCalled();
expect(onAppearStart).not.toHaveBeenCalled();

// hide immediately since motion is disabled
rerender(<Demo motion={false} visible={false} />);
expect(container.querySelector('.hidden')).toBeTruthy();
Expand Down

1 comment on commit 691ecfc

@vercel
Copy link

@vercel vercel bot commented on 691ecfc Apr 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.