From b9a08225c57d12a578dd7182d85b1709ec6f835b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E8=89=B3=E5=85=B5?= Date: Wed, 25 Mar 2026 17:32:43 +0800 Subject: [PATCH 1/8] fix: compose existing child refs in CSSMotion --- src/CSSMotion.tsx | 16 +++++++------- tests/CSSMotion.spec.tsx | 45 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/CSSMotion.tsx b/src/CSSMotion.tsx index bb8798f..71ebe33 100644 --- a/src/CSSMotion.tsx +++ b/src/CSSMotion.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/default-props-match-prop-types, react/no-multi-comp, react/prop-types */ import { getDOM } from '@rc-component/util/lib/Dom/findDOMNode'; -import { getNodeRef, supportRef } from '@rc-component/util/lib/ref'; +import { composeRef, getNodeRef, supportRef } from '@rc-component/util/lib/ref'; import { clsx } from 'clsx'; import * as React from 'react'; import { useRef } from 'react'; @@ -253,14 +253,12 @@ export function genCSSMotion(config: CSSMotionConfig) { ) { const originNodeRef = getNodeRef(motionChildren); - if (!originNodeRef) { - motionChildren = React.cloneElement( - motionChildren as React.ReactElement, - { - ref: nodeRef, - }, - ); - } + motionChildren = React.cloneElement( + motionChildren as React.ReactElement, + { + ref: originNodeRef ? composeRef(originNodeRef, nodeRef) : nodeRef, + }, + ); } return motionChildren; diff --git a/tests/CSSMotion.spec.tsx b/tests/CSSMotion.spec.tsx index 8cb5b32..848bfe9 100644 --- a/tests/CSSMotion.spec.tsx +++ b/tests/CSSMotion.spec.tsx @@ -942,6 +942,51 @@ describe('CSSMotion', () => { expect(ReactDOM.findDOMNode).not.toHaveBeenCalled(); }); + + it('supports existing child refs for motion end', () => { + const motionRef = React.createRef(); + const childRef = React.createRef(); + + const Demo = ({ visible }: { visible: boolean }) => ( + + {({ style, className }) => ( +
+ )} + + ); + + const { container, rerender } = render(); + + act(() => { + jest.runAllTimers(); + }); + + expect(motionRef.current.nativeElement).toBe(childRef.current); + + rerender(); + + act(() => { + jest.runAllTimers(); + }); + + fireEvent.transitionEnd(childRef.current!); + + act(() => { + jest.runAllTimers(); + }); + + expect(container.querySelector('.motion-box')).toBeFalsy(); + expect(ReactDOM.findDOMNode).not.toHaveBeenCalled(); + }); }); describe('onVisibleChanged', () => { From ddf6bf6778d8609db341ce4e863766adf49db407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E8=89=B3=E5=85=B5?= Date: Wed, 25 Mar 2026 18:07:11 +0800 Subject: [PATCH 2/8] refactor: use useComposeRef in CSSMotion --- src/CSSMotion.tsx | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/CSSMotion.tsx b/src/CSSMotion.tsx index 71ebe33..b9ebdcb 100644 --- a/src/CSSMotion.tsx +++ b/src/CSSMotion.tsx @@ -1,6 +1,10 @@ /* eslint-disable react/default-props-match-prop-types, react/no-multi-comp, react/prop-types */ import { getDOM } from '@rc-component/util/lib/Dom/findDOMNode'; -import { composeRef, getNodeRef, supportRef } from '@rc-component/util/lib/ref'; +import { + getNodeRef, + supportRef, + useComposeRef, +} from '@rc-component/util/lib/ref'; import { clsx } from 'clsx'; import * as React from 'react'; import { useRef } from 'react'; @@ -189,7 +193,7 @@ export function genCSSMotion(config: CSSMotionConfig) { } // We should render children when motionStyle is sync with stepStatus - return React.useMemo(() => { + const motionChildren = React.useMemo(() => { if (styleReady === 'NONE') { return null; } @@ -246,23 +250,27 @@ export function genCSSMotion(config: CSSMotionConfig) { ); } - // Auto inject ref if child node not have `ref` props - if ( - React.isValidElement(motionChildren) && - supportRef(motionChildren) - ) { - const originNodeRef = getNodeRef(motionChildren); - - motionChildren = React.cloneElement( - motionChildren as React.ReactElement, - { - ref: originNodeRef ? composeRef(originNodeRef, nodeRef) : nodeRef, - }, - ); - } - return motionChildren; }, [idRef.current]) as React.ReactElement; + + const canHoldRef = + React.isValidElement(motionChildren) && supportRef(motionChildren); + const originNodeRef = canHoldRef ? getNodeRef(motionChildren) : null; + const shouldInjectRef = canHoldRef && originNodeRef !== nodeRef; + const mergedNodeRef = useComposeRef( + shouldInjectRef ? originNodeRef : null, + nodeRef, + ); + + // Preserve original behavior when child already uses motion's ref directly. + // Only compose refs when child owns another ref or misses the motion ref. + if (shouldInjectRef) { + return React.cloneElement(motionChildren, { + ref: mergedNodeRef, + }); + } + + return motionChildren; }, ); From c1d0fe6d8313732a558ee7907face77a0f199993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 25 Mar 2026 21:36:01 +0800 Subject: [PATCH 3/8] refactor: simplify CSSMotion ref injection --- src/CSSMotion.tsx | 43 ++++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/src/CSSMotion.tsx b/src/CSSMotion.tsx index b9ebdcb..cba710b 100644 --- a/src/CSSMotion.tsx +++ b/src/CSSMotion.tsx @@ -1,9 +1,9 @@ /* eslint-disable react/default-props-match-prop-types, react/no-multi-comp, react/prop-types */ import { getDOM } from '@rc-component/util/lib/Dom/findDOMNode'; import { + composeRef, getNodeRef, - supportRef, - useComposeRef, + supportNodeRef, } from '@rc-component/util/lib/ref'; import { clsx } from 'clsx'; import * as React from 'react'; @@ -198,28 +198,28 @@ export function genCSSMotion(config: CSSMotionConfig) { return null; } - let motionChildren: React.ReactNode; + let nextMotionChildren: React.ReactNode; const mergedProps = { ...eventProps, visible }; if (!children) { // No children - motionChildren = null; + nextMotionChildren = null; } else if (status === STATUS_NONE) { // Stable children if (mergedVisible) { - motionChildren = children({ ...mergedProps }, nodeRef); + nextMotionChildren = children({ ...mergedProps }, nodeRef); } else if (!removeOnLeave && renderedRef.current && leavedClassName) { - motionChildren = children( + nextMotionChildren = children( { ...mergedProps, className: leavedClassName }, nodeRef, ); } else if (forceRender || (!removeOnLeave && !leavedClassName)) { - motionChildren = children( + nextMotionChildren = children( { ...mergedProps, style: { display: 'none' } }, nodeRef, ); } else { - motionChildren = null; + nextMotionChildren = null; } } else { // In motion @@ -237,7 +237,7 @@ export function genCSSMotion(config: CSSMotionConfig) { `${status}-${statusSuffix}`, ); - motionChildren = children( + nextMotionChildren = children( { ...mergedProps, className: clsx(getTransitionName(motionName, status), { @@ -250,24 +250,17 @@ export function genCSSMotion(config: CSSMotionConfig) { ); } - return motionChildren; + return nextMotionChildren; }, [idRef.current]) as React.ReactElement; - const canHoldRef = - React.isValidElement(motionChildren) && supportRef(motionChildren); - const originNodeRef = canHoldRef ? getNodeRef(motionChildren) : null; - const shouldInjectRef = canHoldRef && originNodeRef !== nodeRef; - const mergedNodeRef = useComposeRef( - shouldInjectRef ? originNodeRef : null, - nodeRef, - ); - - // Preserve original behavior when child already uses motion's ref directly. - // Only compose refs when child owns another ref or misses the motion ref. - if (shouldInjectRef) { - return React.cloneElement(motionChildren, { - ref: mergedNodeRef, - }); + if (supportNodeRef(motionChildren)) { + const originNodeRef = getNodeRef(motionChildren); + + if (originNodeRef !== nodeRef) { + return React.cloneElement(motionChildren, { + ref: composeRef(originNodeRef, nodeRef), + }); + } } return motionChildren; From 9fb85c57e630f563b76b6a6e0e2f957331b7e308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 25 Mar 2026 21:56:23 +0800 Subject: [PATCH 4/8] refactor: gate auto ref injection by children arity --- src/CSSMotion.tsx | 4 +++- src/CSSMotionList.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/CSSMotion.tsx b/src/CSSMotion.tsx index cba710b..1537c4c 100644 --- a/src/CSSMotion.tsx +++ b/src/CSSMotion.tsx @@ -253,7 +253,9 @@ export function genCSSMotion(config: CSSMotionConfig) { return nextMotionChildren; }, [idRef.current]) as React.ReactElement; - if (supportNodeRef(motionChildren)) { + const shouldAutoInjectRef = children?.length < 2; + + if (shouldAutoInjectRef && supportNodeRef(motionChildren)) { const originNodeRef = getNodeRef(motionChildren); if (originNodeRef !== nodeRef) { diff --git a/src/CSSMotionList.tsx b/src/CSSMotionList.tsx index 6eedfd1..84a5b36 100644 --- a/src/CSSMotionList.tsx +++ b/src/CSSMotionList.tsx @@ -174,7 +174,9 @@ export function genCSSMotionList( } }} > - {(props, ref) => children({ ...props, index }, ref)} + {children.length < 2 + ? props => children({ ...props, index }, undefined as any) + : (props, ref) => children({ ...props, index }, ref)} ); })} From e5079deae5d11febf30028eeccaabdd71d6d315c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 25 Mar 2026 22:09:37 +0800 Subject: [PATCH 5/8] refactor: share ref consume helper --- src/CSSMotion.tsx | 32 +++++++++++++++++--------------- src/CSSMotionList.tsx | 14 +++++++++++--- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/CSSMotion.tsx b/src/CSSMotion.tsx index 1537c4c..5d3a5b7 100644 --- a/src/CSSMotion.tsx +++ b/src/CSSMotion.tsx @@ -110,6 +110,10 @@ export interface CSSMotionState { prevProps?: CSSMotionProps; } +export function isRefConsume(children?: CSSMotionProps['children']) { + return children?.length < 2; +} + /** * `transitionSupport` is used for none transition test case. * Default we use browser transition event support check. @@ -193,33 +197,33 @@ export function genCSSMotion(config: CSSMotionConfig) { } // We should render children when motionStyle is sync with stepStatus - const motionChildren = React.useMemo(() => { + const returnNode = React.useMemo(() => { if (styleReady === 'NONE') { return null; } - let nextMotionChildren: React.ReactNode; + let motionChildren: React.ReactNode; const mergedProps = { ...eventProps, visible }; if (!children) { // No children - nextMotionChildren = null; + motionChildren = null; } else if (status === STATUS_NONE) { // Stable children if (mergedVisible) { - nextMotionChildren = children({ ...mergedProps }, nodeRef); + motionChildren = children({ ...mergedProps }, nodeRef); } else if (!removeOnLeave && renderedRef.current && leavedClassName) { - nextMotionChildren = children( + motionChildren = children( { ...mergedProps, className: leavedClassName }, nodeRef, ); } else if (forceRender || (!removeOnLeave && !leavedClassName)) { - nextMotionChildren = children( + motionChildren = children( { ...mergedProps, style: { display: 'none' } }, nodeRef, ); } else { - nextMotionChildren = null; + motionChildren = null; } } else { // In motion @@ -237,7 +241,7 @@ export function genCSSMotion(config: CSSMotionConfig) { `${status}-${statusSuffix}`, ); - nextMotionChildren = children( + motionChildren = children( { ...mergedProps, className: clsx(getTransitionName(motionName, status), { @@ -250,22 +254,20 @@ export function genCSSMotion(config: CSSMotionConfig) { ); } - return nextMotionChildren; + return motionChildren; }, [idRef.current]) as React.ReactElement; - const shouldAutoInjectRef = children?.length < 2; - - if (shouldAutoInjectRef && supportNodeRef(motionChildren)) { - const originNodeRef = getNodeRef(motionChildren); + if (isRefConsume(children) && supportNodeRef(returnNode)) { + const originNodeRef = getNodeRef(returnNode); if (originNodeRef !== nodeRef) { - return React.cloneElement(motionChildren, { + return React.cloneElement(returnNode, { ref: composeRef(originNodeRef, nodeRef), }); } } - return motionChildren; + return returnNode; }, ); diff --git a/src/CSSMotionList.tsx b/src/CSSMotionList.tsx index 84a5b36..03f56c6 100644 --- a/src/CSSMotionList.tsx +++ b/src/CSSMotionList.tsx @@ -1,7 +1,7 @@ /* eslint react/prop-types: 0 */ import * as React from 'react'; import type { CSSMotionProps } from './CSSMotion'; -import OriginCSSMotion from './CSSMotion'; +import OriginCSSMotion, { isRefConsume } from './CSSMotion'; import type { KeyObject } from './util/diff'; import { diffKeys, @@ -59,6 +59,10 @@ export interface CSSMotionListProps ) => React.ReactElement; } +type ChildrenWithoutRef = ( + props: Parameters[0], +) => ReturnType; + export interface CSSMotionListState { keyEntities: KeyObject[]; } @@ -174,8 +178,12 @@ export function genCSSMotionList( } }} > - {children.length < 2 - ? props => children({ ...props, index }, undefined as any) + {isRefConsume(children) + ? props => + (children as ChildrenWithoutRef)({ + ...props, + index, + }) : (props, ref) => children({ ...props, index }, ref)} ); From f61d33367e77b6d80d4f7d8ab76b7636a1c83f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 25 Mar 2026 22:12:01 +0800 Subject: [PATCH 6/8] refactor: rename ref consumption helper --- src/CSSMotion.tsx | 4 ++-- src/CSSMotionList.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CSSMotion.tsx b/src/CSSMotion.tsx index 5d3a5b7..6f97708 100644 --- a/src/CSSMotion.tsx +++ b/src/CSSMotion.tsx @@ -110,7 +110,7 @@ export interface CSSMotionState { prevProps?: CSSMotionProps; } -export function isRefConsume(children?: CSSMotionProps['children']) { +export function isRefNotConsumed(children?: CSSMotionProps['children']) { return children?.length < 2; } @@ -257,7 +257,7 @@ export function genCSSMotion(config: CSSMotionConfig) { return motionChildren; }, [idRef.current]) as React.ReactElement; - if (isRefConsume(children) && supportNodeRef(returnNode)) { + if (isRefNotConsumed(children) && supportNodeRef(returnNode)) { const originNodeRef = getNodeRef(returnNode); if (originNodeRef !== nodeRef) { diff --git a/src/CSSMotionList.tsx b/src/CSSMotionList.tsx index 03f56c6..04ca651 100644 --- a/src/CSSMotionList.tsx +++ b/src/CSSMotionList.tsx @@ -1,7 +1,7 @@ /* eslint react/prop-types: 0 */ import * as React from 'react'; import type { CSSMotionProps } from './CSSMotion'; -import OriginCSSMotion, { isRefConsume } from './CSSMotion'; +import OriginCSSMotion, { isRefNotConsumed } from './CSSMotion'; import type { KeyObject } from './util/diff'; import { diffKeys, @@ -178,7 +178,7 @@ export function genCSSMotionList( } }} > - {isRefConsume(children) + {isRefNotConsumed(children) ? props => (children as ChildrenWithoutRef)({ ...props, From a52e2a18acba2c5df2daf3930a3b8b9e4007da62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 25 Mar 2026 22:16:46 +0800 Subject: [PATCH 7/8] fix: tighten CSSMotion return node typing --- src/CSSMotion.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CSSMotion.tsx b/src/CSSMotion.tsx index 6f97708..df5c058 100644 --- a/src/CSSMotion.tsx +++ b/src/CSSMotion.tsx @@ -197,12 +197,12 @@ export function genCSSMotion(config: CSSMotionConfig) { } // We should render children when motionStyle is sync with stepStatus - const returnNode = React.useMemo(() => { + const returnNode = React.useMemo(() => { if (styleReady === 'NONE') { return null; } - let motionChildren: React.ReactNode; + let motionChildren: React.ReactElement | null; const mergedProps = { ...eventProps, visible }; if (!children) { @@ -255,7 +255,7 @@ export function genCSSMotion(config: CSSMotionConfig) { } return motionChildren; - }, [idRef.current]) as React.ReactElement; + }, [idRef.current]); if (isRefNotConsumed(children) && supportNodeRef(returnNode)) { const originNodeRef = getNodeRef(returnNode); From 3e84e1bc49af944549907b87b016b5c9c303b751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Wed, 25 Mar 2026 22:23:39 +0800 Subject: [PATCH 8/8] fix: cast cloned motion node for ref injection --- src/CSSMotion.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CSSMotion.tsx b/src/CSSMotion.tsx index df5c058..c1ec2c2 100644 --- a/src/CSSMotion.tsx +++ b/src/CSSMotion.tsx @@ -261,7 +261,7 @@ export function genCSSMotion(config: CSSMotionConfig) { const originNodeRef = getNodeRef(returnNode); if (originNodeRef !== nodeRef) { - return React.cloneElement(returnNode, { + return React.cloneElement(returnNode as any, { ref: composeRef(originNodeRef, nodeRef), }); }