diff --git a/.gitignore b/.gitignore index 2c2d950..03ee30f 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ coverage/ .dumi/tmp .dumi/tmp-production .dumi/tmp-test +.node diff --git a/package.json b/package.json index 0e68872..09f8a43 100644 --- a/package.json +++ b/package.json @@ -48,16 +48,16 @@ "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", - "rc-util": "^5.21.0" + "rc-util": "^5.39.3" }, "devDependencies": { "@rc-component/father-plugin": "^1.0.1", "@testing-library/jest-dom": "^5.16.4", - "@testing-library/react": "^13.0.0", + "@testing-library/react": "^15.0.7", "@types/classnames": "^2.2.9", "@types/jest": "^26.0.8", - "@types/react": "^16.9.2", - "@types/react-dom": "^16.9.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", "@umijs/fabric": "^2.0.8", "cross-env": "^7.0.2", "dumi": "^2.0.18", @@ -69,8 +69,8 @@ "np": "^6.2.4", "prettier": "^2.1.1", "rc-test": "^7.0.14", - "react": "^18.0.0", - "react-dom": "^18.0.0", + "react": "^18.3.0", + "react-dom": "^18.3.0", "typescript": "^4.0.3" }, "peerDependencies": { diff --git a/src/CSSMotion.tsx b/src/CSSMotion.tsx index cc64a48..bff0e6c 100644 --- a/src/CSSMotion.tsx +++ b/src/CSSMotion.tsx @@ -107,9 +107,7 @@ export interface CSSMotionState { * `transitionSupport` is used for none transition test case. * Default we use browser transition event support check. */ -export function genCSSMotion( - config: CSSMotionConfig, -): React.ForwardRefExoticComponent }> { +export function genCSSMotion(config: CSSMotionConfig) { let transitionSupport = config; if (typeof config === 'object') { diff --git a/src/hooks/useDomMotionEvents.ts b/src/hooks/useDomMotionEvents.ts index 20d7bb3..0ec0517 100644 --- a/src/hooks/useDomMotionEvents.ts +++ b/src/hooks/useDomMotionEvents.ts @@ -5,19 +5,10 @@ import type { MotionEvent } from '../interface'; import { animationEndName, transitionEndName } from '../util/motion'; export default ( - callback: (event: MotionEvent) => void, + onInternalMotionEnd: (event: MotionEvent) => void, ): [(element: HTMLElement) => void, (element: HTMLElement) => void] => { const cacheElementRef = useRef(); - // Cache callback - const callbackRef = useRef(callback); - callbackRef.current = callback; - - // Internal motion event handler - const onInternalMotionEnd = React.useCallback((event: MotionEvent) => { - callbackRef.current(event); - }, []); - // Remove events function removeMotionEvents(element: HTMLElement) { if (element) { diff --git a/src/hooks/useStatus.ts b/src/hooks/useStatus.ts index 1250055..fa79cc8 100644 --- a/src/hooks/useStatus.ts +++ b/src/hooks/useStatus.ts @@ -1,3 +1,4 @@ +import { useEvent } from 'rc-util'; import useState from 'rc-util/lib/hooks/useState'; import * as React from 'react'; import { useEffect, useRef } from 'react'; @@ -72,7 +73,13 @@ export default function useStatus( setStyle(null, true); } - function onInternalMotionEnd(event: MotionEvent) { + const onInternalMotionEnd = useEvent((event: MotionEvent) => { + // Do nothing since not in any transition status. + // This may happen when `motionDeadline` trigger. + if (status === STATUS_NONE) { + return; + } + const element = getDomElement(); if (event && !event.deadline && event.target !== element) { // event exists @@ -93,10 +100,10 @@ export default function useStatus( } // Only update status when `canEnd` and not destroyed - if (status !== STATUS_NONE && currentActive && canEnd !== false) { + if (currentActive && canEnd !== false) { updateMotionEndStatus(); } - } + }); const [patchMotionEvents] = useDomMotionEvents(onInternalMotionEnd); @@ -151,7 +158,7 @@ export default function useStatus( setStyle(eventHandlers[step]?.(getDomElement(), null) || null); } - if (step === STEP_ACTIVE) { + if (step === STEP_ACTIVE && status !== STATUS_NONE) { // Patch events when motion needed patchMotionEvents(getDomElement()); diff --git a/src/util/diff.ts b/src/util/diff.ts index 96b88a9..d3d30f6 100644 --- a/src/util/diff.ts +++ b/src/util/diff.ts @@ -8,8 +8,10 @@ export type DiffStatus = | typeof STATUS_REMOVE | typeof STATUS_REMOVED; +type RawKeyType = string | number; + export interface KeyObject { - key: React.Key; + key: RawKeyType; status?: DiffStatus; } @@ -18,7 +20,7 @@ export function wrapKeyToObject(key: React.Key | KeyObject) { if (key && typeof key === 'object' && 'key' in key) { keyObj = key; } else { - keyObj = { key: key as React.Key }; + keyObj = { key: key as RawKeyType }; } return { ...keyObj, @@ -90,7 +92,7 @@ export function diffKeys( * Merge same key when it remove and add again: * [1 - add, 2 - keep, 1 - remove] -> [1 - keep, 2 - keep] */ - const keys = {}; + const keys: Record = {}; list.forEach(({ key }) => { keys[key] = (keys[key] || 0) + 1; }); diff --git a/tests/CSSMotion.spec.tsx b/tests/CSSMotion.spec.tsx index f822936..2ead8a8 100644 --- a/tests/CSSMotion.spec.tsx +++ b/tests/CSSMotion.spec.tsx @@ -2,11 +2,10 @@ 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 classNames from 'classnames'; import React from 'react'; import ReactDOM from 'react-dom'; -import { act } from 'react-dom/test-utils'; import type { CSSMotionProps } from '../src'; import { Provider } from '../src'; import RefCSSMotion, { genCSSMotion } from '../src/CSSMotion'; @@ -342,6 +341,60 @@ describe('CSSMotion', () => { return
; }), ); + + it('not warning on StrictMode', () => { + const onLeaveEnd = jest.fn(); + const errorSpy = jest.spyOn(console, 'error'); + + const renderDemo = (visible: boolean) => ( + + + {({ style, className }) => ( +
+ )} + + + ); + + const { rerender, container } = render(renderDemo(true)); + act(() => { + jest.advanceTimersByTime(100000); + }); + + // Leave + rerender(renderDemo(false)); + act(() => { + jest.advanceTimersByTime(500); + }); + + // Motion end + fireEvent.transitionEnd( + container.querySelector('.transition-leave-active'), + ); + act(() => { + jest.advanceTimersByTime(100); + }); + + // Another timeout + act(() => { + jest.advanceTimersByTime(1000); + }); + + expect(onLeaveEnd).toHaveBeenCalledTimes(1); + expect(errorSpy).not.toHaveBeenCalled(); + + errorSpy.mockRestore(); + }); }); it('not crash when no children', () => {