This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
import * as React from 'react';
import type { MotionEndEventHandler, MotionEventHandler, MotionPrepareEventHandler, MotionStatus } from './interface';
export interface CSSMotionRef {
nativeElement: HTMLElement;
inMotion: () => boolean;
enableMotion: () => boolean;
}
export type CSSMotionConfig = boolean | {
transitionSupport?: boolean;
};
export type MotionName = string | {
appear?: string;
enter?: string;
leave?: string;
appearActive?: string;
enterActive?: string;
leaveActive?: string;
};
export interface CSSMotionProps {
motionName?: MotionName;
visible?: boolean;
motionAppear?: boolean;
motionEnter?: boolean;
motionLeave?: boolean;
motionLeaveImmediately?: boolean;
motionDeadline?: number;
/**
* Create element in view even the element is invisible.
* Will patch `display: none` style on it.
*/
forceRender?: boolean;
/**
* Remove element when motion end. This will not work when `forceRender` is set.
*/
removeOnLeave?: boolean;
leavedClassName?: string;
/** @private Used by CSSMotionList. Do not use in your production. */
eventProps?: object;
/** Prepare phase is used for measure element info. It will always trigger even motion is off */
onAppearPrepare?: MotionPrepareEventHandler;
/** Prepare phase is used for measure element info. It will always trigger even motion is off */
onEnterPrepare?: MotionPrepareEventHandler;
/** Prepare phase is used for measure element info. It will always trigger even motion is off */
onLeavePrepare?: MotionPrepareEventHandler;
onAppearStart?: MotionEventHandler;
onEnterStart?: MotionEventHandler;
onLeaveStart?: MotionEventHandler;
onAppearActive?: MotionEventHandler;
onEnterActive?: MotionEventHandler;
onLeaveActive?: MotionEventHandler;
onAppearEnd?: MotionEndEventHandler;
onEnterEnd?: MotionEndEventHandler;
onLeaveEnd?: MotionEndEventHandler;
/** This will always trigger after final visible changed. Even if no motion configured. */
onVisibleChanged?: (visible: boolean) => void;
internalRef?: React.Ref<any>;
children?: (props: {
visible?: boolean;
className?: string;
style?: React.CSSProperties;
[key: string]: any;
}, ref: React.Ref<any>) => React.ReactElement;
}
export interface CSSMotionState {
status?: MotionStatus;
statusActive?: boolean;
newStatus?: boolean;
statusStyle?: React.CSSProperties;
prevProps?: CSSMotionProps;
}
export declare function isRefNotConsumed(children?: CSSMotionProps['children']): boolean;
/**
* `transitionSupport` is used for none transition test case.
* Default we use browser transition event support check.
*/
export declare function genCSSMotion(config: CSSMotionConfig): React.ForwardRefExoticComponent<CSSMotionProps & React.RefAttributes<CSSMotionRef>>;
declare const _default: React.ForwardRefExoticComponent<CSSMotionProps & React.RefAttributes<CSSMotionRef>>;
export default _default;
+161
View File
@@ -0,0 +1,161 @@
/* eslint-disable react/default-props-match-prop-types, react/no-multi-comp, react/prop-types */
import { getDOM } from "@rc-component/util/es/Dom/findDOMNode";
import { composeRef, getNodeRef, supportNodeRef } from "@rc-component/util/es/ref";
import { clsx } from 'clsx';
import * as React from 'react';
import { useRef } from 'react';
import { Context } from "./context";
import useStatus from "./hooks/useStatus";
import { isActive } from "./hooks/useStepQueue";
import { STATUS_NONE, STEP_PREPARE, STEP_START } from "./interface";
import { getTransitionName, supportTransition } from "./util/motion";
export function isRefNotConsumed(children) {
return children?.length < 2;
}
/**
* `transitionSupport` is used for none transition test case.
* Default we use browser transition event support check.
*/
export function genCSSMotion(config) {
let transitionSupport = config;
if (typeof config === 'object') {
({
transitionSupport
} = config);
}
function isSupportTransition(props, contextMotion) {
return !!(props.motionName && transitionSupport && contextMotion !== false);
}
const CSSMotion = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
// Default config
visible = true,
removeOnLeave = true,
forceRender,
children,
motionName,
leavedClassName,
eventProps
} = props;
const {
motion: contextMotion
} = React.useContext(Context);
const supportMotion = isSupportTransition(props, contextMotion);
// Ref to the react node, it may be a HTMLElement
const nodeRef = useRef();
function getDomElement() {
return getDOM(nodeRef.current);
}
const [getStatus, statusStep, statusStyle, mergedVisible, styleReady] = useStatus(supportMotion, visible, getDomElement, props);
const status = getStatus();
// Record whether content has rendered
// Will return null for un-rendered even when `removeOnLeave={false}`
const renderedRef = React.useRef(mergedVisible);
if (mergedVisible) {
renderedRef.current = true;
}
// ====================== Refs ======================
const refObj = React.useMemo(() => {
const obj = {};
Object.defineProperties(obj, {
nativeElement: {
enumerable: true,
get: getDomElement
},
inMotion: {
enumerable: true,
get: () => () => getStatus() !== STATUS_NONE
},
enableMotion: {
enumerable: true,
get: () => () => supportMotion
}
});
return obj;
}, []);
// We lock `deps` here since function return object
// will repeat trigger ref from `refConfig` -> `null` -> `refConfig`
React.useImperativeHandle(ref, () => refObj, []);
// ===================== Render =====================
// return motionChildren as React.ReactElement;
const idRef = React.useRef(0);
if (styleReady) {
idRef.current += 1;
}
// We should render children when motionStyle is sync with stepStatus
const returnNode = React.useMemo(() => {
if (styleReady === 'NONE') {
return null;
}
let motionChildren;
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;
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]: typeof motionName === 'string'
}),
style: statusStyle
}, nodeRef);
}
return motionChildren;
}, [idRef.current]);
if (isRefNotConsumed(children) && supportNodeRef(returnNode)) {
const originNodeRef = getNodeRef(returnNode);
if (originNodeRef !== nodeRef) {
return /*#__PURE__*/React.cloneElement(returnNode, {
ref: composeRef(originNodeRef, nodeRef)
});
}
}
return returnNode;
});
CSSMotion.displayName = 'CSSMotion';
return CSSMotion;
}
export default genCSSMotion(supportTransition);
@@ -0,0 +1,34 @@
import * as React from 'react';
import type { CSSMotionProps } from './CSSMotion';
import type { KeyObject } from './util/diff';
export interface CSSMotionListProps extends Omit<CSSMotionProps, 'onVisibleChanged' | 'children'>, Omit<React.HTMLAttributes<any>, 'children'> {
keys: (React.Key | {
key: React.Key;
[name: string]: any;
})[];
component?: string | React.ComponentType | false;
/** This will always trigger after final visible changed. Even if no motion configured. */
onVisibleChanged?: (visible: boolean, info: {
key: React.Key;
}) => void;
/** All motion leaves in the screen */
onAllRemoved?: () => void;
children?: (props: {
visible?: boolean;
className?: string;
style?: React.CSSProperties;
index?: number;
[key: string]: any;
}, ref: React.Ref<any>) => React.ReactElement;
}
export interface CSSMotionListState {
keyEntities: KeyObject[];
}
/**
* Generate a CSSMotionList component with config
* @param transitionSupport No need since CSSMotionList no longer depends on transition support
* @param CSSMotion CSSMotion component
*/
export declare function genCSSMotionList(transitionSupport: boolean, CSSMotion?: React.ForwardRefExoticComponent<CSSMotionProps & React.RefAttributes<import("./CSSMotion").CSSMotionRef>>): React.ComponentClass<CSSMotionListProps>;
declare const _default: React.ComponentClass<CSSMotionListProps, any>;
export default _default;
+115
View File
@@ -0,0 +1,115 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
/* eslint react/prop-types: 0 */
import * as React from 'react';
import OriginCSSMotion, { isRefNotConsumed } from "./CSSMotion";
import { diffKeys, parseKeys, STATUS_ADD, STATUS_KEEP, STATUS_REMOVE, STATUS_REMOVED } from "./util/diff";
import { supportTransition } from "./util/motion";
const MOTION_PROP_NAMES = ['eventProps', 'visible', 'children', 'motionName', 'motionAppear', 'motionEnter', 'motionLeave', 'motionLeaveImmediately', 'motionDeadline', 'removeOnLeave', 'leavedClassName', 'onAppearPrepare', 'onAppearStart', 'onAppearActive', 'onAppearEnd', 'onEnterStart', 'onEnterActive', 'onEnterEnd', 'onLeaveStart', 'onLeaveActive', 'onLeaveEnd'];
/**
* Generate a CSSMotionList component with config
* @param transitionSupport No need since CSSMotionList no longer depends on transition support
* @param CSSMotion CSSMotion component
*/
export function genCSSMotionList(transitionSupport, CSSMotion = OriginCSSMotion) {
class CSSMotionList extends React.Component {
static defaultProps = {
component: 'div'
};
state = {
keyEntities: []
};
static getDerivedStateFromProps({
keys
}, {
keyEntities
}) {
const parsedKeyObjects = parseKeys(keys);
const mixedKeyEntities = diffKeys(keyEntities, parsedKeyObjects);
return {
keyEntities: mixedKeyEntities.filter(entity => {
const prevEntity = keyEntities.find(({
key
}) => entity.key === key);
// Remove if already mark as removed
if (prevEntity && prevEntity.status === STATUS_REMOVED && entity.status === STATUS_REMOVE) {
return false;
}
return true;
})
};
}
// ZombieJ: Return the count of rest keys. It's safe to refactor if need more info.
removeKey = removeKey => {
this.setState(prevState => {
const nextKeyEntities = prevState.keyEntities.map(entity => {
if (entity.key !== removeKey) return entity;
return {
...entity,
status: STATUS_REMOVED
};
});
return {
keyEntities: nextKeyEntities
};
}, () => {
const {
keyEntities
} = this.state;
const restKeysCount = keyEntities.filter(({
status
}) => status !== STATUS_REMOVED).length;
if (restKeysCount === 0 && this.props.onAllRemoved) {
this.props.onAllRemoved();
}
});
};
render() {
const {
keyEntities
} = this.state;
const {
component,
children,
onVisibleChanged,
onAllRemoved,
...restProps
} = this.props;
const Component = component || React.Fragment;
const motionProps = {};
MOTION_PROP_NAMES.forEach(prop => {
motionProps[prop] = restProps[prop];
delete restProps[prop];
});
delete restProps.keys;
return /*#__PURE__*/React.createElement(Component, restProps, keyEntities.map(({
status,
...eventProps
}, index) => {
const visible = status === STATUS_ADD || status === STATUS_KEEP;
return /*#__PURE__*/React.createElement(CSSMotion, _extends({}, motionProps, {
key: eventProps.key,
visible: visible,
eventProps: eventProps,
onVisibleChanged: changedVisible => {
onVisibleChanged?.(changedVisible, {
key: eventProps.key
});
if (!changedVisible) {
this.removeKey(eventProps.key);
}
}
}), isRefNotConsumed(children) ? props => children({
...props,
index
}) : (props, ref) => children({
...props,
index
}, ref));
}));
}
}
return CSSMotionList;
}
export default genCSSMotionList(supportTransition);
+7
View File
@@ -0,0 +1,7 @@
import * as React from 'react';
interface MotionContextProps {
motion?: boolean;
}
export declare const Context: React.Context<MotionContextProps>;
declare const MotionProvider: React.FC<React.PropsWithChildren<MotionContextProps>>;
export default MotionProvider;
+17
View File
@@ -0,0 +1,17 @@
import * as React from 'react';
export const Context = /*#__PURE__*/React.createContext({});
const MotionProvider = props => {
const {
children,
...rest
} = props;
const memoizedValue = React.useMemo(() => {
return {
motion: rest.motion
};
}, [rest.motion]);
return /*#__PURE__*/React.createElement(Context.Provider, {
value: memoizedValue
}, children);
};
export default MotionProvider;
@@ -0,0 +1,3 @@
import type { MotionEvent } from '../interface';
declare const _default: (onInternalMotionEnd: (event: MotionEvent) => void) => [(element: HTMLElement) => void, (element: HTMLElement) => void];
export default _default;
@@ -0,0 +1,35 @@
import * as React from 'react';
import { useRef } from 'react';
import { animationEndName, transitionEndName } from "../util/motion";
export default (onInternalMotionEnd => {
const cacheElementRef = useRef();
// Remove events
function removeMotionEvents(element) {
if (element) {
element.removeEventListener(transitionEndName, onInternalMotionEnd);
element.removeEventListener(animationEndName, onInternalMotionEnd);
}
}
// Patch events
function patchMotionEvents(element) {
if (cacheElementRef.current && cacheElementRef.current !== element) {
removeMotionEvents(cacheElementRef.current);
}
if (element && element !== cacheElementRef.current) {
element.addEventListener(transitionEndName, onInternalMotionEnd);
element.addEventListener(animationEndName, onInternalMotionEnd);
// Save as cache in case dom removed trigger by `motionDeadline`
cacheElementRef.current = element;
}
}
// Clean up when removed
React.useEffect(() => () => {
removeMotionEvents(cacheElementRef.current);
cacheElementRef.current = null;
}, []);
return [patchMotionEvents, removeMotionEvents];
});
@@ -0,0 +1,3 @@
import { useEffect } from 'react';
declare const useIsomorphicLayoutEffect: typeof useEffect;
export default useIsomorphicLayoutEffect;
@@ -0,0 +1,6 @@
import canUseDom from "@rc-component/util/es/Dom/canUseDom";
import { useEffect, useLayoutEffect } from 'react';
// It's safe to use `useLayoutEffect` but the warning is annoying
const useIsomorphicLayoutEffect = canUseDom() ? useLayoutEffect : useEffect;
export default useIsomorphicLayoutEffect;
@@ -0,0 +1,4 @@
declare const _default: () => [(callback: (info: {
isCanceled: () => boolean;
}) => void) => void, () => void];
export default _default;
@@ -0,0 +1,25 @@
import raf from "@rc-component/util/es/raf";
import * as React from 'react';
export default (() => {
const nextFrameRef = React.useRef(null);
function cancelNextFrame() {
raf.cancel(nextFrameRef.current);
}
function nextFrame(callback, delay = 2) {
cancelNextFrame();
const nextFrameId = raf(() => {
if (delay <= 1) {
callback({
isCanceled: () => nextFrameId !== nextFrameRef.current
});
} else {
nextFrame(callback, delay - 1);
}
});
nextFrameRef.current = nextFrameId;
}
React.useEffect(() => () => {
cancelNextFrame();
}, []);
return [nextFrame, cancelNextFrame];
});
@@ -0,0 +1,10 @@
import * as React from 'react';
import type { CSSMotionProps } from '../CSSMotion';
import type { MotionStatus, StepStatus } from '../interface';
export default function useStatus(supportMotion: boolean, visible: boolean, getElement: () => HTMLElement, { motionEnter, motionAppear, motionLeave, motionDeadline, motionLeaveImmediately, onAppearPrepare, onEnterPrepare, onLeavePrepare, onAppearStart, onEnterStart, onLeaveStart, onAppearActive, onEnterActive, onLeaveActive, onAppearEnd, onEnterEnd, onLeaveEnd, onVisibleChanged, }: CSSMotionProps): [
status: () => MotionStatus,
stepStatus: StepStatus,
style: React.CSSProperties,
visible: boolean,
styleReady: 'NONE' | boolean
];
+239
View File
@@ -0,0 +1,239 @@
import { useEvent } from '@rc-component/util';
import useSyncState from "@rc-component/util/es/hooks/useSyncState";
import * as React from 'react';
import { useEffect, useRef } from 'react';
import { STATUS_APPEAR, STATUS_ENTER, STATUS_LEAVE, STATUS_NONE, STEP_ACTIVE, STEP_PREPARE, STEP_PREPARED, STEP_START } from "../interface";
import useDomMotionEvents from "./useDomMotionEvents";
import useIsomorphicLayoutEffect from "./useIsomorphicLayoutEffect";
import useStepQueue, { DoStep, isActive, SkipStep } from "./useStepQueue";
export default function useStatus(supportMotion, visible, getElement, {
motionEnter = true,
motionAppear = true,
motionLeave = true,
motionDeadline,
motionLeaveImmediately,
onAppearPrepare,
onEnterPrepare,
onLeavePrepare,
onAppearStart,
onEnterStart,
onLeaveStart,
onAppearActive,
onEnterActive,
onLeaveActive,
onAppearEnd,
onEnterEnd,
onLeaveEnd,
onVisibleChanged
}) {
// Used for outer render usage to avoid `visible: false & status: none` to render nothing
const [asyncVisible, setAsyncVisible] = React.useState();
const [getStatus, setStatus] = useSyncState(STATUS_NONE);
const [style, setStyle] = React.useState([null, null]);
const currentStatus = getStatus();
const mountedRef = useRef(false);
const deadlineRef = useRef(null);
// =========================== Dom Node ===========================
function getDomElement() {
return getElement();
}
// ========================== Motion End ==========================
const activeRef = useRef(false);
/**
* Clean up status & style
*/
function updateMotionEndStatus() {
setStatus(STATUS_NONE);
setStyle([null, null]);
}
const onInternalMotionEnd = useEvent(event => {
const status = getStatus();
// 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
// not initiated by deadline
// transitionEnd not fired by inner elements
return;
}
const currentActive = activeRef.current;
let canEnd;
if (status === STATUS_APPEAR && currentActive) {
canEnd = onAppearEnd?.(element, event);
} else if (status === STATUS_ENTER && currentActive) {
canEnd = onEnterEnd?.(element, event);
} else if (status === STATUS_LEAVE && currentActive) {
canEnd = onLeaveEnd?.(element, event);
}
// Only update status when `canEnd` and not destroyed
if (currentActive && canEnd !== false) {
updateMotionEndStatus();
}
});
const [patchMotionEvents] = useDomMotionEvents(onInternalMotionEnd);
// ============================= Step =============================
const getEventHandlers = targetStatus => {
switch (targetStatus) {
case STATUS_APPEAR:
return {
[STEP_PREPARE]: onAppearPrepare,
[STEP_START]: onAppearStart,
[STEP_ACTIVE]: onAppearActive
};
case STATUS_ENTER:
return {
[STEP_PREPARE]: onEnterPrepare,
[STEP_START]: onEnterStart,
[STEP_ACTIVE]: onEnterActive
};
case STATUS_LEAVE:
return {
[STEP_PREPARE]: onLeavePrepare,
[STEP_START]: onLeaveStart,
[STEP_ACTIVE]: onLeaveActive
};
default:
return {};
}
};
const eventHandlers = React.useMemo(() => getEventHandlers(currentStatus), [currentStatus]);
const [startStep, step] = useStepQueue(currentStatus, !supportMotion, newStep => {
// Only prepare step can be skip
if (newStep === STEP_PREPARE) {
const onPrepare = eventHandlers[STEP_PREPARE];
if (!onPrepare) {
return SkipStep;
}
return onPrepare(getDomElement());
}
// Rest step is sync update
if (newStep in eventHandlers) {
setStyle([eventHandlers[newStep]?.(getDomElement(), null) || null, newStep]);
}
if (newStep === STEP_ACTIVE && currentStatus !== STATUS_NONE) {
// Patch events when motion needed
patchMotionEvents(getDomElement());
if (motionDeadline > 0) {
clearTimeout(deadlineRef.current);
deadlineRef.current = setTimeout(() => {
onInternalMotionEnd({
deadline: true
});
}, motionDeadline);
}
}
if (newStep === STEP_PREPARED) {
updateMotionEndStatus();
}
return DoStep;
});
const active = isActive(step);
activeRef.current = active;
// ============================ Status ============================
const visibleRef = useRef(null);
// Update with new status
useIsomorphicLayoutEffect(() => {
// When use Suspense, the `visible` will repeat trigger,
// But not real change of the `visible`, we need to skip it.
// https://github.com/ant-design/ant-design/issues/44379
if (mountedRef.current && visibleRef.current === visible) {
return;
}
setAsyncVisible(visible);
const isMounted = mountedRef.current;
mountedRef.current = true;
// if (!supportMotion) {
// return;
// }
let nextStatus;
// Appear
if (!isMounted && visible && motionAppear) {
nextStatus = STATUS_APPEAR;
}
// Enter
if (isMounted && visible && motionEnter) {
nextStatus = STATUS_ENTER;
}
// Leave
if (isMounted && !visible && motionLeave || !isMounted && motionLeaveImmediately && !visible && motionLeave) {
nextStatus = STATUS_LEAVE;
}
const nextEventHandlers = getEventHandlers(nextStatus);
// Update to next status
if (nextStatus && (supportMotion || nextEventHandlers[STEP_PREPARE])) {
setStatus(nextStatus);
startStep();
} else {
// Set back in case no motion but prev status has prepare step
setStatus(STATUS_NONE);
}
visibleRef.current = visible;
}, [visible]);
// ============================ Effect ============================
// Reset when motion changed
useEffect(() => {
if (
// Cancel appear
currentStatus === STATUS_APPEAR && !motionAppear ||
// Cancel enter
currentStatus === STATUS_ENTER && !motionEnter ||
// Cancel leave
currentStatus === STATUS_LEAVE && !motionLeave) {
setStatus(STATUS_NONE);
}
}, [motionAppear, motionEnter, motionLeave]);
useEffect(() => () => {
mountedRef.current = false;
clearTimeout(deadlineRef.current);
}, []);
// Trigger `onVisibleChanged`
const firstMountChangeRef = React.useRef(false);
useEffect(() => {
// [visible & motion not end] => [!visible & motion end] still need trigger onVisibleChanged
if (asyncVisible) {
firstMountChangeRef.current = true;
}
if (asyncVisible !== undefined && currentStatus === STATUS_NONE) {
// Skip first render is invisible since it's nothing changed
if (firstMountChangeRef.current || asyncVisible) {
onVisibleChanged?.(asyncVisible);
}
firstMountChangeRef.current = true;
}
}, [asyncVisible, currentStatus]);
// ============================ Styles ============================
let mergedStyle = style[0];
if (eventHandlers[STEP_PREPARE] && step === STEP_START) {
mergedStyle = {
transition: 'none',
...mergedStyle
};
}
const styleStep = style[1];
return [getStatus, step, mergedStyle, asyncVisible ?? visible,
// Appear Check
!mountedRef.current && currentStatus === STATUS_NONE && supportMotion && motionAppear ? 'NONE' :
// Enter or Leave check
step === STEP_START || step === STEP_ACTIVE ? styleStep === step : true];
}
@@ -0,0 +1,8 @@
import type { MotionStatus, StepStatus } from '../interface';
/** Skip current step */
export declare const SkipStep: false;
/** Current step should be update in */
export declare const DoStep: true;
export declare function isActive(step: StepStatus): boolean;
declare const _default: (status: MotionStatus, prepareOnly: boolean, callback: (step: StepStatus) => Promise<void> | void | typeof SkipStep | typeof DoStep) => [() => void, StepStatus];
export default _default;
@@ -0,0 +1,53 @@
import useState from "@rc-component/util/es/hooks/useState";
import * as React from 'react';
import { STEP_ACTIVATED, STEP_ACTIVE, STEP_NONE, STEP_PREPARE, STEP_PREPARED, STEP_START } from "../interface";
import useIsomorphicLayoutEffect from "./useIsomorphicLayoutEffect";
import useNextFrame from "./useNextFrame";
const FULL_STEP_QUEUE = [STEP_PREPARE, STEP_START, STEP_ACTIVE, STEP_ACTIVATED];
const SIMPLE_STEP_QUEUE = [STEP_PREPARE, STEP_PREPARED];
/** Skip current step */
export const SkipStep = false;
/** Current step should be update in */
export const DoStep = true;
export function isActive(step) {
return step === STEP_ACTIVE || step === STEP_ACTIVATED;
}
export default ((status, prepareOnly, callback) => {
const [step, setStep] = useState(STEP_NONE);
const [nextFrame, cancelNextFrame] = useNextFrame();
function startQueue() {
setStep(STEP_PREPARE, true);
}
const STEP_QUEUE = prepareOnly ? SIMPLE_STEP_QUEUE : FULL_STEP_QUEUE;
useIsomorphicLayoutEffect(() => {
if (step !== STEP_NONE && step !== STEP_ACTIVATED) {
const index = STEP_QUEUE.indexOf(step);
const nextStep = STEP_QUEUE[index + 1];
const result = callback(step);
if (result === SkipStep) {
// Skip when no needed
setStep(nextStep, true);
} else if (nextStep) {
// Do as frame for step update
nextFrame(info => {
function doNext() {
// Skip since current queue is ood
if (info.isCanceled()) return;
setStep(nextStep, true);
}
if (result === true) {
doNext();
} else {
// Only promise should be async
Promise.resolve(result).then(doNext);
}
});
}
}
}, [status, step]);
React.useEffect(() => () => {
cancelNextFrame();
}, []);
return [startQueue, step];
});
+9
View File
@@ -0,0 +1,9 @@
import type { CSSMotionProps } from './CSSMotion';
import CSSMotion from './CSSMotion';
import type { CSSMotionListProps } from './CSSMotionList';
import CSSMotionList from './CSSMotionList';
import type { MotionEndEventHandler, MotionEventHandler } from './interface';
export { default as Provider } from './context';
export { CSSMotionList };
export type { CSSMotionProps, CSSMotionListProps, MotionEventHandler, MotionEndEventHandler, };
export default CSSMotion;
+5
View File
@@ -0,0 +1,5 @@
import CSSMotion from "./CSSMotion";
import CSSMotionList from "./CSSMotionList";
export { default as Provider } from "./context";
export { CSSMotionList };
export default CSSMotion;
+23
View File
@@ -0,0 +1,23 @@
/// <reference types="react" />
export declare const STATUS_NONE: "none";
export declare const STATUS_APPEAR: "appear";
export declare const STATUS_ENTER: "enter";
export declare const STATUS_LEAVE: "leave";
export type MotionStatus = typeof STATUS_NONE | typeof STATUS_APPEAR | typeof STATUS_ENTER | typeof STATUS_LEAVE;
export declare const STEP_NONE: "none";
export declare const STEP_PREPARE: "prepare";
export declare const STEP_START: "start";
export declare const STEP_ACTIVE: "active";
export declare const STEP_ACTIVATED: "end";
/**
* Used for disabled motion case.
* Prepare stage will still work but start & active will be skipped.
*/
export declare const STEP_PREPARED: "prepared";
export type StepStatus = typeof STEP_NONE | typeof STEP_PREPARE | typeof STEP_START | typeof STEP_ACTIVE | typeof STEP_ACTIVATED | typeof STEP_PREPARED;
export type MotionEvent = (TransitionEvent | AnimationEvent) & {
deadline?: boolean;
};
export type MotionPrepareEventHandler = (element: HTMLElement) => Promise<any> | void;
export type MotionEventHandler = (element: HTMLElement, event: MotionEvent) => React.CSSProperties | void;
export type MotionEndEventHandler = (element: HTMLElement, event: MotionEvent) => boolean | void;
+14
View File
@@ -0,0 +1,14 @@
export const STATUS_NONE = 'none';
export const STATUS_APPEAR = 'appear';
export const STATUS_ENTER = 'enter';
export const STATUS_LEAVE = 'leave';
export const STEP_NONE = 'none';
export const STEP_PREPARE = 'prepare';
export const STEP_START = 'start';
export const STEP_ACTIVE = 'active';
export const STEP_ACTIVATED = 'end';
/**
* Used for disabled motion case.
* Prepare stage will still work but start & active will be skipped.
*/
export const STEP_PREPARED = 'prepared';
+21
View File
@@ -0,0 +1,21 @@
/// <reference types="react" />
export declare const STATUS_ADD: "add";
export declare const STATUS_KEEP: "keep";
export declare const STATUS_REMOVE: "remove";
export declare const STATUS_REMOVED: "removed";
export type DiffStatus = typeof STATUS_ADD | typeof STATUS_KEEP | typeof STATUS_REMOVE | typeof STATUS_REMOVED;
type RawKeyType = string | number;
export interface KeyObject {
key: RawKeyType;
status?: DiffStatus;
}
export declare function wrapKeyToObject(key: React.Key | KeyObject): {
key: string;
status?: DiffStatus;
};
export declare function parseKeys(keys?: any[]): {
key: string;
status?: DiffStatus;
}[];
export declare function diffKeys(prevKeys?: KeyObject[], currentKeys?: KeyObject[]): KeyObject[];
export {};
+97
View File
@@ -0,0 +1,97 @@
export const STATUS_ADD = 'add';
export const STATUS_KEEP = 'keep';
export const STATUS_REMOVE = 'remove';
export const STATUS_REMOVED = 'removed';
export function wrapKeyToObject(key) {
let keyObj;
if (key && typeof key === 'object' && 'key' in key) {
keyObj = key;
} else {
keyObj = {
key: key
};
}
return {
...keyObj,
key: String(keyObj.key)
};
}
export function parseKeys(keys = []) {
return keys.map(wrapKeyToObject);
}
export function diffKeys(prevKeys = [], currentKeys = []) {
let list = [];
let currentIndex = 0;
const currentLen = currentKeys.length;
const prevKeyObjects = parseKeys(prevKeys);
const currentKeyObjects = parseKeys(currentKeys);
// Check prev keys to insert or keep
prevKeyObjects.forEach(keyObj => {
let hit = false;
for (let i = currentIndex; i < currentLen; i += 1) {
const currentKeyObj = currentKeyObjects[i];
if (currentKeyObj.key === keyObj.key) {
// New added keys should add before current key
if (currentIndex < i) {
list = list.concat(currentKeyObjects.slice(currentIndex, i).map(obj => ({
...obj,
status: STATUS_ADD
})));
currentIndex = i;
}
list.push({
...currentKeyObj,
status: STATUS_KEEP
});
currentIndex += 1;
hit = true;
break;
}
}
// If not hit, it means key is removed
if (!hit) {
list.push({
...keyObj,
status: STATUS_REMOVE
});
}
});
// Add rest to the list
if (currentIndex < currentLen) {
list = list.concat(currentKeyObjects.slice(currentIndex).map(obj => ({
...obj,
status: STATUS_ADD
})));
}
/**
* Merge same key when it remove and add again:
* [1 - add, 2 - keep, 1 - remove] -> [1 - keep, 2 - keep]
*/
const keys = {};
list.forEach(({
key
}) => {
keys[key] = (keys[key] || 0) + 1;
});
const duplicatedKeys = Object.keys(keys).filter(key => keys[key] > 1);
duplicatedKeys.forEach(matchKey => {
// Remove `STATUS_REMOVE` node.
list = list.filter(({
key,
status
}) => key !== matchKey || status !== STATUS_REMOVE);
// Update `STATUS_ADD` to `STATUS_KEEP`
list.forEach(node => {
if (node.key === matchKey) {
// eslint-disable-next-line no-param-reassign
node.status = STATUS_KEEP;
}
});
});
return list;
}
+10
View File
@@ -0,0 +1,10 @@
import type { MotionName } from '../CSSMotion';
export declare function getVendorPrefixes(domSupport: boolean, win: object): {
animationend: Record<string, string>;
transitionend: Record<string, string>;
};
export declare function getVendorPrefixedEventName(eventName: string): any;
export declare const supportTransition: boolean;
export declare const animationEndName: any;
export declare const transitionEndName: any;
export declare function getTransitionName(transitionName: MotionName, transitionType: string): any;
+66
View File
@@ -0,0 +1,66 @@
import canUseDOM from "@rc-component/util/es/Dom/canUseDom";
// ================= Transition =================
// Event wrapper. Copy from react source code
function makePrefixMap(styleProp, eventName) {
const prefixes = {};
prefixes[styleProp.toLowerCase()] = eventName.toLowerCase();
prefixes[`Webkit${styleProp}`] = `webkit${eventName}`;
prefixes[`Moz${styleProp}`] = `moz${eventName}`;
prefixes[`ms${styleProp}`] = `MS${eventName}`;
prefixes[`O${styleProp}`] = `o${eventName.toLowerCase()}`;
return prefixes;
}
export function getVendorPrefixes(domSupport, win) {
const prefixes = {
animationend: makePrefixMap('Animation', 'AnimationEnd'),
transitionend: makePrefixMap('Transition', 'TransitionEnd')
};
if (domSupport) {
if (!('AnimationEvent' in win)) {
delete prefixes.animationend.animation;
}
if (!('TransitionEvent' in win)) {
delete prefixes.transitionend.transition;
}
}
return prefixes;
}
const vendorPrefixes = getVendorPrefixes(canUseDOM(), typeof window !== 'undefined' ? window : {});
let style = {};
if (canUseDOM()) {
({
style
} = document.createElement('div'));
}
const prefixedEventNames = {};
export function getVendorPrefixedEventName(eventName) {
if (prefixedEventNames[eventName]) {
return prefixedEventNames[eventName];
}
const prefixMap = vendorPrefixes[eventName];
if (prefixMap) {
const stylePropList = Object.keys(prefixMap);
const len = stylePropList.length;
for (let i = 0; i < len; i += 1) {
const styleProp = stylePropList[i];
if (Object.prototype.hasOwnProperty.call(prefixMap, styleProp) && styleProp in style) {
prefixedEventNames[eventName] = prefixMap[styleProp];
return prefixedEventNames[eventName];
}
}
}
return '';
}
const internalAnimationEndName = getVendorPrefixedEventName('animationend');
const internalTransitionEndName = getVendorPrefixedEventName('transitionend');
export const supportTransition = !!(internalAnimationEndName && internalTransitionEndName);
export const animationEndName = internalAnimationEndName || 'animationend';
export const transitionEndName = internalTransitionEndName || 'transitionend';
export function getTransitionName(transitionName, transitionType) {
if (!transitionName) return null;
if (typeof transitionName === 'object') {
const type = transitionType.replace(/-\w/g, match => match[1].toUpperCase());
return transitionName[type];
}
return `${transitionName}-${transitionType}`;
}