diff --git a/README.md b/README.md index d9612ab9..34d6a062 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 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) diff --git a/docs/demo/scroll-position.md b/docs/demo/scroll-position.md new file mode 100644 index 00000000..bfc38294 --- /dev/null +++ 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/scroll-position.tsx b/docs/examples/scroll-position.tsx new file mode 100644 index 00000000..139f410e --- /dev/null +++ b/docs/examples/scroll-position.tsx @@ -0,0 +1,76 @@ +import type { ScrollPosition } from '@/interface'; +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 [scrollPosition, setScrollPosition] = React.useState('end'); + + return ( + <> +
+ + + + +
+
+ +
+ +
+ +
+ + ); +}; diff --git a/src/TabNavList/index.tsx b/src/TabNavList/index.tsx index e51553af..c71e5ff1 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,7 +352,6 @@ const TabNavList = React.forwardRef((props, ref onTabClick(key, e); }} onFocus={() => { - scrollToTab(key); doLockAnimation(); if (!tabsWrapperRef.current) { return; @@ -411,6 +439,7 @@ const TabNavList = React.forwardRef((props, ref stringify(activeTabOffset), stringify(tabOffsets as any), tabPositionTopOrBottom, + scrollPosition, ]); // Should recalculate when rtl changed 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';