Skip to content

Commit c487db0

Browse files
authored
feat: motion support ref (#61)
* feat: motion support ref * test: add test case * chore: add np deps
1 parent 3ec24ae commit c487db0

File tree

4 files changed

+129
-112
lines changed

4 files changed

+129
-112
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"lint:tsc": "tsc --noEmit",
3535
"now-build": "npm run docs:build",
3636
"prepare": "husky install",
37-
"prepublishOnly": "npm run compile && np --yolo --no-publish",
37+
"prepublishOnly": "npm run compile && rc-np",
3838
"prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"",
3939
"postpublish": "npm run docs:build && npm run docs:deploy",
4040
"start": "dumi dev",
@@ -52,6 +52,7 @@
5252
},
5353
"devDependencies": {
5454
"@rc-component/father-plugin": "^2.0.1",
55+
"@rc-component/np": "^1.0.3",
5556
"@testing-library/jest-dom": "^5.16.4",
5657
"@testing-library/react": "^15.0.7",
5758
"@types/classnames": "^2.2.9",
@@ -66,7 +67,6 @@
6667
"gh-pages": "^6.0.0",
6768
"husky": "^8.0.3",
6869
"lint-staged": "^14.0.1",
69-
"np": "^6.2.4",
7070
"prettier": "^2.1.1",
7171
"rc-test": "^7.0.14",
7272
"react": "^18.3.0",

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: 5 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>
@@ -50,13 +50,15 @@ describe('StrictMode', () => {
5050
act(() => {
5151
jest.runAllTimers();
5252
});
53+
expect(ref.current.inMotion()).toBeTruthy();
5354
expect(node).not.toHaveClass('transition-appear-start');
5455
expect(node).toHaveClass('transition-appear-active');
5556

5657
// Trigger End
5758
fireEvent.transitionEnd(node);
5859
expect(node).not.toHaveClass('transition-appear');
5960

60-
expect(ref.current).toBe(node);
61+
expect(ref.current.inMotion()).toBeFalsy();
62+
expect(ref.current.nativeElement).toBe(node);
6163
});
6264
});

0 commit comments

Comments
 (0)