diff --git a/docs/demo/debug.md b/docs/demo/debug.md
new file mode 100644
index 0000000..258ca77
--- /dev/null
+++ b/docs/demo/debug.md
@@ -0,0 +1,3 @@
+# Debug Demo
+
+
diff --git a/docs/examples/debug.less b/docs/examples/debug.less
new file mode 100644
index 0000000..7c00927
--- /dev/null
+++ b/docs/examples/debug.less
@@ -0,0 +1,18 @@
+.debug-demo-block {
+ overflow: hidden;
+ box-shadow: 0 0 0 3px red;
+}
+
+.debug-motion {
+ &-appear,
+ &-enter {
+ &-start {
+ opacity: 0;
+ }
+
+ &-active {
+ opacity: 1;
+ transition: background 0.3s, height 1.3s, opacity 1.3s;
+ }
+ }
+}
diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx
new file mode 100644
index 0000000..9939815
--- /dev/null
+++ b/docs/examples/debug.tsx
@@ -0,0 +1,65 @@
+import { clsx } from 'clsx';
+import CSSMotion, { type CSSMotionProps } from 'rc-motion';
+import React, { useState } from 'react';
+import './debug.less';
+
+const onCollapse = () => {
+ console.log('🔥 Collapse');
+ return { height: 0 };
+};
+
+const onExpand: CSSMotionProps['onAppearActive'] = node => {
+ console.log('🔥 Expand');
+ return { height: node.scrollHeight };
+};
+
+function DebugDemo() {
+ const [key, setKey] = useState(0);
+
+ return (
+
+
+
+
+ {({ style, className }, ref) => {
+ console.log('render', className, style);
+
+ return (
+
+ );
+ }}
+
+
+ );
+}
+
+export default () => (
+
+
+
+);
diff --git a/src/CSSMotion.tsx b/src/CSSMotion.tsx
index fc16278..d7550c2 100644
--- a/src/CSSMotion.tsx
+++ b/src/CSSMotion.tsx
@@ -146,12 +146,8 @@ export function genCSSMotion(config: CSSMotionConfig) {
return getDOM(nodeRef.current) as HTMLElement;
}
- const [getStatus, statusStep, statusStyle, mergedVisible] = useStatus(
- supportMotion,
- visible,
- getDomElement,
- props,
- );
+ const [getStatus, statusStep, statusStyle, mergedVisible, styleReady] =
+ useStatus(supportMotion, visible, getDomElement, props);
const status = getStatus();
// Record whether content has rendered
@@ -186,73 +182,85 @@ export function genCSSMotion(config: CSSMotionConfig) {
React.useImperativeHandle(ref, () => refObj, []);
// ===================== Render =====================
- let motionChildren: React.ReactNode;
- const mergedProps = { ...eventProps, visible };
-
- if (!children) {
- // No children
- motionChildren = null;
- } else if (status === STATUS_NONE) {
- // Stable children
- if (mergedVisible) {
- motionChildren = children({ ...mergedProps }, nodeRef);
- } else if (!removeOnLeave && renderedRef.current && leavedClassName) {
- motionChildren = children(
- { ...mergedProps, className: leavedClassName },
- nodeRef,
- );
- } else if (forceRender || (!removeOnLeave && !leavedClassName)) {
- motionChildren = children(
- { ...mergedProps, style: { display: 'none' } },
- nodeRef,
- );
- } else {
- motionChildren = null;
- }
- } else {
- // In motion
- let statusSuffix: string;
- if (statusStep === STEP_PREPARE) {
- statusSuffix = 'prepare';
- } else if (isActive(statusStep)) {
- statusSuffix = 'active';
- } else if (statusStep === STEP_START) {
- statusSuffix = 'start';
- }
-
- const motionCls = getTransitionName(
- motionName,
- `${status}-${statusSuffix}`,
- );
-
- motionChildren = children(
- {
- ...mergedProps,
- className: clsx(getTransitionName(motionName, status), {
- [motionCls]: motionCls && statusSuffix,
- [motionName as string]: typeof motionName === 'string',
- }),
- style: statusStyle,
- },
- nodeRef,
- );
+ // return motionChildren as React.ReactElement;
+ const idRef = React.useRef(0);
+ if (styleReady) {
+ idRef.current += 1;
}
- // Auto inject ref if child node not have `ref` props
- if (React.isValidElement(motionChildren) && supportRef(motionChildren)) {
- const originNodeRef = getNodeRef(motionChildren);
+ // We should render children when motionStyle is sync with stepStatus
+ return React.useMemo(() => {
+ let motionChildren: React.ReactNode;
+ const mergedProps = { ...eventProps, visible };
+
+ if (!children) {
+ // No children
+ motionChildren = null;
+ } else if (status === STATUS_NONE) {
+ // Stable children
+ if (mergedVisible) {
+ motionChildren = children({ ...mergedProps }, nodeRef);
+ } else if (!removeOnLeave && renderedRef.current && leavedClassName) {
+ motionChildren = children(
+ { ...mergedProps, className: leavedClassName },
+ nodeRef,
+ );
+ } else if (forceRender || (!removeOnLeave && !leavedClassName)) {
+ motionChildren = children(
+ { ...mergedProps, style: { display: 'none' } },
+ nodeRef,
+ );
+ } else {
+ motionChildren = null;
+ }
+ } else {
+ // In motion
+ let statusSuffix: string;
+ if (statusStep === STEP_PREPARE) {
+ statusSuffix = 'prepare';
+ } else if (isActive(statusStep)) {
+ statusSuffix = 'active';
+ } else if (statusStep === STEP_START) {
+ statusSuffix = 'start';
+ }
+
+ const motionCls = getTransitionName(
+ motionName,
+ `${status}-${statusSuffix}`,
+ );
- if (!originNodeRef) {
- motionChildren = React.cloneElement(
- motionChildren as React.ReactElement,
+ motionChildren = children(
{
- ref: nodeRef,
+ ...mergedProps,
+ className: clsx(getTransitionName(motionName, status), {
+ [motionCls]: motionCls && statusSuffix,
+ [motionName as string]: typeof motionName === 'string',
+ }),
+ style: statusStyle,
},
+ nodeRef,
);
}
- }
- return motionChildren as React.ReactElement;
+ // Auto inject ref if child node not have `ref` props
+ if (
+ React.isValidElement(motionChildren) &&
+ supportRef(motionChildren)
+ ) {
+ const originNodeRef = getNodeRef(motionChildren);
+
+ if (!originNodeRef) {
+ motionChildren = React.cloneElement(
+ motionChildren as React.ReactElement,
+ {
+ ref: nodeRef,
+ },
+ );
+ }
+ }
+
+ return motionChildren;
+ }, [idRef.current]) as React.ReactElement;
},
);
diff --git a/src/hooks/useStatus.ts b/src/hooks/useStatus.ts
index 6d24f16..b2fde69 100644
--- a/src/hooks/useStatus.ts
+++ b/src/hooks/useStatus.ts
@@ -1,5 +1,4 @@
import { useEvent } from '@rc-component/util';
-import useState from '@rc-component/util/lib/hooks/useState';
import useSyncState from '@rc-component/util/lib/hooks/useSyncState';
import * as React from 'react';
import { useEffect, useRef } from 'react';
@@ -49,11 +48,19 @@ export default function useStatus(
onLeaveEnd,
onVisibleChanged,
}: CSSMotionProps,
-): [() => MotionStatus, StepStatus, React.CSSProperties, boolean] {
+): [
+ status: () => MotionStatus,
+ stepStatus: StepStatus,
+ style: React.CSSProperties,
+ visible: boolean,
+ styleReady: boolean,
+] {
// Used for outer render usage to avoid `visible: false & status: none` to render nothing
- const [asyncVisible, setAsyncVisible] = useState();
+ const [asyncVisible, setAsyncVisible] = React.useState();
const [getStatus, setStatus] = useSyncState(STATUS_NONE);
- const [style, setStyle] = useState(null);
+ const [style, setStyle] = React.useState<
+ [style: React.CSSProperties | undefined, step: StepStatus]
+ >([null, null]);
const currentStatus = getStatus();
@@ -73,7 +80,7 @@ export default function useStatus(
*/
function updateMotionEndStatus() {
setStatus(STATUS_NONE);
- setStyle(null, true);
+ setStyle([null, null]);
}
const onInternalMotionEnd = useEvent((event: MotionEvent) => {
@@ -161,11 +168,14 @@ export default function useStatus(
}
// Rest step is sync update
- if (step in eventHandlers) {
- setStyle(eventHandlers[step]?.(getDomElement(), null) || null);
+ if (newStep in eventHandlers) {
+ setStyle([
+ eventHandlers[newStep]?.(getDomElement(), null) || null,
+ newStep,
+ ]);
}
- if (step === STEP_ACTIVE && currentStatus !== STATUS_NONE) {
+ if (newStep === STEP_ACTIVE && currentStatus !== STATUS_NONE) {
// Patch events when motion needed
patchMotionEvents(getDomElement());
@@ -179,7 +189,7 @@ export default function useStatus(
}
}
- if (step === STEP_PREPARED) {
+ if (newStep === STEP_PREPARED) {
updateMotionEndStatus();
}
@@ -286,7 +296,7 @@ export default function useStatus(
}, [asyncVisible, currentStatus]);
// ============================ Styles ============================
- let mergedStyle = style;
+ let mergedStyle = style[0];
if (eventHandlers[STEP_PREPARE] && step === STEP_START) {
mergedStyle = {
transition: 'none',
@@ -294,5 +304,13 @@ export default function useStatus(
};
}
- return [getStatus, step, mergedStyle, asyncVisible ?? visible];
+ const styleStep = style[1];
+
+ return [
+ getStatus,
+ step,
+ mergedStyle,
+ asyncVisible ?? visible,
+ step === STEP_START || step === STEP_ACTIVE ? styleStep === step : true,
+ ];
}
diff --git a/tests/CSSMotion.spec.tsx b/tests/CSSMotion.spec.tsx
index d455ad9..54dc31d 100644
--- a/tests/CSSMotion.spec.tsx
+++ b/tests/CSSMotion.spec.tsx
@@ -194,6 +194,9 @@ describe('CSSMotion', () => {
});
fireEvent.transitionEnd(container.querySelector('.motion-box'));
+ act(() => {
+ jest.runAllTimers();
+ });
expect(container.querySelector('.motion-box')).toHaveClass('removed');
@@ -805,6 +808,9 @@ describe('CSSMotion', () => {
});
fireEvent.transitionEnd(container.querySelector('.motion-box'));
+ act(() => {
+ jest.runAllTimers();
+ });
expect(container.querySelector('.motion-box')).toBeTruthy();
expect(container.querySelector('.motion-box')).toHaveClass('removed');
diff --git a/tests/StrictMode.spec.tsx b/tests/StrictMode.spec.tsx
index 9b82653..7dc872f 100644
--- a/tests/StrictMode.spec.tsx
+++ b/tests/StrictMode.spec.tsx
@@ -2,10 +2,9 @@
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 { act, fireEvent, render } from '@testing-library/react';
import { clsx } from 'clsx';
import React from 'react';
-import { act } from 'react-dom/test-utils';
import { genCSSMotion, type CSSMotionRef } from '../src/CSSMotion';
describe('StrictMode', () => {
@@ -52,6 +51,9 @@ describe('StrictMode', () => {
// Trigger End
fireEvent.transitionEnd(node);
+ act(() => {
+ jest.runAllTimers();
+ });
expect(node).not.toHaveClass('transition-appear');
expect(ref.current.inMotion()).toBeFalsy();