From 2ff39cf22db3dfde5a43255abd990e107d2783cd Mon Sep 17 00:00:00 2001 From: qiqiboy Date: Sat, 14 May 2022 02:26:33 +0800 Subject: [PATCH] feat: support react-router v6 --- README.md | 68 ++-- animate.css | 15 + dist/AnimatedRouter.d.ts | 39 +- dist/react-animated-router.cjs.development.js | 377 +++++++++--------- dist/react-animated-router.cjs.production.js | 2 +- dist/react-animated-router.esm.development.js | 353 ++++++++-------- dist/react-animated-router.esm.production.js | 2 +- eslint.config.js | 95 +++++ package.json | 123 +++--- rollup.config.js | 118 +++--- src/AnimatedRouter.tsx | 325 +++++++++------ src/animate.scss | 16 + tsconfig.json | 1 + 13 files changed, 855 insertions(+), 679 deletions(-) create mode 100644 eslint.config.js diff --git a/README.md b/README.md index 924d0ca..94972e2 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ react 路由切换动画,支持嵌套路由 (nested routers)和动态路由(dynamic routers)。 +> 当前版本要求`react-router-dom`为`v6`版本,如果您还在使用`v4`或`v5`,请查看 [react-animated-router@0.2.4](https://github.com/qiqiboy/react-animated-router/tree/0.2.4) + ### 安装 ```bash @@ -21,8 +23,8 @@ import 'react-animated-router/animate.css'; //导入默认的切换动画样式 > > 如果路由在多个页面文件中都有定义,为了避免每次都需要同时导入 react-animated-router 和 animate.css(如果只有一处引入 animate.css,其它地方不引入的话,在有 code split 的项目中,可能会有样式丢失),有两种办法可以优化: > -> * 一,将`animate.css`在入口文件中引入,其它地方可以只引用 react-animated-router -> * 二,将 react-animated-router 和 animate.css 包装到一个模块文件中再默认导出,在其他地方引用该新模块: +> - 一,将`animate.css`在入口文件中引入,其它地方可以只引用 react-animated-router +> - 二,将 react-animated-router 和 animate.css 包装到一个模块文件中再默认导出,在其他地方引用该新模块: ```javascript // 自己项目中的AnimatedRouter模块 @@ -35,49 +37,57 @@ export { default } from 'react-animated-router'; //直接将react-animated-route ### 如何使用 -`AnimatedRouter`是一个标准的 React 组件,类似`react-router`中的`Switch`,将它放入你的项目中,然后在需要支持动画的地方,使用`AnimatedRouter`替换你的`Switch`组件即可。 +`AnimatedRouter`是一个标准的 React 组件,它可以给一组`Route`组件增加动画切换效果,将它放入你的项目中,然后在需要支持动画的地方,使用`AnimatedRouter`替换你的`Routes`组件即可。 -| 属性 | 类型 | 默认值 | 描述 | -| :-----------: | :----------: | :---------------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| prefix | 字符串,可选 | `animated-router` | 应用到 CSSTransition 组件的 classNames 前缀。如果要在同一个项目中使用不同的动画,可以通过设置前缀来定义不同的动画。关于如何自定义动画,请查看下方说明 | -| timeout | 数字,可选 | 无 | 动画持续时间(毫秒),可以不传,默认为监听 transitionend 时间来判断动画结束。如果有动画异常,可以尝试设置该值,需要注意的是,该值应该与动画样式中定义的过渡时间一致 | -| className | 字符串,可选 | 无 | 如果传入 className 则会添加到动画节点所在容器节点上 | -| transitionKey | 字符串,可选 | 无 | 即每个页面节点需要的 key 值,如果不传则会使用当前页面地址的 pathname。
该属性可以用于处理路由页面中还有子路由时的情况,用来避免子路由切换会导致父级页面也一块被重载。 | -| component | 布尔,可选 | 'div' | AnimatedRouter 默认会 render 一个 div 节点,你可以通过该字段修改 render 的节点类型,例如,`component="section"`将会 render `
`节点。在 react v16+中,可以传入 `null` 来避免渲染该节点。 | -| appear | 布尔,可选 | false | [文档:appear](http://reactcommunity.org/react-transition-group/transition-group#TransitionGroup-prop-appear):是否启用组件首次挂载动画(启用的话将会触发 enter 进场动画) | -| enter | 布尔,可选 | true | [文档:enter](http://reactcommunity.org/react-transition-group/transition-group#TransitionGroup-prop-enter):是否启用进场动画 | -| exit | 布尔,可选 | true | [文档:exit](http://reactcommunity.org/react-transition-group/transition-group#TransitionGroup-prop-exit):是否启用离场动画 | +**`AnimatedRouter` 对于嵌套的路由声明也有效!** -> 例如,可以对父级路由使用 AnimatedRouter 时定义使用父级路由 path 当作 key: -> transitionKey={this.props.location.pathname.split('/').slice(0, 2).join('/')} +| 属性 | 类型 | 默认值 | 描述 | +| :-----------: | :-------------------: | :---------------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| location | `Location` 对象,可选 | 当前页面地址 | 等同于`Routes`的同名属性 | +| prefix | 字符串,可选 | `animated-router` | 应用到 CSSTransition 组件的 classNames 前缀。如果要在同一个项目中使用不同的动画,可以通过设置前缀来定义不同的动画。关于如何自定义动画,请查看下方说明 | +| timeout | 数字,可选 | 无 | 动画持续时间(毫秒),可以不传,默认为监听 transitionend 时间来判断动画结束。如果有动画异常,可以尝试设置该值,需要注意的是,该值应该与动画样式中定义的过渡时间一致 | +| className | 字符串,可选 | 无 | 如果传入 className 则会添加到动画节点所在容器节点上 | +| transitionKey | 字符串,可选 | 无 | 该属性用于标识路由动画节点,默认情况下为变换的路由所对应的`pathname`地址;`AnimatedRouter`已经智能处理了路由嵌套的情形,一般无需特定制定该参数。如果在嵌套路由场景中,要控制子路由变换时,父级路由的动画方式,可以传递该参数 | +| component | 布尔,可选 | 'div' | AnimatedRouter 默认会 render 一个 div 节点,你可以通过该字段修改 render 的节点类型,例如,`component="section"`将会 render `
`节点。在 react v16+中,可以传入 `null` 来避免渲染该节点。 | +| appear | 布尔,可选 | false | [文档:appear](http://reactcommunity.org/react-transition-group/transition-group#TransitionGroup-prop-appear):是否启用组件首次挂载动画(启用的话将会触发 enter 进场动画) | +| enter | 布尔,可选 | true | [文档:enter](http://reactcommunity.org/react-transition-group/transition-group#TransitionGroup-prop-enter):是否启用进场动画 | +| exit | 布尔,可选 | true | [文档:exit](http://reactcommunity.org/react-transition-group/transition-group#TransitionGroup-prop-exit):是否启用离场动画 | ```javascript import React, { Component } from 'react'; import { render } from 'react-dom'; -import { Route, Redirect, Switch, BrowserRouter } from 'react-router-dom'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; import AnimatedRouter from 'react-animated-router'; //导入我们的的AnimatedRouter组件 import 'react-animated-router/animate.css'; //导入默认的切换动画样式,如果需要其它切换样式,可以导入自己的动画样式定义文件 import Login from 'modules/Login'; import Signup from 'modules/Signup'; +import NestLayout from 'modules/NestLayout'; +import Nested from 'modules/Nested'; -class App extends Component { +function App() { render() { - /** 假如你的代码如此,则可直接使用最下方代码代替,即直接使用 AnimatedRouter 替换掉Switch + /** 假如你的代码如此,则可直接使用最下方代码代替,即直接使用 AnimatedRouter 替换掉 Routes * return ( - * - * - * - * - * + * + * } /> + * } /> + * }> + * } /> + * } /> + * + * * ); **/ return ( - - - + } /> + } /> + }> + } /> + } /> + ); } @@ -113,9 +123,5 @@ render( ### FAQ -* **Q: 动画执行异常?** +- **Q: 动画执行异常?** A: 可以尝试设置 timeout 属性,并保持与动画样式中定义的过渡时间一致(默认的 animate.scss 中为 300) - ---- - -更多说明以及我在探索路由动画应用中所遇到的坑,可以阅读我的[博客文章](http://www.qiqiboy.com/post/111)。 diff --git a/animate.css b/animate.css index 66b2f48..329e2fa 100644 --- a/animate.css +++ b/animate.css @@ -12,6 +12,12 @@ width: 100%; overflow: hidden; } +.animated-router-forward-appear { + opacity: 0; +} +.animated-router-forward-appear-active { + opacity: 1; +} .animated-router-forward-enter { transform: translate(100%); } @@ -24,6 +30,12 @@ .animated-router-forward-exit-active { transform: translate(-100%); } +.animated-router-backward-appear { + opacity: 1; +} +.animated-router-backward-appear-active { + opacity: 0; +} .animated-router-backward-enter { transform: translate(-100%); } @@ -36,6 +48,9 @@ .animated-router-backward-exit-active { transform: translate(100%); } +.animated-router-forward-appear-active, .animated-router-backward-appear-active { + transition: opacity 0.3s linear; +} .animated-router-forward-enter-active, .animated-router-forward-exit-active, .animated-router-backward-enter-active, .animated-router-backward-exit-active { /* 不同过渡阶段需要的过渡时间与缓动效果 */ transition: transform 0.3s linear; diff --git a/dist/AnimatedRouter.d.ts b/dist/AnimatedRouter.d.ts index c04c061..2812148 100644 --- a/dist/AnimatedRouter.d.ts +++ b/dist/AnimatedRouter.d.ts @@ -1,7 +1,6 @@ -import React, { Component, ReactType } from 'react'; +import React, { ReactType } from 'react'; +import { Location, RouteObject } from 'react-router'; import { TransitionActions } from 'react-transition-group/Transition'; -import { RouteComponentProps } from 'react-router'; -import PropTypes from 'prop-types'; import './animate.scss'; interface AnimatedRouterProps extends TransitionActions { className?: string; @@ -9,6 +8,8 @@ interface AnimatedRouterProps extends TransitionActions { timeout?: number; prefix?: string; component?: ReactType | null; + location?: Location; + children: React.ReactNode | RouteObject[]; } /** * @desc 路由动画组件 @@ -18,31 +19,7 @@ interface AnimatedRouterProps extends TransitionActions { * import AnimatedRouter from 'react-animated-router'; * import 'react-animated-router/animate.css'; */ -declare class AnimatedRouter extends Component { - static propTypes: { - className: PropTypes.Requireable; - transitionKey: PropTypes.Requireable; - timeout: PropTypes.Requireable; - prefix: PropTypes.Requireable; - appear: PropTypes.Requireable; - enter: PropTypes.Requireable; - exit: PropTypes.Requireable; - component: PropTypes.Requireable; - }; - static defaultProps: { - prefix: string; - }; - inTransition: boolean; - rootNode: Element; - lastTransitionNode: Element; - setInTransition(isAdd: any): void; - onEnter: (node: any) => void; - onEntering: (node: any) => void; - onEntered: (node: any) => void; - componentDidMount(): void; - render(): JSX.Element; -} -declare const _default: React.ComponentClass, "enter" | "exit" | "timeout" | "className" | "transitionKey" | "prefix" | "component" | "appear"> & { - wrappedComponentRef?: ((instance: AnimatedRouter | null) => void) | React.RefObject | null | undefined; -}, any> & import("react-router").WithRouterStatics; -export default _default; +declare const AnimatedRouter: React.FC; +export default AnimatedRouter; diff --git a/dist/react-animated-router.cjs.development.js b/dist/react-animated-router.cjs.development.js index 9904b8a..f7cc0da 100644 --- a/dist/react-animated-router.cjs.development.js +++ b/dist/react-animated-router.cjs.development.js @@ -2,100 +2,80 @@ Object.defineProperty(exports, '__esModule', { value: true }); -function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } - var React = require('react'); -var React__default = _interopDefault(React); var reactDom = require('react-dom'); +var PropTypes = require('prop-types'); var reactTransitionGroup = require('react-transition-group'); var reactRouter = require('react-router'); -var PropTypes = _interopDefault(require('prop-types')); -function _classCallCheck(instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } -} +function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } -function _defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); - } -} +var React__default = /*#__PURE__*/_interopDefaultLegacy(React); +var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes); -function _createClass(Constructor, protoProps, staticProps) { - if (protoProps) _defineProperties(Constructor.prototype, protoProps); - if (staticProps) _defineProperties(Constructor, staticProps); - return Constructor; +function unwrapExports (x) { + return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } -function _typeof2(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof2 = function _typeof2(obj) { return typeof obj; }; } else { _typeof2 = function _typeof2(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof2(obj); } +function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; +} -function _typeof(obj) { - if (typeof Symbol === "function" && _typeof2(Symbol.iterator) === "symbol") { - _typeof = function _typeof(obj) { - return _typeof2(obj); - }; +var defineProperty$1 = createCommonjsModule(function (module) { +function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); } else { - _typeof = function _typeof(obj) { - return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : _typeof2(obj); - }; + obj[key] = value; } - return _typeof(obj); + return obj; } -function _assertThisInitialized(self) { - if (self === void 0) { - throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); - } - - return self; -} +module.exports = _defineProperty, module.exports.__esModule = true, module.exports["default"] = module.exports; +}); -function _possibleConstructorReturn(self, call) { - if (call && (_typeof(call) === "object" || typeof call === "function")) { - return call; - } +unwrapExports(defineProperty$1); - return _assertThisInitialized(self); -} +var defineProperty = defineProperty$1; -function _getPrototypeOf(o) { - _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { - return o.__proto__ || Object.getPrototypeOf(o); - }; - return _getPrototypeOf(o); -} +var objectSpread2 = createCommonjsModule(function (module) { +function ownKeys(object, enumerableOnly) { + var keys = Object.keys(object); -function _setPrototypeOf(o, p) { - _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { - o.__proto__ = p; - return o; - }; + if (Object.getOwnPropertySymbols) { + var symbols = Object.getOwnPropertySymbols(object); + enumerableOnly && (symbols = symbols.filter(function (sym) { + return Object.getOwnPropertyDescriptor(object, sym).enumerable; + })), keys.push.apply(keys, symbols); + } - return _setPrototypeOf(o, p); + return keys; } -function _inherits(subClass, superClass) { - if (typeof superClass !== "function" && superClass !== null) { - throw new TypeError("Super expression must either be null or a function"); +function _objectSpread2(target) { + for (var i = 1; i < arguments.length; i++) { + var source = null != arguments[i] ? arguments[i] : {}; + i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { + defineProperty(target, key, source[key]); + }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { + Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); + }); } - subClass.prototype = Object.create(superClass && superClass.prototype, { - constructor: { - value: subClass, - writable: true, - configurable: true - } - }); - if (superClass) _setPrototypeOf(subClass, superClass); + return target; } +module.exports = _objectSpread2, module.exports.__esModule = true, module.exports["default"] = module.exports; +}); + +var _objectSpread = unwrapExports(objectSpread2); + var isSSR = typeof window === 'undefined'; var lastLocation = { key: '', @@ -138,150 +118,157 @@ var isHistoryPush = function isHistoryPush(location, update) { * import AnimatedRouter from 'react-animated-router'; * import 'react-animated-router/animate.css'; */ -var AnimatedRouter = -/*#__PURE__*/ -function (_Component) { - _inherits(AnimatedRouter, _Component); - - function AnimatedRouter() { - var _getPrototypeOf2; +var AnimatedRouter = function AnimatedRouter(props) { + var baseLocation = reactRouter.useLocation(); + var rootRef = React.useRef(null); + var className = props.className, + children = props.children, + timeout = props.timeout, + prefix = props.prefix, + appear = props.appear, + enter = props.enter, + exit = props.exit, + transitionKey = props.transitionKey, + component = props.component, + _parentPath = props._parentPath, + _props$location = props.location, + location = _props$location === void 0 ? baseLocation : _props$location; + var self = React.useRef({ + inTransition: false, + inAppearTransition: !!appear + }).current; + var childrenRoutes = React.useMemo(function () { + return !Array.isArray(children) || React.isValidElement(children[0]) ? reactRouter.createRoutesFromChildren(children) : children; + }, [children]) || []; + + if (!transitionKey && childrenRoutes.length) { + var routes = reactRouter.matchRoutes(childrenRoutes.map(function (route) { + return _objectSpread(_objectSpread({}, route), {}, { + path: reactRouter.createPath(reactRouter.resolvePath(route.path || '', _parentPath)) + }); + }), location); + transitionKey = routes === null || routes === void 0 ? void 0 : routes[0].pathname; + } - var _this; + var animatedRoute = function animatedRoute(routes) { + return routes.map(function (route) { + var _route$children; + + if ((_route$children = route.children) !== null && _route$children !== void 0 && _route$children.length) { + var animatedElement = /*#__PURE__*/React__default["default"].createElement(AnimatedRouter, Object.assign({}, props, { + children: route.children, + location: location, + _parentPath: reactRouter.createPath(reactRouter.resolvePath(route.path || '', _parentPath)) + })); + return _objectSpread(_objectSpread({}, route), {}, { + children: [{ + element: animatedElement, + children: route.children + }] + }); + } - _classCallCheck(this, AnimatedRouter); + return route; + }); + }; - for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { - args[_key] = arguments[_key]; + var childElement = reactRouter.useRoutes(animatedRoute(childrenRoutes), location); + var setInTransition = React.useCallback(function (isAdd) { + if (self.rootNode) { + var inName = "".concat(prefix, "-in-transition"); + self.rootNode.className = self.rootNode.className.split(/\s+/).filter(function (name) { + return name !== inName; + }).concat(isAdd ? inName : []).join(' '); + } + }, [prefix, self]); + var onEnter = React.useCallback(function (node) { + self.inTransition || setInTransition(self.inTransition = true); + self.lastTransitionNode = node; + }, [self, setInTransition]); + var onEntering = React.useCallback(function (node) { + if (node && typeof timeout === 'number') { + node.style.transitionDuration = node.style.WebkitTransitionDuration = node.style.MozTransitionDuration = "".concat(timeout, "ms"); + } + }, [timeout]); + var onEntered = React.useCallback(function (node) { + if (self.lastTransitionNode === node) { + self.inTransition && setInTransition(self.inTransition = false); } - _this = _possibleConstructorReturn(this, (_getPrototypeOf2 = _getPrototypeOf(AnimatedRouter)).call.apply(_getPrototypeOf2, [this].concat(args))); - _this.inTransition = false; - _this.rootNode = void 0; - _this.lastTransitionNode = void 0; - - _this.onEnter = function (node) { - _this.inTransition || _this.setInTransition(_this.inTransition = true); - _this.lastTransitionNode = node; - }; - - _this.onEntering = function (node) { - var timeout = _this.props.timeout; - - if (node && typeof timeout === 'number') { - node.style.transitionDuration = node.style.WebkitTransitionDuration = node.style.MozTransitionDuration = timeout + 'ms'; - } - }; - - _this.onEntered = function (node) { - if (_this.lastTransitionNode === node) { - _this.inTransition && _this.setInTransition(_this.inTransition = false); - } - - if (node) { - var timeout = _this.props.timeout; // remove all transition classNames + if (self.inAppearTransition) { + self.inAppearTransition = false; + } - node.className = node.className.split(/\s+/).filter(function (name) { - return !/-(?:forward|backward)-(?:enter|exit)(?:-active)?$/.test(name); - }).join(' '); + if (node) { + // remove all transition classNames + node.className = node.className.split(/\s+/).filter(function (name) { + return !/-(?:forward|backward)-(?:enter|exit)(?:-active)?$/.test(name); + }).join(' '); - if (typeof timeout === 'number') { - node.style.transitionDuration = node.style.WebkitTransitionDuration = node.style.MozTransitionDuration = ''; - } + if (typeof timeout === 'number') { + node.style.transitionDuration = node.style.WebkitTransitionDuration = node.style.MozTransitionDuration = ''; } - }; + } + }, [self, setInTransition, timeout]); + var groupProps = { + appear: appear, + enter: enter, + exit: exit, + component: component + }; + var cssProps = { + onExit: onEnter, + onExiting: onEntering, + onExited: onEntered, + onEnter: onEnter, + onEntering: onEntering, + onEntered: onEntered + }; + var cls = ["".concat(prefix, "-container"), 'react-animated-router', className]; + React.useEffect(function () { + self.rootNode = reactDom.findDOMNode(rootRef.current); + }, [self]); - return _this; + if (isSSR || !childElement) { + return childElement; } - _createClass(AnimatedRouter, [{ - key: "setInTransition", - value: function setInTransition(isAdd) { - if (this.rootNode) { - var inName = this.props.prefix + '-in-transition'; - this.rootNode.className = this.rootNode.className.split(/\s+/).filter(function (name) { - return name !== inName; - }).concat(isAdd ? inName : []).join(' '); - } + return /*#__PURE__*/React__default["default"].createElement(reactTransitionGroup.TransitionGroup, Object.assign({ + ref: rootRef, + className: cls.filter(Boolean).join(' '), + childFactory: function childFactory(child) { + var classNames = "".concat(prefix, "-").concat(isHistoryPush(location, self.inAppearTransition ? false : child.props.in) ? 'forward' : 'backward'); + return React__default["default"].cloneElement(child, { + classNames: classNames + }); } - }, { - key: "componentDidMount", - value: function componentDidMount() { - this.rootNode = reactDom.findDOMNode(this); - } - }, { - key: "render", - value: function render() { - if (isSSR) { - return React__default.createElement(reactRouter.Switch, null, this.props.children); - } - - var _this$props = this.props, - className = _this$props.className, - location = _this$props.location, - children = _this$props.children, - timeout = _this$props.timeout, - prefix = _this$props.prefix, - appear = _this$props.appear, - enter = _this$props.enter, - exit = _this$props.exit, - component = _this$props.component; - var groupProps = { - appear: appear, - enter: enter, - exit: exit, - component: component - }; - var cssProps = { - onExit: this.onEnter, - onExiting: this.onEntering, - onExited: this.onEntered, - onEnter: this.onEnter, - onEntering: this.onEntering, - onEntered: this.onEntered - }; - var cls = [prefix + '-container', 'react-animated-router', className]; - return React__default.createElement(reactTransitionGroup.TransitionGroup, Object.assign({ - className: cls.filter(Boolean).join(' '), - childFactory: function childFactory(child) { - var classNames = prefix + '-' + (isHistoryPush(location, child.props.in) ? 'forward' : 'backward'); - return React__default.cloneElement(child, { - classNames: classNames - }); + }, groupProps), /*#__PURE__*/React__default["default"].createElement(reactTransitionGroup.CSSTransition, Object.assign({ + key: transitionKey || location.pathname, + addEndListener: function addEndListener(node, done) { + node.addEventListener('transitionend', function (e) { + // 确保动画来自于目标节点 + if (e.target === node) { + done(); } - }, groupProps), React__default.createElement(reactTransitionGroup.CSSTransition, Object.assign({ - key: this.props.transitionKey || location.pathname, - addEndListener: function addEndListener(node, done) { - node.addEventListener('transitionend', function (e) { - // 确保动画来自于目标节点 - if (e.target === node) { - done(); - } - }, false); - }, - unmountOnExit: true, - timeout: timeout - }, cssProps), React__default.createElement(reactRouter.Switch, { - location: location - }, children))); - } - }]); - - return AnimatedRouter; -}(React.Component); + }, false); + }, + unmountOnExit: true, + timeout: timeout + }, cssProps), childElement)); +}; AnimatedRouter.propTypes = { - className: PropTypes.string, - transitionKey: PropTypes.any, - timeout: PropTypes.number, - prefix: PropTypes.string, - appear: PropTypes.bool, - enter: PropTypes.bool, - exit: PropTypes.bool, - component: PropTypes.any + className: PropTypes__default["default"].string, + transitionKey: PropTypes__default["default"].oneOfType([PropTypes__default["default"].string, PropTypes__default["default"].number]), + timeout: PropTypes__default["default"].number, + prefix: PropTypes__default["default"].string, + appear: PropTypes__default["default"].bool, + enter: PropTypes__default["default"].bool, + exit: PropTypes__default["default"].bool, + component: PropTypes__default["default"].any }; AnimatedRouter.defaultProps = { prefix: 'animated-router' }; -var AnimatedRouter$1 = reactRouter.withRouter(AnimatedRouter); -exports.default = AnimatedRouter$1; +exports["default"] = AnimatedRouter; diff --git a/dist/react-animated-router.cjs.production.js b/dist/react-animated-router.cjs.production.js index 347fc99..194fe06 100644 --- a/dist/react-animated-router.cjs.production.js +++ b/dist/react-animated-router.cjs.production.js @@ -1 +1 @@ -"use strict";Object.defineProperty(exports,"__esModule",{value:true});function t(t){return t&&typeof t==="object"&&"default"in t?t["default"]:t}var e=require("react");var n=t(e);var r=require("react-dom");var o=require("react-transition-group");var i=require("react-router");function a(t,e){if(!(t instanceof e)){throw new TypeError("Cannot call a class as a function")}}function s(t,e){for(var n=0;n-1){E.splice(o+1)}else{E.push(r)}sessionStorage.setItem(v,E.join(","));h={isPush:o<0,key:r}}return h.isPush};var S=function(t){m(e,t);function e(){var t;var n;a(this,e);for(var r=arguments.length,o=new Array(r),i=0;i-1){m.splice(a+1)}else{m.push(n)}sessionStorage.setItem(v,m.join(","));d={isPush:a<0,key:n}}return d.isPush};var h=function a(o){var s=n.useLocation();var u=e.useRef(null);var c=o.className,l=o.children,d=o.timeout,v=o.prefix,m=o.appear,h=o.enter,y=o.exit,O=o.transitionKey,x=o.component,j=o._parentPath,P=o.location,g=P===void 0?s:P;var E=e.useRef({inTransition:false,inAppearTransition:!!m}).current;var T=e.useMemo((function(){return!Array.isArray(l)||e.isValidElement(l[0])?n.createRoutesFromChildren(l):l}),[l])||[];if(!O&&T.length){var k=n.matchRoutes(T.map((function(e){return f(f({},e),{},{path:n.createPath(n.resolvePath(e.path||"",j))})})),g);O=k===null||k===void 0?void 0:k[0].pathname}var w=function e(t){return t.map((function(e){var t;if((t=e.children)!==null&&t!==void 0&&t.length){var r=i["default"].createElement(a,Object.assign({},o,{children:e.children,location:g,_parentPath:n.createPath(n.resolvePath(e.path||"",j))}));return f(f({},e),{},{children:[{element:r,children:e.children}]})}return e}))};var N=n.useRoutes(w(T),g);var _=e.useCallback((function(e){if(E.rootNode){var t="".concat(v,"-in-transition");E.rootNode.className=E.rootNode.className.split(/\s+/).filter((function(e){return e!==t})).concat(e?t:[]).join(" ")}}),[v,E]);var D=e.useCallback((function(e){E.inTransition||_(E.inTransition=true);E.lastTransitionNode=e}),[E,_]);var M=e.useCallback((function(e){if(e&&typeof d==="number"){e.style.transitionDuration=e.style.WebkitTransitionDuration=e.style.MozTransitionDuration="".concat(d,"ms")}}),[d]);var S=e.useCallback((function(e){if(E.lastTransitionNode===e){E.inTransition&&_(E.inTransition=false)}if(E.inAppearTransition){E.inAppearTransition=false}if(e){e.className=e.className.split(/\s+/).filter((function(e){return!/-(?:forward|backward)-(?:enter|exit)(?:-active)?$/.test(e)})).join(" ");if(typeof d==="number"){e.style.transitionDuration=e.style.WebkitTransitionDuration=e.style.MozTransitionDuration=""}}}),[E,_,d]);var A={appear:m,enter:h,exit:y,component:x};var C={onExit:D,onExiting:M,onExited:S,onEnter:D,onEntering:M,onEntered:S};var R=["".concat(v,"-container"),"react-animated-router",c];e.useEffect((function(){E.rootNode=t.findDOMNode(u.current)}),[E]);if(p||!N){return N}return i["default"].createElement(r.TransitionGroup,Object.assign({ref:u,className:R.filter(Boolean).join(" "),childFactory:function e(t){var r="".concat(v,"-").concat(b(g,E.inAppearTransition?false:t.props.in)?"forward":"backward");return i["default"].cloneElement(t,{classNames:r})}},A),i["default"].createElement(r.CSSTransition,Object.assign({key:O||g.pathname,addEndListener:function e(t,r){t.addEventListener("transitionend",(function(e){if(e.target===t){r()}}),false)},unmountOnExit:true,timeout:d},C),N))};h.defaultProps={prefix:"animated-router"};exports["default"]=h; diff --git a/dist/react-animated-router.esm.development.js b/dist/react-animated-router.esm.development.js index 7ccd106..f0cb841 100644 --- a/dist/react-animated-router.esm.development.js +++ b/dist/react-animated-router.esm.development.js @@ -1,92 +1,48 @@ -import React, { Component } from 'react'; +import React, { useRef, useMemo, isValidElement, useCallback, useEffect } from 'react'; import { findDOMNode } from 'react-dom'; -import { TransitionGroup, CSSTransition } from 'react-transition-group'; -import { withRouter, Switch } from 'react-router'; import PropTypes from 'prop-types'; - -function _classCallCheck(instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } -} - -function _defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); - } -} - -function _createClass(Constructor, protoProps, staticProps) { - if (protoProps) _defineProperties(Constructor.prototype, protoProps); - if (staticProps) _defineProperties(Constructor, staticProps); - return Constructor; -} - -function _typeof2(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof2 = function _typeof2(obj) { return typeof obj; }; } else { _typeof2 = function _typeof2(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof2(obj); } - -function _typeof(obj) { - if (typeof Symbol === "function" && _typeof2(Symbol.iterator) === "symbol") { - _typeof = function _typeof(obj) { - return _typeof2(obj); - }; +import { TransitionGroup, CSSTransition } from 'react-transition-group'; +import { useLocation, createRoutesFromChildren, matchRoutes, createPath, resolvePath, useRoutes } from 'react-router'; + +function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); } else { - _typeof = function _typeof(obj) { - return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : _typeof2(obj); - }; + obj[key] = value; } - return _typeof(obj); + return obj; } -function _assertThisInitialized(self) { - if (self === void 0) { - throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); - } - - return self; -} +function ownKeys(object, enumerableOnly) { + var keys = Object.keys(object); -function _possibleConstructorReturn(self, call) { - if (call && (_typeof(call) === "object" || typeof call === "function")) { - return call; + if (Object.getOwnPropertySymbols) { + var symbols = Object.getOwnPropertySymbols(object); + enumerableOnly && (symbols = symbols.filter(function (sym) { + return Object.getOwnPropertyDescriptor(object, sym).enumerable; + })), keys.push.apply(keys, symbols); } - return _assertThisInitialized(self); + return keys; } -function _getPrototypeOf(o) { - _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { - return o.__proto__ || Object.getPrototypeOf(o); - }; - return _getPrototypeOf(o); -} - -function _setPrototypeOf(o, p) { - _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { - o.__proto__ = p; - return o; - }; - - return _setPrototypeOf(o, p); -} - -function _inherits(subClass, superClass) { - if (typeof superClass !== "function" && superClass !== null) { - throw new TypeError("Super expression must either be null or a function"); +function _objectSpread2(target) { + for (var i = 1; i < arguments.length; i++) { + var source = null != arguments[i] ? arguments[i] : {}; + i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { + _defineProperty(target, key, source[key]); + }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { + Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); + }); } - subClass.prototype = Object.create(superClass && superClass.prototype, { - constructor: { - value: subClass, - writable: true, - configurable: true - } - }); - if (superClass) _setPrototypeOf(subClass, superClass); + return target; } var isSSR = typeof window === 'undefined'; @@ -131,140 +87,148 @@ var isHistoryPush = function isHistoryPush(location, update) { * import AnimatedRouter from 'react-animated-router'; * import 'react-animated-router/animate.css'; */ -var AnimatedRouter = -/*#__PURE__*/ -function (_Component) { - _inherits(AnimatedRouter, _Component); - - function AnimatedRouter() { - var _getPrototypeOf2; +var AnimatedRouter = function AnimatedRouter(props) { + var baseLocation = useLocation(); + var rootRef = useRef(null); + var className = props.className, + children = props.children, + timeout = props.timeout, + prefix = props.prefix, + appear = props.appear, + enter = props.enter, + exit = props.exit, + transitionKey = props.transitionKey, + component = props.component, + _parentPath = props._parentPath, + _props$location = props.location, + location = _props$location === void 0 ? baseLocation : _props$location; + var self = useRef({ + inTransition: false, + inAppearTransition: !!appear + }).current; + var childrenRoutes = useMemo(function () { + return !Array.isArray(children) || isValidElement(children[0]) ? createRoutesFromChildren(children) : children; + }, [children]) || []; + + if (!transitionKey && childrenRoutes.length) { + var routes = matchRoutes(childrenRoutes.map(function (route) { + return _objectSpread2(_objectSpread2({}, route), {}, { + path: createPath(resolvePath(route.path || '', _parentPath)) + }); + }), location); + transitionKey = routes === null || routes === void 0 ? void 0 : routes[0].pathname; + } - var _this; + var animatedRoute = function animatedRoute(routes) { + return routes.map(function (route) { + var _route$children; + + if ((_route$children = route.children) !== null && _route$children !== void 0 && _route$children.length) { + var animatedElement = /*#__PURE__*/React.createElement(AnimatedRouter, Object.assign({}, props, { + children: route.children, + location: location, + _parentPath: createPath(resolvePath(route.path || '', _parentPath)) + })); + return _objectSpread2(_objectSpread2({}, route), {}, { + children: [{ + element: animatedElement, + children: route.children + }] + }); + } - _classCallCheck(this, AnimatedRouter); + return route; + }); + }; - for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { - args[_key] = arguments[_key]; + var childElement = useRoutes(animatedRoute(childrenRoutes), location); + var setInTransition = useCallback(function (isAdd) { + if (self.rootNode) { + var inName = "".concat(prefix, "-in-transition"); + self.rootNode.className = self.rootNode.className.split(/\s+/).filter(function (name) { + return name !== inName; + }).concat(isAdd ? inName : []).join(' '); + } + }, [prefix, self]); + var onEnter = useCallback(function (node) { + self.inTransition || setInTransition(self.inTransition = true); + self.lastTransitionNode = node; + }, [self, setInTransition]); + var onEntering = useCallback(function (node) { + if (node && typeof timeout === 'number') { + node.style.transitionDuration = node.style.WebkitTransitionDuration = node.style.MozTransitionDuration = "".concat(timeout, "ms"); + } + }, [timeout]); + var onEntered = useCallback(function (node) { + if (self.lastTransitionNode === node) { + self.inTransition && setInTransition(self.inTransition = false); } - _this = _possibleConstructorReturn(this, (_getPrototypeOf2 = _getPrototypeOf(AnimatedRouter)).call.apply(_getPrototypeOf2, [this].concat(args))); - _this.inTransition = false; - _this.rootNode = void 0; - _this.lastTransitionNode = void 0; - - _this.onEnter = function (node) { - _this.inTransition || _this.setInTransition(_this.inTransition = true); - _this.lastTransitionNode = node; - }; - - _this.onEntering = function (node) { - var timeout = _this.props.timeout; - - if (node && typeof timeout === 'number') { - node.style.transitionDuration = node.style.WebkitTransitionDuration = node.style.MozTransitionDuration = timeout + 'ms'; - } - }; - - _this.onEntered = function (node) { - if (_this.lastTransitionNode === node) { - _this.inTransition && _this.setInTransition(_this.inTransition = false); - } - - if (node) { - var timeout = _this.props.timeout; // remove all transition classNames + if (self.inAppearTransition) { + self.inAppearTransition = false; + } - node.className = node.className.split(/\s+/).filter(function (name) { - return !/-(?:forward|backward)-(?:enter|exit)(?:-active)?$/.test(name); - }).join(' '); + if (node) { + // remove all transition classNames + node.className = node.className.split(/\s+/).filter(function (name) { + return !/-(?:forward|backward)-(?:enter|exit)(?:-active)?$/.test(name); + }).join(' '); - if (typeof timeout === 'number') { - node.style.transitionDuration = node.style.WebkitTransitionDuration = node.style.MozTransitionDuration = ''; - } + if (typeof timeout === 'number') { + node.style.transitionDuration = node.style.WebkitTransitionDuration = node.style.MozTransitionDuration = ''; } - }; + } + }, [self, setInTransition, timeout]); + var groupProps = { + appear: appear, + enter: enter, + exit: exit, + component: component + }; + var cssProps = { + onExit: onEnter, + onExiting: onEntering, + onExited: onEntered, + onEnter: onEnter, + onEntering: onEntering, + onEntered: onEntered + }; + var cls = ["".concat(prefix, "-container"), 'react-animated-router', className]; + useEffect(function () { + self.rootNode = findDOMNode(rootRef.current); + }, [self]); - return _this; + if (isSSR || !childElement) { + return childElement; } - _createClass(AnimatedRouter, [{ - key: "setInTransition", - value: function setInTransition(isAdd) { - if (this.rootNode) { - var inName = this.props.prefix + '-in-transition'; - this.rootNode.className = this.rootNode.className.split(/\s+/).filter(function (name) { - return name !== inName; - }).concat(isAdd ? inName : []).join(' '); - } + return /*#__PURE__*/React.createElement(TransitionGroup, Object.assign({ + ref: rootRef, + className: cls.filter(Boolean).join(' '), + childFactory: function childFactory(child) { + var classNames = "".concat(prefix, "-").concat(isHistoryPush(location, self.inAppearTransition ? false : child.props.in) ? 'forward' : 'backward'); + return React.cloneElement(child, { + classNames: classNames + }); } - }, { - key: "componentDidMount", - value: function componentDidMount() { - this.rootNode = findDOMNode(this); - } - }, { - key: "render", - value: function render() { - if (isSSR) { - return React.createElement(Switch, null, this.props.children); - } - - var _this$props = this.props, - className = _this$props.className, - location = _this$props.location, - children = _this$props.children, - timeout = _this$props.timeout, - prefix = _this$props.prefix, - appear = _this$props.appear, - enter = _this$props.enter, - exit = _this$props.exit, - component = _this$props.component; - var groupProps = { - appear: appear, - enter: enter, - exit: exit, - component: component - }; - var cssProps = { - onExit: this.onEnter, - onExiting: this.onEntering, - onExited: this.onEntered, - onEnter: this.onEnter, - onEntering: this.onEntering, - onEntered: this.onEntered - }; - var cls = [prefix + '-container', 'react-animated-router', className]; - return React.createElement(TransitionGroup, Object.assign({ - className: cls.filter(Boolean).join(' '), - childFactory: function childFactory(child) { - var classNames = prefix + '-' + (isHistoryPush(location, child.props.in) ? 'forward' : 'backward'); - return React.cloneElement(child, { - classNames: classNames - }); + }, groupProps), /*#__PURE__*/React.createElement(CSSTransition, Object.assign({ + key: transitionKey || location.pathname, + addEndListener: function addEndListener(node, done) { + node.addEventListener('transitionend', function (e) { + // 确保动画来自于目标节点 + if (e.target === node) { + done(); } - }, groupProps), React.createElement(CSSTransition, Object.assign({ - key: this.props.transitionKey || location.pathname, - addEndListener: function addEndListener(node, done) { - node.addEventListener('transitionend', function (e) { - // 确保动画来自于目标节点 - if (e.target === node) { - done(); - } - }, false); - }, - unmountOnExit: true, - timeout: timeout - }, cssProps), React.createElement(Switch, { - location: location - }, children))); - } - }]); - - return AnimatedRouter; -}(Component); + }, false); + }, + unmountOnExit: true, + timeout: timeout + }, cssProps), childElement)); +}; AnimatedRouter.propTypes = { className: PropTypes.string, - transitionKey: PropTypes.any, + transitionKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), timeout: PropTypes.number, prefix: PropTypes.string, appear: PropTypes.bool, @@ -275,6 +239,5 @@ AnimatedRouter.propTypes = { AnimatedRouter.defaultProps = { prefix: 'animated-router' }; -var AnimatedRouter$1 = withRouter(AnimatedRouter); -export default AnimatedRouter$1; +export { AnimatedRouter as default }; diff --git a/dist/react-animated-router.esm.production.js b/dist/react-animated-router.esm.production.js index f6c4089..58362a4 100644 --- a/dist/react-animated-router.esm.production.js +++ b/dist/react-animated-router.esm.production.js @@ -1 +1 @@ -import t,{Component as n}from"react";import{findDOMNode as e}from"react-dom";import{TransitionGroup as r,CSSTransition as o}from"react-transition-group";import{withRouter as i,Switch as a}from"react-router";function s(t,n){if(!(t instanceof n)){throw new TypeError("Cannot call a class as a function")}}function u(t,n){for(var e=0;e-1){T.splice(o+1)}else{T.push(r)}sessionStorage.setItem(E,T.join(","));v={isPush:o<0,key:r}}return v.isPush};var w=function(n){b(i,n);function i(){var t;var n;s(this,i);for(var e=arguments.length,r=new Array(e),o=0;o-1){j.splice(i+1)}else{j.push(r)}sessionStorage.setItem(g,j.join(","));O={isPush:i<0,key:r}}return O.isPush};var T=function v(y){var O=u();var g=e(null);var j=y.className,T=y.children,P=y.timeout,w=y.prefix,N=y.appear,x=y.enter,k=y.exit,D=y.transitionKey,A=y.component,S=y._parentPath,I=y.location,_=I===void 0?O:I;var z=e({inTransition:false,inAppearTransition:!!N}).current;var B=n((function(){return!Array.isArray(T)||r(T[0])?l(T):T}),[T])||[];if(!D&&B.length){var K=f(B.map((function(t){return b(b({},t),{},{path:p(d(t.path||"",S))})})),_);D=K===null||K===void 0?void 0:K[0].pathname}var L=function e(n){return n.map((function(e){var n;if((n=e.children)!==null&&n!==void 0&&n.length){var r=t.createElement(v,Object.assign({},y,{children:e.children,location:_,_parentPath:p(d(e.path||"",S))}));return b(b({},e),{},{children:[{element:r,children:e.children}]})}return e}))};var M=m(L(B),_);var R=i((function(t){if(z.rootNode){var e="".concat(w,"-in-transition");z.rootNode.className=z.rootNode.className.split(/\s+/).filter((function(t){return t!==e})).concat(t?e:[]).join(" ")}}),[w,z]);var W=i((function(t){z.inTransition||R(z.inTransition=true);z.lastTransitionNode=t}),[z,R]);var C=i((function(t){if(t&&typeof P==="number"){t.style.transitionDuration=t.style.WebkitTransitionDuration=t.style.MozTransitionDuration="".concat(P,"ms")}}),[P]);var F=i((function(t){if(z.lastTransitionNode===t){z.inTransition&&R(z.inTransition=false)}if(z.inAppearTransition){z.inAppearTransition=false}if(t){t.className=t.className.split(/\s+/).filter((function(t){return!/-(?:forward|backward)-(?:enter|exit)(?:-active)?$/.test(t)})).join(" ");if(typeof P==="number"){t.style.transitionDuration=t.style.WebkitTransitionDuration=t.style.MozTransitionDuration=""}}}),[z,R,P]);var H={appear:N,enter:x,exit:k,component:A};var Y={onExit:W,onExiting:C,onExited:F,onEnter:W,onEntering:C,onEntered:F};var $=["".concat(w,"-container"),"react-animated-router",j];a((function(){z.rootNode=o(g.current)}),[z]);if(h||!M){return M}return t.createElement(s,Object.assign({ref:g,className:$.filter(Boolean).join(" "),childFactory:function e(n){var r="".concat(w,"-").concat(E(_,z.inAppearTransition?false:n.props.in)?"forward":"backward");return t.cloneElement(n,{classNames:r})}},H),t.createElement(c,Object.assign({key:D||_.pathname,addEndListener:function t(e,n){e.addEventListener("transitionend",(function(t){if(t.target===e){n()}}),false)},unmountOnExit:true,timeout:P},Y),M))};T.defaultProps={prefix:"animated-router"};export{T as default}; diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..11d4e18 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,95 @@ +const pkg = require('./package.json'); + +/** + * 0: off + * 1: warn + * 2: error + */ +module.exports = { + overrides: [ + { + files: ['**/__tests__/**/*', '**/*.{spec,test}.*'], + rules: { + 'jest/consistent-test-it': [1, { fn: 'test' }], + 'jest/expect-expect': 1, + 'jest/no-deprecated-functions': 2 + } + } + ], + settings: { + 'import/core-modules': [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})] + }, + rules: { + 'react/react-in-jsx-scope': 2, + 'react/no-unsafe': [2, { checkAliases: true }], + 'react/no-deprecated': 2, + 'import/no-anonymous-default-export': [ + 2, + { + allowArray: true, + allowArrowFunction: false, + allowAnonymousClass: false, + allowAnonymousFunction: false, + allowCallExpression: true, // The true value here is for backward compatibility + allowLiteral: true, + allowObject: true + } + ], + 'import/no-duplicates': 1, + 'import/order': [ + 1, + { + groups: ['builtin', 'external', 'internal', ['parent', 'sibling', 'index'], 'object', 'unknown'] + } + ], + 'import/no-useless-path-segments': [ + 1, + { + noUselessIndex: true + } + ], + 'lines-between-class-members': [1, 'always', { exceptAfterSingleLine: true }], + 'padding-line-between-statements': [ + 1, + { + blankLine: 'always', + prev: [ + 'multiline-block-like', + 'multiline-expression', + 'const', + 'let', + 'var', + 'cjs-import', + 'import', + 'export', + 'cjs-export', + 'class', + 'throw', + 'directive' + ], + next: '*' + }, + { + blankLine: 'always', + prev: '*', + next: [ + 'multiline-block-like', + 'multiline-expression', + 'const', + 'let', + 'var', + 'cjs-import', + 'import', + 'export', + 'cjs-export', + 'class', + 'throw', + 'return' + ] + }, + { blankLine: 'any', prev: ['cjs-import', 'import'], next: ['cjs-import', 'import'] }, + { blankLine: 'any', prev: ['export', 'cjs-export'], next: ['export', 'cjs-export'] }, + { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] } + ] + } +}; diff --git a/package.json b/package.json index 1a388ee..ed5e959 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { "name": "react-animated-router", - "version": "0.2.4", + "version": "1.0.0", "description": "Dynamic transitions with react-router and react-transition-group", "author": "qiqiboy", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", "types": "dist/AnimatedRouter.d.ts", "engines": { - "node": ">=8.0.0", - "tiger-new": "4.1.9" + "node": ">=10.13.0", + "tiger-new": "7.3.1" }, "entryFile": "src/AnimatedRouter.tsx", "exportName": "react-animated-router", @@ -20,11 +20,9 @@ "src/" ], "scripts": { - "build": "npm run lint && npm run clear && npm run build:declaration && npm run build:bundle", - "build:bundle": "rollup -c", - "build:declaration": "tsc --emitDeclarationOnly", - "clear": "rimraf dist", - "lint": "node_modules/.bin/eslint 'src/**/*.{js,jsx,ts,tsx}'" + "build": "rimraf dist && tsc --emitDeclarationOnly && rollup -c", + "test": "node jest/test.js", + "tsc": "node -e \"require('fs-extra').outputJsonSync('.git-tsconfig.json',{ extends: './tsconfig.json', include: ['*.d.ts'].concat(process.env.StagedFiles.split(/\\n+/)) })\" && echo 'TS checking...\\n' && tsc -p .git-tsconfig.json --noEmit --checkJs false" }, "browserslist": [ ">0.2%", @@ -39,7 +37,9 @@ }, "eslintConfig": { "extends": [ - "react-app" + "react-app", + "react-app/jest", + "./eslint.config.js" ] }, "commitlint": { @@ -64,7 +64,6 @@ "printWidth": 120, "tabWidth": 4, "trailingComma": "none", - "jsxBracketSameLine": true, "semi": true, "singleQuote": true, "overrides": [ @@ -74,17 +73,17 @@ "tabWidth": 2 } } - ] + ], + "arrowParens": "avoid", + "bracketSameLine": true }, "lint-staged": { "src/**/*.{js,jsx,mjs,ts,tsx}": [ "node_modules/.bin/prettier --write", - "node_modules/.bin/eslint --fix", - "git add" + "node_modules/.bin/eslint --fix" ], "src/**/*.{css,scss,less,json,html,md}": [ - "node_modules/.bin/prettier --write", - "git add" + "node_modules/.bin/prettier --write" ] }, "stylelint": { @@ -92,11 +91,11 @@ }, "peerDependencies": { "@babel/runtime": ">7.0.0", - "react": ">16.0.0", - "react-dom": ">16.0.0", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", "prop-types": ">15.5.0", - "react-transition-group": ">2.0.0", - "react-router": ">4.0.0" + "react-transition-group": ">=2.0.0", + "react-router": ">=6.0.0" }, "repository": { "type": "git", @@ -106,49 +105,65 @@ "bugs": { "url": "https://github.com/qiqiboy/react-animated-router/issues" }, - "dependencies": {}, "devDependencies": { - "@babel/cli": "7.6.2", - "@babel/core": "7.6.2", - "@commitlint/cli": "8.2.0", - "@commitlint/config-conventional": "8.2.0", - "@types/node": "11.13.21", + "@babel/cli": "7.17.10", + "@babel/core": "7.17.10", + "@commitlint/cli": "11.0.0", + "@commitlint/config-conventional": "11.0.0", + "@rollup/plugin-babel": "5.3.1", + "@rollup/plugin-commonjs": "11.1.0", + "@rollup/plugin-eslint": "8.0.2", + "@rollup/plugin-json": "4.1.0", + "@rollup/plugin-node-resolve": "10.0.0", + "@rollup/plugin-replace": "2.4.2", + "@testing-library/jest-dom": "5.16.4", + "@testing-library/react": "11.2.7", + "@testing-library/user-event": "12.8.3", + "@types/jest": "26.0.24", + "@types/node": "14.18.18", "@types/prop-types": "^15.7.3", - "@types/react": "^16.9.3", - "@types/react-dom": "^16.9.1", - "@types/react-is": "16.7.1", - "@types/react-router-dom": "^5.1.0", + "@types/react": "17.0.45", + "@types/react-dom": "17.0.17", + "@types/react-is": "17.0.3", "@types/react-transition-group": "^4.2.2", - "@typescript-eslint/eslint-plugin": "2.3.1", - "@typescript-eslint/parser": "2.3.1", - "babel-eslint": "10.0.3", - "babel-preset-react-app": "9.0.2", - "eslint": "6.5.0", - "eslint-config-react-app": "5.0.2", - "eslint-plugin-flowtype": "4.3.0", - "eslint-plugin-import": "2.18.2", - "eslint-plugin-jsx-a11y": "6.2.3", - "eslint-plugin-react": "7.14.3", - "eslint-plugin-react-hooks": "2.1.1", - "husky": "2.7.0", - "lint-staged": "8.2.1", - "prettier": "1.18.2", - "prop-types": "^15.7.2", - "react": "^16.10.1", - "react-dom": "^16.10.1", - "react-router-dom": "^5.1.1", + "@typescript-eslint/eslint-plugin": "4.33.0", + "@typescript-eslint/parser": "4.33.0", + "babel-eslint": "10.1.0", + "babel-jest": "26.6.3", + "babel-preset-react-app": "10.0.1", + "eslint": "7.32.0", + "eslint-config-react-app": "6.0.0", + "eslint-plugin-flowtype": "5.10.0", + "eslint-plugin-import": "2.26.0", + "eslint-plugin-jest": "24.7.0", + "eslint-plugin-jsx-a11y": "6.5.1", + "eslint-plugin-react": "7.29.4", + "eslint-plugin-react-hooks": "4.5.0", + "eslint-plugin-testing-library": "3.10.2", + "husky": "3.1.0", + "jest": "26.6.3", + "jest-environment-jsdom-fourteen": "1.0.1", + "jest-resolve": "26.6.2", + "jest-runner-eslint": "0.11.1", + "jest-watch-typeahead": "0.4.2", + "lint-staged": "10.5.4", + "prettier": "2.6.2", + "prop-types": "15.8.1", + "react": "17.0.2", + "react-dom": "17.0.2", + "react-router": "^6.3.0", "react-transition-group": "^4.3.0", - "rimraf": "2.6.3", - "rollup": "1.22.0", + "rimraf": "3.0.2", + "rollup": "2.72.1", "rollup-plugin-babel": "4.3.3", "rollup-plugin-commonjs": "10.1.0", - "rollup-plugin-copy": "1.1.0", - "rollup-plugin-filesize": "6.2.0", + "rollup-plugin-copy": "3.4.0", + "rollup-plugin-filesize": "9.1.2", "rollup-plugin-node-resolve": "5.2.0", "rollup-plugin-replace": "2.2.0", - "rollup-plugin-sass": "1.2.2", + "rollup-plugin-sass": "1.2.12", "rollup-plugin-sourcemaps": "0.4.2", - "rollup-plugin-terser": "4.0.4", - "typescript": "3.6.3" + "rollup-plugin-terser": "7.0.2", + "typescript": "^4.4.4" } } diff --git a/rollup.config.js b/rollup.config.js index bf446d1..57f2626 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,15 +1,17 @@ process.env.NODE_ENV = 'production'; const path = require('path'); -const commonjs = require('rollup-plugin-commonjs'); -const replace = require('rollup-plugin-replace'); -const nodeResolve = require('rollup-plugin-node-resolve'); -const babel = require('rollup-plugin-babel'); -const sourceMaps = require('rollup-plugin-sourcemaps'); +const fs = require('fs'); +const commonjs = require('@rollup/plugin-commonjs'); +const replace = require('@rollup/plugin-replace'); +const { nodeResolve } = require('@rollup/plugin-node-resolve'); +const babel = require('@rollup/plugin-babel').default; const filesize = require('rollup-plugin-filesize'); const copy = require('rollup-plugin-copy'); const sass = require('rollup-plugin-sass'); const { terser } = require('rollup-plugin-terser'); +const eslint = require('@rollup/plugin-eslint'); +const json = require('@rollup/plugin-json'); const pkg = require('./package.json'); /** @@ -23,19 +25,32 @@ const exportName = pkg.exportName || pkg.name.split('/').slice(-1)[0]; /** * 如果你希望编译后的代码里依然自动包含进去编译后的css,那么这里可以设置为 true */ -const shouldPreserveCss = false; function createConfig(env, module) { const isProd = env === 'production'; + const shouldPreserveCss = false; + + // for umd globals + const globals = { + react: 'React', + 'react-dom': 'ReactDOM', + 'prop-types': 'PropTypes' + }; return { /** * 入口文件位置,如果你更改了entryFile,别忘了同时修改 npm/index.cjs.js 和 npm/index.esm.js 里的文件引用名称 */ input: pkg.entryFile || 'src/index.ts', - external: id => - !id.startsWith('.') && !externalExclude.some(name => id.startsWith(name)) && !path.isAbsolute(id), + external: + module === 'umd' + ? Object.keys(globals) + : (id) => + !externalExclude.some((name) => id.startsWith(name)) && + !id.startsWith('.') && + !path.isAbsolute(id), output: { + name: exportName, file: `dist/${exportName}.${module}.${env}.js`, format: module, exports: 'named', @@ -46,11 +61,7 @@ function createConfig(env, module) { ? `require('./${exportName}.css');` : `import('./${exportName}.css');` : undefined, - globals: { - react: 'React', - 'react-dom': 'ReactDOM', - 'prop-types': 'PropTypes' - } + globals }, treeshake: { /** @@ -59,8 +70,11 @@ function createConfig(env, module) { moduleSideEffects: false }, plugins: [ - replace({ - 'process.env.NODE_ENV': JSON.stringify(env) + eslint({ + fix: true, + throwOnError: true, + throwOnWarning: true, + include: '**/*.{js,jsx,ts,tsx,mjs}' }), nodeResolve({ extensions: ['.js', '.jsx', '.ts', '.tsx'] @@ -68,11 +82,16 @@ function createConfig(env, module) { commonjs({ include: /node_modules/ }), + replace({ + preventAssignment: true, + 'process.env.NODE_ENV': JSON.stringify(env) + }), babel({ exclude: 'node_modules/**', extensions: ['.js', '.jsx', '.ts', '.tsx'], - runtimeHelpers: true, + babelHelpers: 'runtime', babelrc: false, + configFile: false, presets: [ [ '@babel/preset-env', @@ -87,33 +106,14 @@ function createConfig(env, module) { '@babel/preset-react', { development: false, - useBuiltIns: true + useBuiltIns: true, + runtime: 'classic' } ], ['@babel/preset-typescript'] ], plugins: [ 'babel-plugin-macros', - [ - '@babel/plugin-transform-destructuring', - { - // https://github.com/facebook/create-react-app/issues/5602 - loose: false, - useBuiltIns: true, - selectiveLoose: [ - 'useState', - 'useEffect', - 'useContext', - 'useReducer', - 'useCallback', - 'useMemo', - 'useRef', - 'useImperativeHandle', - 'useLayoutEffect', - 'useDebugValue' - ] - } - ], ['@babel/plugin-proposal-decorators', { legacy: true }], [ '@babel/plugin-proposal-class-properties', @@ -121,12 +121,6 @@ function createConfig(env, module) { loose: true } ], - [ - '@babel/plugin-proposal-object-rest-spread', - { - useBuiltIns: true - } - ], [ '@babel/plugin-transform-runtime', { @@ -134,7 +128,7 @@ function createConfig(env, module) { corejs: false, helpers: true, regenerator: true, - useESModules: true, + useESModules: module === 'esm', absoluteRuntime: false } ], @@ -144,16 +138,20 @@ function createConfig(env, module) { { removeImport: true } - ] + ], + require('@babel/plugin-proposal-optional-chaining').default, + require('@babel/plugin-proposal-nullish-coalescing-operator').default, + // Adds Numeric Separators + require('@babel/plugin-proposal-numeric-separator').default ].filter(Boolean) }), - sass({ - output: `animate.css` - }), - sourceMaps(), + module !== 'umd' && + sass({ + output: `animate.css` + }), + json(), isProd && terser({ - sourcemap: true, output: { comments: false }, compress: false, warnings: false, @@ -163,16 +161,20 @@ function createConfig(env, module) { }), filesize(), copy({ - targets: [`npm/index.${module}.js`], - verbose: true + targets: [ + { + src: `npm/index.${module}.js`, + dest: 'dist' + } + ], + verbose: false }) ].filter(Boolean) }; } -module.exports = [ - createConfig('development', 'cjs'), - createConfig('production', 'cjs'), - createConfig('development', 'esm'), - createConfig('production', 'esm') -]; +module.exports = ['cjs', 'esm', 'umd'].reduce((configQueue, module) => { + return fs.existsSync(`./npm/index.${module}.js`) + ? configQueue.concat(createConfig('development', module), createConfig('production', module)) + : configQueue; +}, []); diff --git a/src/AnimatedRouter.tsx b/src/AnimatedRouter.tsx index 7a5ffdb..346f1aa 100644 --- a/src/AnimatedRouter.tsx +++ b/src/AnimatedRouter.tsx @@ -1,9 +1,18 @@ -import React, { Component, ReactType } from 'react'; +import React, { isValidElement, ReactType, useCallback, useEffect, useMemo, useRef } from 'react'; import { findDOMNode } from 'react-dom'; +import PropTypes from 'prop-types'; import { TransitionGroup, CSSTransition } from 'react-transition-group'; +import { + Location, + createPath, + useLocation, + createRoutesFromChildren, + resolvePath, + matchRoutes, + useRoutes, + RouteObject +} from 'react-router'; import { TransitionActions } from 'react-transition-group/Transition'; -import { Switch, withRouter, RouteComponentProps } from 'react-router'; -import PropTypes from 'prop-types'; import './animate.scss'; const isSSR = typeof window === 'undefined'; @@ -44,6 +53,8 @@ interface AnimatedRouterProps extends TransitionActions { timeout?: number; prefix?: string; component?: ReactType | null; + location?: Location; + children: React.ReactNode | RouteObject[]; } /** @@ -54,133 +65,221 @@ interface AnimatedRouterProps extends TransitionActions { * import AnimatedRouter from 'react-animated-router'; * import 'react-animated-router/animate.css'; */ -class AnimatedRouter extends Component { - static propTypes = { - className: PropTypes.string, - transitionKey: PropTypes.any, - timeout: PropTypes.number, - prefix: PropTypes.string, - appear: PropTypes.bool, - enter: PropTypes.bool, - exit: PropTypes.bool, - component: PropTypes.any - }; +const AnimatedRouter: React.FC< + AnimatedRouterProps & { + _parentPath?: string; + } +> = props => { + const baseLocation = useLocation(); + const rootRef = useRef(null); - static defaultProps = { - prefix: 'animated-router' - }; + let { + className, + children, + timeout, + prefix, + appear, + enter, + exit, + transitionKey, + component, + _parentPath, + location = baseLocation + } = props; + const self = useRef<{ + inTransition: boolean; + inAppearTransition: boolean; + rootNode?: Element; + lastTransitionNode?: Element; + }>({ + inTransition: false, + inAppearTransition: !!appear + }).current; - inTransition = false; - rootNode: Element; - lastTransitionNode: Element; + const childrenRoutes = + useMemo( + () => + !Array.isArray(children) || isValidElement(children[0]) ? createRoutesFromChildren(children) : children, + [children] + ) || []; - setInTransition(isAdd) { - if (this.rootNode) { - const inName = this.props.prefix + '-in-transition'; + if (!transitionKey && childrenRoutes.length) { + const routes = matchRoutes( + childrenRoutes.map(route => ({ + ...route, + path: createPath(resolvePath(route.path || '', _parentPath)) + })), + location + ); - this.rootNode.className = this.rootNode - .className!.split(/\s+/) - .filter(name => name !== inName) - .concat(isAdd ? inName : []) - .join(' '); - } + transitionKey = routes?.[0].pathname; } - onEnter = node => { - this.inTransition || this.setInTransition((this.inTransition = true)); - this.lastTransitionNode = node; - }; + const animatedRoute = (routes: RouteObject[]) => + routes.map(route => { + if (route.children?.length) { + const animatedElement = ( + + ); + + return { + ...route, + children: [ + { + element: animatedElement, + children: route.children + } + ] + }; + } - onEntering = node => { - const { timeout } = this.props; + return route; + }); - if (node && typeof timeout === 'number') { - node.style.transitionDuration = node.style.WebkitTransitionDuration = node.style.MozTransitionDuration = - timeout + 'ms'; - } - }; + const childElement = useRoutes(animatedRoute(childrenRoutes), location); - onEntered = node => { - if (this.lastTransitionNode === node) { - this.inTransition && this.setInTransition((this.inTransition = false)); - } + const setInTransition = useCallback( + isAdd => { + if (self.rootNode) { + const inName = `${prefix}-in-transition`; - if (node) { - const { timeout } = this.props; + self.rootNode.className = self.rootNode + .className!.split(/\s+/) + .filter(name => name !== inName) + .concat(isAdd ? inName : []) + .join(' '); + } + }, + [prefix, self] + ); - // remove all transition classNames - node.className = node.className - .split(/\s+/) - .filter(name => !/-(?:forward|backward)-(?:enter|exit)(?:-active)?$/.test(name)) - .join(' '); + const onEnter = useCallback( + node => { + self.inTransition || setInTransition((self.inTransition = true)); + self.lastTransitionNode = node; + }, + [self, setInTransition] + ); - if (typeof timeout === 'number') { - node.style.transitionDuration = node.style.WebkitTransitionDuration = node.style.MozTransitionDuration = - ''; + const onEntering = useCallback( + node => { + if (node && typeof timeout === 'number') { + node.style.transitionDuration = + node.style.WebkitTransitionDuration = + node.style.MozTransitionDuration = + `${timeout}ms`; } - } + }, + [timeout] + ); + + const onEntered = useCallback( + node => { + if (self.lastTransitionNode === node) { + self.inTransition && setInTransition((self.inTransition = false)); + } + + if (self.inAppearTransition) { + self.inAppearTransition = false; + } + + if (node) { + // remove all transition classNames + node.className = node.className + .split(/\s+/) + .filter(name => !/-(?:forward|backward)-(?:enter|exit)(?:-active)?$/.test(name)) + .join(' '); + + if (typeof timeout === 'number') { + node.style.transitionDuration = + node.style.WebkitTransitionDuration = + node.style.MozTransitionDuration = + ''; + } + } + }, + [self, setInTransition, timeout] + ); + + const groupProps = { + appear, + enter, + exit, + component }; + const cssProps = { + onExit: onEnter, + onExiting: onEntering, + onExited: onEntered, + onEnter, + onEntering, + onEntered + }; + const cls = [`${prefix}-container`, 'react-animated-router', className]; + + useEffect(() => { + self.rootNode = findDOMNode(rootRef.current) as Element; + }, [self]); - componentDidMount() { - this.rootNode = findDOMNode(this) as Element; + if (isSSR || !childElement) { + return childElement; } - render() { - if (isSSR) { - return {this.props.children}; - } + return ( + { + const classNames = `${prefix}-${ + isHistoryPush(location, self.inAppearTransition ? false : child.props.in) ? 'forward' : 'backward' + }`; - const { className, location, children, timeout, prefix, appear, enter, exit, component } = this.props; - const groupProps = { - appear, - enter, - exit, - component - }; - const cssProps = { - onExit: this.onEnter, - onExiting: this.onEntering, - onExited: this.onEntered, - onEnter: this.onEnter, - onEntering: this.onEntering, - onEntered: this.onEntered - }; - const cls = [prefix + '-container', 'react-animated-router', className]; - - return ( - { - const classNames = - prefix + '-' + (isHistoryPush(location, child.props.in) ? 'forward' : 'backward'); - - return React.cloneElement(child, { - classNames - }); + return React.cloneElement(child, { + classNames + }); + }} + {...groupProps}> + { + node.addEventListener( + 'transitionend', + function (e) { + // 确保动画来自于目标节点 + if (e.target === node) { + done(); + } + }, + false + ); }} - {...groupProps}> - { - node.addEventListener( - 'transitionend', - function(e) { - // 确保动画来自于目标节点 - if (e.target === node) { - done(); - } - }, - false - ); - }} - unmountOnExit={true} - timeout={timeout as any} - {...cssProps}> - {children} - - - ); - } -} + unmountOnExit={true} + timeout={timeout as any} + {...cssProps}> + {childElement} + + + ); +}; + +AnimatedRouter.propTypes = { + className: PropTypes.string, + transitionKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + timeout: PropTypes.number, + prefix: PropTypes.string, + appear: PropTypes.bool, + enter: PropTypes.bool, + exit: PropTypes.bool, + component: PropTypes.any +}; + +AnimatedRouter.defaultProps = { + prefix: 'animated-router' +}; -export default withRouter(AnimatedRouter); +export default AnimatedRouter; diff --git a/src/animate.scss b/src/animate.scss index c11f609..84eb22a 100644 --- a/src/animate.scss +++ b/src/animate.scss @@ -13,6 +13,12 @@ $prefix: animated-router !default; } /** START: 自定义进出场动画 **/ + &-forward-appear { + opacity: 0; + } + &-forward-appear-active { + opacity: 1; + } &-forward-enter { transform: translate(100%); } @@ -26,6 +32,12 @@ $prefix: animated-router !default; transform: translate(-100%); } + &-backward-appear { + opacity: 1; + } + &-backward-appear-active { + opacity: 0; + } &-backward-enter { transform: translate(-100%); } @@ -40,6 +52,10 @@ $prefix: animated-router !default; } /** END **/ + &-forward-appear-active, + &-backward-appear-active { + transition: opacity 0.3s linear; + } &-forward-enter-active, &-forward-exit-active, &-backward-enter-active, diff --git a/tsconfig.json b/tsconfig.json index a0900b7..f393a93 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "moduleResolution": "node", "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, "noImplicitReturns": true, "noImplicitThis": true, "noImplicitAny": false,