Skip to content

Commit f6cc6fa

Browse files
QDyanbingzombieJ
andauthored
fix: compose existing child refs in CSSMotion (#76)
* fix: compose existing child refs in CSSMotion * refactor: use useComposeRef in CSSMotion * refactor: simplify CSSMotion ref injection * refactor: gate auto ref injection by children arity * refactor: share ref consume helper * refactor: rename ref consumption helper * fix: tighten CSSMotion return node typing * fix: cast cloned motion node for ref injection --------- Co-authored-by: 二货机器人 <smith3816@gmail.com>
1 parent 3ea1a5d commit f6cc6fa

File tree

3 files changed

+80
-22
lines changed

3 files changed

+80
-22
lines changed

src/CSSMotion.tsx

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
/* eslint-disable react/default-props-match-prop-types, react/no-multi-comp, react/prop-types */
22
import { getDOM } from '@rc-component/util/lib/Dom/findDOMNode';
3-
import { getNodeRef, supportRef } from '@rc-component/util/lib/ref';
3+
import {
4+
composeRef,
5+
getNodeRef,
6+
supportNodeRef,
7+
} from '@rc-component/util/lib/ref';
48
import { clsx } from 'clsx';
59
import * as React from 'react';
610
import { useRef } from 'react';
@@ -106,6 +110,10 @@ export interface CSSMotionState {
106110
prevProps?: CSSMotionProps;
107111
}
108112

113+
export function isRefNotConsumed(children?: CSSMotionProps['children']) {
114+
return children?.length < 2;
115+
}
116+
109117
/**
110118
* `transitionSupport` is used for none transition test case.
111119
* Default we use browser transition event support check.
@@ -189,12 +197,12 @@ export function genCSSMotion(config: CSSMotionConfig) {
189197
}
190198

191199
// We should render children when motionStyle is sync with stepStatus
192-
return React.useMemo(() => {
200+
const returnNode = React.useMemo<React.ReactElement | null>(() => {
193201
if (styleReady === 'NONE') {
194202
return null;
195203
}
196204

197-
let motionChildren: React.ReactNode;
205+
let motionChildren: React.ReactElement | null;
198206
const mergedProps = { ...eventProps, visible };
199207

200208
if (!children) {
@@ -246,25 +254,20 @@ export function genCSSMotion(config: CSSMotionConfig) {
246254
);
247255
}
248256

249-
// Auto inject ref if child node not have `ref` props
250-
if (
251-
React.isValidElement(motionChildren) &&
252-
supportRef(motionChildren)
253-
) {
254-
const originNodeRef = getNodeRef(motionChildren);
255-
256-
if (!originNodeRef) {
257-
motionChildren = React.cloneElement(
258-
motionChildren as React.ReactElement,
259-
{
260-
ref: nodeRef,
261-
},
262-
);
263-
}
257+
return motionChildren;
258+
}, [idRef.current]);
259+
260+
if (isRefNotConsumed(children) && supportNodeRef(returnNode)) {
261+
const originNodeRef = getNodeRef(returnNode);
262+
263+
if (originNodeRef !== nodeRef) {
264+
return React.cloneElement(returnNode as any, {
265+
ref: composeRef(originNodeRef, nodeRef),
266+
});
264267
}
268+
}
265269

266-
return motionChildren;
267-
}, [idRef.current]) as React.ReactElement;
270+
return returnNode;
268271
},
269272
);
270273

src/CSSMotionList.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint react/prop-types: 0 */
22
import * as React from 'react';
33
import type { CSSMotionProps } from './CSSMotion';
4-
import OriginCSSMotion from './CSSMotion';
4+
import OriginCSSMotion, { isRefNotConsumed } from './CSSMotion';
55
import type { KeyObject } from './util/diff';
66
import {
77
diffKeys,
@@ -59,6 +59,10 @@ export interface CSSMotionListProps
5959
) => React.ReactElement;
6060
}
6161

62+
type ChildrenWithoutRef = (
63+
props: Parameters<CSSMotionListProps['children']>[0],
64+
) => ReturnType<CSSMotionListProps['children']>;
65+
6266
export interface CSSMotionListState {
6367
keyEntities: KeyObject[];
6468
}
@@ -174,7 +178,13 @@ export function genCSSMotionList(
174178
}
175179
}}
176180
>
177-
{(props, ref) => children({ ...props, index }, ref)}
181+
{isRefNotConsumed(children)
182+
? props =>
183+
(children as ChildrenWithoutRef)({
184+
...props,
185+
index,
186+
})
187+
: (props, ref) => children({ ...props, index }, ref)}
178188
</CSSMotion>
179189
);
180190
})}

tests/CSSMotion.spec.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -942,6 +942,51 @@ describe('CSSMotion', () => {
942942

943943
expect(ReactDOM.findDOMNode).not.toHaveBeenCalled();
944944
});
945+
946+
it('supports existing child refs for motion end', () => {
947+
const motionRef = React.createRef<CSSMotionRef>();
948+
const childRef = React.createRef<HTMLDivElement>();
949+
950+
const Demo = ({ visible }: { visible: boolean }) => (
951+
<CSSMotion
952+
motionName="transition"
953+
motionAppear={false}
954+
visible={visible}
955+
ref={motionRef}
956+
>
957+
{({ style, className }) => (
958+
<div
959+
ref={childRef}
960+
style={style}
961+
className={clsx('motion-box', className)}
962+
/>
963+
)}
964+
</CSSMotion>
965+
);
966+
967+
const { container, rerender } = render(<Demo visible />);
968+
969+
act(() => {
970+
jest.runAllTimers();
971+
});
972+
973+
expect(motionRef.current.nativeElement).toBe(childRef.current);
974+
975+
rerender(<Demo visible={false} />);
976+
977+
act(() => {
978+
jest.runAllTimers();
979+
});
980+
981+
fireEvent.transitionEnd(childRef.current!);
982+
983+
act(() => {
984+
jest.runAllTimers();
985+
});
986+
987+
expect(container.querySelector('.motion-box')).toBeFalsy();
988+
expect(ReactDOM.findDOMNode).not.toHaveBeenCalled();
989+
});
945990
});
946991

947992
describe('onVisibleChanged', () => {

0 commit comments

Comments
 (0)