From 0f37aae2c26ed535d3c49d2cec472b61f6fa0a13 Mon Sep 17 00:00:00 2001 From: Toru Kobayashi Date: Tue, 5 Apr 2022 23:41:12 +0900 Subject: [PATCH 1/9] build: add tsx files as targets of Babel --- .storybook/main.js | 13 +++++++++++-- package.json | 2 +- rollup.config.js | 1 + yarn.lock | 10 ++++++++++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/.storybook/main.js b/.storybook/main.js index 6d952e40..03c63cc6 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -4,9 +4,18 @@ module.exports = { stories: ['../stories/index.js'], webpackFinal: (config) => { config.module = { - rules: [rules.js(), rules.astroturf(), rules.css({ extract: false })], + rules: [ + { + test: /\.[t|j]sx?$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + }, + }, + rules.astroturf(), + rules.css({ extract: false }), + ], }; - config.plugins.push(plugins.extractCss({ disable: true })); return config; diff --git a/package.json b/package.json index 52c8a435..fe2204e6 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "@typescript-eslint/eslint-plugin": "^4.26.1", "astroturf": "^0.10.4", "babel-eslint": "^10.1.0", - "babel-loader": "^8.1.0", + "babel-loader": "^8.2.3", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "babel-preset-jason": "^6.2.0", "cherry-pick": "^0.5.0", diff --git a/rollup.config.js b/rollup.config.js index 6d1d2afc..bbabeb46 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -14,6 +14,7 @@ const globals = { const babelOptions = { exclude: /node_modules/, + extensions: ['.js', '.ts', '.tsx'], runtimeHelpers: true, }; diff --git a/yarn.lock b/yarn.lock index ebd7ae7b..d15772a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5005,6 +5005,16 @@ babel-loader@^8.2.2: make-dir "^3.1.0" schema-utils "^2.6.5" +babel-loader@^8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.3.tgz#8986b40f1a64cacfcb4b8429320085ef68b1342d" + integrity sha512-n4Zeta8NC3QAsuyiizu0GkmRcQ6clkV9WFUnUf1iXP//IeSKbWjofW3UHyZVwlOB4y039YQKefawyTn64Zwbuw== + dependencies: + find-cache-dir "^3.3.1" + loader-utils "^1.4.0" + make-dir "^3.1.0" + schema-utils "^2.6.5" + babel-plugin-add-module-exports@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-1.0.2.tgz#96cd610d089af664f016467fc4567c099cce2d9c" From 63fe523075bf283458c0f10ec33f4be56a380daf Mon Sep 17 00:00:00 2001 From: Toru Kobayashi Date: Tue, 5 Apr 2022 23:41:37 +0900 Subject: [PATCH 2/9] refactor: migrate ChildMapping to TypeScript --- .../{ChildMapping.js => ChildMapping.ts} | 44 +++++++++++++++---- 1 file changed, 35 insertions(+), 9 deletions(-) rename src/utils/{ChildMapping.js => ChildMapping.ts} (77%) diff --git a/src/utils/ChildMapping.js b/src/utils/ChildMapping.ts similarity index 77% rename from src/utils/ChildMapping.js rename to src/utils/ChildMapping.ts index cdd33a3e..98740760 100644 --- a/src/utils/ChildMapping.js +++ b/src/utils/ChildMapping.ts @@ -1,4 +1,5 @@ import { Children, cloneElement, isValidElement } from 'react'; +import type { ReactChild, ReactElement, ReactNode } from 'react'; /** * Given `this.props.children`, return an object mapping key to child. @@ -6,14 +7,19 @@ import { Children, cloneElement, isValidElement } from 'react'; * @param {*} children `this.props.children` * @return {object} Mapping of key to child */ -export function getChildMapping(children, mapFn) { - let mapper = (child) => +export function getChildMapping( + children: ReactNode[] | ReactNode, + mapFn?: (child: ReactElement) => ReactElement +) { + let mapper = (child: ReactNode) => mapFn && isValidElement(child) ? mapFn(child) : child; let result = Object.create(null); if (children) + // @ts-expect-error FIXME: Object is possibly 'null' or 'undefined'.ts(2533) Children.map(children, (c) => c).forEach((child) => { // run the map function here instead so that the key is the computed one + // @ts-expect-error FIXME: Property 'key' does not exist on type 'string'.ts(2339) result[child.key] = mapper(child); }); return result; @@ -36,11 +42,14 @@ export function getChildMapping(children, mapFn) { * @return {object} a key set that contains all keys in `prev` and all keys * in `next` in a reasonable order. */ -export function mergeChildMappings(prev, next) { +export function mergeChildMappings( + prev: Record, + next: Record +) { prev = prev || {}; next = next || {}; - function getValueForKey(key) { + function getValueForKey(key: string) { return key in next ? next[key] : prev[key]; } @@ -61,7 +70,7 @@ export function mergeChildMappings(prev, next) { } let i; - let childMapping = {}; + let childMapping: Record = {}; for (let nextKey in next) { if (nextKeysPending[nextKey]) { for (i = 0; i < nextKeysPending[nextKey].length; i++) { @@ -81,12 +90,22 @@ export function mergeChildMappings(prev, next) { return childMapping; } -function getProp(child, prop, props) { +function getProp( + child: ReactElement, + prop: string, + props: Record +) { return props[prop] != null ? props[prop] : child.props[prop]; } -export function getInitialChildMapping(props, onExited) { - return getChildMapping(props.children, (child) => { +export function getInitialChildMapping( + props: { children: ReactNode[] }, + onExited: ( + child: ReactElement<{ onExited: (node: HTMLElement) => void }>, + node: HTMLElement + ) => void +) { + return getChildMapping(props.children, (child: ReactElement) => { return cloneElement(child, { onExited: onExited.bind(null, child), in: true, @@ -97,7 +116,14 @@ export function getInitialChildMapping(props, onExited) { }); } -export function getNextChildMapping(nextProps, prevChildMapping, onExited) { +export function getNextChildMapping( + nextProps: { children: ReactNode[] }, + prevChildMapping: Record, + onExited: ( + child: ReactElement<{ onExited: (node: HTMLElement) => void }>, + node: HTMLElement + ) => void +) { let nextChildMapping = getChildMapping(nextProps.children); let children = mergeChildMappings(prevChildMapping, nextChildMapping); From b0b4e5370dbf1e0155479ae335176c2137656534 Mon Sep 17 00:00:00 2001 From: Toru Kobayashi Date: Tue, 5 Apr 2022 23:42:00 +0900 Subject: [PATCH 3/9] refactor: add types for TransitionGroupContext --- src/TransitionGroupContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TransitionGroupContext.ts b/src/TransitionGroupContext.ts index 51b82c7d..afe8ddbb 100644 --- a/src/TransitionGroupContext.ts +++ b/src/TransitionGroupContext.ts @@ -1,3 +1,3 @@ import React from 'react'; -export default React.createContext(null); +export default React.createContext<{ isMounting: boolean } | null>(null); From 471aa6d8705073d38e8b48c835f5502c9828d25f Mon Sep 17 00:00:00 2001 From: Toru Kobayashi Date: Tue, 5 Apr 2022 23:44:13 +0900 Subject: [PATCH 4/9] refactor: migrate SwitchTransition to TypeScript --- ...itchTransition.js => SwitchTransition.tsx} | 86 ++++++++++++------- 1 file changed, 55 insertions(+), 31 deletions(-) rename src/{SwitchTransition.js => SwitchTransition.tsx} (66%) diff --git a/src/SwitchTransition.js b/src/SwitchTransition.tsx similarity index 66% rename from src/SwitchTransition.js rename to src/SwitchTransition.tsx index cb3a45b3..ee74f773 100644 --- a/src/SwitchTransition.js +++ b/src/SwitchTransition.tsx @@ -1,9 +1,13 @@ import React from 'react'; +import { ReactElement } from 'react'; import PropTypes from 'prop-types'; -import { ENTERED, ENTERING, EXITING } from './Transition'; +import { ENTERED, ENTERING, EXITING, TransitionState } from './Transition'; import TransitionGroupContext from './TransitionGroupContext'; -function areChildrenDifferent(oldChildren, newChildren) { +function areChildrenDifferent( + oldChildren: ReactElement, + newChildren: ReactElement +) { if (oldChildren === newChildren) return false; if ( React.isValidElement(oldChildren) && @@ -26,21 +30,29 @@ export const modes = { }; const callHook = - (element, name, cb) => - (...args) => { + (element: ReactElement, name: string, cb: () => void) => + (...args: unknown[]) => { element.props[name] && element.props[name](...args); cb(); }; +type RenderCallbackParameter = { + current: ReactElement | null; + changeState: (status: TransitionState, current?: ReactElement | null) => void; + children: ReactElement; +}; + const leaveRenders = { - [modes.out]: ({ current, changeState }) => + [modes.out]: ({ current, changeState }: RenderCallbackParameter) => + // @ts-expect-error FIXME: Type 'null' is not assignable to type 'ReactElement>'.ts(2769) React.cloneElement(current, { in: false, + // @ts-expect-error FIXME: Type 'null' is not assignable to type 'ReactElement>'.ts(2345) onExited: callHook(current, 'onExited', () => { changeState(ENTERING, null); }), }), - [modes.in]: ({ current, changeState, children }) => [ + [modes.in]: ({ current, changeState, children }: RenderCallbackParameter) => [ current, React.cloneElement(children, { in: true, @@ -52,16 +64,18 @@ const leaveRenders = { }; const enterRenders = { - [modes.out]: ({ children, changeState }) => + [modes.out]: ({ children, changeState }: RenderCallbackParameter) => React.cloneElement(children, { in: true, onEntered: callHook(children, 'onEntered', () => { changeState(ENTERED, React.cloneElement(children, { in: true })); }), }), - [modes.in]: ({ current, children, changeState }) => [ + [modes.in]: ({ current, children, changeState }: RenderCallbackParameter) => [ + // @ts-expect-error FIXME: Type 'null' is not assignable to type 'ReactElement>'.ts(2769) React.cloneElement(current, { in: false, + // @ts-expect-error FIXME: Type 'null' is not assignable to type 'ReactElement>'.ts(2345) onExited: callHook(current, 'onExited', () => { changeState(ENTERED, React.cloneElement(children, { in: true })); }), @@ -72,6 +86,16 @@ const enterRenders = { ], }; +type Props = { + mode: 'in-out' | 'out-in'; + children: ReactElement; +}; + +type State = { + status: TransitionState; + current: ReactElement | null; +}; + /** * A transition component inspired by the [vue transition modes](https://vuejs.org/v2/guide/transitions.html#Transition-Modes). * You can use it when you want to control the render between state transitions. @@ -124,19 +148,38 @@ const enterRenders = { * } * ``` */ -class SwitchTransition extends React.Component { - state = { +class SwitchTransition extends React.Component { + state: State = { status: ENTERED, current: null, }; + static propTypes = { + /** + * Transition modes. + * `out-in`: Current element transitions out first, then when complete, the new element transitions in. + * `in-out`: New element transitions in first, then when complete, the current element transitions out. + * + * @type {'out-in'|'in-out'} + */ + mode: PropTypes.oneOf([modes.in, modes.out]), + /** + * Any `Transition` or `CSSTransition` component. + */ + children: PropTypes.oneOfType([PropTypes.element.isRequired]), + }; + + static defaultProps = { + mode: modes.out, + }; + appeared = false; componentDidMount() { this.appeared = true; } - static getDerivedStateFromProps(props, state) { + static getDerivedStateFromProps(props: Props, state: State) { if (props.children == null) { return { current: null, @@ -162,7 +205,7 @@ class SwitchTransition extends React.Component { }; } - changeState = (status, current = this.state.current) => { + changeState = (status: TransitionState, current = this.state.current) => { this.setState({ status, current, @@ -196,23 +239,4 @@ class SwitchTransition extends React.Component { } } -SwitchTransition.propTypes = { - /** - * Transition modes. - * `out-in`: Current element transitions out first, then when complete, the new element transitions in. - * `in-out`: New element transitions in first, then when complete, the current element transitions out. - * - * @type {'out-in'|'in-out'} - */ - mode: PropTypes.oneOf([modes.in, modes.out]), - /** - * Any `Transition` or `CSSTransition` component. - */ - children: PropTypes.oneOfType([PropTypes.element.isRequired]), -}; - -SwitchTransition.defaultProps = { - mode: modes.out, -}; - export default SwitchTransition; From 3f1263cec022cde034ca1a560532b93cdb3b6b40 Mon Sep 17 00:00:00 2001 From: Toru Kobayashi Date: Tue, 5 Apr 2022 23:44:33 +0900 Subject: [PATCH 5/9] refactor: migrate Transition to TypeScript --- src/{Transition.js => Transition.tsx} | 516 ++++++++++++++------------ 1 file changed, 281 insertions(+), 235 deletions(-) rename src/{Transition.js => Transition.tsx} (50%) diff --git a/src/Transition.js b/src/Transition.tsx similarity index 50% rename from src/Transition.js rename to src/Transition.tsx index 04e845db..47698e66 100644 --- a/src/Transition.js +++ b/src/Transition.tsx @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; +import type { RefObject, ReactNode } from 'react'; import ReactDOM from 'react-dom'; import config from './config'; @@ -12,6 +13,37 @@ export const ENTERING = 'entering'; export const ENTERED = 'entered'; export const EXITING = 'exiting'; +export type TransitionState = + | 'unmounted' + | 'exited' + | 'entering' + | 'entered' + | 'exiting'; + +export type Props = { + nodeRef?: RefObject; + // The childProps argument is not documented + children: (state: TransitionState, childProps: any) => ReactNode; + in: boolean; + mountOnEnter: boolean; + unmountOnExit: boolean; + appear: boolean; + enter: boolean; + exit: boolean; + timeout: number | { appear?: number; enter?: number; exit?: number }; + addEndListener: (node: HTMLElement | undefined, done: boolean) => void; + onEnter: (maybeNode: HTMLElement | boolean, isAppearing?: boolean) => void; + onEntering: (maybeNode: HTMLElement | boolean, isAppearing?: boolean) => void; + onEntered: (maybeNode: HTMLElement | boolean, isAppearing?: boolean) => void; + onExit: (node?: HTMLElement) => void; + onExiting: (node?: HTMLElement) => void; + onExited: (node?: HTMLElement) => void; +}; + +type State = { + status: TransitionState; +}; + /** * The Transition component lets you describe a transition from one component * state to another _over time_ with a simple declarative API. Most commonly @@ -103,10 +135,234 @@ export const EXITING = 'exiting'; * When `in` is `false` the same thing happens except the state moves from * `'exiting'` to `'exited'`. */ -class Transition extends React.Component { +class Transition extends React.Component { + appearStatus: TransitionState | null; + nextCallback: any; + + static 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, + }; + static UNMOUNTED = UNMOUNTED; + static EXITED = EXITED; + static ENTERING = ENTERING; + static ENTERED = ENTERED; + static EXITING = EXITING; + + static propTypes = { + /** + * A React reference to DOM element that need to transition: + * https://stackoverflow.com/a/51127130/4671932 + * + * - 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({ + // @ts-expect-error We'll remove the PropTypes definition + current: + typeof Element === 'undefined' + ? PropTypes.any + : // @ts-expect-error We'll remove the PropTypes definition + (propValue, key, componentName, location, propFullName, secret) => { + const value = propValue[key]; + + return PropTypes.instanceOf( + value && 'ownerDocument' in value + ? value.ownerDocument.defaultView.Element + : Element + // @ts-expect-error We'll remove the PropTypes definition + )(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: any, ...args: any[]) => { + let pt = timeoutsShape; + // @ts-expect-error We'll remove the PropTypes definition + if (!props.addEndListener) pt = pt.isRequired; + // @ts-expect-error We'll remove the PropTypes definition + 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. + * + * ```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. + * + * @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. + * + * @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. + * + * @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, + }; + static contextType = TransitionGroupContext; - constructor(props, context) { + constructor(props: Props, context: any) { super(props, context); let parentGroup = context; @@ -114,7 +370,7 @@ class Transition extends React.Component { let appear = parentGroup && !parentGroup.isMounting ? props.enter : props.appear; - let initialStatus; + let initialStatus: TransitionState; this.appearStatus = null; @@ -138,7 +394,10 @@ class Transition extends React.Component { this.nextCallback = null; } - static getDerivedStateFromProps({ in: nextIn }, prevState) { + static getDerivedStateFromProps( + { in: nextIn }: { in: boolean }, + prevState: State + ) { if (nextIn && prevState.status === UNMOUNTED) { return { status: EXITED }; } @@ -169,8 +428,8 @@ class Transition extends React.Component { this.updateStatus(true, this.appearStatus); } - componentDidUpdate(prevProps) { - let nextStatus = null; + componentDidUpdate(prevProps: Props) { + let nextStatus: TransitionState | null = null; if (prevProps !== this.props) { const { status } = this.state; @@ -195,18 +454,18 @@ class Transition extends React.Component { const { timeout } = this.props; let exit, enter, 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; + } else { + exit = enter = appear = timeout; } return { exit, enter, appear }; } - updateStatus(mounting = false, nextStatus) { + updateStatus(mounting = false, nextStatus: TransitionState | null) { if (nextStatus !== null) { // nextStatus will always be ENTERING or EXITING. this.cancelNextCallback(); @@ -221,7 +480,7 @@ class Transition extends React.Component { } } - performEnter(mounting) { + performEnter(mounting: boolean) { const { enter } = this.props; const appearing = this.context ? this.context.isMounting : mounting; const [maybeNode, maybeAppearing] = this.props.nodeRef @@ -255,7 +514,8 @@ class Transition extends React.Component { performExit() { const { exit } = this.props; const timeouts = this.getTimeouts(); - const maybeNode = this.props.nodeRef + // @ts-expect-error FIXME: Type 'Element | Text | null | undefined' is not assignable to type 'HTMLElement | undefined' Type 'null' is not assignable to type 'HTMLElement | undefined'.ts(2322) + const maybeNode: HTMLElement | undefined = this.props.nodeRef ? undefined : ReactDOM.findDOMNode(this); @@ -287,7 +547,7 @@ class Transition extends React.Component { } } - safeSetState(nextState, callback) { + safeSetState(nextState: State, 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. @@ -295,15 +555,15 @@ class Transition extends React.Component { this.setState(nextState, callback); } - setNextCallback(callback) { + setNextCallback(callback: () => void) { let active = true; - this.nextCallback = (event) => { + this.nextCallback = () => { if (active) { active = false; this.nextCallback = null; - callback(event); + callback(); } }; @@ -314,7 +574,7 @@ class Transition extends React.Component { return this.nextCallback; } - onTransitionEnd(timeout, handler) { + onTransitionEnd(timeout: number | undefined, handler: () => void) { this.setNextCallback(handler); const node = this.props.nodeRef ? this.props.nodeRef.current @@ -372,230 +632,16 @@ class Transition extends React.Component { {typeof children === 'function' ? children(status, childProps) - : React.cloneElement(React.Children.only(children), childProps)} + : // @ts-expect-error FIXME: Type 'ReactChildren' is missing the following properties from type 'ReactElement>': type, props, keyts(2769) + React.cloneElement(React.Children.only(children), childProps)} ); } } -Transition.propTypes = { - /** - * A React reference to DOM element that need to transition: - * https://stackoverflow.com/a/51127130/4671932 - * - * - 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. - * - * ```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. - * - * @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. - * - * @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. - * - * @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; +function noop() { + /* noop */ +} export default Transition; From c48efa9d003b7694ee04f5b0e83ff0d651bf13b8 Mon Sep 17 00:00:00 2001 From: Toru Kobayashi Date: Tue, 5 Apr 2022 23:46:10 +0900 Subject: [PATCH 6/9] refactor: migrate CSSTransition to TypeScript --- src/{CSSTransition.js => CSSTransition.tsx} | 65 ++++++++++++++++----- 1 file changed, 50 insertions(+), 15 deletions(-) rename src/{CSSTransition.js => CSSTransition.tsx} (78%) diff --git a/src/CSSTransition.js b/src/CSSTransition.tsx similarity index 78% rename from src/CSSTransition.js rename to src/CSSTransition.tsx index ef97b825..4ca8d4b8 100644 --- a/src/CSSTransition.js +++ b/src/CSSTransition.tsx @@ -4,14 +4,32 @@ import addOneClass from 'dom-helpers/addClass'; import removeOneClass from 'dom-helpers/removeClass'; import React from 'react'; -import Transition from './Transition'; +import Transition, { Props as TransitionProps } from './Transition'; import { classNamesShape } from './utils/PropTypes'; -const addClass = (node, classes) => +const addClass = (node: HTMLElement, classes: string) => node && classes && classes.split(' ').forEach((c) => addOneClass(node, c)); -const removeClass = (node, classes) => +const removeClass = (node: HTMLElement, classes: string) => node && classes && classes.split(' ').forEach((c) => removeOneClass(node, c)); +type TransitionClassNames = { + appear: string; + appearActive: string; + appearDone: string; + enter: string; + enterActive: string; + enterDone: string; + exit: string; + exitActive: string; + exitDone: string; +}; + +type Props = TransitionProps & { + classNames: string | Partial; +}; + +type TransitionClassNameKeys = 'appear' | 'enter' | 'exit'; + /** * A transition component inspired by the excellent * [ng-animate](https://docs.angularjs.org/api/ngAnimate) library, you should @@ -81,7 +99,7 @@ const removeClass = (node, classes) => * [`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 { +class CSSTransition extends React.Component { static defaultProps = { classNames: '', }; @@ -92,7 +110,7 @@ class CSSTransition extends React.Component { exit: {}, }; - onEnter = (maybeNode, maybeAppearing) => { + onEnter = (maybeNode: HTMLElement | boolean, maybeAppearing?: boolean) => { const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing); this.removeClasses(node, 'exit'); this.addClass(node, appearing ? 'appear' : 'enter', 'base'); @@ -102,7 +120,7 @@ class CSSTransition extends React.Component { } }; - onEntering = (maybeNode, maybeAppearing) => { + onEntering = (maybeNode: HTMLElement | boolean, maybeAppearing?: boolean) => { const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing); const type = appearing ? 'appear' : 'enter'; this.addClass(node, type, 'active'); @@ -112,7 +130,7 @@ class CSSTransition extends React.Component { } }; - onEntered = (maybeNode, maybeAppearing) => { + onEntered = (maybeNode: HTMLElement | boolean, maybeAppearing?: boolean) => { const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing); const type = appearing ? 'appear' : 'enter'; this.removeClasses(node, type); @@ -123,7 +141,7 @@ class CSSTransition extends React.Component { } }; - onExit = (maybeNode) => { + onExit = (maybeNode?: HTMLElement) => { const [node] = this.resolveArguments(maybeNode); this.removeClasses(node, 'appear'); this.removeClasses(node, 'enter'); @@ -134,7 +152,7 @@ class CSSTransition extends React.Component { } }; - onExiting = (maybeNode) => { + onExiting = (maybeNode?: HTMLElement) => { const [node] = this.resolveArguments(maybeNode); this.addClass(node, 'exit', 'active'); @@ -143,7 +161,7 @@ class CSSTransition extends React.Component { } }; - onExited = (maybeNode) => { + onExited = (maybeNode?: HTMLElement) => { const [node] = this.resolveArguments(maybeNode); this.removeClasses(node, 'exit'); this.addClass(node, 'exit', 'done'); @@ -154,12 +172,16 @@ class CSSTransition extends React.Component { }; // when prop `nodeRef` is provided `node` is excluded - resolveArguments = (maybeNode, maybeAppearing) => + resolveArguments = ( + maybeNode: HTMLElement | boolean | undefined, + maybeAppearing?: boolean + ): [HTMLElement, boolean] => + // @ts-expect-error FIXME: Type at position 1 in source is not compatible with type at position 1 in target. Type 'boolean | HTMLElement' is not assignable to type 'boolean'. Type 'HTMLElement' is not assignable to type 'boolean'.ts(2322) this.props.nodeRef ? [this.props.nodeRef.current, maybeNode] // here `maybeNode` is actually `appearing` : [maybeNode, maybeAppearing]; // `findDOMNode` was used - getClassNames = (type) => { + getClassNames = (type: TransitionClassNameKeys) => { const { classNames } = this.props; const isStringClassNames = typeof classNames === 'string'; const prefix = isStringClassNames && classNames ? `${classNames}-` : ''; @@ -183,7 +205,11 @@ class CSSTransition extends React.Component { }; }; - addClass(node, type, phase) { + addClass( + node: HTMLElement | null, + type: TransitionClassNameKeys, + phase: 'base' | 'active' | 'done' + ) { let className = this.getClassNames(type)[`${phase}ClassName`]; const { doneClassName } = this.getClassNames('enter'); @@ -194,32 +220,40 @@ class CSSTransition extends React.Component { // This is to force a repaint, // which is necessary in order to transition styles when adding a class name. if (phase === 'active') { - /* eslint-disable no-unused-expressions */ + /* eslint-disable no-unused-expressions, @typescript-eslint/no-unused-expressions */ node && node.scrollTop; } if (className) { + // @ts-expect-error FIXME: Property 'active' does not exist on type '{} | {} | {}'.ts(7053) this.appliedClasses[type][phase] = className; + // @ts-expect-error FIXME: Argument of type 'HTMLElement | null' is not assignable to parameter of type 'HTMLElement'. Type 'null' is not assignable to type 'HTMLElement'.ts(2345) addClass(node, className); } } - removeClasses(node, type) { + removeClasses(node: HTMLElement | null, type: TransitionClassNameKeys) { const { + // @ts-expect-error FIXME: Property 'base' does not exist on type '{} | {} | {}'.ts(2339) base: baseClassName, + // @ts-expect-error FIXME: Property 'active' does not exist on type '{} | {} | {}'.ts(2339) active: activeClassName, + // @ts-expect-error FIMXE: Property 'done' does not exist on type '{} | {} | {}'.ts(2339) done: doneClassName, } = this.appliedClasses[type]; this.appliedClasses[type] = {}; if (baseClassName) { + // @ts-expect-error FIXME: Argument of type 'HTMLElement | null' is not assignable to parameter of type 'HTMLElement'. Type 'null' is not assignable to type 'HTMLElement'.ts(2345) removeClass(node, baseClassName); } if (activeClassName) { + // @ts-expect-error FIXME: Argument of type 'HTMLElement | null' is not assignable to parameter of type 'HTMLElement'. Type 'null' is not assignable to type 'HTMLElement'.ts(2345) removeClass(node, activeClassName); } if (doneClassName) { + // @ts-expect-error FIXME: Argument of type 'HTMLElement | null' is not assignable to parameter of type 'HTMLElement'. Type 'null' is not assignable to type 'HTMLElement'.ts(2345) removeClass(node, doneClassName); } } @@ -241,6 +275,7 @@ class CSSTransition extends React.Component { } } +// @ts-expect-error To make TS migration diffs minimum, I've left propTypes here instead of defining a static property CSSTransition.propTypes = { ...Transition.propTypes, From 8673957bb47fdcb00032c55ac0b902d7e5c30b79 Mon Sep 17 00:00:00 2001 From: Toru Kobayashi Date: Tue, 5 Apr 2022 23:47:00 +0900 Subject: [PATCH 7/9] refactor: migrate TransitionGroup to TypeScript --- ...TransitionGroup.js => TransitionGroup.tsx} | 67 ++++++++++++++----- 1 file changed, 49 insertions(+), 18 deletions(-) rename src/{TransitionGroup.js => TransitionGroup.tsx} (71%) diff --git a/src/TransitionGroup.js b/src/TransitionGroup.tsx similarity index 71% rename from src/TransitionGroup.js rename to src/TransitionGroup.tsx index 14783c0f..e6f64164 100644 --- a/src/TransitionGroup.js +++ b/src/TransitionGroup.tsx @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; +import type { ReactElement, ReactChild } from 'react'; import TransitionGroupContext from './TransitionGroupContext'; import { @@ -8,11 +9,32 @@ import { getNextChildMapping, } from './utils/ChildMapping'; -const values = Object.values || ((obj) => Object.keys(obj).map((k) => obj[k])); +const values = + Object.values || + ((obj: Record) => Object.keys(obj).map((k) => obj[k])); const defaultProps = { component: 'div', - childFactory: (child) => child, + childFactory: (child: ReactElement) => child, +}; + +type Props = { + component: any; + children: any; + appear: boolean; + enter: boolean; + exit: boolean; + childFactory: (child: ReactElement) => ReactElement; +}; + +type State = { + children: Record; + contextValue: { isMounting: boolean }; + handleExited: ( + child: ReactElement<{ onExited: (node: HTMLElement) => void }>, + node: HTMLElement + ) => void; + firstRender: boolean; }; /** @@ -29,13 +51,17 @@ const defaultProps = { * component. This means you can mix and match animations across different list * items. */ -class TransitionGroup extends React.Component { - constructor(props, context) { +class TransitionGroup extends React.Component { + static defaultProps = defaultProps; + + mounted = false; + constructor(props: Props, context: any) { super(props, context); const handleExited = this.handleExited.bind(this); // Initial children should all be entering, dependent on appear + // @ts-expect-error FIXME: Property 'children' is missing in type '{ contextValue: { isMounting: true; }; handleExited: (child: React.ReactElement<{ onExited: (node: HTMLElement) => void; }, string | React.JSXElementConstructor>, node: HTMLElement) => void; firstRender: true; }' but required in type 'Readonly'.ts(2741) this.state = { contextValue: { isMounting: true }, handleExited, @@ -55,8 +81,8 @@ class TransitionGroup extends React.Component { } static getDerivedStateFromProps( - nextProps, - { children: prevChildMapping, handleExited, firstRender } + nextProps: Props, + { children: prevChildMapping, handleExited, firstRender }: State ) { return { children: firstRender @@ -67,10 +93,12 @@ class TransitionGroup extends React.Component { } // node is `undefined` when user provided `nodeRef` prop - handleExited(child, node) { + handleExited( + child: ReactElement<{ onExited: (node: HTMLElement) => void }>, + node: HTMLElement + ) { let currentChildMapping = getChildMapping(this.props.children); - - if (child.key in currentChildMapping) return; + if (child.key && child.key in currentChildMapping) return; if (child.props.onExited) { child.props.onExited(node); @@ -79,8 +107,9 @@ class TransitionGroup extends React.Component { if (this.mounted) { this.setState((state) => { let children = { ...state.children }; - - delete children[child.key]; + if (child.key) { + delete children[child.key]; + } return { children }; }); } @@ -89,11 +118,14 @@ class TransitionGroup extends React.Component { render() { const { component: Component, childFactory, ...props } = this.props; const { contextValue } = this.state; + // @ts-expect-error FIXME: Type 'undefined' is not assignable to type 'ReactElement>'.ts(2345) const children = values(this.state.children).map(childFactory); - - delete props.appear; - delete props.enter; - delete props.exit; + const { + appear: _appear, + enter: _enter, + exit: _exit, + ...delegatingProps + } = props; if (Component === null) { return ( @@ -104,12 +136,13 @@ class TransitionGroup extends React.Component { } return ( - {children} + {children} ); } } +// @ts-expect-error To make TS migration diffs minimum, I've left propTypes here instead of defining a static property TransitionGroup.propTypes = { /** * `` renders a `
` by default. You can change this @@ -166,6 +199,4 @@ TransitionGroup.propTypes = { childFactory: PropTypes.func, }; -TransitionGroup.defaultProps = defaultProps; - export default TransitionGroup; From 4e6b33d1703f149a6082756671c8c2cde4c1a20f Mon Sep 17 00:00:00 2001 From: Toru Kobayashi Date: Wed, 6 Apr 2022 00:10:23 +0900 Subject: [PATCH 8/9] refactor: migrate ReplaceTransition to TypeScript --- ...aceTransition.js => ReplaceTransition.tsx} | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) rename src/{ReplaceTransition.js => ReplaceTransition.tsx} (50%) diff --git a/src/ReplaceTransition.js b/src/ReplaceTransition.tsx similarity index 50% rename from src/ReplaceTransition.js rename to src/ReplaceTransition.tsx index 7c0b9092..5e7144bb 100644 --- a/src/ReplaceTransition.js +++ b/src/ReplaceTransition.tsx @@ -1,7 +1,13 @@ import PropTypes from 'prop-types'; import React from 'react'; +import type { ReactElement } from 'react'; import ReactDOM from 'react-dom'; import TransitionGroup from './TransitionGroup'; +import type { Props as TransitionProps } from './Transition'; + +type Props = Omit & { + children: [ReactElement, ReactElement]; +}; /** * The `` component is a specialized `Transition` component @@ -14,32 +20,35 @@ import TransitionGroup from './TransitionGroup'; * * ``` */ -class ReplaceTransition extends React.Component { - handleEnter = (...args) => this.handleLifecycle('onEnter', 0, args); - handleEntering = (...args) => this.handleLifecycle('onEntering', 0, args); - handleEntered = (...args) => this.handleLifecycle('onEntered', 0, args); +class ReplaceTransition extends React.Component { + handleEnter = (...args: any) => this.handleLifecycle('onEnter', 0, args); + handleEntering = (...args: any) => + this.handleLifecycle('onEntering', 0, args); + handleEntered = (...args: any) => this.handleLifecycle('onEntered', 0, args); - handleExit = (...args) => this.handleLifecycle('onExit', 1, args); - handleExiting = (...args) => this.handleLifecycle('onExiting', 1, args); - handleExited = (...args) => this.handleLifecycle('onExited', 1, args); + handleExit = (...args: any) => this.handleLifecycle('onExit', 1, args); + handleExiting = (...args: any) => this.handleLifecycle('onExiting', 1, args); + handleExited = (...args: any) => this.handleLifecycle('onExited', 1, args); - handleLifecycle(handler, idx, originalArgs) { + handleLifecycle(handler: any, idx: number, originalArgs: any) { const { children } = this.props; - const child = React.Children.toArray(children)[idx]; + // @ts-expect-error FIXME: Type 'string' is not assignable to type 'ReactElement>'.ts(2322) + const child: ChildElement = React.Children.toArray(children)[idx]; if (child.props[handler]) child.props[handler](...originalArgs); + // @ts-expect-error Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'Readonly & Readonly<{ children?: ReactNode; }>'.ts(7053) if (this.props[handler]) { const maybeNode = child.props.nodeRef ? undefined : ReactDOM.findDOMNode(this); - + // @ts-expect-error FIXME: Argument of type 'Element | Text | null | undefined' is not assignable to parameter of type 'HTMLElement'.ts(2769) this.props[handler](maybeNode); } } render() { - const { children, in: inProp, ...props } = this.props; - const [first, second] = React.Children.toArray(children); + const { children, in: inProp, ...props }: any = this.props; + const [first, second]: any = React.Children.toArray(children); delete props.onEnter; delete props.onEntering; @@ -68,9 +77,10 @@ class ReplaceTransition extends React.Component { } } +// @ts-expect-error To make TS migration diffs minimum, I've left propTypes here instead of defining a static property ReplaceTransition.propTypes = { in: PropTypes.bool.isRequired, - children(props, propName) { + children(props: any, propName: any) { if (React.Children.count(props[propName]) !== 2) return new Error( `"${propName}" must be exactly two transition components.` From 4aa3aed085d2f577f77c054bea5eaf4d2c00860e Mon Sep 17 00:00:00 2001 From: Toru Kobayashi Date: Wed, 6 Apr 2022 00:10:35 +0900 Subject: [PATCH 9/9] chore: update size-snapshot --- .size-snapshot.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.size-snapshot.json b/.size-snapshot.json index 7825d6c3..c90cbc0d 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,12 +1,12 @@ { "lib/dist/react-transition-group.js": { - "bundled": 98066, - "minified": 26272, - "gzipped": 8027 + "bundled": 101943, + "minified": 26331, + "gzipped": 8046 }, "lib/dist/react-transition-group.min.js": { - "bundled": 55134, - "minified": 17999, - "gzipped": 5654 + "bundled": 58919, + "minified": 18068, + "gzipped": 5664 } }