diff --git a/docs/demo/provider.md b/docs/demo/provider.md new file mode 100644 index 0000000..669bbce --- /dev/null +++ b/docs/demo/provider.md @@ -0,0 +1,8 @@ +--- +title: provider +nav: + title: Demo + path: /demo +--- + + diff --git a/docs/examples/provider.tsx b/docs/examples/provider.tsx new file mode 100644 index 0000000..fbe3c30 --- /dev/null +++ b/docs/examples/provider.tsx @@ -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 ( + + + + + { + console.log('Visible Changed:', visible); + }} + > + {({ style, className }, ref) => ( + <> +
+
    +
  • ClassName: {JSON.stringify(className)}
  • +
  • Style: {JSON.stringify(style)}
  • +
+ + )} + + + ); +}; diff --git a/src/CSSMotion.tsx b/src/CSSMotion.tsx index cc2f329..51acad1 100644 --- a/src/CSSMotion.tsx +++ b/src/CSSMotion.tsx @@ -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 @@ -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 @@ -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); diff --git a/src/hooks/useStatus.ts b/src/hooks/useStatus.ts index 1a1f466..b3d60b1 100644 --- a/src/hooks/useStatus.ts +++ b/src/hooks/useStatus.ts @@ -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, @@ -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) { @@ -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(); } } @@ -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]; @@ -155,6 +163,10 @@ export default function useStatus( } } + if (step === STEP_PREPARED) { + updateMotionEndStatus(); + } + return DoStep; }); @@ -169,9 +181,9 @@ export default function useStatus( const isMounted = mountedRef.current; mountedRef.current = true; - if (!supportMotion) { - return; - } + // if (!supportMotion) { + // return; + // } let nextStatus: MotionStatus; diff --git a/src/hooks/useStepQueue.ts b/src/hooks/useStepQueue.ts index 6b690ff..48a4605 100644 --- a/src/hooks/useStepQueue.ts +++ b/src/hooks/useStepQueue.ts @@ -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 */ @@ -29,6 +32,7 @@ export function isActive(step: StepStatus) { export default ( status: MotionStatus, + prepareOnly: boolean, callback: ( step: StepStatus, ) => Promise | void | typeof SkipStep | typeof DoStep, @@ -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); @@ -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() { diff --git a/src/interface.ts b/src/interface.ts index 9c333d8..c513565 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -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; diff --git a/tests/CSSMotion.spec.tsx b/tests/CSSMotion.spec.tsx index d89266d..7886042 100644 --- a/tests/CSSMotion.spec.tsx +++ b/tests/CSSMotion.spec.tsx @@ -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({ @@ -482,6 +482,9 @@ describe('CSSMotion', () => { }); it('MotionProvider to disable motion', () => { + const onAppearPrepare = jest.fn(); + const onAppearStart = jest.fn(); + const Demo = ({ motion, visible, @@ -495,6 +498,9 @@ describe('CSSMotion', () => { visible={visible} removeOnLeave={false} leavedClassName="hidden" + motionAppear + onAppearPrepare={onAppearPrepare} + onAppearStart={onAppearStart} > {({ style, className }) => (
{ const { container, rerender } = render(); expect(container.querySelector('.motion-box')).toBeTruthy(); + act(() => { + jest.runAllTimers(); + }); + + expect(onAppearPrepare).toHaveBeenCalled(); + expect(onAppearStart).not.toHaveBeenCalled(); + // hide immediately since motion is disabled rerender(); expect(container.querySelector('.hidden')).toBeTruthy();