From 9cbd9e6061b181f43c93c5e372472b4d656dde53 Mon Sep 17 00:00:00 2001 From: Oyster Lee Date: Mon, 25 Nov 2024 13:22:43 +0800 Subject: [PATCH 01/10] feat: add tab scroll --- docs/demo/scroll-position.md | 0 docs/examples/dynamic-extra.tsx | 27 +++++++-------- docs/examples/scroll-position.tsx | 28 ++++++++++++++++ src/TabNavList/TabNode.tsx | 1 + src/TabNavList/index.tsx | 56 ++++++++++++++++++++++++------- src/Tabs.tsx | 5 +++ src/interface.ts | 10 +++--- 7 files changed, 96 insertions(+), 31 deletions(-) create mode 100644 docs/demo/scroll-position.md create mode 100644 docs/examples/scroll-position.tsx diff --git a/docs/demo/scroll-position.md b/docs/demo/scroll-position.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/examples/dynamic-extra.tsx b/docs/examples/dynamic-extra.tsx index 9db9721d..485bb85e 100644 --- a/docs/examples/dynamic-extra.tsx +++ b/docs/examples/dynamic-extra.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import Tabs from '../../src'; -import type { TabsProps } from '../../src'; import '../../assets/index.less'; +import type { TabsProps } from '../../src'; +import Tabs from '../../src'; const items: TabsProps['items'] = []; for (let i = 0; i < 50; i += 1) { @@ -16,22 +16,21 @@ export default () => { const extra = React.useMemo(() => { if (key === '0') { - return ( -
额外内容
- ) - } - return null - }, [key]) + return
额外内容
; + } + return null; + }, [key]); return (
- setKey(curKey)} - tabBarExtraContent={extra} - defaultActiveKey="8" - items={items} + onChange={curKey => setKey(curKey)} + tabBarExtraContent={extra} + defaultActiveKey="8" + items={items} + scrollPosition="end" />
); -}; \ No newline at end of file +}; diff --git a/docs/examples/scroll-position.tsx b/docs/examples/scroll-position.tsx new file mode 100644 index 00000000..96d302a4 --- /dev/null +++ b/docs/examples/scroll-position.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import '../../assets/index.less'; +import type { TabsProps } from '../../src'; +import Tabs from '../../src'; + +const items: TabsProps['items'] = []; +for (let i = 0; i < 50; i += 1) { + items.push({ + key: String(i), + label: `Tab ${i}`, + children: `Content of ${i}`, + }); +} +export default () => { + const [key, setKey] = React.useState('0'); + + return ( +
+ setKey(curKey)} + defaultActiveKey="8" + items={items} + scrollPosition="end" + /> +
+ ); +}; diff --git a/src/TabNavList/TabNode.tsx b/src/TabNavList/TabNode.tsx index 939bc2cf..deabfc0a 100644 --- a/src/TabNavList/TabNode.tsx +++ b/src/TabNavList/TabNode.tsx @@ -80,6 +80,7 @@ const TabNode: React.FC = props => { tabIndex={disabled ? null : 0} onClick={e => { e.stopPropagation(); + console.log('onClick', key); onInternalClick(e); }} onKeyDown={e => { diff --git a/src/TabNavList/index.tsx b/src/TabNavList/index.tsx index e51553af..73ef903b 100644 --- a/src/TabNavList/index.tsx +++ b/src/TabNavList/index.tsx @@ -19,6 +19,7 @@ import type { MoreProps, OnTabScroll, RenderTabBar, + ScrollPosition, SizeInfo, TabBarExtraContent, TabPosition, @@ -55,6 +56,7 @@ export interface TabNavListProps { size?: GetIndicatorSize; align?: 'start' | 'center' | 'end'; }; + scrollPosition?: ScrollPosition; } const getTabSize = (tab: HTMLElement, containerRect: { left: number; top: number }) => { @@ -104,6 +106,7 @@ const TabNavList = React.forwardRef((props, ref editable, locale, tabPosition, + scrollPosition, tabBarGutter, children, onTabClick, @@ -150,7 +153,8 @@ const TabNavList = React.forwardRef((props, ref const addSizeValue = getUnitValue(addSize, tabPositionTopOrBottom); const operationSizeValue = getUnitValue(operationSize, tabPositionTopOrBottom); - const needScroll = Math.floor(containerExcludeExtraSizeValue) < Math.floor(tabContentSizeValue + addSizeValue); + const needScroll = + Math.floor(containerExcludeExtraSizeValue) < Math.floor(tabContentSizeValue + addSizeValue); const visibleTabContentValue = needScroll ? containerExcludeExtraSizeValue - operationSizeValue : containerExcludeExtraSizeValue - addSizeValue; @@ -264,19 +268,36 @@ const TabNavList = React.forwardRef((props, ref // ============ Align with top & bottom ============ let newTransform = transformLeft; - // RTL if (rtl) { - if (tabOffset.right < transformLeft) { + // RTL logic + if (scrollPosition === 'auto') { + if (tabOffset.right < transformLeft) { + newTransform = tabOffset.right; + } else if (tabOffset.right + tabOffset.width > transformLeft + visibleTabContentValue) { + newTransform = tabOffset.right + tabOffset.width - visibleTabContentValue; + } + } else if (scrollPosition === 'start') { newTransform = tabOffset.right; - } else if (tabOffset.right + tabOffset.width > transformLeft + visibleTabContentValue) { + } else if (scrollPosition === 'end') { newTransform = tabOffset.right + tabOffset.width - visibleTabContentValue; + } else if (scrollPosition === 'center') { + newTransform = tabOffset.right + tabOffset.width / 2 - visibleTabContentValue / 2; + } + } else { + // LTR logic + if (scrollPosition === 'auto') { + if (tabOffset.left < -transformLeft) { + newTransform = -tabOffset.left; + } else if (tabOffset.left + tabOffset.width > -transformLeft + visibleTabContentValue) { + newTransform = -(tabOffset.left + tabOffset.width - visibleTabContentValue); + } + } else if (scrollPosition === 'start') { + newTransform = -tabOffset.left; + } else if (scrollPosition === 'end') { + newTransform = -(tabOffset.left + tabOffset.width - visibleTabContentValue); + } else if (scrollPosition === 'center') { + newTransform = -(tabOffset.left + tabOffset.width / 2 - visibleTabContentValue / 2); } - } - // LTR - else if (tabOffset.left < -transformLeft) { - newTransform = -tabOffset.left; - } else if (tabOffset.left + tabOffset.width > -transformLeft + visibleTabContentValue) { - newTransform = -(tabOffset.left + tabOffset.width - visibleTabContentValue); } setTransformTop(0); @@ -285,10 +306,18 @@ const TabNavList = React.forwardRef((props, ref // ============ Align with left & right ============ let newTransform = transformTop; - if (tabOffset.top < -transformTop) { + if (scrollPosition === 'auto') { + if (tabOffset.top < -transformTop) { + newTransform = -tabOffset.top; + } else if (tabOffset.top + tabOffset.height > -transformTop + visibleTabContentValue) { + newTransform = -(tabOffset.top + tabOffset.height - visibleTabContentValue); + } + } else if (scrollPosition === 'start') { newTransform = -tabOffset.top; - } else if (tabOffset.top + tabOffset.height > -transformTop + visibleTabContentValue) { + } else if (scrollPosition === 'end') { newTransform = -(tabOffset.top + tabOffset.height - visibleTabContentValue); + } else if (scrollPosition === 'center') { + newTransform = -(tabOffset.top + tabOffset.height / 2 - visibleTabContentValue / 2); } setTransformLeft(0); @@ -323,8 +352,9 @@ const TabNavList = React.forwardRef((props, ref onTabClick(key, e); }} onFocus={() => { + console.log('onFocus', key); scrollToTab(key); - doLockAnimation(); + // doLockAnimation(); if (!tabsWrapperRef.current) { return; } diff --git a/src/Tabs.tsx b/src/Tabs.tsx index 7d6df768..e4febb2c 100644 --- a/src/Tabs.tsx +++ b/src/Tabs.tsx @@ -15,6 +15,7 @@ import type { MoreProps, OnTabScroll, RenderTabBar, + ScrollPosition, Tab, TabBarExtraContent, TabPosition, @@ -72,6 +73,8 @@ export interface TabsProps size?: GetIndicatorSize; align?: 'start' | 'center' | 'end'; }; + + scrollPosition?: ScrollPosition; } const Tabs = React.forwardRef((props, ref) => { @@ -99,6 +102,7 @@ const Tabs = React.forwardRef((props, ref) => { getPopupContainer, popupClassName, indicator, + scrollPosition = 'auto', ...restProps } = props; const tabs = React.useMemo( @@ -182,6 +186,7 @@ const Tabs = React.forwardRef((props, ref) => { getPopupContainer, popupClassName, indicator, + scrollPosition, }; return ( diff --git a/src/interface.ts b/src/interface.ts index ea7958af..878fc61f 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,15 +1,15 @@ +import type { DropdownProps } from 'rc-dropdown/lib/Dropdown'; import type { CSSMotionProps } from 'rc-motion'; import type React from 'react'; import type { TabNavListProps } from './TabNavList'; import type { TabPaneProps } from './TabPanelList/TabPane'; -import { DropdownProps } from 'rc-dropdown/lib/Dropdown'; export type TriggerProps = { trigger?: 'hover' | 'click'; -} +}; export type moreIcon = React.ReactNode; export type MoreProps = { - icon?: moreIcon, + icon?: moreIcon; } & Omit; export type SizeInfo = [width: number, height: number]; @@ -45,7 +45,7 @@ type RenderTabBarProps = { mobile: boolean; editable: EditableConfig; locale: TabsLocale; - more: MoreProps, + more: MoreProps; tabBarGutter: number; onTabClick: (key: string, e: React.MouseEvent | React.KeyboardEvent) => void; onTabScroll: OnTabScroll; @@ -89,3 +89,5 @@ export type TabBarExtraPosition = 'left' | 'right'; export type TabBarExtraMap = Partial>; export type TabBarExtraContent = React.ReactNode | TabBarExtraMap; + +export type ScrollPosition = 'start' | 'end' | 'center' | 'auto'; From 246ec4f1d050c84b4e5c88b65114c63108b5d6b9 Mon Sep 17 00:00:00 2001 From: Oyster Lee Date: Mon, 25 Nov 2024 16:30:52 +0800 Subject: [PATCH 02/10] chore: add docs --- docs/demo/scroll-position.md | 8 +++++ docs/examples/dynamic-extra.tsx | 1 - docs/examples/scroll-position.tsx | 54 +++++++++++++++++++++++++------ 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/docs/demo/scroll-position.md b/docs/demo/scroll-position.md index e69de29b..bfc38294 100644 --- a/docs/demo/scroll-position.md +++ b/docs/demo/scroll-position.md @@ -0,0 +1,8 @@ +--- +title: scroll-position +nav: + title: Demo + path: /demo +--- + + \ No newline at end of file diff --git a/docs/examples/dynamic-extra.tsx b/docs/examples/dynamic-extra.tsx index 485bb85e..f20014aa 100644 --- a/docs/examples/dynamic-extra.tsx +++ b/docs/examples/dynamic-extra.tsx @@ -29,7 +29,6 @@ export default () => { tabBarExtraContent={extra} defaultActiveKey="8" items={items} - scrollPosition="end" /> ); diff --git a/docs/examples/scroll-position.tsx b/docs/examples/scroll-position.tsx index 96d302a4..6e1dc643 100644 --- a/docs/examples/scroll-position.tsx +++ b/docs/examples/scroll-position.tsx @@ -1,3 +1,4 @@ +import type { ScrollPosition } from '@/interface'; import React from 'react'; import '../../assets/index.less'; import type { TabsProps } from '../../src'; @@ -13,16 +14,51 @@ for (let i = 0; i < 50; i += 1) { } export default () => { const [key, setKey] = React.useState('0'); + const [scrollPosition, setScrollPosition] = React.useState('end'); return ( -
- setKey(curKey)} - defaultActiveKey="8" - items={items} - scrollPosition="end" - /> -
+ <> +
+ + + +
+
+ setKey(curKey)} + defaultActiveKey="8" + items={items} + scrollPosition={scrollPosition} + /> +
+ ); }; From 3fd06e47ce2f19d749c5b1cb2c0f84b04f89f9eb Mon Sep 17 00:00:00 2001 From: Oyster Lee Date: Mon, 25 Nov 2024 16:31:06 +0800 Subject: [PATCH 03/10] refactor: add setTimeout --- src/TabNavList/TabNode.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/TabNavList/TabNode.tsx b/src/TabNavList/TabNode.tsx index deabfc0a..c1b58a47 100644 --- a/src/TabNavList/TabNode.tsx +++ b/src/TabNavList/TabNode.tsx @@ -80,7 +80,6 @@ const TabNode: React.FC = props => { tabIndex={disabled ? null : 0} onClick={e => { e.stopPropagation(); - console.log('onClick', key); onInternalClick(e); }} onKeyDown={e => { @@ -89,7 +88,12 @@ const TabNode: React.FC = props => { onInternalClick(e); } }} - onFocus={onFocus} + onFocus={e => { + // without setTimeout, the onClick won't trigger + setTimeout(() => { + onFocus(e); + }, 50); + }} > {icon && {icon}} {label && labelNode} From 7abb2ec442421dbe67ba4a0ca8bb0c37d8e307ed Mon Sep 17 00:00:00 2001 From: Oyster Lee Date: Mon, 25 Nov 2024 16:37:33 +0800 Subject: [PATCH 04/10] feat: add scrollPosition as deps --- src/TabNavList/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/TabNavList/index.tsx b/src/TabNavList/index.tsx index 73ef903b..b08f57c1 100644 --- a/src/TabNavList/index.tsx +++ b/src/TabNavList/index.tsx @@ -352,9 +352,8 @@ const TabNavList = React.forwardRef((props, ref onTabClick(key, e); }} onFocus={() => { - console.log('onFocus', key); scrollToTab(key); - // doLockAnimation(); + doLockAnimation(); if (!tabsWrapperRef.current) { return; } @@ -441,6 +440,7 @@ const TabNavList = React.forwardRef((props, ref stringify(activeTabOffset), stringify(tabOffsets as any), tabPositionTopOrBottom, + scrollPosition, ]); // Should recalculate when rtl changed From 5458709af5f38ab4d73641129649a527a237cc46 Mon Sep 17 00:00:00 2001 From: Oyster Lee Date: Mon, 25 Nov 2024 16:41:30 +0800 Subject: [PATCH 05/10] chore: add vertical demo --- docs/examples/scroll-position.tsx | 12 ++++++++++++ src/TabNavList/TabNode.tsx | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/examples/scroll-position.tsx b/docs/examples/scroll-position.tsx index 6e1dc643..284ef368 100644 --- a/docs/examples/scroll-position.tsx +++ b/docs/examples/scroll-position.tsx @@ -59,6 +59,18 @@ export default () => { scrollPosition={scrollPosition} /> + +
+ setKey(curKey)} + defaultActiveKey="8" + items={items} + scrollPosition={scrollPosition} + tabPosition="left" + /> +
); }; diff --git a/src/TabNavList/TabNode.tsx b/src/TabNavList/TabNode.tsx index c1b58a47..009e5b70 100644 --- a/src/TabNavList/TabNode.tsx +++ b/src/TabNavList/TabNode.tsx @@ -92,7 +92,7 @@ const TabNode: React.FC = props => { // without setTimeout, the onClick won't trigger setTimeout(() => { onFocus(e); - }, 50); + }, 100); }} > {icon && {icon}} From 48ffbbd7284aed4aeb61632298b073d0469fcb11 Mon Sep 17 00:00:00 2001 From: Oyster Lee Date: Mon, 25 Nov 2024 16:46:12 +0800 Subject: [PATCH 06/10] chore: add api to readme --- README.md | 1 + docs/examples/scroll-position.tsx | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/README.md b/README.md index d9612ab9..c3b2b5a0 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,7 @@ ReactDom.render( | destroyInactiveTabPane | boolean | false | whether destroy inactive TabPane when change tab | | active | boolean | false | active feature of tab item | | tabKey | string | - | key linked to tab | +| scrollPosition | `'start' \| 'end' \| 'center' \| 'auto'` | `'auto'` | scroll position | ### TabPane(support in older versions) diff --git a/docs/examples/scroll-position.tsx b/docs/examples/scroll-position.tsx index 284ef368..a4cbf0de 100644 --- a/docs/examples/scroll-position.tsx +++ b/docs/examples/scroll-position.tsx @@ -49,6 +49,16 @@ export default () => { onChange={e => setScrollPosition(e.target.value as ScrollPosition)} /> +
Date: Mon, 25 Nov 2024 17:02:20 +0800 Subject: [PATCH 07/10] revert commit --- docs/examples/dynamic-extra.tsx | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/examples/dynamic-extra.tsx b/docs/examples/dynamic-extra.tsx index f20014aa..9db9721d 100644 --- a/docs/examples/dynamic-extra.tsx +++ b/docs/examples/dynamic-extra.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import '../../assets/index.less'; -import type { TabsProps } from '../../src'; import Tabs from '../../src'; +import type { TabsProps } from '../../src'; +import '../../assets/index.less'; const items: TabsProps['items'] = []; for (let i = 0; i < 50; i += 1) { @@ -16,20 +16,22 @@ export default () => { const extra = React.useMemo(() => { if (key === '0') { - return
额外内容
; - } - return null; - }, [key]); + return ( +
额外内容
+ ) + } + return null + }, [key]) return (
- setKey(curKey)} - tabBarExtraContent={extra} - defaultActiveKey="8" - items={items} + onChange={(curKey) => setKey(curKey)} + tabBarExtraContent={extra} + defaultActiveKey="8" + items={items} />
); -}; +}; \ No newline at end of file From d7537b5d04de9cde5057aade50d6792cef71d9f5 Mon Sep 17 00:00:00 2001 From: Oyster Lee Date: Mon, 25 Nov 2024 17:28:03 +0800 Subject: [PATCH 08/10] chore: update demo --- docs/examples/scroll-position.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/docs/examples/scroll-position.tsx b/docs/examples/scroll-position.tsx index a4cbf0de..139f410e 100644 --- a/docs/examples/scroll-position.tsx +++ b/docs/examples/scroll-position.tsx @@ -13,7 +13,6 @@ for (let i = 0; i < 50; i += 1) { }); } export default () => { - const [key, setKey] = React.useState('0'); const [scrollPosition, setScrollPosition] = React.useState('end'); return ( @@ -61,21 +60,12 @@ export default () => {
- setKey(curKey)} - defaultActiveKey="8" - items={items} - scrollPosition={scrollPosition} - /> +
setKey(curKey)} - defaultActiveKey="8" items={items} scrollPosition={scrollPosition} tabPosition="left" From 15ce46df94ea2d26ec92eee980705e2200acaa19 Mon Sep 17 00:00:00 2001 From: Oyster Lee Date: Mon, 25 Nov 2024 17:28:14 +0800 Subject: [PATCH 09/10] fix: remove settimeout --- src/TabNavList/TabNode.tsx | 7 +------ src/TabNavList/index.tsx | 1 - 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/TabNavList/TabNode.tsx b/src/TabNavList/TabNode.tsx index 009e5b70..939bc2cf 100644 --- a/src/TabNavList/TabNode.tsx +++ b/src/TabNavList/TabNode.tsx @@ -88,12 +88,7 @@ const TabNode: React.FC = props => { onInternalClick(e); } }} - onFocus={e => { - // without setTimeout, the onClick won't trigger - setTimeout(() => { - onFocus(e); - }, 100); - }} + onFocus={onFocus} > {icon && {icon}} {label && labelNode} diff --git a/src/TabNavList/index.tsx b/src/TabNavList/index.tsx index b08f57c1..c71e5ff1 100644 --- a/src/TabNavList/index.tsx +++ b/src/TabNavList/index.tsx @@ -352,7 +352,6 @@ const TabNavList = React.forwardRef((props, ref onTabClick(key, e); }} onFocus={() => { - scrollToTab(key); doLockAnimation(); if (!tabsWrapperRef.current) { return; From 0078378a4cec148a4d2f6b4e9efa5edf5202f626 Mon Sep 17 00:00:00 2001 From: Oyster Lee Date: Mon, 25 Nov 2024 17:32:40 +0800 Subject: [PATCH 10/10] chore: update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c3b2b5a0..34d6a062 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ ReactDom.render( | destroyInactiveTabPane | boolean | false | whether destroy inactive TabPane when change tab | | active | boolean | false | active feature of tab item | | tabKey | string | - | key linked to tab | -| scrollPosition | `'start' \| 'end' \| 'center' \| 'auto'` | `'auto'` | scroll position | +| scrollPosition | `'start' \| 'end' \| 'center' \| 'auto'` | `'auto'` | scroll position of tab
`'start'` means active tab will be at the leftmost, `'end'` means active tab will be at the rightmost, `'center'` means active tab will be at the center, `'auto'` means active tab will be visible regardless of scroll position | ### TabPane(support in older versions)