Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/demo/debug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Debug Demo

<code src="../examples/debug.tsx"></code>
18 changes: 18 additions & 0 deletions docs/examples/debug.less
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
65 changes: 65 additions & 0 deletions docs/examples/debug.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<button
onClick={() => {
setKey(prev => prev + 1);
}}
>
Start
</button>

<CSSMotion
visible
motionName="debug-motion"
motionAppear
onAppearStart={onCollapse}
onAppearActive={onExpand}
key={key}
>
{({ style, className }, ref) => {
console.log('render', className, style);

return (
<div
ref={ref}
className={clsx('debug-demo-block', className)}
style={style}
>
<div
style={{
height: 100,
width: 100,
background: 'blue',
}}
/>
</div>
);
}}
</CSSMotion>
</div>
);
}

export default () => (
<React.StrictMode>
<DebugDemo />
</React.StrictMode>
);
138 changes: 73 additions & 65 deletions src/CSSMotion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
},
);

Expand Down
40 changes: 29 additions & 11 deletions src/hooks/useStatus.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<boolean>();
const [asyncVisible, setAsyncVisible] = React.useState<boolean>();
const [getStatus, setStatus] = useSyncState<MotionStatus>(STATUS_NONE);
const [style, setStyle] = useState<React.CSSProperties | undefined>(null);
const [style, setStyle] = React.useState<
[style: React.CSSProperties | undefined, step: StepStatus]
>([null, null]);

const currentStatus = getStatus();

Expand All @@ -73,7 +80,7 @@ export default function useStatus(
*/
function updateMotionEndStatus() {
setStatus(STATUS_NONE);
setStyle(null, true);
setStyle([null, null]);
}

const onInternalMotionEnd = useEvent((event: MotionEvent) => {
Expand Down Expand Up @@ -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());

Expand All @@ -179,7 +189,7 @@ export default function useStatus(
}
}

if (step === STEP_PREPARED) {
if (newStep === STEP_PREPARED) {
updateMotionEndStatus();
}

Expand Down Expand Up @@ -286,13 +296,21 @@ export default function useStatus(
}, [asyncVisible, currentStatus]);

// ============================ Styles ============================
let mergedStyle = style;
let mergedStyle = style[0];
if (eventHandlers[STEP_PREPARE] && step === STEP_START) {
mergedStyle = {
transition: 'none',
...mergedStyle,
};
}

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,
];
}
6 changes: 6 additions & 0 deletions tests/CSSMotion.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ describe('CSSMotion', () => {
});

fireEvent.transitionEnd(container.querySelector('.motion-box'));
act(() => {
jest.runAllTimers();
});

expect(container.querySelector('.motion-box')).toHaveClass('removed');

Expand Down Expand Up @@ -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');
Expand Down
6 changes: 4 additions & 2 deletions tests/StrictMode.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
Expand Down