Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions docs/demo/scroll-position.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: scroll-position
nav:
title: Demo
path: /demo
---

<code src="../examples/scroll-position.tsx"></code>
76 changes: 76 additions & 0 deletions docs/examples/scroll-position.tsx
Original file line number Diff line number Diff line change
@@ -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<ScrollPosition>('end');

return (
<>
<div style={{ marginBottom: 10, display: 'flex', gap: 10 }}>
<label>
Start
<input
type="radio"
name="scrollPosition"
value="start"
checked={scrollPosition === 'start'}
onChange={e => setScrollPosition(e.target.value as ScrollPosition)}
/>
</label>
<label>
Center
<input
type="radio"
name="scrollPosition"
value="center"
checked={scrollPosition === 'center'}
onChange={e => setScrollPosition(e.target.value as ScrollPosition)}
/>
</label>
<label>
End
<input
type="radio"
name="scrollPosition"
value="end"
checked={scrollPosition === 'end'}
onChange={e => setScrollPosition(e.target.value as ScrollPosition)}
/>
</label>
<label>
Auto
<input
type="radio"
name="scrollPosition"
value="auto"
checked={scrollPosition === 'auto'}
onChange={e => setScrollPosition(e.target.value as ScrollPosition)}
/>
</label>
</div>
<div style={{ maxWidth: 550 }}>
<Tabs items={items} scrollPosition={scrollPosition} />
</div>

<div style={{ maxHeight: 550 }}>
<Tabs
style={{ height: 550 }}
items={items}
scrollPosition={scrollPosition}
tabPosition="left"
/>
</div>
</>
);
};
55 changes: 42 additions & 13 deletions src/TabNavList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
MoreProps,
OnTabScroll,
RenderTabBar,
ScrollPosition,
SizeInfo,
TabBarExtraContent,
TabPosition,
Expand Down Expand Up @@ -55,6 +56,7 @@ export interface TabNavListProps {
size?: GetIndicatorSize;
align?: 'start' | 'center' | 'end';
};
scrollPosition?: ScrollPosition;
}

const getTabSize = (tab: HTMLElement, containerRect: { left: number; top: number }) => {
Expand Down Expand Up @@ -104,6 +106,7 @@ const TabNavList = React.forwardRef<HTMLDivElement, TabNavListProps>((props, ref
editable,
locale,
tabPosition,
scrollPosition,
tabBarGutter,
children,
onTabClick,
Expand Down Expand Up @@ -150,7 +153,8 @@ const TabNavList = React.forwardRef<HTMLDivElement, TabNavListProps>((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;
Expand Down Expand Up @@ -264,19 +268,36 @@ const TabNavList = React.forwardRef<HTMLDivElement, TabNavListProps>((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);
Expand All @@ -285,10 +306,18 @@ const TabNavList = React.forwardRef<HTMLDivElement, TabNavListProps>((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);
Expand Down Expand Up @@ -323,7 +352,6 @@ const TabNavList = React.forwardRef<HTMLDivElement, TabNavListProps>((props, ref
onTabClick(key, e);
}}
onFocus={() => {
scrollToTab(key);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个部分有点多余,因为useEffect里每当activeKey更换的时候会触发scrollToTab,所以没必要在这里触发多一次

doLockAnimation();
if (!tabsWrapperRef.current) {
return;
Expand Down Expand Up @@ -411,6 +439,7 @@ const TabNavList = React.forwardRef<HTMLDivElement, TabNavListProps>((props, ref
stringify(activeTabOffset),
stringify(tabOffsets as any),
tabPositionTopOrBottom,
scrollPosition,
]);

// Should recalculate when rtl changed
Expand Down
5 changes: 5 additions & 0 deletions src/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
MoreProps,
OnTabScroll,
RenderTabBar,
ScrollPosition,
Tab,
TabBarExtraContent,
TabPosition,
Expand Down Expand Up @@ -72,6 +73,8 @@ export interface TabsProps
size?: GetIndicatorSize;
align?: 'start' | 'center' | 'end';
};

scrollPosition?: ScrollPosition;
}

const Tabs = React.forwardRef<HTMLDivElement, TabsProps>((props, ref) => {
Expand Down Expand Up @@ -99,6 +102,7 @@ const Tabs = React.forwardRef<HTMLDivElement, TabsProps>((props, ref) => {
getPopupContainer,
popupClassName,
indicator,
scrollPosition = 'auto',
...restProps
} = props;
const tabs = React.useMemo<Tab[]>(
Expand Down Expand Up @@ -182,6 +186,7 @@ const Tabs = React.forwardRef<HTMLDivElement, TabsProps>((props, ref) => {
getPopupContainer,
popupClassName,
indicator,
scrollPosition,
};

return (
Expand Down
10 changes: 6 additions & 4 deletions src/interface.ts
Original file line number Diff line number Diff line change
@@ -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<DropdownProps, 'children'>;

export type SizeInfo = [width: number, height: number];
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -89,3 +89,5 @@ export type TabBarExtraPosition = 'left' | 'right';
export type TabBarExtraMap = Partial<Record<TabBarExtraPosition, React.ReactNode>>;

export type TabBarExtraContent = React.ReactNode | TabBarExtraMap;

export type ScrollPosition = 'start' | 'end' | 'center' | 'auto';