Skip to content

Commit c61f65a

Browse files
li-jia-nanMadCcc
andauthored
feat: Tabs support indicatorPosition prop (#691)
* feat: Tabs support indicatorPosition prop * test: add test case * update demo * test: add test cas * revert function * Update src/hooks/useIndicator.ts Co-authored-by: MadCcc <[email protected]> * fix: fix * fix: fix --------- Co-authored-by: MadCcc <[email protected]>
1 parent 9bde7d1 commit c61f65a

File tree

10 files changed

+193
-71
lines changed

10 files changed

+193
-71
lines changed

.dumirc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ export default defineConfig({
55
name: 'Tabs',
66
},
77
mfsu: false,
8-
});
8+
});

assets/position.less

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@
5252
flex-direction: column;
5353
min-width: 50px;
5454

55-
&-list, &-operations {
55+
&-list,
56+
&-operations {
5657
flex: 1 0 auto; // fix safari scroll problem
5758
flex-direction: column;
5859
}

docs/examples/indicator.tsx

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,63 @@
11
import React from 'react';
22
import '../../assets/index.less';
3+
import type { TabsProps } from '../../src';
34
import Tabs from '../../src';
45

5-
export default () => {
6-
const [destroy] = React.useState(false);
7-
const [items] = React.useState([
8-
{
9-
label: 'Light',
10-
key: 'light',
11-
children: 'Light!',
12-
},
13-
{
14-
label: 'Bamboo',
15-
key: 'bamboo',
16-
children: 'Bamboo!',
17-
},
18-
{
19-
label: 'Cute',
20-
key: 'cute',
21-
children: 'Cute!',
22-
disabled: true,
23-
},
24-
]);
6+
const items: TabsProps['items'] = [
7+
{
8+
label: 'Light',
9+
key: 'light',
10+
children: 'Light!',
11+
},
12+
{
13+
label: 'Bamboo',
14+
key: 'bamboo',
15+
children: 'Bamboo!',
16+
},
17+
{
18+
label: 'Cute',
19+
key: 'cute',
20+
children: 'Cute!',
21+
},
22+
];
2523

26-
if (destroy) {
27-
return null;
28-
}
29-
30-
return (
31-
<React.StrictMode>
32-
<Tabs tabBarExtraContent="extra" items={items} indicatorSize={origin => origin - 16} />
33-
</React.StrictMode>
34-
);
35-
};
24+
export default () => (
25+
<>
26+
<Tabs
27+
tabPosition="top"
28+
items={items}
29+
indicatorSize={origin => origin - 20}
30+
indicatorAlign="start"
31+
/>
32+
<Tabs
33+
tabPosition="top"
34+
items={items}
35+
indicatorSize={origin => origin - 20}
36+
indicatorAlign="center"
37+
/>
38+
<Tabs
39+
tabPosition="top"
40+
items={items}
41+
indicatorSize={origin => origin - 20}
42+
indicatorAlign="end"
43+
/>
44+
<Tabs
45+
tabPosition="left"
46+
items={items}
47+
indicatorSize={origin => origin - 20}
48+
indicatorAlign="start"
49+
/>
50+
<Tabs
51+
tabPosition="left"
52+
items={items}
53+
indicatorSize={origin => origin - 20}
54+
indicatorAlign="center"
55+
/>
56+
<Tabs
57+
tabPosition="left"
58+
items={items}
59+
indicatorSize={origin => origin - 20}
60+
indicatorAlign="end"
61+
/>
62+
</>
63+
);

src/TabNavList/Wrapper.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ const TabNavListWrapper: React.FC<TabNavListWrapperProps> = ({ renderTabBar, ...
1515
if (renderTabBar) {
1616
const tabNavBarProps = {
1717
...restProps,
18-
1918
// Legacy support. We do not use this actually
2019
panes: tabs.map<React.ReactNode>(({ label, key, ...restTabProps }) => (
2120
<TabPane tab={label} key={key} tabKey={key} {...restTabProps} />

src/TabNavList/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import useEvent from 'rc-util/lib/hooks/useEvent';
55
import { useComposeRef } from 'rc-util/lib/ref';
66
import * as React from 'react';
77
import { useEffect, useRef, useState } from 'react';
8+
import TabContext from '../TabContext';
89
import type { GetIndicatorSize } from '../hooks/useIndicator';
910
import useIndicator from '../hooks/useIndicator';
1011
import useOffsets from '../hooks/useOffsets';
@@ -23,7 +24,6 @@ import type {
2324
TabSizeMap,
2425
TabsLocale,
2526
} from '../interface';
26-
import TabContext from '../TabContext';
2727
import { genDataNodeKey, stringify } from '../util';
2828
import AddButton from './AddButton';
2929
import ExtraContent from './ExtraContent';
@@ -52,6 +52,7 @@ export interface TabNavListProps {
5252
getPopupContainer?: (node: HTMLElement) => HTMLElement;
5353
popupClassName?: string;
5454
indicatorSize?: GetIndicatorSize;
55+
indicatorAlign?: 'start' | 'center' | 'end';
5556
}
5657

5758
const getTabSize = (tab: HTMLElement, containerRect: { x: number; y: number }) => {
@@ -106,6 +107,7 @@ const TabNavList = React.forwardRef<HTMLDivElement, TabNavListProps>((props, ref
106107
onTabClick,
107108
onTabScroll,
108109
indicatorSize,
110+
indicatorAlign,
109111
} = props;
110112
const { prefixCls, tabs } = React.useContext(TabContext);
111113
const containerRef = useRef<HTMLDivElement>(null);
@@ -395,6 +397,7 @@ const TabNavList = React.forwardRef<HTMLDivElement, TabNavListProps>((props, ref
395397
horizontal: tabPositionTopOrBottom,
396398
rtl,
397399
indicatorSize,
400+
indicatorAlign,
398401
});
399402

400403
// ========================= Effect ========================
@@ -480,7 +483,6 @@ const TabNavList = React.forwardRef<HTMLDivElement, TabNavListProps>((props, ref
480483
visibility: hasDropdown ? 'hidden' : null,
481484
}}
482485
/>
483-
484486
<div
485487
className={classNames(`${prefixCls}-ink-bar`, {
486488
[`${prefixCls}-ink-bar-animated`]: animated.inkBar,

src/Tabs.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import useMergedState from 'rc-util/lib/hooks/useMergedState';
44
import isMobile from 'rc-util/lib/isMobile';
55
import * as React from 'react';
66
import { useEffect, useState } from 'react';
7+
import TabContext from './TabContext';
8+
import TabNavListWrapper from './TabNavList/Wrapper';
9+
import TabPanelList from './TabPanelList';
710
import useAnimateConfig from './hooks/useAnimateConfig';
811
import type { GetIndicatorSize } from './hooks/useIndicator';
912
import type {
@@ -16,9 +19,6 @@ import type {
1619
TabPosition,
1720
TabsLocale,
1821
} from './interface';
19-
import TabContext from './TabContext';
20-
import TabNavListWrapper from './TabNavList/Wrapper';
21-
import TabPanelList from './TabPanelList';
2222

2323
/**
2424
* Should added antd:
@@ -72,6 +72,7 @@ export interface TabsProps
7272

7373
// Indicator
7474
indicatorSize?: GetIndicatorSize;
75+
indicatorAlign?: 'start' | 'center' | 'end';
7576
}
7677

7778
const Tabs = React.forwardRef<HTMLDivElement, TabsProps>((props, ref) => {
@@ -100,9 +101,10 @@ const Tabs = React.forwardRef<HTMLDivElement, TabsProps>((props, ref) => {
100101
getPopupContainer,
101102
popupClassName,
102103
indicatorSize,
104+
indicatorAlign = 'center',
103105
...restProps
104106
} = props;
105-
const tabs = React.useMemo(
107+
const tabs = React.useMemo<Tab[]>(
106108
() => (items || []).filter(item => item && typeof item === 'object' && 'key' in item),
107109
[items],
108110
);
@@ -184,6 +186,7 @@ const Tabs = React.forwardRef<HTMLDivElement, TabsProps>((props, ref) => {
184186
getPopupContainer,
185187
popupClassName,
186188
indicatorSize,
189+
indicatorAlign,
187190
};
188191

189192
return (

src/hooks/useIndicator.ts

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,34 @@
11
import raf from 'rc-util/lib/raf';
2-
import type React from 'react';
3-
import { useEffect, useRef, useState } from 'react';
2+
import React, { useEffect, useRef, useState } from 'react';
43
import type { TabOffset } from '../interface';
54

65
export type GetIndicatorSize = number | ((origin: number) => number);
76

8-
export type UseIndicator = (options: {
7+
interface UseIndicatorOptions {
98
activeTabOffset: TabOffset;
109
horizontal: boolean;
1110
rtl: boolean;
1211
indicatorSize: GetIndicatorSize;
13-
}) => {
14-
style: React.CSSProperties;
15-
};
12+
indicatorAlign: 'start' | 'center' | 'end';
13+
}
1614

17-
const useIndicator: UseIndicator = ({ activeTabOffset, horizontal, rtl, indicatorSize }) => {
15+
const useIndicator = (options: UseIndicatorOptions) => {
16+
const { activeTabOffset, horizontal, rtl, indicatorSize, indicatorAlign } = options;
1817
const [inkStyle, setInkStyle] = useState<React.CSSProperties>();
1918
const inkBarRafRef = useRef<number>();
2019

21-
const getLength = (origin: number) => {
22-
if (typeof indicatorSize === 'function') {
23-
return indicatorSize(origin);
24-
}
25-
if (typeof indicatorSize === 'number') {
26-
return indicatorSize;
27-
}
28-
return origin;
29-
};
20+
const getLength = React.useCallback(
21+
(origin: number) => {
22+
if (typeof indicatorSize === 'function') {
23+
return indicatorSize(origin);
24+
}
25+
if (typeof indicatorSize === 'number') {
26+
return indicatorSize;
27+
}
28+
return origin;
29+
},
30+
[indicatorSize],
31+
);
3032

3133
// Delay set ink style to avoid remove tab blink
3234
function cleanInkBarRaf() {
@@ -38,18 +40,32 @@ const useIndicator: UseIndicator = ({ activeTabOffset, horizontal, rtl, indicato
3840

3941
if (activeTabOffset) {
4042
if (horizontal) {
41-
if (rtl) {
42-
newInkStyle.right = activeTabOffset.right + activeTabOffset.width / 2;
43-
newInkStyle.transform = 'translateX(50%)';
44-
} else {
45-
newInkStyle.left = activeTabOffset.left + activeTabOffset.width / 2;
46-
newInkStyle.transform = 'translateX(-50%)';
47-
}
4843
newInkStyle.width = getLength(activeTabOffset.width);
44+
const key = rtl ? 'right' : 'left';
45+
if (indicatorAlign === 'start') {
46+
newInkStyle[key] = activeTabOffset[key];
47+
}
48+
if (indicatorAlign === 'center') {
49+
newInkStyle[key] = activeTabOffset[key] + activeTabOffset.width / 2;
50+
newInkStyle.transform = rtl ? 'translateX(50%)' : 'translateX(-50%)';
51+
}
52+
if (indicatorAlign === 'end') {
53+
newInkStyle[key] = activeTabOffset[key] + activeTabOffset.width;
54+
newInkStyle.transform = 'translateX(-100%)';
55+
}
4956
} else {
50-
newInkStyle.top = activeTabOffset.top + activeTabOffset.height / 2;
51-
newInkStyle.transform = 'translateY(-50%)';
5257
newInkStyle.height = getLength(activeTabOffset.height);
58+
if (indicatorAlign === 'start') {
59+
newInkStyle.top = activeTabOffset.top;
60+
}
61+
if (indicatorAlign === 'center') {
62+
newInkStyle.top = activeTabOffset.top + activeTabOffset.height / 2;
63+
newInkStyle.transform = 'translateY(-50%)';
64+
}
65+
if (indicatorAlign === 'end') {
66+
newInkStyle.top = activeTabOffset.top + activeTabOffset.height;
67+
newInkStyle.transform = 'translateY(-100%)';
68+
}
5369
}
5470
}
5571

@@ -59,11 +75,9 @@ const useIndicator: UseIndicator = ({ activeTabOffset, horizontal, rtl, indicato
5975
});
6076

6177
return cleanInkBarRaf;
62-
}, [activeTabOffset, horizontal, rtl, indicatorSize]);
78+
}, [activeTabOffset, horizontal, rtl, indicatorSize, indicatorAlign, getLength]);
6379

64-
return {
65-
style: inkStyle,
66-
};
80+
return { style: inkStyle };
6781
};
6882

6983
export default useIndicator;

src/interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface TabOffset {
1717
right: number;
1818
top: number;
1919
}
20+
2021
export type TabOffsetMap = Map<React.Key, TabOffset>;
2122

2223
export type TabPosition = 'left' | 'right' | 'top' | 'bottom';

0 commit comments

Comments
 (0)