From d8405bef30d661496e6936c568be2cac0dc86225 Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Fri, 23 May 2025 19:30:20 +0800 Subject: [PATCH 1/4] =?UTF-8?q?refactor:=20=E5=BC=95=E5=85=A5=20css=20tran?= =?UTF-8?q?sition=20=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/packages/overlay/overlay.tsx | 2 +- src/utils/css-transition/CSSTransition.jsx | 376 +++++++++++ src/utils/css-transition/Transition.jsx | 619 ++++++++++++++++++ .../css-transition/TransitionGroupContext.jsx | 3 + src/utils/css-transition/config.js | 3 + src/utils/css-transition/index.js | 6 + src/utils/css-transition/utils/PropTypes.js | 33 + src/utils/css-transition/utils/reflow.js | 1 + 9 files changed, 1043 insertions(+), 1 deletion(-) create mode 100644 src/utils/css-transition/CSSTransition.jsx create mode 100644 src/utils/css-transition/Transition.jsx create mode 100644 src/utils/css-transition/TransitionGroupContext.jsx create mode 100644 src/utils/css-transition/config.js create mode 100644 src/utils/css-transition/index.js create mode 100644 src/utils/css-transition/utils/PropTypes.js create mode 100644 src/utils/css-transition/utils/reflow.js diff --git a/package.json b/package.json index 93d58ade94..bfc840934e 100644 --- a/package.json +++ b/package.json @@ -169,6 +169,7 @@ "axios": "^1.6.7", "cypress": "^13.15.0", "del": "^8.0.0", + "dom-helpers": "5.2.1", "eslint": "^8.56.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^9.1.0", diff --git a/src/packages/overlay/overlay.tsx b/src/packages/overlay/overlay.tsx index 98667d689b..56227fdc69 100644 --- a/src/packages/overlay/overlay.tsx +++ b/src/packages/overlay/overlay.tsx @@ -5,8 +5,8 @@ import React, { useRef, useState, } from 'react' -import { CSSTransition } from 'react-transition-group' import classNames from 'classnames' +import CSSTransition from '@/utils/css-transition/CSSTransition' import { ComponentDefaults } from '@/utils/typings' import { useLockScroll } from '@/hooks/use-lock-scroll' import { WebOverlayProps } from '@/types' diff --git a/src/utils/css-transition/CSSTransition.jsx b/src/utils/css-transition/CSSTransition.jsx new file mode 100644 index 0000000000..3f2708eee5 --- /dev/null +++ b/src/utils/css-transition/CSSTransition.jsx @@ -0,0 +1,376 @@ +import addOneClass from 'dom-helpers/addClass' + +import removeOneClass from 'dom-helpers/removeClass' +import React from 'react' + +import Transition from './Transition' +import { forceReflow } from './utils/reflow' + +const addClass = (node, classes) => + node && classes && classes.split(' ').forEach((c) => addOneClass(node, c)) +const removeClass = (node, classes) => + node && classes && classes.split(' ').forEach((c) => removeOneClass(node, c)) + +/** + * A transition component inspired by the excellent + * [ng-animate](https://docs.angularjs.org/api/ngAnimate) library, you should + * use it if you're using CSS transitions or animations. It's built upon the + * [`Transition`](https://reactcommunity.org/react-transition-group/transition) + * component, so it inherits all of its props. + * + * `CSSTransition` applies a pair of class names during the `appear`, `enter`, + * and `exit` states of the transition. The first class is applied and then a + * second `*-active` class in order to activate the CSS transition. After the + * transition, matching `*-done` class names are applied to persist the + * transition state. + * + * ```jsx + * function App() { + * const [inProp, setInProp] = useState(false); + * const nodeRef = useRef(null); + * return ( + *
+ * + *
+ * {"I'll receive my-node-* classes"} + *
+ *
+ * + *
+ * ); + * } + * ``` + * + * When the `in` prop is set to `true`, the child component will first receive + * the class `example-enter`, then the `example-enter-active` will be added in + * the next tick. `CSSTransition` [forces a + * reflow](https://github.com/reactjs/react-transition-group/blob/5007303e729a74be66a21c3e2205e4916821524b/src/CSSTransition.js#L208-L215) + * between before adding the `example-enter-active`. This is an important trick + * because it allows us to transition between `example-enter` and + * `example-enter-active` even though they were added immediately one after + * another. Most notably, this is what makes it possible for us to animate + * _appearance_. + * + * ```css + * .my-node-enter { + * opacity: 0; + * } + * .my-node-enter-active { + * opacity: 1; + * transition: opacity 200ms; + * } + * .my-node-exit { + * opacity: 1; + * } + * .my-node-exit-active { + * opacity: 0; + * transition: opacity 200ms; + * } + * ``` + * + * `*-active` classes represent which styles you want to animate **to**, so it's + * important to add `transition` declaration only to them, otherwise transitions + * might not behave as intended! This might not be obvious when the transitions + * are symmetrical, i.e. when `*-enter-active` is the same as `*-exit`, like in + * the example above (minus `transition`), but it becomes apparent in more + * complex transitions. + * + * **Note**: If you're using the + * [`appear`](http://reactcommunity.org/react-transition-group/transition#Transition-prop-appear) + * prop, make sure to define styles for `.appear-*` classes as well. + */ +class CSSTransition extends React.Component { + static defaultProps = { + classNames: '', + } + + appliedClasses = { + appear: {}, + enter: {}, + exit: {}, + } + + onEnter = (maybeNode, maybeAppearing) => { + const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing) + this.removeClasses(node, 'exit') + this.addClass(node, appearing ? 'appear' : 'enter', 'base') + + if (this.props.onEnter) { + this.props.onEnter(maybeNode, maybeAppearing) + } + } + + onEntering = (maybeNode, maybeAppearing) => { + const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing) + const type = appearing ? 'appear' : 'enter' + this.addClass(node, type, 'active') + + if (this.props.onEntering) { + this.props.onEntering(maybeNode, maybeAppearing) + } + } + + onEntered = (maybeNode, maybeAppearing) => { + const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing) + const type = appearing ? 'appear' : 'enter' + this.removeClasses(node, type) + this.addClass(node, type, 'done') + + if (this.props.onEntered) { + this.props.onEntered(maybeNode, maybeAppearing) + } + } + + onExit = (maybeNode) => { + const [node] = this.resolveArguments(maybeNode) + this.removeClasses(node, 'appear') + this.removeClasses(node, 'enter') + this.addClass(node, 'exit', 'base') + + if (this.props.onExit) { + this.props.onExit(maybeNode) + } + } + + onExiting = (maybeNode) => { + const [node] = this.resolveArguments(maybeNode) + this.addClass(node, 'exit', 'active') + + if (this.props.onExiting) { + this.props.onExiting(maybeNode) + } + } + + onExited = (maybeNode) => { + const [node] = this.resolveArguments(maybeNode) + this.removeClasses(node, 'exit') + this.addClass(node, 'exit', 'done') + + if (this.props.onExited) { + this.props.onExited(maybeNode) + } + } + + // when prop `nodeRef` is provided `node` is excluded + resolveArguments = (maybeNode, maybeAppearing) => + this.props.nodeRef + ? [this.props.nodeRef.current, maybeNode] // here `maybeNode` is actually `appearing` + : [maybeNode, maybeAppearing] // `findDOMNode` was used + + getClassNames = (type) => { + const { classNames } = this.props + const isStringClassNames = typeof classNames === 'string' + const prefix = isStringClassNames && classNames ? `${classNames}-` : '' + + const baseClassName = isStringClassNames + ? `${prefix}${type}` + : classNames[type] + + const activeClassName = isStringClassNames + ? `${baseClassName}-active` + : classNames[`${type}Active`] + + const doneClassName = isStringClassNames + ? `${baseClassName}-done` + : classNames[`${type}Done`] + + return { + baseClassName, + activeClassName, + doneClassName, + } + } + + addClass(node, type, phase) { + let className = this.getClassNames(type)[`${phase}ClassName`] + const { doneClassName } = this.getClassNames('enter') + + if (type === 'appear' && phase === 'done' && doneClassName) { + className += ` ${doneClassName}` + } + + // This is to force a repaint, + // which is necessary in order to transition styles when adding a class name. + if (phase === 'active') { + if (node) forceReflow(node) + } + + if (className) { + this.appliedClasses[type][phase] = className + addClass(node, className) + } + } + + removeClasses(node, type) { + const { + base: baseClassName, + active: activeClassName, + done: doneClassName, + } = this.appliedClasses[type] + + this.appliedClasses[type] = {} + + if (baseClassName) { + removeClass(node, baseClassName) + } + if (activeClassName) { + removeClass(node, activeClassName) + } + if (doneClassName) { + removeClass(node, doneClassName) + } + } + + render() { + const { classNames: _, ...props } = this.props + + return ( + + ) + } +} + +// CSSTransition.propTypes = { +// ...Transition.propTypes, + +// /** +// * The animation classNames applied to the component as it appears, enters, +// * exits or has finished the transition. A single name can be provided, which +// * will be suffixed for each stage, e.g. `classNames="fade"` applies: +// * +// * - `fade-appear`, `fade-appear-active`, `fade-appear-done` +// * - `fade-enter`, `fade-enter-active`, `fade-enter-done` +// * - `fade-exit`, `fade-exit-active`, `fade-exit-done` +// * +// * A few details to note about how these classes are applied: +// * +// * 1. They are _joined_ with the ones that are already defined on the child +// * component, so if you want to add some base styles, you can use +// * `className` without worrying that it will be overridden. +// * +// * 2. If the transition component mounts with `in={false}`, no classes are +// * applied yet. You might be expecting `*-exit-done`, but if you think +// * about it, a component cannot finish exiting if it hasn't entered yet. +// * +// * 2. `fade-appear-done` and `fade-enter-done` will _both_ be applied. This +// * allows you to define different behavior for when appearing is done and +// * when regular entering is done, using selectors like +// * `.fade-enter-done:not(.fade-appear-done)`. For example, you could apply +// * an epic entrance animation when element first appears in the DOM using +// * [Animate.css](https://daneden.github.io/animate.css/). Otherwise you can +// * simply use `fade-enter-done` for defining both cases. +// * +// * Each individual classNames can also be specified independently like: +// * +// * ```js +// * classNames={{ +// * appear: 'my-appear', +// * appearActive: 'my-active-appear', +// * appearDone: 'my-done-appear', +// * enter: 'my-enter', +// * enterActive: 'my-active-enter', +// * enterDone: 'my-done-enter', +// * exit: 'my-exit', +// * exitActive: 'my-active-exit', +// * exitDone: 'my-done-exit', +// * }} +// * ``` +// * +// * If you want to set these classes using CSS Modules: +// * +// * ```js +// * import styles from './styles.css'; +// * ``` +// * +// * you might want to use camelCase in your CSS file, that way could simply +// * spread them instead of listing them one by one: +// * +// * ```js +// * classNames={{ ...styles }} +// * ``` +// * +// * @type {string | { +// * appear?: string, +// * appearActive?: string, +// * appearDone?: string, +// * enter?: string, +// * enterActive?: string, +// * enterDone?: string, +// * exit?: string, +// * exitActive?: string, +// * exitDone?: string, +// * }} +// */ +// classNames: classNamesShape, + +// /** +// * A `` callback fired immediately after the 'enter' or 'appear' class is +// * applied. +// * +// * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. +// * +// * @type Function(node: HtmlElement, isAppearing: bool) +// */ +// onEnter: PropTypes.func, + +// /** +// * A `` callback fired immediately after the 'enter-active' or +// * 'appear-active' class is applied. +// * +// * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. +// * +// * @type Function(node: HtmlElement, isAppearing: bool) +// */ +// onEntering: PropTypes.func, + +// /** +// * A `` callback fired immediately after the 'enter' or +// * 'appear' classes are **removed** and the `done` class is added to the DOM node. +// * +// * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. +// * +// * @type Function(node: HtmlElement, isAppearing: bool) +// */ +// onEntered: PropTypes.func, + +// /** +// * A `` callback fired immediately after the 'exit' class is +// * applied. +// * +// * **Note**: when `nodeRef` prop is passed, `node` is not passed +// * +// * @type Function(node: HtmlElement) +// */ +// onExit: PropTypes.func, + +// /** +// * A `` callback fired immediately after the 'exit-active' is applied. +// * +// * **Note**: when `nodeRef` prop is passed, `node` is not passed +// * +// * @type Function(node: HtmlElement) +// */ +// onExiting: PropTypes.func, + +// /** +// * A `` callback fired immediately after the 'exit' classes +// * are **removed** and the `exit-done` class is added to the DOM node. +// * +// * **Note**: when `nodeRef` prop is passed, `node` is not passed +// * +// * @type Function(node: HtmlElement) +// */ +// onExited: PropTypes.func, +// } + +export default CSSTransition diff --git a/src/utils/css-transition/Transition.jsx b/src/utils/css-transition/Transition.jsx new file mode 100644 index 0000000000..5050b8c085 --- /dev/null +++ b/src/utils/css-transition/Transition.jsx @@ -0,0 +1,619 @@ +import React from 'react' +import ReactDOM from 'react-dom' + +import config from './config' +// import { timeoutsShape } from './utils/PropTypes' +import TransitionGroupContext from './TransitionGroupContext' +import { forceReflow } from './utils/reflow' + +export const UNMOUNTED = 'unmounted' +export const EXITED = 'exited' +export const ENTERING = 'entering' +export const ENTERED = 'entered' +export const EXITING = 'exiting' + +/** + * The Transition component lets you describe a transition from one component + * state to another _over time_ with a simple declarative API. Most commonly + * it's used to animate the mounting and unmounting of a component, but can also + * be used to describe in-place transition states as well. + * + * --- + * + * **Note**: `Transition` is a platform-agnostic base component. If you're using + * transitions in CSS, you'll probably want to use + * [`CSSTransition`](https://reactcommunity.org/react-transition-group/css-transition) + * instead. It inherits all the features of `Transition`, but contains + * additional features necessary to play nice with CSS transitions (hence the + * name of the component). + * + * --- + * + * By default the `Transition` component does not alter the behavior of the + * component it renders, it only tracks "enter" and "exit" states for the + * components. It's up to you to give meaning and effect to those states. For + * example we can add styles to a component when it enters or exits: + * + * ```jsx + * import { Transition } from 'react-transition-group'; + * import { useRef } from 'react'; + * + * const duration = 300; + * + * const defaultStyle = { + * transition: `opacity ${duration}ms ease-in-out`, + * opacity: 0, + * } + * + * const transitionStyles = { + * entering: { opacity: 1 }, + * entered: { opacity: 1 }, + * exiting: { opacity: 0 }, + * exited: { opacity: 0 }, + * }; + * + * function Fade({ in: inProp }) { + * const nodeRef = useRef(null); + * return ( + * + * {state => ( + *
+ * I'm a fade Transition! + *
+ * )} + *
+ * ); + * } + * ``` + * + * There are 4 main states a Transition can be in: + * - `'entering'` + * - `'entered'` + * - `'exiting'` + * - `'exited'` + * + * Transition state is toggled via the `in` prop. When `true` the component + * begins the "Enter" stage. During this stage, the component will shift from + * its current transition state, to `'entering'` for the duration of the + * transition and then to the `'entered'` stage once it's complete. Let's take + * the following example (we'll use the + * [useState](https://reactjs.org/docs/hooks-reference.html#usestate) hook): + * + * ```jsx + * import { Transition } from 'react-transition-group'; + * import { useState, useRef } from 'react'; + * + * function App() { + * const [inProp, setInProp] = useState(false); + * const nodeRef = useRef(null); + * return ( + *
+ * + * {state => ( + * // ... + * )} + * + * + *
+ * ); + * } + * ``` + * + * When the button is clicked the component will shift to the `'entering'` state + * and stay there for 500ms (the value of `timeout`) before it finally switches + * to `'entered'`. + * + * When `in` is `false` the same thing happens except the state moves from + * `'exiting'` to `'exited'`. + */ +class Transition extends React.Component { + static contextType = TransitionGroupContext + + constructor(props, context) { + super(props, context) + + const parentGroup = context + // In the context of a TransitionGroup all enters are really appears + const appear = + parentGroup && !parentGroup.isMounting ? props.enter : props.appear + + let initialStatus + + this.appearStatus = null + + if (props.in) { + if (appear) { + initialStatus = EXITED + this.appearStatus = ENTERING + } else { + initialStatus = ENTERED + } + } else if (props.unmountOnExit || props.mountOnEnter) { + initialStatus = UNMOUNTED + } else { + initialStatus = EXITED + } + + this.state = { status: initialStatus } + + this.nextCallback = null + } + + static getDerivedStateFromProps({ in: nextIn }, prevState) { + if (nextIn && prevState.status === UNMOUNTED) { + return { status: EXITED } + } + return null + } + + // getSnapshotBeforeUpdate(prevProps) { + // let nextStatus = null + + // if (prevProps !== this.props) { + // const { status } = this.state + + // if (this.props.in) { + // if (status !== ENTERING && status !== ENTERED) { + // nextStatus = ENTERING + // } + // } else { + // if (status === ENTERING || status === ENTERED) { + // nextStatus = EXITING + // } + // } + // } + + // return { nextStatus } + // } + + componentDidMount() { + this.updateStatus(true, this.appearStatus) + } + + componentDidUpdate(prevProps) { + let nextStatus = null + if (prevProps !== this.props) { + const { status } = this.state + + if (this.props.in) { + if (status !== ENTERING && status !== ENTERED) { + nextStatus = ENTERING + } + } else if (status === ENTERING || status === ENTERED) { + nextStatus = EXITING + } + } + this.updateStatus(false, nextStatus) + } + + componentWillUnmount() { + this.cancelNextCallback() + } + + getTimeouts() { + const { timeout } = this.props + let exit + let enter + let appear + + exit = enter = appear = timeout + + if (timeout != null && typeof timeout !== 'number') { + exit = timeout.exit + enter = timeout.enter + // TODO: remove fallback for next major + appear = timeout.appear !== undefined ? timeout.appear : enter + } + return { exit, enter, appear } + } + + updateStatus(mounting = false, nextStatus) { + if (nextStatus !== null) { + // nextStatus will always be ENTERING or EXITING. + this.cancelNextCallback() + + if (nextStatus === ENTERING) { + if (this.props.unmountOnExit || this.props.mountOnEnter) { + const node = this.props.nodeRef + ? this.props.nodeRef.current + : ReactDOM.findDOMNode(this) + // https://github.com/reactjs/react-transition-group/pull/749 + // With unmountOnExit or mountOnEnter, the enter animation should happen at the transition between `exited` and `entering`. + // To make the animation happen, we have to separate each rendering and avoid being processed as batched. + if (node) forceReflow(node) + } + this.performEnter(mounting) + } else { + this.performExit() + } + } else if (this.props.unmountOnExit && this.state.status === EXITED) { + this.setState({ status: UNMOUNTED }) + } + } + + performEnter(mounting) { + const { enter } = this.props + const appearing = this.context ? this.context.isMounting : mounting + const [maybeNode, maybeAppearing] = this.props.nodeRef + ? [appearing] + : [ReactDOM.findDOMNode(this), appearing] + + const timeouts = this.getTimeouts() + const enterTimeout = appearing ? timeouts.appear : timeouts.enter + // no enter animation skip right to ENTERED + // if we are mounting and running this it means appear _must_ be set + if ((!mounting && !enter) || config.disabled) { + this.safeSetState({ status: ENTERED }, () => { + this.props.onEntered(maybeNode) + }) + return + } + + this.props.onEnter(maybeNode, maybeAppearing) + + this.safeSetState({ status: ENTERING }, () => { + this.props.onEntering(maybeNode, maybeAppearing) + + this.onTransitionEnd(enterTimeout, () => { + this.safeSetState({ status: ENTERED }, () => { + this.props.onEntered(maybeNode, maybeAppearing) + }) + }) + }) + } + + performExit() { + const { exit } = this.props + const timeouts = this.getTimeouts() + const maybeNode = this.props.nodeRef + ? undefined + : ReactDOM.findDOMNode(this) + + // no exit animation skip right to EXITED + if (!exit || config.disabled) { + this.safeSetState({ status: EXITED }, () => { + this.props.onExited(maybeNode) + }) + return + } + + this.props.onExit(maybeNode) + + this.safeSetState({ status: EXITING }, () => { + this.props.onExiting(maybeNode) + + this.onTransitionEnd(timeouts.exit, () => { + this.safeSetState({ status: EXITED }, () => { + this.props.onExited(maybeNode) + }) + }) + }) + } + + cancelNextCallback() { + if (this.nextCallback !== null) { + this.nextCallback.cancel() + this.nextCallback = null + } + } + + safeSetState(nextState, callback) { + // This shouldn't be necessary, but there are weird race conditions with + // setState callbacks and unmounting in testing, so always make sure that + // we can cancel any pending setState callbacks after we unmount. + callback = this.setNextCallback(callback) + this.setState(nextState, callback) + } + + setNextCallback(callback) { + let active = true + + this.nextCallback = (event) => { + if (active) { + active = false + this.nextCallback = null + + callback(event) + } + } + + this.nextCallback.cancel = () => { + active = false + } + + return this.nextCallback + } + + onTransitionEnd(timeout, handler) { + this.setNextCallback(handler) + const node = this.props.nodeRef + ? this.props.nodeRef.current + : ReactDOM.findDOMNode(this) + + const doesNotHaveTimeoutOrListener = + timeout == null && !this.props.addEndListener + if (!node || doesNotHaveTimeoutOrListener) { + setTimeout(this.nextCallback, 0) + return + } + + if (this.props.addEndListener) { + const [maybeNode, maybeNextCallback] = this.props.nodeRef + ? [this.nextCallback] + : [node, this.nextCallback] + this.props.addEndListener(maybeNode, maybeNextCallback) + } + + if (timeout != null) { + setTimeout(this.nextCallback, timeout) + } + } + + render() { + const status = this.state.status + + if (status === UNMOUNTED) { + return null + } + + const { + children, + // filter props for `Transition` + in: _in, + mountOnEnter: _mountOnEnter, + unmountOnExit: _unmountOnExit, + appear: _appear, + enter: _enter, + exit: _exit, + timeout: _timeout, + addEndListener: _addEndListener, + onEnter: _onEnter, + onEntering: _onEntering, + onEntered: _onEntered, + onExit: _onExit, + onExiting: _onExiting, + onExited: _onExited, + nodeRef: _nodeRef, + ...childProps + } = this.props + + return ( + // allows for nested Transitions + + {typeof children === 'function' + ? children(status, childProps) + : React.cloneElement(React.Children.only(children), childProps)} + + ) + } +} + +// Transition.propTypes = { +// /** +// * A React reference to the DOM element that needs to transition: +// * https://stackoverflow.com/a/51127130/4671932 +// * +// * - This prop is optional, but recommended in order to avoid defaulting to +// * [`ReactDOM.findDOMNode`](https://reactjs.org/docs/react-dom.html#finddomnode), +// * which is deprecated in `StrictMode` +// * - When `nodeRef` prop is used, `node` is not passed to callback functions +// * (e.g. `onEnter`) because user already has direct access to the node. +// * - When changing `key` prop of `Transition` in a `TransitionGroup` a new +// * `nodeRef` need to be provided to `Transition` with changed `key` prop +// * (see +// * [test/CSSTransition-test.js](https://github.com/reactjs/react-transition-group/blob/13435f897b3ab71f6e19d724f145596f5910581c/test/CSSTransition-test.js#L362-L437)). +// */ +// nodeRef: PropTypes.shape({ +// current: +// typeof Element === 'undefined' +// ? PropTypes.any +// : (propValue, key, componentName, location, propFullName, secret) => { +// const value = propValue[key] + +// return PropTypes.instanceOf( +// value && 'ownerDocument' in value +// ? value.ownerDocument.defaultView.Element +// : Element +// )(propValue, key, componentName, location, propFullName, secret) +// }, +// }), + +// /** +// * A `function` child can be used instead of a React element. This function is +// * called with the current transition status (`'entering'`, `'entered'`, +// * `'exiting'`, `'exited'`), which can be used to apply context +// * specific props to a component. +// * +// * ```jsx +// * +// * {state => ( +// * +// * )} +// * +// * ``` +// */ +// children: PropTypes.oneOfType([ +// PropTypes.func.isRequired, +// PropTypes.element.isRequired, +// ]).isRequired, + +// /** +// * Show the component; triggers the enter or exit states +// */ +// in: PropTypes.bool, + +// /** +// * By default the child component is mounted immediately along with +// * the parent `Transition` component. If you want to "lazy mount" the component on the +// * first `in={true}` you can set `mountOnEnter`. After the first enter transition the component will stay +// * mounted, even on "exited", unless you also specify `unmountOnExit`. +// */ +// mountOnEnter: PropTypes.bool, + +// /** +// * By default the child component stays mounted after it reaches the `'exited'` state. +// * Set `unmountOnExit` if you'd prefer to unmount the component after it finishes exiting. +// */ +// unmountOnExit: PropTypes.bool, + +// /** +// * By default the child component does not perform the enter transition when +// * it first mounts, regardless of the value of `in`. If you want this +// * behavior, set both `appear` and `in` to `true`. +// * +// * > **Note**: there are no special appear states like `appearing`/`appeared`, this prop +// * > only adds an additional enter transition. However, in the +// * > `` component that first enter transition does result in +// * > additional `.appear-*` classes, that way you can choose to style it +// * > differently. +// */ +// appear: PropTypes.bool, + +// /** +// * Enable or disable enter transitions. +// */ +// enter: PropTypes.bool, + +// /** +// * Enable or disable exit transitions. +// */ +// exit: PropTypes.bool, + +// /** +// * The duration of the transition, in milliseconds. +// * Required unless `addEndListener` is provided. +// * +// * You may specify a single timeout for all transitions: +// * +// * ```jsx +// * timeout={500} +// * ``` +// * +// * or individually: +// * +// * ```jsx +// * timeout={{ +// * appear: 500, +// * enter: 300, +// * exit: 500, +// * }} +// * ``` +// * +// * - `appear` defaults to the value of `enter` +// * - `enter` defaults to `0` +// * - `exit` defaults to `0` +// * +// * @type {number | { enter?: number, exit?: number, appear?: number }} +// */ +// timeout: (props, ...args) => { +// let pt = timeoutsShape +// if (!props.addEndListener) pt = pt.isRequired +// return pt(props, ...args) +// }, + +// /** +// * Add a custom transition end trigger. Called with the transitioning +// * DOM node and a `done` callback. Allows for more fine grained transition end +// * logic. Timeouts are still used as a fallback if provided. +// * +// * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `done` is being passed as the first argument. +// * +// * ```jsx +// * addEndListener={(node, done) => { +// * // use the css transitionend event to mark the finish of a transition +// * node.addEventListener('transitionend', done, false); +// * }} +// * ``` +// */ +// addEndListener: PropTypes.func, + +// /** +// * Callback fired before the "entering" status is applied. An extra parameter +// * `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount +// * +// * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. +// * +// * @type Function(node: HtmlElement, isAppearing: bool) -> void +// */ +// onEnter: PropTypes.func, + +// /** +// * Callback fired after the "entering" status is applied. An extra parameter +// * `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount +// * +// * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. +// * +// * @type Function(node: HtmlElement, isAppearing: bool) +// */ +// onEntering: PropTypes.func, + +// /** +// * Callback fired after the "entered" status is applied. An extra parameter +// * `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount +// * +// * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. +// * +// * @type Function(node: HtmlElement, isAppearing: bool) -> void +// */ +// onEntered: PropTypes.func, + +// /** +// * Callback fired before the "exiting" status is applied. +// * +// * **Note**: when `nodeRef` prop is passed, `node` is not passed. +// * +// * @type Function(node: HtmlElement) -> void +// */ +// onExit: PropTypes.func, + +// /** +// * Callback fired after the "exiting" status is applied. +// * +// * **Note**: when `nodeRef` prop is passed, `node` is not passed. +// * +// * @type Function(node: HtmlElement) -> void +// */ +// onExiting: PropTypes.func, + +// /** +// * Callback fired after the "exited" status is applied. +// * +// * **Note**: when `nodeRef` prop is passed, `node` is not passed +// * +// * @type Function(node: HtmlElement) -> void +// */ +// onExited: PropTypes.func, +// } + +// Name the function so it is clearer in the documentation +function noop() {} + +Transition.defaultProps = { + in: false, + mountOnEnter: false, + unmountOnExit: false, + appear: false, + enter: true, + exit: true, + + onEnter: noop, + onEntering: noop, + onEntered: noop, + + onExit: noop, + onExiting: noop, + onExited: noop, +} + +Transition.UNMOUNTED = UNMOUNTED +Transition.EXITED = EXITED +Transition.ENTERING = ENTERING +Transition.ENTERED = ENTERED +Transition.EXITING = EXITING + +export default Transition diff --git a/src/utils/css-transition/TransitionGroupContext.jsx b/src/utils/css-transition/TransitionGroupContext.jsx new file mode 100644 index 0000000000..8bd924bdf2 --- /dev/null +++ b/src/utils/css-transition/TransitionGroupContext.jsx @@ -0,0 +1,3 @@ +import React from 'react' + +export default React.createContext(null) diff --git a/src/utils/css-transition/config.js b/src/utils/css-transition/config.js new file mode 100644 index 0000000000..a726e96eab --- /dev/null +++ b/src/utils/css-transition/config.js @@ -0,0 +1,3 @@ +export default { + disabled: false, +} diff --git a/src/utils/css-transition/index.js b/src/utils/css-transition/index.js new file mode 100644 index 0000000000..8e11afca16 --- /dev/null +++ b/src/utils/css-transition/index.js @@ -0,0 +1,6 @@ +export { default as CSSTransition } from './CSSTransition' +export { default as ReplaceTransition } from './ReplaceTransition' +export { default as SwitchTransition } from './SwitchTransition' +export { default as TransitionGroup } from './TransitionGroup' +export { default as Transition } from './Transition' +export { default as config } from './config' diff --git a/src/utils/css-transition/utils/PropTypes.js b/src/utils/css-transition/utils/PropTypes.js new file mode 100644 index 0000000000..a38b6b73c7 --- /dev/null +++ b/src/utils/css-transition/utils/PropTypes.js @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types' + +export const timeoutsShape = + process.env.NODE_ENV !== 'production' + ? PropTypes.oneOfType([ + PropTypes.number, + PropTypes.shape({ + enter: PropTypes.number, + exit: PropTypes.number, + appear: PropTypes.number, + }).isRequired, + ]) + : null + +export const classNamesShape = + process.env.NODE_ENV !== 'production' + ? PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({ + enter: PropTypes.string, + exit: PropTypes.string, + active: PropTypes.string, + }), + PropTypes.shape({ + enter: PropTypes.string, + enterDone: PropTypes.string, + enterActive: PropTypes.string, + exit: PropTypes.string, + exitDone: PropTypes.string, + exitActive: PropTypes.string, + }), + ]) + : null diff --git a/src/utils/css-transition/utils/reflow.js b/src/utils/css-transition/utils/reflow.js new file mode 100644 index 0000000000..cf7e19a057 --- /dev/null +++ b/src/utils/css-transition/utils/reflow.js @@ -0,0 +1 @@ +export const forceReflow = (node) => node.scrollTop From afc564573f678a244ab2c8be1c1b7094cf5d4cc8 Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Sat, 24 May 2025 19:56:22 +0800 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=20transition?= =?UTF-8?q?=20=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/css-transition/CSSTransition.jsx | 376 ----------- src/utils/css-transition/CSSTransition.tsx | 197 ++++++ src/utils/css-transition/Transition.jsx | 619 ------------------ src/utils/css-transition/Transition.tsx | 285 ++++++++ ...Context.jsx => TransitionGroupContext.tsx} | 0 5 files changed, 482 insertions(+), 995 deletions(-) delete mode 100644 src/utils/css-transition/CSSTransition.jsx create mode 100644 src/utils/css-transition/CSSTransition.tsx delete mode 100644 src/utils/css-transition/Transition.jsx create mode 100644 src/utils/css-transition/Transition.tsx rename src/utils/css-transition/{TransitionGroupContext.jsx => TransitionGroupContext.tsx} (100%) diff --git a/src/utils/css-transition/CSSTransition.jsx b/src/utils/css-transition/CSSTransition.jsx deleted file mode 100644 index 3f2708eee5..0000000000 --- a/src/utils/css-transition/CSSTransition.jsx +++ /dev/null @@ -1,376 +0,0 @@ -import addOneClass from 'dom-helpers/addClass' - -import removeOneClass from 'dom-helpers/removeClass' -import React from 'react' - -import Transition from './Transition' -import { forceReflow } from './utils/reflow' - -const addClass = (node, classes) => - node && classes && classes.split(' ').forEach((c) => addOneClass(node, c)) -const removeClass = (node, classes) => - node && classes && classes.split(' ').forEach((c) => removeOneClass(node, c)) - -/** - * A transition component inspired by the excellent - * [ng-animate](https://docs.angularjs.org/api/ngAnimate) library, you should - * use it if you're using CSS transitions or animations. It's built upon the - * [`Transition`](https://reactcommunity.org/react-transition-group/transition) - * component, so it inherits all of its props. - * - * `CSSTransition` applies a pair of class names during the `appear`, `enter`, - * and `exit` states of the transition. The first class is applied and then a - * second `*-active` class in order to activate the CSS transition. After the - * transition, matching `*-done` class names are applied to persist the - * transition state. - * - * ```jsx - * function App() { - * const [inProp, setInProp] = useState(false); - * const nodeRef = useRef(null); - * return ( - *
- * - *
- * {"I'll receive my-node-* classes"} - *
- *
- * - *
- * ); - * } - * ``` - * - * When the `in` prop is set to `true`, the child component will first receive - * the class `example-enter`, then the `example-enter-active` will be added in - * the next tick. `CSSTransition` [forces a - * reflow](https://github.com/reactjs/react-transition-group/blob/5007303e729a74be66a21c3e2205e4916821524b/src/CSSTransition.js#L208-L215) - * between before adding the `example-enter-active`. This is an important trick - * because it allows us to transition between `example-enter` and - * `example-enter-active` even though they were added immediately one after - * another. Most notably, this is what makes it possible for us to animate - * _appearance_. - * - * ```css - * .my-node-enter { - * opacity: 0; - * } - * .my-node-enter-active { - * opacity: 1; - * transition: opacity 200ms; - * } - * .my-node-exit { - * opacity: 1; - * } - * .my-node-exit-active { - * opacity: 0; - * transition: opacity 200ms; - * } - * ``` - * - * `*-active` classes represent which styles you want to animate **to**, so it's - * important to add `transition` declaration only to them, otherwise transitions - * might not behave as intended! This might not be obvious when the transitions - * are symmetrical, i.e. when `*-enter-active` is the same as `*-exit`, like in - * the example above (minus `transition`), but it becomes apparent in more - * complex transitions. - * - * **Note**: If you're using the - * [`appear`](http://reactcommunity.org/react-transition-group/transition#Transition-prop-appear) - * prop, make sure to define styles for `.appear-*` classes as well. - */ -class CSSTransition extends React.Component { - static defaultProps = { - classNames: '', - } - - appliedClasses = { - appear: {}, - enter: {}, - exit: {}, - } - - onEnter = (maybeNode, maybeAppearing) => { - const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing) - this.removeClasses(node, 'exit') - this.addClass(node, appearing ? 'appear' : 'enter', 'base') - - if (this.props.onEnter) { - this.props.onEnter(maybeNode, maybeAppearing) - } - } - - onEntering = (maybeNode, maybeAppearing) => { - const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing) - const type = appearing ? 'appear' : 'enter' - this.addClass(node, type, 'active') - - if (this.props.onEntering) { - this.props.onEntering(maybeNode, maybeAppearing) - } - } - - onEntered = (maybeNode, maybeAppearing) => { - const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing) - const type = appearing ? 'appear' : 'enter' - this.removeClasses(node, type) - this.addClass(node, type, 'done') - - if (this.props.onEntered) { - this.props.onEntered(maybeNode, maybeAppearing) - } - } - - onExit = (maybeNode) => { - const [node] = this.resolveArguments(maybeNode) - this.removeClasses(node, 'appear') - this.removeClasses(node, 'enter') - this.addClass(node, 'exit', 'base') - - if (this.props.onExit) { - this.props.onExit(maybeNode) - } - } - - onExiting = (maybeNode) => { - const [node] = this.resolveArguments(maybeNode) - this.addClass(node, 'exit', 'active') - - if (this.props.onExiting) { - this.props.onExiting(maybeNode) - } - } - - onExited = (maybeNode) => { - const [node] = this.resolveArguments(maybeNode) - this.removeClasses(node, 'exit') - this.addClass(node, 'exit', 'done') - - if (this.props.onExited) { - this.props.onExited(maybeNode) - } - } - - // when prop `nodeRef` is provided `node` is excluded - resolveArguments = (maybeNode, maybeAppearing) => - this.props.nodeRef - ? [this.props.nodeRef.current, maybeNode] // here `maybeNode` is actually `appearing` - : [maybeNode, maybeAppearing] // `findDOMNode` was used - - getClassNames = (type) => { - const { classNames } = this.props - const isStringClassNames = typeof classNames === 'string' - const prefix = isStringClassNames && classNames ? `${classNames}-` : '' - - const baseClassName = isStringClassNames - ? `${prefix}${type}` - : classNames[type] - - const activeClassName = isStringClassNames - ? `${baseClassName}-active` - : classNames[`${type}Active`] - - const doneClassName = isStringClassNames - ? `${baseClassName}-done` - : classNames[`${type}Done`] - - return { - baseClassName, - activeClassName, - doneClassName, - } - } - - addClass(node, type, phase) { - let className = this.getClassNames(type)[`${phase}ClassName`] - const { doneClassName } = this.getClassNames('enter') - - if (type === 'appear' && phase === 'done' && doneClassName) { - className += ` ${doneClassName}` - } - - // This is to force a repaint, - // which is necessary in order to transition styles when adding a class name. - if (phase === 'active') { - if (node) forceReflow(node) - } - - if (className) { - this.appliedClasses[type][phase] = className - addClass(node, className) - } - } - - removeClasses(node, type) { - const { - base: baseClassName, - active: activeClassName, - done: doneClassName, - } = this.appliedClasses[type] - - this.appliedClasses[type] = {} - - if (baseClassName) { - removeClass(node, baseClassName) - } - if (activeClassName) { - removeClass(node, activeClassName) - } - if (doneClassName) { - removeClass(node, doneClassName) - } - } - - render() { - const { classNames: _, ...props } = this.props - - return ( - - ) - } -} - -// CSSTransition.propTypes = { -// ...Transition.propTypes, - -// /** -// * The animation classNames applied to the component as it appears, enters, -// * exits or has finished the transition. A single name can be provided, which -// * will be suffixed for each stage, e.g. `classNames="fade"` applies: -// * -// * - `fade-appear`, `fade-appear-active`, `fade-appear-done` -// * - `fade-enter`, `fade-enter-active`, `fade-enter-done` -// * - `fade-exit`, `fade-exit-active`, `fade-exit-done` -// * -// * A few details to note about how these classes are applied: -// * -// * 1. They are _joined_ with the ones that are already defined on the child -// * component, so if you want to add some base styles, you can use -// * `className` without worrying that it will be overridden. -// * -// * 2. If the transition component mounts with `in={false}`, no classes are -// * applied yet. You might be expecting `*-exit-done`, but if you think -// * about it, a component cannot finish exiting if it hasn't entered yet. -// * -// * 2. `fade-appear-done` and `fade-enter-done` will _both_ be applied. This -// * allows you to define different behavior for when appearing is done and -// * when regular entering is done, using selectors like -// * `.fade-enter-done:not(.fade-appear-done)`. For example, you could apply -// * an epic entrance animation when element first appears in the DOM using -// * [Animate.css](https://daneden.github.io/animate.css/). Otherwise you can -// * simply use `fade-enter-done` for defining both cases. -// * -// * Each individual classNames can also be specified independently like: -// * -// * ```js -// * classNames={{ -// * appear: 'my-appear', -// * appearActive: 'my-active-appear', -// * appearDone: 'my-done-appear', -// * enter: 'my-enter', -// * enterActive: 'my-active-enter', -// * enterDone: 'my-done-enter', -// * exit: 'my-exit', -// * exitActive: 'my-active-exit', -// * exitDone: 'my-done-exit', -// * }} -// * ``` -// * -// * If you want to set these classes using CSS Modules: -// * -// * ```js -// * import styles from './styles.css'; -// * ``` -// * -// * you might want to use camelCase in your CSS file, that way could simply -// * spread them instead of listing them one by one: -// * -// * ```js -// * classNames={{ ...styles }} -// * ``` -// * -// * @type {string | { -// * appear?: string, -// * appearActive?: string, -// * appearDone?: string, -// * enter?: string, -// * enterActive?: string, -// * enterDone?: string, -// * exit?: string, -// * exitActive?: string, -// * exitDone?: string, -// * }} -// */ -// classNames: classNamesShape, - -// /** -// * A `` callback fired immediately after the 'enter' or 'appear' class is -// * applied. -// * -// * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. -// * -// * @type Function(node: HtmlElement, isAppearing: bool) -// */ -// onEnter: PropTypes.func, - -// /** -// * A `` callback fired immediately after the 'enter-active' or -// * 'appear-active' class is applied. -// * -// * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. -// * -// * @type Function(node: HtmlElement, isAppearing: bool) -// */ -// onEntering: PropTypes.func, - -// /** -// * A `` callback fired immediately after the 'enter' or -// * 'appear' classes are **removed** and the `done` class is added to the DOM node. -// * -// * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. -// * -// * @type Function(node: HtmlElement, isAppearing: bool) -// */ -// onEntered: PropTypes.func, - -// /** -// * A `` callback fired immediately after the 'exit' class is -// * applied. -// * -// * **Note**: when `nodeRef` prop is passed, `node` is not passed -// * -// * @type Function(node: HtmlElement) -// */ -// onExit: PropTypes.func, - -// /** -// * A `` callback fired immediately after the 'exit-active' is applied. -// * -// * **Note**: when `nodeRef` prop is passed, `node` is not passed -// * -// * @type Function(node: HtmlElement) -// */ -// onExiting: PropTypes.func, - -// /** -// * A `` callback fired immediately after the 'exit' classes -// * are **removed** and the `exit-done` class is added to the DOM node. -// * -// * **Note**: when `nodeRef` prop is passed, `node` is not passed -// * -// * @type Function(node: HtmlElement) -// */ -// onExited: PropTypes.func, -// } - -export default CSSTransition diff --git a/src/utils/css-transition/CSSTransition.tsx b/src/utils/css-transition/CSSTransition.tsx new file mode 100644 index 0000000000..f830036791 --- /dev/null +++ b/src/utils/css-transition/CSSTransition.tsx @@ -0,0 +1,197 @@ +import addOneClass from 'dom-helpers/addClass' + +import removeOneClass from 'dom-helpers/removeClass' +import React from 'react' + +import Transition from './Transition' +import { forceReflow } from './utils/reflow' + +const addClass = (node, classes) => + node && classes && classes.split(' ').forEach((c) => addOneClass(node, c)) +const removeClass = (node, classes) => + node && classes && classes.split(' ').forEach((c) => removeOneClass(node, c)) + +type classNamesShape = + | string + | { + appear: string + appearActive: string + appearDone: string + enter: string + enterActive: string + enterDone: string + exit: string + exitActive: string + exitDone: string + } + +interface CSSTransitionProps { + classNames: classNamesShape + onEnter: (node: HTMLElement, isAppearing: boolean) => void + onEntering: (node: HTMLElement, isAppearing: boolean) => void + onEntered: (node: HTMLElement, isAppearing: boolean) => void + onExit: (node: HTMLElement) => void + onExiting: (node: HTMLElement) => void + onExited: (node: HTMLElement) => void +} + +class CSSTransition extends React.Component { + static defaultProps = { + classNames: '', + } + + appliedClasses = { + appear: {}, + enter: {}, + exit: {}, + } + + onEnter = (maybeNode, maybeAppearing) => { + const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing) + this.removeClasses(node, 'exit') + this.addClass(node, appearing ? 'appear' : 'enter', 'base') + + if (this.props.onEnter) { + this.props.onEnter(maybeNode, maybeAppearing) + } + } + + onEntering = (maybeNode, maybeAppearing) => { + const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing) + const type = appearing ? 'appear' : 'enter' + this.addClass(node, type, 'active') + + if (this.props.onEntering) { + this.props.onEntering(maybeNode, maybeAppearing) + } + } + + onEntered = (maybeNode, maybeAppearing) => { + const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing) + const type = appearing ? 'appear' : 'enter' + this.removeClasses(node, type) + this.addClass(node, type, 'done') + + if (this.props.onEntered) { + this.props.onEntered(maybeNode, maybeAppearing) + } + } + + onExit = (maybeNode) => { + const [node] = this.resolveArguments(maybeNode) + this.removeClasses(node, 'appear') + this.removeClasses(node, 'enter') + this.addClass(node, 'exit', 'base') + + if (this.props.onExit) { + this.props.onExit(maybeNode) + } + } + + onExiting = (maybeNode) => { + const [node] = this.resolveArguments(maybeNode) + this.addClass(node, 'exit', 'active') + + if (this.props.onExiting) { + this.props.onExiting(maybeNode) + } + } + + onExited = (maybeNode) => { + const [node] = this.resolveArguments(maybeNode) + this.removeClasses(node, 'exit') + this.addClass(node, 'exit', 'done') + + if (this.props.onExited) { + this.props.onExited(maybeNode) + } + } + + // when prop `nodeRef` is provided `node` is excluded + resolveArguments = (maybeNode, maybeAppearing) => + this.props.nodeRef + ? [this.props.nodeRef.current, maybeNode] // here `maybeNode` is actually `appearing` + : [maybeNode, maybeAppearing] // `findDOMNode` was used + + getClassNames = (type) => { + const { classNames } = this.props + const isStringClassNames = typeof classNames === 'string' + const prefix = isStringClassNames && classNames ? `${classNames}-` : '' + + const baseClassName = isStringClassNames + ? `${prefix}${type}` + : classNames[type] + + const activeClassName = isStringClassNames + ? `${baseClassName}-active` + : classNames[`${type}Active`] + + const doneClassName = isStringClassNames + ? `${baseClassName}-done` + : classNames[`${type}Done`] + + return { + baseClassName, + activeClassName, + doneClassName, + } + } + + addClass(node, type, phase) { + let className = this.getClassNames(type)[`${phase}ClassName`] + const { doneClassName } = this.getClassNames('enter') + + if (type === 'appear' && phase === 'done' && doneClassName) { + className += ` ${doneClassName}` + } + + // This is to force a repaint, + // which is necessary in order to transition styles when adding a class name. + if (phase === 'active') { + if (node) forceReflow(node) + } + + if (className) { + this.appliedClasses[type][phase] = className + addClass(node, className) + } + } + + removeClasses(node, type) { + const { + base: baseClassName, + active: activeClassName, + done: doneClassName, + } = this.appliedClasses[type] + + this.appliedClasses[type] = {} + + if (baseClassName) { + removeClass(node, baseClassName) + } + if (activeClassName) { + removeClass(node, activeClassName) + } + if (doneClassName) { + removeClass(node, doneClassName) + } + } + + render() { + const { classNames: _, ...props } = this.props + + return ( + + ) + } +} + +export default CSSTransition diff --git a/src/utils/css-transition/Transition.jsx b/src/utils/css-transition/Transition.jsx deleted file mode 100644 index 5050b8c085..0000000000 --- a/src/utils/css-transition/Transition.jsx +++ /dev/null @@ -1,619 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom' - -import config from './config' -// import { timeoutsShape } from './utils/PropTypes' -import TransitionGroupContext from './TransitionGroupContext' -import { forceReflow } from './utils/reflow' - -export const UNMOUNTED = 'unmounted' -export const EXITED = 'exited' -export const ENTERING = 'entering' -export const ENTERED = 'entered' -export const EXITING = 'exiting' - -/** - * The Transition component lets you describe a transition from one component - * state to another _over time_ with a simple declarative API. Most commonly - * it's used to animate the mounting and unmounting of a component, but can also - * be used to describe in-place transition states as well. - * - * --- - * - * **Note**: `Transition` is a platform-agnostic base component. If you're using - * transitions in CSS, you'll probably want to use - * [`CSSTransition`](https://reactcommunity.org/react-transition-group/css-transition) - * instead. It inherits all the features of `Transition`, but contains - * additional features necessary to play nice with CSS transitions (hence the - * name of the component). - * - * --- - * - * By default the `Transition` component does not alter the behavior of the - * component it renders, it only tracks "enter" and "exit" states for the - * components. It's up to you to give meaning and effect to those states. For - * example we can add styles to a component when it enters or exits: - * - * ```jsx - * import { Transition } from 'react-transition-group'; - * import { useRef } from 'react'; - * - * const duration = 300; - * - * const defaultStyle = { - * transition: `opacity ${duration}ms ease-in-out`, - * opacity: 0, - * } - * - * const transitionStyles = { - * entering: { opacity: 1 }, - * entered: { opacity: 1 }, - * exiting: { opacity: 0 }, - * exited: { opacity: 0 }, - * }; - * - * function Fade({ in: inProp }) { - * const nodeRef = useRef(null); - * return ( - * - * {state => ( - *
- * I'm a fade Transition! - *
- * )} - *
- * ); - * } - * ``` - * - * There are 4 main states a Transition can be in: - * - `'entering'` - * - `'entered'` - * - `'exiting'` - * - `'exited'` - * - * Transition state is toggled via the `in` prop. When `true` the component - * begins the "Enter" stage. During this stage, the component will shift from - * its current transition state, to `'entering'` for the duration of the - * transition and then to the `'entered'` stage once it's complete. Let's take - * the following example (we'll use the - * [useState](https://reactjs.org/docs/hooks-reference.html#usestate) hook): - * - * ```jsx - * import { Transition } from 'react-transition-group'; - * import { useState, useRef } from 'react'; - * - * function App() { - * const [inProp, setInProp] = useState(false); - * const nodeRef = useRef(null); - * return ( - *
- * - * {state => ( - * // ... - * )} - * - * - *
- * ); - * } - * ``` - * - * When the button is clicked the component will shift to the `'entering'` state - * and stay there for 500ms (the value of `timeout`) before it finally switches - * to `'entered'`. - * - * When `in` is `false` the same thing happens except the state moves from - * `'exiting'` to `'exited'`. - */ -class Transition extends React.Component { - static contextType = TransitionGroupContext - - constructor(props, context) { - super(props, context) - - const parentGroup = context - // In the context of a TransitionGroup all enters are really appears - const appear = - parentGroup && !parentGroup.isMounting ? props.enter : props.appear - - let initialStatus - - this.appearStatus = null - - if (props.in) { - if (appear) { - initialStatus = EXITED - this.appearStatus = ENTERING - } else { - initialStatus = ENTERED - } - } else if (props.unmountOnExit || props.mountOnEnter) { - initialStatus = UNMOUNTED - } else { - initialStatus = EXITED - } - - this.state = { status: initialStatus } - - this.nextCallback = null - } - - static getDerivedStateFromProps({ in: nextIn }, prevState) { - if (nextIn && prevState.status === UNMOUNTED) { - return { status: EXITED } - } - return null - } - - // getSnapshotBeforeUpdate(prevProps) { - // let nextStatus = null - - // if (prevProps !== this.props) { - // const { status } = this.state - - // if (this.props.in) { - // if (status !== ENTERING && status !== ENTERED) { - // nextStatus = ENTERING - // } - // } else { - // if (status === ENTERING || status === ENTERED) { - // nextStatus = EXITING - // } - // } - // } - - // return { nextStatus } - // } - - componentDidMount() { - this.updateStatus(true, this.appearStatus) - } - - componentDidUpdate(prevProps) { - let nextStatus = null - if (prevProps !== this.props) { - const { status } = this.state - - if (this.props.in) { - if (status !== ENTERING && status !== ENTERED) { - nextStatus = ENTERING - } - } else if (status === ENTERING || status === ENTERED) { - nextStatus = EXITING - } - } - this.updateStatus(false, nextStatus) - } - - componentWillUnmount() { - this.cancelNextCallback() - } - - getTimeouts() { - const { timeout } = this.props - let exit - let enter - let appear - - exit = enter = appear = timeout - - if (timeout != null && typeof timeout !== 'number') { - exit = timeout.exit - enter = timeout.enter - // TODO: remove fallback for next major - appear = timeout.appear !== undefined ? timeout.appear : enter - } - return { exit, enter, appear } - } - - updateStatus(mounting = false, nextStatus) { - if (nextStatus !== null) { - // nextStatus will always be ENTERING or EXITING. - this.cancelNextCallback() - - if (nextStatus === ENTERING) { - if (this.props.unmountOnExit || this.props.mountOnEnter) { - const node = this.props.nodeRef - ? this.props.nodeRef.current - : ReactDOM.findDOMNode(this) - // https://github.com/reactjs/react-transition-group/pull/749 - // With unmountOnExit or mountOnEnter, the enter animation should happen at the transition between `exited` and `entering`. - // To make the animation happen, we have to separate each rendering and avoid being processed as batched. - if (node) forceReflow(node) - } - this.performEnter(mounting) - } else { - this.performExit() - } - } else if (this.props.unmountOnExit && this.state.status === EXITED) { - this.setState({ status: UNMOUNTED }) - } - } - - performEnter(mounting) { - const { enter } = this.props - const appearing = this.context ? this.context.isMounting : mounting - const [maybeNode, maybeAppearing] = this.props.nodeRef - ? [appearing] - : [ReactDOM.findDOMNode(this), appearing] - - const timeouts = this.getTimeouts() - const enterTimeout = appearing ? timeouts.appear : timeouts.enter - // no enter animation skip right to ENTERED - // if we are mounting and running this it means appear _must_ be set - if ((!mounting && !enter) || config.disabled) { - this.safeSetState({ status: ENTERED }, () => { - this.props.onEntered(maybeNode) - }) - return - } - - this.props.onEnter(maybeNode, maybeAppearing) - - this.safeSetState({ status: ENTERING }, () => { - this.props.onEntering(maybeNode, maybeAppearing) - - this.onTransitionEnd(enterTimeout, () => { - this.safeSetState({ status: ENTERED }, () => { - this.props.onEntered(maybeNode, maybeAppearing) - }) - }) - }) - } - - performExit() { - const { exit } = this.props - const timeouts = this.getTimeouts() - const maybeNode = this.props.nodeRef - ? undefined - : ReactDOM.findDOMNode(this) - - // no exit animation skip right to EXITED - if (!exit || config.disabled) { - this.safeSetState({ status: EXITED }, () => { - this.props.onExited(maybeNode) - }) - return - } - - this.props.onExit(maybeNode) - - this.safeSetState({ status: EXITING }, () => { - this.props.onExiting(maybeNode) - - this.onTransitionEnd(timeouts.exit, () => { - this.safeSetState({ status: EXITED }, () => { - this.props.onExited(maybeNode) - }) - }) - }) - } - - cancelNextCallback() { - if (this.nextCallback !== null) { - this.nextCallback.cancel() - this.nextCallback = null - } - } - - safeSetState(nextState, callback) { - // This shouldn't be necessary, but there are weird race conditions with - // setState callbacks and unmounting in testing, so always make sure that - // we can cancel any pending setState callbacks after we unmount. - callback = this.setNextCallback(callback) - this.setState(nextState, callback) - } - - setNextCallback(callback) { - let active = true - - this.nextCallback = (event) => { - if (active) { - active = false - this.nextCallback = null - - callback(event) - } - } - - this.nextCallback.cancel = () => { - active = false - } - - return this.nextCallback - } - - onTransitionEnd(timeout, handler) { - this.setNextCallback(handler) - const node = this.props.nodeRef - ? this.props.nodeRef.current - : ReactDOM.findDOMNode(this) - - const doesNotHaveTimeoutOrListener = - timeout == null && !this.props.addEndListener - if (!node || doesNotHaveTimeoutOrListener) { - setTimeout(this.nextCallback, 0) - return - } - - if (this.props.addEndListener) { - const [maybeNode, maybeNextCallback] = this.props.nodeRef - ? [this.nextCallback] - : [node, this.nextCallback] - this.props.addEndListener(maybeNode, maybeNextCallback) - } - - if (timeout != null) { - setTimeout(this.nextCallback, timeout) - } - } - - render() { - const status = this.state.status - - if (status === UNMOUNTED) { - return null - } - - const { - children, - // filter props for `Transition` - in: _in, - mountOnEnter: _mountOnEnter, - unmountOnExit: _unmountOnExit, - appear: _appear, - enter: _enter, - exit: _exit, - timeout: _timeout, - addEndListener: _addEndListener, - onEnter: _onEnter, - onEntering: _onEntering, - onEntered: _onEntered, - onExit: _onExit, - onExiting: _onExiting, - onExited: _onExited, - nodeRef: _nodeRef, - ...childProps - } = this.props - - return ( - // allows for nested Transitions - - {typeof children === 'function' - ? children(status, childProps) - : React.cloneElement(React.Children.only(children), childProps)} - - ) - } -} - -// Transition.propTypes = { -// /** -// * A React reference to the DOM element that needs to transition: -// * https://stackoverflow.com/a/51127130/4671932 -// * -// * - This prop is optional, but recommended in order to avoid defaulting to -// * [`ReactDOM.findDOMNode`](https://reactjs.org/docs/react-dom.html#finddomnode), -// * which is deprecated in `StrictMode` -// * - When `nodeRef` prop is used, `node` is not passed to callback functions -// * (e.g. `onEnter`) because user already has direct access to the node. -// * - When changing `key` prop of `Transition` in a `TransitionGroup` a new -// * `nodeRef` need to be provided to `Transition` with changed `key` prop -// * (see -// * [test/CSSTransition-test.js](https://github.com/reactjs/react-transition-group/blob/13435f897b3ab71f6e19d724f145596f5910581c/test/CSSTransition-test.js#L362-L437)). -// */ -// nodeRef: PropTypes.shape({ -// current: -// typeof Element === 'undefined' -// ? PropTypes.any -// : (propValue, key, componentName, location, propFullName, secret) => { -// const value = propValue[key] - -// return PropTypes.instanceOf( -// value && 'ownerDocument' in value -// ? value.ownerDocument.defaultView.Element -// : Element -// )(propValue, key, componentName, location, propFullName, secret) -// }, -// }), - -// /** -// * A `function` child can be used instead of a React element. This function is -// * called with the current transition status (`'entering'`, `'entered'`, -// * `'exiting'`, `'exited'`), which can be used to apply context -// * specific props to a component. -// * -// * ```jsx -// * -// * {state => ( -// * -// * )} -// * -// * ``` -// */ -// children: PropTypes.oneOfType([ -// PropTypes.func.isRequired, -// PropTypes.element.isRequired, -// ]).isRequired, - -// /** -// * Show the component; triggers the enter or exit states -// */ -// in: PropTypes.bool, - -// /** -// * By default the child component is mounted immediately along with -// * the parent `Transition` component. If you want to "lazy mount" the component on the -// * first `in={true}` you can set `mountOnEnter`. After the first enter transition the component will stay -// * mounted, even on "exited", unless you also specify `unmountOnExit`. -// */ -// mountOnEnter: PropTypes.bool, - -// /** -// * By default the child component stays mounted after it reaches the `'exited'` state. -// * Set `unmountOnExit` if you'd prefer to unmount the component after it finishes exiting. -// */ -// unmountOnExit: PropTypes.bool, - -// /** -// * By default the child component does not perform the enter transition when -// * it first mounts, regardless of the value of `in`. If you want this -// * behavior, set both `appear` and `in` to `true`. -// * -// * > **Note**: there are no special appear states like `appearing`/`appeared`, this prop -// * > only adds an additional enter transition. However, in the -// * > `` component that first enter transition does result in -// * > additional `.appear-*` classes, that way you can choose to style it -// * > differently. -// */ -// appear: PropTypes.bool, - -// /** -// * Enable or disable enter transitions. -// */ -// enter: PropTypes.bool, - -// /** -// * Enable or disable exit transitions. -// */ -// exit: PropTypes.bool, - -// /** -// * The duration of the transition, in milliseconds. -// * Required unless `addEndListener` is provided. -// * -// * You may specify a single timeout for all transitions: -// * -// * ```jsx -// * timeout={500} -// * ``` -// * -// * or individually: -// * -// * ```jsx -// * timeout={{ -// * appear: 500, -// * enter: 300, -// * exit: 500, -// * }} -// * ``` -// * -// * - `appear` defaults to the value of `enter` -// * - `enter` defaults to `0` -// * - `exit` defaults to `0` -// * -// * @type {number | { enter?: number, exit?: number, appear?: number }} -// */ -// timeout: (props, ...args) => { -// let pt = timeoutsShape -// if (!props.addEndListener) pt = pt.isRequired -// return pt(props, ...args) -// }, - -// /** -// * Add a custom transition end trigger. Called with the transitioning -// * DOM node and a `done` callback. Allows for more fine grained transition end -// * logic. Timeouts are still used as a fallback if provided. -// * -// * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `done` is being passed as the first argument. -// * -// * ```jsx -// * addEndListener={(node, done) => { -// * // use the css transitionend event to mark the finish of a transition -// * node.addEventListener('transitionend', done, false); -// * }} -// * ``` -// */ -// addEndListener: PropTypes.func, - -// /** -// * Callback fired before the "entering" status is applied. An extra parameter -// * `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount -// * -// * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. -// * -// * @type Function(node: HtmlElement, isAppearing: bool) -> void -// */ -// onEnter: PropTypes.func, - -// /** -// * Callback fired after the "entering" status is applied. An extra parameter -// * `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount -// * -// * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. -// * -// * @type Function(node: HtmlElement, isAppearing: bool) -// */ -// onEntering: PropTypes.func, - -// /** -// * Callback fired after the "entered" status is applied. An extra parameter -// * `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount -// * -// * **Note**: when `nodeRef` prop is passed, `node` is not passed, so `isAppearing` is being passed as the first argument. -// * -// * @type Function(node: HtmlElement, isAppearing: bool) -> void -// */ -// onEntered: PropTypes.func, - -// /** -// * Callback fired before the "exiting" status is applied. -// * -// * **Note**: when `nodeRef` prop is passed, `node` is not passed. -// * -// * @type Function(node: HtmlElement) -> void -// */ -// onExit: PropTypes.func, - -// /** -// * Callback fired after the "exiting" status is applied. -// * -// * **Note**: when `nodeRef` prop is passed, `node` is not passed. -// * -// * @type Function(node: HtmlElement) -> void -// */ -// onExiting: PropTypes.func, - -// /** -// * Callback fired after the "exited" status is applied. -// * -// * **Note**: when `nodeRef` prop is passed, `node` is not passed -// * -// * @type Function(node: HtmlElement) -> void -// */ -// onExited: PropTypes.func, -// } - -// Name the function so it is clearer in the documentation -function noop() {} - -Transition.defaultProps = { - in: false, - mountOnEnter: false, - unmountOnExit: false, - appear: false, - enter: true, - exit: true, - - onEnter: noop, - onEntering: noop, - onEntered: noop, - - onExit: noop, - onExiting: noop, - onExited: noop, -} - -Transition.UNMOUNTED = UNMOUNTED -Transition.EXITED = EXITED -Transition.ENTERING = ENTERING -Transition.ENTERED = ENTERED -Transition.EXITING = EXITING - -export default Transition diff --git a/src/utils/css-transition/Transition.tsx b/src/utils/css-transition/Transition.tsx new file mode 100644 index 0000000000..d35992434a --- /dev/null +++ b/src/utils/css-transition/Transition.tsx @@ -0,0 +1,285 @@ +import React, { + FunctionComponent, + useEffect, + useRef, + useState, + useContext, +} from 'react' +import config from './config' +import TransitionGroupContext from './TransitionGroupContext' +import { forceReflow } from './utils/reflow' + +export const UNMOUNTED = 'unmounted' +export const EXITED = 'exited' +export const ENTERING = 'entering' +export const ENTERED = 'entered' +export const EXITING = 'exiting' + +interface TransitionProps { + in: boolean + mountOnEnter: boolean + unmountOnExit: boolean + appear: boolean + enter: boolean + exit: boolean + timeout?: number | { enter?: number; exit?: number; appear?: number } + addEndListener?: (node: HTMLElement, done: () => void) => void + onEnter?: (node: HTMLElement, appearing: boolean) => void + onEntering?: (node: HTMLElement, appearing: boolean) => void + onEntered?: (node: HTMLElement, appearing: boolean) => void + onExit?: (node: HTMLElement) => void + onExiting?: (node: HTMLElement) => void + onExited?: (node: HTMLElement) => void + nodeRef?: React.RefObject + children: + | React.ReactNode + | ((status: string, childProps: any) => React.ReactNode) +} + +const defaultProps = { + in: false, + mountOnEnter: false, + unmountOnExit: false, + appear: false, + enter: true, + exit: true, + onEnter: () => {}, + onEntering: () => {}, + onEntered: () => {}, + onExit: () => {}, + onExiting: () => {}, + onExited: () => {}, +} + +const Transition: FunctionComponent> = (props) => { + const { + children, + // filter props for `Transition` + in: _in, + mountOnEnter, + unmountOnExit, + appear, + enter, + exit, + timeout, + addEndListener, + onEnter, + onEntering, + onEntered, + onExit, + onExiting, + onExited, + nodeRef: _nodeRef, + ...childProps + } = { ...defaultProps, ...props } + const context = useContext(TransitionGroupContext) + const [status, setStatus] = useState(UNMOUNTED) + const nextCallback = useRef(null) + const ref = useRef(null) + const nodeRef = _nodeRef || ref + + useEffect(() => { + // In the context of a TransitionGroup all enters are really appears + const appear = context && !context.isMounting ? enter : props.appear + + let initialStatus + if (_in) { + if (appear) { + initialStatus = EXITED + } else { + initialStatus = ENTERED + } + } else if (unmountOnExit || mountOnEnter) { + initialStatus = UNMOUNTED + } else { + initialStatus = EXITED + } + + setStatus(initialStatus) + }, [_in]) + + useEffect(() => { + if (status === EXITED && unmountOnExit) { + setStatus(UNMOUNTED) + } else { + updateStatus(true, null) + } + }, []) + + useEffect(() => { + let nextStatus = null + if (_in) { + if (status !== ENTERING && status !== ENTERED) { + nextStatus = ENTERING + } + } else if (status === ENTERING || status === ENTERED) { + nextStatus = EXITING + } + updateStatus(false, nextStatus) + }, [_in]) + + useEffect(() => { + return () => { + cancelNextCallback() + } + }, []) + + const getTimeouts = () => { + const { timeout } = props + let exit = timeout + let enter = timeout + let appear = timeout + + if (timeout != null && typeof timeout !== 'number') { + exit = timeout.exit + enter = timeout.enter + // TODO: remove fallback for next major + appear = timeout.appear !== undefined ? timeout.appear : enter + } + return { exit, enter, appear } + } + + const updateStatus = (mounting: boolean, nextStatus: string | null) => { + if (nextStatus !== null) { + // nextStatus will always be ENTERING or EXITING. + cancelNextCallback() + + if (nextStatus === ENTERING) { + if (unmountOnExit || mountOnEnter) { + const node = nodeRef.current + // https://github.com/reactjs/react-transition-group/pull/749 + // With unmountOnExit or mountOnEnter, the enter animation should happen at the transition between `exited` and `entering`. + // To make the animation happen, we have to separate each rendering and avoid being processed as batched. + if (node) forceReflow(node) + } + performEnter(mounting) + } else { + performExit() + } + } else if (unmountOnExit && status === EXITED) { + setStatus(UNMOUNTED) + } + } + + const performEnter = (mounting: boolean) => { + const appearing = context ? context.isMounting : mounting + const maybeNode = nodeRef.current + + const timeouts = getTimeouts() + const enterTimeout = appearing ? timeouts.appear : timeouts.enter + // no enter animation skip right to ENTERED + // if we are mounting and running this it means appear _must_ be set + if ((!mounting && !enter) || config.disabled) { + safeSetState({ status: ENTERED }, () => { + onEntered(maybeNode) + }) + return + } + + onEnter(maybeNode, appearing) + safeSetState({ status: ENTERING }, () => { + onEntering(maybeNode, appearing) + onTransitionEnd(enterTimeout, () => { + safeSetState({ status: ENTERED }, () => { + onEntered(maybeNode, appearing) + }) + }) + }) + } + + const performExit = () => { + const maybeNode = nodeRef.current + + // no exit animation skip right to EXITED + if (!exit || config.disabled) { + safeSetState({ status: EXITED }, () => { + onExited(maybeNode) + }) + return + } + + onExit(maybeNode) + safeSetState({ status: EXITING }, () => { + onExiting(maybeNode) + const timeouts = getTimeouts() + onTransitionEnd(timeouts.exit, () => { + safeSetState({ status: EXITED }, () => { + onExited(maybeNode) + }) + }) + }) + } + + const cancelNextCallback = () => { + if (nextCallback.current !== null) { + nextCallback.current.cancel() + nextCallback.current = null + } + } + + const safeSetState = (nextState: any, callback?: () => void) => { + // This shouldn't be necessary, but there are weird race conditions with + // setState callbacks and unmounting in testing, so always make sure that + // we can cancel any pending setState callbacks after we unmount. + callback = setNextCallback(callback) + setStatus((prev) => ({ ...prev, ...nextState }), callback) + } + + const setNextCallback = (callback?: () => void) => { + let active = true + + nextCallback.current = (event: any) => { + if (active) { + active = false + nextCallback.current = null + callback?.(event) + } + } + + nextCallback.current.cancel = () => { + active = false + } + + return nextCallback.current + } + + const onTransitionEnd = (timeout: number | null, handler: () => void) => { + setNextCallback(handler) + const node = nodeRef.current + + const doesNotHaveTimeoutOrListener = timeout == null && !addEndListener + if (!node || doesNotHaveTimeoutOrListener) { + setTimeout(nextCallback.current, 0) + return + } + + if (addEndListener) { + addEndListener(node, nextCallback.current) + } + + if (timeout != null) { + setTimeout(nextCallback.current, timeout) + } + } + + if (status === UNMOUNTED) { + return null + } + + return ( + // allows for nested Transitions + + {typeof children === 'function' + ? children(status, childProps) + : React.cloneElement(React.Children.only(children), childProps)} + + ) +} + +Transition.UNMOUNTED = UNMOUNTED +Transition.EXITED = EXITED +Transition.ENTERING = ENTERING +Transition.ENTERED = ENTERED +Transition.EXITING = EXITING + +export default Transition diff --git a/src/utils/css-transition/TransitionGroupContext.jsx b/src/utils/css-transition/TransitionGroupContext.tsx similarity index 100% rename from src/utils/css-transition/TransitionGroupContext.jsx rename to src/utils/css-transition/TransitionGroupContext.tsx From 3df12a5e7d430c64f4b99b38d6b1fb99ff9a1c64 Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Sat, 24 May 2025 21:14:10 +0800 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84css=20transit?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/overlay/overlay.taro.tsx | 2 +- src/utils/css-transition/CSSTransition.tsx | 163 ++++++++++-------- .../css-transition/{config.js => config.ts} | 0 src/utils/css-transition/utils/reflow.js | 1 - src/utils/css-transition/utils/reflow.ts | 1 + 5 files changed, 89 insertions(+), 78 deletions(-) rename src/utils/css-transition/{config.js => config.ts} (100%) delete mode 100644 src/utils/css-transition/utils/reflow.js create mode 100644 src/utils/css-transition/utils/reflow.ts diff --git a/src/packages/overlay/overlay.taro.tsx b/src/packages/overlay/overlay.taro.tsx index 328835b230..8ebd540fb2 100644 --- a/src/packages/overlay/overlay.taro.tsx +++ b/src/packages/overlay/overlay.taro.tsx @@ -1,7 +1,7 @@ import React, { FunctionComponent, useEffect, useState } from 'react' -import { CSSTransition } from 'react-transition-group' import classNames from 'classnames' import { ITouchEvent, View } from '@tarojs/components' +import CSSTransition from '@/utils/css-transition/CSSTransition' import { ComponentDefaults } from '@/utils/typings' import { useLockScrollTaro } from '@/hooks/taro/use-lock-scoll' import { TaroOverlayProps } from '@/types' diff --git a/src/utils/css-transition/CSSTransition.tsx b/src/utils/css-transition/CSSTransition.tsx index f830036791..44feaa8efb 100644 --- a/src/utils/css-transition/CSSTransition.tsx +++ b/src/utils/css-transition/CSSTransition.tsx @@ -1,17 +1,17 @@ import addOneClass from 'dom-helpers/addClass' import removeOneClass from 'dom-helpers/removeClass' -import React from 'react' +import React, { FunctionComponent } from 'react' import Transition from './Transition' import { forceReflow } from './utils/reflow' -const addClass = (node, classes) => +const addClassCommon = (node: HTMLElement | null, classes: string) => node && classes && classes.split(' ').forEach((c) => addOneClass(node, c)) -const removeClass = (node, classes) => +const removeClassCommon = (node: HTMLElement | null, classes: string) => node && classes && classes.split(' ').forEach((c) => removeOneClass(node, c)) -type classNamesShape = +type ClassNamesShape = | string | { appear: string @@ -26,95 +26,110 @@ type classNamesShape = } interface CSSTransitionProps { - classNames: classNamesShape + classNames: ClassNamesShape onEnter: (node: HTMLElement, isAppearing: boolean) => void onEntering: (node: HTMLElement, isAppearing: boolean) => void onEntered: (node: HTMLElement, isAppearing: boolean) => void onExit: (node: HTMLElement) => void onExiting: (node: HTMLElement) => void onExited: (node: HTMLElement) => void + nodeRef?: React.RefObject + children: React.ReactNode } -class CSSTransition extends React.Component { - static defaultProps = { - classNames: '', - } +const defaultProps = { + classNames: '', +} - appliedClasses = { +const CSSTransition: FunctionComponent> = ( + props +) => { + const { + classNames, + onEnter: _onEnter, + onEntering: _onEntering, + onEntered: _onEntered, + onExit: _onExit, + onExiting: _onExiting, + onExited: _onExited, + nodeRef: _nodeRef, + ...childProps + } = { ...defaultProps, ...props } + + const appliedClasses = { appear: {}, enter: {}, exit: {}, } - onEnter = (maybeNode, maybeAppearing) => { - const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing) - this.removeClasses(node, 'exit') - this.addClass(node, appearing ? 'appear' : 'enter', 'base') + const onEnter = (maybeNode: any, maybeAppearing: boolean) => { + const [node, appearing] = resolveArguments(maybeNode, maybeAppearing) + removeClasses(node, 'exit') + addClass(node, appearing ? 'appear' : 'enter', 'base') - if (this.props.onEnter) { - this.props.onEnter(maybeNode, maybeAppearing) + if (_onEnter) { + _onEnter(maybeNode, maybeAppearing) } } - onEntering = (maybeNode, maybeAppearing) => { - const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing) + const onEntering = (maybeNode: any, maybeAppearing: boolean) => { + const [node, appearing] = resolveArguments(maybeNode, maybeAppearing) const type = appearing ? 'appear' : 'enter' - this.addClass(node, type, 'active') + addClass(node, type, 'active') - if (this.props.onEntering) { - this.props.onEntering(maybeNode, maybeAppearing) + if (_onEntering) { + _onEntering(maybeNode, maybeAppearing) } } - onEntered = (maybeNode, maybeAppearing) => { - const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing) + const onEntered = (maybeNode: any, maybeAppearing: boolean) => { + const [node, appearing] = resolveArguments(maybeNode, maybeAppearing) const type = appearing ? 'appear' : 'enter' - this.removeClasses(node, type) - this.addClass(node, type, 'done') + removeClasses(node, type) + addClass(node, type, 'done') - if (this.props.onEntered) { - this.props.onEntered(maybeNode, maybeAppearing) + if (_onEntered) { + _onEntered(maybeNode, maybeAppearing) } } - onExit = (maybeNode) => { - const [node] = this.resolveArguments(maybeNode) - this.removeClasses(node, 'appear') - this.removeClasses(node, 'enter') - this.addClass(node, 'exit', 'base') + const onExit = (maybeNode: any) => { + const [node] = resolveArguments(maybeNode) + removeClasses(node, 'appear') + removeClasses(node, 'enter') + addClass(node, 'exit', 'base') - if (this.props.onExit) { - this.props.onExit(maybeNode) + if (_onExit) { + _onExit(maybeNode) } } - onExiting = (maybeNode) => { - const [node] = this.resolveArguments(maybeNode) - this.addClass(node, 'exit', 'active') + const onExiting = (maybeNode: any) => { + const [node] = resolveArguments(maybeNode) + addClass(node, 'exit', 'active') - if (this.props.onExiting) { - this.props.onExiting(maybeNode) + if (_onExiting) { + _onExiting(maybeNode) } } - onExited = (maybeNode) => { - const [node] = this.resolveArguments(maybeNode) - this.removeClasses(node, 'exit') - this.addClass(node, 'exit', 'done') + const onExited = (maybeNode: any) => { + const [node] = resolveArguments(maybeNode) + removeClasses(node, 'exit') + addClass(node, 'exit', 'done') - if (this.props.onExited) { - this.props.onExited(maybeNode) + if (_onExited) { + _onExited(maybeNode) } } // when prop `nodeRef` is provided `node` is excluded - resolveArguments = (maybeNode, maybeAppearing) => - this.props.nodeRef - ? [this.props.nodeRef.current, maybeNode] // here `maybeNode` is actually `appearing` + const resolveArguments = (maybeNode: any, maybeAppearing: boolean) => + _nodeRef + ? [_nodeRef.current, maybeNode] // here `maybeNode` is actually `appearing` : [maybeNode, maybeAppearing] // `findDOMNode` was used - getClassNames = (type) => { - const { classNames } = this.props + const getClassNames = (type: string) => { const isStringClassNames = typeof classNames === 'string' const prefix = isStringClassNames && classNames ? `${classNames}-` : '' @@ -137,9 +152,9 @@ class CSSTransition extends React.Component { } } - addClass(node, type, phase) { - let className = this.getClassNames(type)[`${phase}ClassName`] - const { doneClassName } = this.getClassNames('enter') + const addClass = (node: HTMLElement | null, type: string, phase: string) => { + let className = getClassNames(type)[`${phase}ClassName`] + const { doneClassName } = getClassNames('enter') if (type === 'appear' && phase === 'done' && doneClassName) { className += ` ${doneClassName}` @@ -152,46 +167,42 @@ class CSSTransition extends React.Component { } if (className) { - this.appliedClasses[type][phase] = className - addClass(node, className) + appliedClasses[type][phase] = className + addClassCommon(node, className) } } - removeClasses(node, type) { + const removeClasses = (node: HTMLElement | null, type: string) => { const { base: baseClassName, active: activeClassName, done: doneClassName, - } = this.appliedClasses[type] + } = appliedClasses[type] - this.appliedClasses[type] = {} + appliedClasses[type] = {} if (baseClassName) { - removeClass(node, baseClassName) + removeClassCommon(node, baseClassName) } if (activeClassName) { - removeClass(node, activeClassName) + removeClassCommon(node, activeClassName) } if (doneClassName) { - removeClass(node, doneClassName) + removeClassCommon(node, doneClassName) } } - render() { - const { classNames: _, ...props } = this.props - - return ( - - ) - } + return ( + + ) } export default CSSTransition diff --git a/src/utils/css-transition/config.js b/src/utils/css-transition/config.ts similarity index 100% rename from src/utils/css-transition/config.js rename to src/utils/css-transition/config.ts diff --git a/src/utils/css-transition/utils/reflow.js b/src/utils/css-transition/utils/reflow.js deleted file mode 100644 index cf7e19a057..0000000000 --- a/src/utils/css-transition/utils/reflow.js +++ /dev/null @@ -1 +0,0 @@ -export const forceReflow = (node) => node.scrollTop diff --git a/src/utils/css-transition/utils/reflow.ts b/src/utils/css-transition/utils/reflow.ts new file mode 100644 index 0000000000..718f4ced42 --- /dev/null +++ b/src/utils/css-transition/utils/reflow.ts @@ -0,0 +1 @@ +export const forceReflow = (node: HTMLElement) => node.scrollTop From 5ef471d92ac50bc13168eae56dd2bad104b8765b Mon Sep 17 00:00:00 2001 From: hanyuxinting Date: Fri, 30 May 2025 10:00:44 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9ai=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- src/utils/css-transition/CSSTransition.tsx | 11 ++-------- src/utils/css-transition/Transition.tsx | 21 +++++++++++-------- .../css-transition/TransitionGroupContext.tsx | 10 ++++++++- src/utils/css-transition/index.js | 6 ------ src/utils/css-transition/index.ts | 3 +++ 6 files changed, 27 insertions(+), 26 deletions(-) delete mode 100644 src/utils/css-transition/index.js create mode 100644 src/utils/css-transition/index.ts diff --git a/package.json b/package.json index bfc840934e..6242ccfc53 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "async-validator": "^4.2.5", "classnames": "^2.5.1", "codesandbox": "^2.2.3", + "dom-helpers": "5.2.1", "lodash.kebabcase": "^4.1.1", "lottie-react": "^2.4.0", "react-fast-compare": "^3.2.2", @@ -169,7 +170,6 @@ "axios": "^1.6.7", "cypress": "^13.15.0", "del": "^8.0.0", - "dom-helpers": "5.2.1", "eslint": "^8.56.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^9.1.0", diff --git a/src/utils/css-transition/CSSTransition.tsx b/src/utils/css-transition/CSSTransition.tsx index 44feaa8efb..d61befc02d 100644 --- a/src/utils/css-transition/CSSTransition.tsx +++ b/src/utils/css-transition/CSSTransition.tsx @@ -3,7 +3,7 @@ import addOneClass from 'dom-helpers/addClass' import removeOneClass from 'dom-helpers/removeClass' import React, { FunctionComponent } from 'react' -import Transition from './Transition' +import { Transition, TransitionProps } from './Transition' import { forceReflow } from './utils/reflow' const addClassCommon = (node: HTMLElement | null, classes: string) => @@ -25,15 +25,8 @@ type ClassNamesShape = exitDone: string } -interface CSSTransitionProps { +interface CSSTransitionProps extends TransitionProps { classNames: ClassNamesShape - onEnter: (node: HTMLElement, isAppearing: boolean) => void - onEntering: (node: HTMLElement, isAppearing: boolean) => void - onEntered: (node: HTMLElement, isAppearing: boolean) => void - onExit: (node: HTMLElement) => void - onExiting: (node: HTMLElement) => void - onExited: (node: HTMLElement) => void - nodeRef?: React.RefObject children: React.ReactNode } diff --git a/src/utils/css-transition/Transition.tsx b/src/utils/css-transition/Transition.tsx index d35992434a..9326012d5e 100644 --- a/src/utils/css-transition/Transition.tsx +++ b/src/utils/css-transition/Transition.tsx @@ -15,7 +15,7 @@ export const ENTERING = 'entering' export const ENTERED = 'entered' export const EXITING = 'exiting' -interface TransitionProps { +export interface TransitionProps { in: boolean mountOnEnter: boolean unmountOnExit: boolean @@ -51,7 +51,9 @@ const defaultProps = { onExited: () => {}, } -const Transition: FunctionComponent> = (props) => { +export const Transition: FunctionComponent> = ( + props +) => { const { children, // filter props for `Transition` @@ -96,7 +98,7 @@ const Transition: FunctionComponent> = (props) => { } setStatus(initialStatus) - }, [_in]) + }, [_in, enter, props.appear, unmountOnExit, mountOnEnter, context]) useEffect(() => { if (status === EXITED && unmountOnExit) { @@ -104,7 +106,7 @@ const Transition: FunctionComponent> = (props) => { } else { updateStatus(true, null) } - }, []) + }, [status, unmountOnExit]) useEffect(() => { let nextStatus = null @@ -115,8 +117,9 @@ const Transition: FunctionComponent> = (props) => { } else if (status === ENTERING || status === ENTERED) { nextStatus = EXITING } + console.log('nextStatus', nextStatus) updateStatus(false, nextStatus) - }, [_in]) + }, [_in, status]) useEffect(() => { return () => { @@ -167,6 +170,8 @@ const Transition: FunctionComponent> = (props) => { const timeouts = getTimeouts() const enterTimeout = appearing ? timeouts.appear : timeouts.enter + + console.log('performEnter') // no enter animation skip right to ENTERED // if we are mounting and running this it means appear _must_ be set if ((!mounting && !enter) || config.disabled) { @@ -221,8 +226,8 @@ const Transition: FunctionComponent> = (props) => { // This shouldn't be necessary, but there are weird race conditions with // setState callbacks and unmounting in testing, so always make sure that // we can cancel any pending setState callbacks after we unmount. - callback = setNextCallback(callback) - setStatus((prev) => ({ ...prev, ...nextState }), callback) + const nextCallback = setNextCallback(callback) + setStatus((prev) => ({ ...prev, ...nextState }), nextCallback) } const setNextCallback = (callback?: () => void) => { @@ -281,5 +286,3 @@ Transition.EXITED = EXITED Transition.ENTERING = ENTERING Transition.ENTERED = ENTERED Transition.EXITING = EXITING - -export default Transition diff --git a/src/utils/css-transition/TransitionGroupContext.tsx b/src/utils/css-transition/TransitionGroupContext.tsx index 8bd924bdf2..46abec3b61 100644 --- a/src/utils/css-transition/TransitionGroupContext.tsx +++ b/src/utils/css-transition/TransitionGroupContext.tsx @@ -1,3 +1,11 @@ import React from 'react' -export default React.createContext(null) +/** + * TransitionGroup 上下文,用于管理过渡组中的挂载行为和状态协调 + */ +interface TransitionGroupContextValue { + isMounting?: boolean + // 根据实际使用场景添加其他属性 +} + +export default React.createContext(null) diff --git a/src/utils/css-transition/index.js b/src/utils/css-transition/index.js deleted file mode 100644 index 8e11afca16..0000000000 --- a/src/utils/css-transition/index.js +++ /dev/null @@ -1,6 +0,0 @@ -export { default as CSSTransition } from './CSSTransition' -export { default as ReplaceTransition } from './ReplaceTransition' -export { default as SwitchTransition } from './SwitchTransition' -export { default as TransitionGroup } from './TransitionGroup' -export { default as Transition } from './Transition' -export { default as config } from './config' diff --git a/src/utils/css-transition/index.ts b/src/utils/css-transition/index.ts new file mode 100644 index 0000000000..0d708a18d8 --- /dev/null +++ b/src/utils/css-transition/index.ts @@ -0,0 +1,3 @@ +export { default as CSSTransition } from './CSSTransition' +export { default as Transition } from './Transition' +export { default as config } from './config'