Skip to content

Commit 1e8679c

Browse files
committed
feat: motion support ref
1 parent 3ec24ae commit 1e8679c

File tree

3 files changed

+125
-110
lines changed

3 files changed

+125
-110
lines changed

src/CSSMotion.tsx

Lines changed: 109 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ import type {
1616
import { STATUS_NONE, STEP_PREPARE, STEP_START } from './interface';
1717
import { getTransitionName, supportTransition } from './util/motion';
1818

19+
export interface CSSMotionRef {
20+
nativeElement: HTMLElement;
21+
inMotion: () => boolean;
22+
}
23+
1924
export type CSSMotionConfig =
2025
| boolean
2126
| {
@@ -117,116 +122,121 @@ export function genCSSMotion(config: CSSMotionConfig) {
117122
return !!(props.motionName && transitionSupport && contextMotion !== false);
118123
}
119124

120-
const CSSMotion = React.forwardRef<any, CSSMotionProps>((props, ref) => {
121-
const {
122-
// Default config
123-
visible = true,
124-
removeOnLeave = true,
125-
126-
forceRender,
127-
children,
128-
motionName,
129-
leavedClassName,
130-
eventProps,
131-
} = props;
132-
133-
const { motion: contextMotion } = React.useContext(Context);
134-
135-
const supportMotion = isSupportTransition(props, contextMotion);
136-
137-
// Ref to the react node, it may be a HTMLElement
138-
const nodeRef = useRef<any>();
139-
140-
function getDomElement() {
141-
return getDOM(nodeRef.current) as HTMLElement;
142-
}
143-
144-
const [status, statusStep, statusStyle, mergedVisible] = useStatus(
145-
supportMotion,
146-
visible,
147-
getDomElement,
148-
props,
149-
);
150-
151-
// Record whether content has rendered
152-
// Will return null for un-rendered even when `removeOnLeave={false}`
153-
const renderedRef = React.useRef(mergedVisible);
154-
if (mergedVisible) {
155-
renderedRef.current = true;
156-
}
157-
158-
// ====================== Refs ======================
159-
React.useImperativeHandle(ref, () => getDomElement());
160-
161-
// ===================== Render =====================
162-
let motionChildren: React.ReactNode;
163-
const mergedProps = { ...eventProps, visible };
164-
165-
if (!children) {
166-
// No children
167-
motionChildren = null;
168-
} else if (status === STATUS_NONE) {
169-
// Stable children
170-
if (mergedVisible) {
171-
motionChildren = children({ ...mergedProps }, nodeRef);
172-
} else if (!removeOnLeave && renderedRef.current && leavedClassName) {
173-
motionChildren = children(
174-
{ ...mergedProps, className: leavedClassName },
175-
nodeRef,
176-
);
177-
} else if (forceRender || (!removeOnLeave && !leavedClassName)) {
178-
motionChildren = children(
179-
{ ...mergedProps, style: { display: 'none' } },
180-
nodeRef,
181-
);
182-
} else {
183-
motionChildren = null;
184-
}
185-
} else {
186-
// In motion
187-
let statusSuffix: string;
188-
if (statusStep === STEP_PREPARE) {
189-
statusSuffix = 'prepare';
190-
} else if (isActive(statusStep)) {
191-
statusSuffix = 'active';
192-
} else if (statusStep === STEP_START) {
193-
statusSuffix = 'start';
194-
}
125+
const CSSMotion = React.forwardRef<CSSMotionRef, CSSMotionProps>(
126+
(props, ref) => {
127+
const {
128+
// Default config
129+
visible = true,
130+
removeOnLeave = true,
195131

196-
const motionCls = getTransitionName(
132+
forceRender,
133+
children,
197134
motionName,
198-
`${status}-${statusSuffix}`,
199-
);
135+
leavedClassName,
136+
eventProps,
137+
} = props;
138+
139+
const { motion: contextMotion } = React.useContext(Context);
140+
141+
const supportMotion = isSupportTransition(props, contextMotion);
200142

201-
motionChildren = children(
202-
{
203-
...mergedProps,
204-
className: classNames(getTransitionName(motionName, status), {
205-
[motionCls]: motionCls && statusSuffix,
206-
[motionName as string]: typeof motionName === 'string',
207-
}),
208-
style: statusStyle,
209-
},
210-
nodeRef,
143+
// Ref to the react node, it may be a HTMLElement
144+
const nodeRef = useRef<any>();
145+
146+
function getDomElement() {
147+
return getDOM(nodeRef.current) as HTMLElement;
148+
}
149+
150+
const [status, statusStep, statusStyle, mergedVisible] = useStatus(
151+
supportMotion,
152+
visible,
153+
getDomElement,
154+
props,
211155
);
212-
}
213156

214-
// Auto inject ref if child node not have `ref` props
215-
if (React.isValidElement(motionChildren) && supportRef(motionChildren)) {
216-
const originNodeRef = getNodeRef(motionChildren);
157+
// Record whether content has rendered
158+
// Will return null for un-rendered even when `removeOnLeave={false}`
159+
const renderedRef = React.useRef(mergedVisible);
160+
if (mergedVisible) {
161+
renderedRef.current = true;
162+
}
163+
164+
// ====================== Refs ======================
165+
React.useImperativeHandle(ref, () => ({
166+
nativeElement: getDomElement(),
167+
inMotion: () => status !== STATUS_NONE,
168+
}));
169+
170+
// ===================== Render =====================
171+
let motionChildren: React.ReactNode;
172+
const mergedProps = { ...eventProps, visible };
173+
174+
if (!children) {
175+
// No children
176+
motionChildren = null;
177+
} else if (status === STATUS_NONE) {
178+
// Stable children
179+
if (mergedVisible) {
180+
motionChildren = children({ ...mergedProps }, nodeRef);
181+
} else if (!removeOnLeave && renderedRef.current && leavedClassName) {
182+
motionChildren = children(
183+
{ ...mergedProps, className: leavedClassName },
184+
nodeRef,
185+
);
186+
} else if (forceRender || (!removeOnLeave && !leavedClassName)) {
187+
motionChildren = children(
188+
{ ...mergedProps, style: { display: 'none' } },
189+
nodeRef,
190+
);
191+
} else {
192+
motionChildren = null;
193+
}
194+
} else {
195+
// In motion
196+
let statusSuffix: string;
197+
if (statusStep === STEP_PREPARE) {
198+
statusSuffix = 'prepare';
199+
} else if (isActive(statusStep)) {
200+
statusSuffix = 'active';
201+
} else if (statusStep === STEP_START) {
202+
statusSuffix = 'start';
203+
}
204+
205+
const motionCls = getTransitionName(
206+
motionName,
207+
`${status}-${statusSuffix}`,
208+
);
217209

218-
if (!originNodeRef) {
219-
motionChildren = React.cloneElement(
220-
motionChildren as React.ReactElement,
210+
motionChildren = children(
221211
{
222-
ref: nodeRef,
212+
...mergedProps,
213+
className: classNames(getTransitionName(motionName, status), {
214+
[motionCls]: motionCls && statusSuffix,
215+
[motionName as string]: typeof motionName === 'string',
216+
}),
217+
style: statusStyle,
223218
},
219+
nodeRef,
224220
);
225221
}
226-
}
227222

228-
return motionChildren as React.ReactElement;
229-
});
223+
// Auto inject ref if child node not have `ref` props
224+
if (React.isValidElement(motionChildren) && supportRef(motionChildren)) {
225+
const originNodeRef = getNodeRef(motionChildren);
226+
227+
if (!originNodeRef) {
228+
motionChildren = React.cloneElement(
229+
motionChildren as React.ReactElement,
230+
{
231+
ref: nodeRef,
232+
},
233+
);
234+
}
235+
}
236+
237+
return motionChildren as React.ReactElement;
238+
},
239+
);
230240

231241
CSSMotion.displayName = 'CSSMotion';
232242

tests/CSSMotion.spec.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import React from 'react';
88
import ReactDOM from 'react-dom';
99
import type { CSSMotionProps } from '../src';
1010
import { Provider } from '../src';
11-
import RefCSSMotion, { genCSSMotion } from '../src/CSSMotion';
11+
import RefCSSMotion, {
12+
genCSSMotion,
13+
type CSSMotionRef,
14+
} from '../src/CSSMotion';
1215

1316
describe('CSSMotion', () => {
1417
const CSSMotion = genCSSMotion({
@@ -628,7 +631,7 @@ describe('CSSMotion', () => {
628631
});
629632

630633
it('forwardRef', () => {
631-
const domRef = React.createRef();
634+
const domRef = React.createRef<CSSMotionRef>();
632635
render(
633636
<RefCSSMotion motionName="transition" ref={domRef}>
634637
{({ style, className }, ref) => (
@@ -641,7 +644,7 @@ describe('CSSMotion', () => {
641644
</RefCSSMotion>,
642645
);
643646

644-
expect(domRef.current instanceof HTMLElement).toBeTruthy();
647+
expect(domRef.current.nativeElement instanceof HTMLElement).toBeTruthy();
645648
});
646649

647650
it("onMotionEnd shouldn't be fired by inner element", () => {
@@ -844,7 +847,7 @@ describe('CSSMotion', () => {
844847

845848
it('not crash when no refs are passed', () => {
846849
const Div = () => <div />;
847-
const cssMotionRef = React.createRef();
850+
const cssMotionRef = React.createRef<CSSMotionRef>();
848851
render(
849852
<CSSMotion motionName="transition" visible ref={cssMotionRef}>
850853
{() => <Div />}
@@ -855,7 +858,7 @@ describe('CSSMotion', () => {
855858
jest.runAllTimers();
856859
});
857860

858-
expect(cssMotionRef.current).toBeFalsy();
861+
expect(cssMotionRef.current.nativeElement).toBeFalsy();
859862
expect(ReactDOM.findDOMNode).not.toHaveBeenCalled();
860863
});
861864

@@ -874,7 +877,7 @@ describe('CSSMotion', () => {
874877
});
875878

876879
it('support nativeElement of ref', () => {
877-
const domRef = React.createRef();
880+
const domRef = React.createRef<CSSMotionRef>();
878881
const Div = React.forwardRef<
879882
{
880883
nativeElement: HTMLDivElement;
@@ -900,12 +903,14 @@ describe('CSSMotion', () => {
900903
jest.runAllTimers();
901904
});
902905

903-
expect(domRef.current).toBe(container.querySelector('.bamboo'));
906+
expect(domRef.current.nativeElement).toBe(
907+
container.querySelector('.bamboo'),
908+
);
904909
expect(ReactDOM.findDOMNode).not.toHaveBeenCalled();
905910
});
906911

907912
it('does not call findDOMNode when refs are forwarded and assigned', () => {
908-
const domRef = React.createRef();
913+
const domRef = React.createRef<CSSMotionRef>();
909914

910915
render(
911916
<CSSMotion motionName="transition" visible ref={domRef}>

tests/StrictMode.spec.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import classNames from 'classnames';
77
import React from 'react';
88
import { act } from 'react-dom/test-utils';
99
// import type { CSSMotionProps } from '../src/CSSMotion';
10-
import { genCSSMotion } from '../src/CSSMotion';
10+
import { genCSSMotion, type CSSMotionRef } from '../src/CSSMotion';
1111
// import RefCSSMotion, { genCSSMotion } from '../src/CSSMotion';
1212
// import ReactDOM from 'react-dom';
1313

@@ -26,7 +26,7 @@ describe('StrictMode', () => {
2626
});
2727

2828
it('motion should end', () => {
29-
const ref = React.createRef();
29+
const ref = React.createRef<CSSMotionRef>();
3030

3131
const { container } = render(
3232
<React.StrictMode>
@@ -57,6 +57,6 @@ describe('StrictMode', () => {
5757
fireEvent.transitionEnd(node);
5858
expect(node).not.toHaveClass('transition-appear');
5959

60-
expect(ref.current).toBe(node);
60+
expect(ref.current.nativeElement).toBe(node);
6161
});
6262
});

0 commit comments

Comments
 (0)