From 8291d0fc2928034a5a318421fae0aaaeb526b098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E7=88=B1=E5=90=83=E7=99=BD=E8=90=9D?= =?UTF-8?q?=E5=8D=9C?= Date: Sun, 7 Aug 2022 16:25:19 +0800 Subject: [PATCH] refactor: Customize Motion support (#563) * 12.0.0-alpha.1 * docs: add animated demo * docs: adjust style * refactor: basic transition * chore: clean part * test: fix test case --- assets/panels.less | 17 ++++-- docs/demo/animated.md | 3 + docs/examples/animated.less | 32 +++++++++++ docs/examples/animated.tsx | 50 +++++++++++++++++ jest.config.js | 1 + package.json | 5 +- src/TabPanelList/TabPane.tsx | 71 ++++++++--------------- src/TabPanelList/index.tsx | 57 ++++++++++++------- src/Tabs.tsx | 25 +-------- src/hooks/useAnimateConfig.ts | 46 +++++++++++++++ src/interface.ts | 2 + tests/__snapshots__/index.test.tsx.snap | 18 ------ tests/index.test.tsx | 75 ++++++++++++++++--------- tests/setupFilesAfterEnv.js | 1 + 14 files changed, 262 insertions(+), 141 deletions(-) create mode 100644 docs/demo/animated.md create mode 100644 docs/examples/animated.less create mode 100644 docs/examples/animated.tsx create mode 100644 src/hooks/useAnimateConfig.ts create mode 100644 tests/setupFilesAfterEnv.js diff --git a/assets/panels.less b/assets/panels.less index 55be4b13..19819126 100644 --- a/assets/panels.less +++ b/assets/panels.less @@ -5,17 +5,22 @@ &-holder { flex: auto; } + position: relative; - display: flex; + // display: flex; width: 100%; - &-animated { - transition: margin 0.3s; - } + // &-animated { + // transition: margin 0.3s; + // } } &-tabpane { - width: 100%; - flex: none; + // flex: none; + // width: 100%; + + &-hidden { + display: none; + } } } diff --git a/docs/demo/animated.md b/docs/demo/animated.md new file mode 100644 index 00000000..4848788e --- /dev/null +++ b/docs/demo/animated.md @@ -0,0 +1,3 @@ +## animated + + \ No newline at end of file diff --git a/docs/examples/animated.less b/docs/examples/animated.less new file mode 100644 index 00000000..61cfd2e0 --- /dev/null +++ b/docs/examples/animated.less @@ -0,0 +1,32 @@ +@duration: 0.3s; + +.switch { + &-appear, + &-enter { + transition: none; + + &-start { + opacity: 0; + } + + &-active { + opacity: 1; + transition: all @duration; + } + } + + &-leave { + position: absolute; + transition: none; + inset: 0; + + &-start { + opacity: 1; + } + + &-active { + opacity: 0; + transition: all @duration; + } + } +} diff --git a/docs/examples/animated.tsx b/docs/examples/animated.tsx new file mode 100644 index 00000000..dd7a1772 --- /dev/null +++ b/docs/examples/animated.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import Tabs from 'rc-tabs'; +import type { CSSMotionProps } from 'rc-motion'; +import '../../assets/index.less'; +import './animated.less'; + +const motion: CSSMotionProps = { + motionName: 'switch', + motionAppear: false, + motionEnter: true, + motionLeave: true, +}; + +export default () => ( + + + +); diff --git a/jest.config.js b/jest.config.js index 86627c33..ec5296ce 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,5 @@ module.exports = { setupFiles: ['./tests/setup.js'], snapshotSerializers: [require.resolve('enzyme-to-json/serializer')], + setupFilesAfterEnv: ['/tests/setupFilesAfterEnv.js'], }; diff --git a/package.json b/package.json index dcf40d1b..42f2da4c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rc-tabs", - "version": "12.0.0-alpha.0", + "version": "12.0.0-alpha.1", "description": "tabs ui component for react", "engines": { "node": ">=8.x" @@ -41,6 +41,8 @@ "prepublishOnly": "npm run lint && npm run test && npm run compile && np --yolo --no-publish" }, "devDependencies": { + "@testing-library/jest-dom": "^5.16.4", + "@testing-library/react": "^12.0.0", "@types/classnames": "^2.2.10", "@types/enzyme": "^3.10.5", "@types/jest": "^25.2.3", @@ -78,6 +80,7 @@ "classnames": "2.x", "rc-dropdown": "~4.0.0", "rc-menu": "~9.6.0", + "rc-motion": "^2.6.2", "rc-resize-observer": "^1.0.0", "rc-util": "^5.5.0" }, diff --git a/src/TabPanelList/TabPane.tsx b/src/TabPanelList/TabPane.tsx index c21996ab..e153623d 100644 --- a/src/TabPanelList/TabPane.tsx +++ b/src/TabPanelList/TabPane.tsx @@ -20,54 +20,27 @@ export interface TabPaneProps { destroyInactiveTabPane?: boolean; } -export default function TabPane({ - prefixCls, - forceRender, - className, - style, - id, - active, - animated, - destroyInactiveTabPane, - tabKey, - children, -}: TabPaneProps) { - const [visited, setVisited] = React.useState(forceRender); +const TabPane = React.forwardRef( + ({ prefixCls, className, style, id, active, tabKey, children }, ref) => { + return ( +
+ {children} +
+ ); + }, +); - React.useEffect(() => { - if (active) { - setVisited(true); - } else if (destroyInactiveTabPane) { - setVisited(false); - } - }, [active, destroyInactiveTabPane]); - - const mergedStyle: React.CSSProperties = {}; - if (!active) { - if (animated) { - mergedStyle.visibility = 'hidden'; - mergedStyle.height = 0; - mergedStyle.overflowY = 'hidden'; - } else { - mergedStyle.display = 'none'; - } - } - - return ( -
- {(active || visited || forceRender) && children} -
- ); +if (process.env.NODE_ENV !== 'production') { + TabPane.displayName = 'TabPane'; } + +export default TabPane; diff --git a/src/TabPanelList/index.tsx b/src/TabPanelList/index.tsx index 11f15190..94f730dd 100644 --- a/src/TabPanelList/index.tsx +++ b/src/TabPanelList/index.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import classNames from 'classnames'; +import CSSMotion from 'rc-motion'; import TabContext from '../TabContext'; import type { TabPosition, AnimatedConfig } from '../interface'; import TabPane from './TabPane'; @@ -7,7 +8,6 @@ import TabPane from './TabPane'; export interface TabPanelListProps { activeKey: string; id: string; - rtl: boolean; animated?: AnimatedConfig; tabPosition?: TabPosition; destroyInactiveTabPane?: boolean; @@ -18,13 +18,12 @@ export default function TabPanelList({ activeKey, animated, tabPosition, - rtl, destroyInactiveTabPane, }: TabPanelListProps) { const { prefixCls, tabs } = React.useContext(TabContext); const tabPaneAnimated = animated.tabPane; - const activeIndex = tabs.findIndex(tab => tab.key === activeKey); + const tabPanePrefixCls = `${prefixCls}-tabpane`; return (
@@ -32,24 +31,42 @@ export default function TabPanelList({ className={classNames(`${prefixCls}-content`, `${prefixCls}-content-${tabPosition}`, { [`${prefixCls}-content-animated`]: tabPaneAnimated, })} - style={ - activeIndex && tabPaneAnimated - ? { [rtl ? 'marginRight' : 'marginLeft']: `-${activeIndex}00%` } - : null - } > - {tabs.map(tab => ( - - ))} + {tabs.map( + ({ key, forceRender, style: paneStyle, className: paneClassName, ...restTabProps }) => { + const active = key === activeKey; + + return ( + + {({ style: motionStyle, className: motionClassName }, ref) => { + return ( + + ); + }} + + ); + }, + )}
); diff --git a/src/Tabs.tsx b/src/Tabs.tsx index 20308e7d..a8f5474e 100644 --- a/src/Tabs.tsx +++ b/src/Tabs.tsx @@ -17,6 +17,7 @@ import type { } from './interface'; import TabContext from './TabContext'; import TabNavListWrapper from './TabNavList/Wrapper'; +import useAnimateConfig from './hooks/useAnimateConfig'; /** * Should added antd: @@ -79,10 +80,7 @@ function Tabs( activeKey, defaultActiveKey, editable, - animated = { - inkBar: true, - tabPane: false, - }, + animated, tabPosition = 'top', tabBarGutter, tabBarStyle, @@ -107,24 +105,7 @@ function Tabs( ); const rtl = direction === 'rtl'; - let mergedAnimated: AnimatedConfig | false; - if (animated === false) { - mergedAnimated = { - inkBar: false, - tabPane: false, - }; - } else if (animated === true) { - mergedAnimated = { - inkBar: true, - tabPane: true, - }; - } else { - mergedAnimated = { - inkBar: true, - tabPane: false, - ...(typeof animated === 'object' ? animated : {}), - }; - } + const mergedAnimated = useAnimateConfig(animated); // ======================== Mobile ======================== const [mobile, setMobile] = useState(false); diff --git a/src/hooks/useAnimateConfig.ts b/src/hooks/useAnimateConfig.ts new file mode 100644 index 00000000..a7cb2698 --- /dev/null +++ b/src/hooks/useAnimateConfig.ts @@ -0,0 +1,46 @@ +import warning from 'rc-util/lib/warning'; +import type { TabsProps } from '..'; +import type { AnimatedConfig } from '../interface'; + +export default function useAnimateConfig( + animated: TabsProps['animated'] = { + inkBar: true, + tabPane: false, + }, +): AnimatedConfig { + let mergedAnimated: AnimatedConfig; + + if (animated === false) { + mergedAnimated = { + inkBar: false, + tabPane: false, + }; + } else if (animated === true) { + mergedAnimated = { + inkBar: true, + tabPane: false, + }; + } else { + mergedAnimated = { + inkBar: true, + ...(typeof animated === 'object' ? animated : {}), + }; + } + + // Enable tabPane animation if provide motion + if (mergedAnimated.tabPaneMotion && mergedAnimated.tabPane === undefined) { + mergedAnimated.tabPane = true; + } + + if (!mergedAnimated.tabPaneMotion && mergedAnimated.tabPane) { + if (process.env.NODE_ENV !== 'production') { + warning( + false, + '`animated.tabPane` is true but `animated.tabPaneMotion` is not provided. Motion will not work.', + ); + } + mergedAnimated.tabPane = false; + } + + return mergedAnimated; +} diff --git a/src/interface.ts b/src/interface.ts index 57138889..14ef5c7b 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,4 +1,5 @@ import type React from 'react'; +import type { CSSMotionProps } from 'rc-motion'; import type { TabNavListProps } from './TabNavList'; import type { TabPaneProps } from './TabPanelList/TabPane'; @@ -67,6 +68,7 @@ export interface EditableConfig { export interface AnimatedConfig { inkBar?: boolean; tabPane?: boolean; + tabPaneMotion?: CSSMotionProps; } export type OnTabScroll = (info: { direction: 'left' | 'right' | 'top' | 'bottom' }) => void; diff --git a/tests/__snapshots__/index.test.tsx.snap b/tests/__snapshots__/index.test.tsx.snap index 08483b9b..9fcec63a 100644 --- a/tests/__snapshots__/index.test.tsx.snap +++ b/tests/__snapshots__/index.test.tsx.snap @@ -86,15 +86,6 @@ exports[`Tabs.Basic Normal 1`] = `
-
diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 042bfcc0..23b1a446 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -1,5 +1,6 @@ import React from 'react'; import type { ReactWrapper } from 'enzyme'; +import { render } from '@testing-library/react'; import { mount } from 'enzyme'; import KeyCode from 'rc-util/lib/KeyCode'; import Tabs from '../src'; @@ -203,32 +204,37 @@ describe('Tabs.Basic', () => { }); it('destroyInactiveTabPane', () => { - const wrapper = mount( - getTabs({ - activeKey: 'light', - destroyInactiveTabPane: true, - items: [ - { - key: 'light', - children: 'Light', - }, - { - key: 'bamboo', - children: 'Bamboo', - }, - ] as any, - }), - ); + const props = { + activeKey: 'light', + destroyInactiveTabPane: true, + items: [ + { + key: 'light', + children: 'Light', + }, + { + key: 'bamboo', + children: 'Bamboo', + }, + ] as any, + }; + + const { container, rerender } = render(getTabs(props)); - function matchText(light: string, bamboo: string) { - expect(wrapper.find('.rc-tabs-tabpane').first().text()).toEqual(light); - expect(wrapper.find('.rc-tabs-tabpane').last().text()).toEqual(bamboo); + function matchText(text: string) { + expect(container.querySelectorAll('.rc-tabs-tabpane')).toHaveLength(1); + expect(container.querySelector('.rc-tabs-tabpane-active').textContent).toEqual(text); } - matchText('Light', ''); + matchText('Light'); - wrapper.setProps({ activeKey: 'bamboo' }); - matchText('', 'Bamboo'); + rerender( + getTabs({ + ...props, + activeKey: 'bamboo', + }), + ); + matchText('Bamboo'); }); describe('editable', () => { @@ -325,16 +331,35 @@ describe('Tabs.Basic', () => { const wrapper = mount(getTabs({ animated: true })); expect(wrapper.find('TabPanelList').prop('animated')).toEqual({ inkBar: true, - tabPane: true, + tabPane: false, }); }); - it('customize', () => { + it('customize but !tabPaneMotion', () => { + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const wrapper = mount(getTabs({ animated: { inkBar: false, tabPane: true } })); expect(wrapper.find('TabPanelList').prop('animated')).toEqual({ inkBar: false, - tabPane: true, + tabPane: false, }); + + expect(errorSpy).toHaveBeenCalledWith( + 'Warning: `animated.tabPane` is true but `animated.tabPaneMotion` is not provided. Motion will not work.', + ); + errorSpy.mockRestore(); + }); + + it('customize', () => { + const wrapper = mount( + getTabs({ animated: { inkBar: true, tabPane: true, tabPaneMotion: {} } }), + ); + expect(wrapper.find('TabPanelList').prop('animated')).toEqual( + expect.objectContaining({ + inkBar: true, + tabPane: true, + }), + ); }); }); diff --git a/tests/setupFilesAfterEnv.js b/tests/setupFilesAfterEnv.js new file mode 100644 index 00000000..7b0828bf --- /dev/null +++ b/tests/setupFilesAfterEnv.js @@ -0,0 +1 @@ +import '@testing-library/jest-dom';