Skip to content

Commit 939f116

Browse files
honkinglinxiaosansiji
authored andcommitted
重构 Tabs 组件 & 对齐最新 api (merge request !129)
Squash merge branch 'feature/new_tabs' into 'develop' * fix: tabs 组件children迭代方法替换成 React.Children.map * fix: tabs组件展示 panel 内容的判断条件修复 * fix: 修复 tabs 组件children 判断的问题 * fix: tabs组件修改引入 Icon 的方式,按需引入所有的 Icon * feature: tabs 组件文档添加 调整size,禁用选项卡。
1 parent b5194e9 commit 939f116

19 files changed

+726
-846
lines changed

.prettierrc.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ module.exports = {
2323
arrowParens: 'always',
2424
// 每个文件格式化的范围是文件的全部内容
2525
rangeStart: 0,
26-
// 每个文件格式化的范围是文件的全部内容
2726
rangeEnd: Infinity,
2827
// 不需要写文件开头的 @prettier
2928
requirePragma: false,

common

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Subproject commit 8def2d7c31cc3576ef9ed77c5d0f68ce3b2ad8ed
1+
Subproject commit 91bc5689f7c597d0c83def80806ee9188374d789

src/tabs/TabNav.tsx

Lines changed: 132 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -1,213 +1,162 @@
1-
import React, { useCallback, useRef, useState, useEffect, useMemo } from 'react';
1+
import React, { useEffect, useRef, useState } from 'react';
22
import classNames from 'classnames';
3-
import { Combine } from '../_type';
4-
import useConfig from '../_util/useConfig';
5-
import { CloseIcon, ChevronRightIcon, ChevronLeftIcon } from '../icon';
3+
import { AddIcon, ChevronLeftIcon, ChevronRightIcon } from '@tencent/tdesign-react';
4+
import { TdTabsProps, TdTabPanelProps, TabValue } from '../_type/components/tabs';
65
import noop from '../_util/noop';
7-
import { TabsProps, TabPanelProps } from './TabProps';
6+
import { useTabClass } from './useTabClass';
7+
import TabNavItem from './TabNavItem';
88
import TabBar from './TabBar';
99

10-
const TabNav: React.FC<Combine<
11-
TabsProps,
12-
{
13-
panels: Combine<TabPanelProps, { key: string }>[];
14-
activeId: any;
15-
onClick: (e, idx: number) => any;
16-
}
17-
>> = (props) => {
18-
const { classPrefix } = useConfig();
19-
const [wrapTranslateX, setWrapTranslateX] = useState<number>(0);
20-
const navContainerRef = useRef<HTMLDivElement>(null);
21-
const navScrollRef = useRef<HTMLDivElement>(null);
22-
const wrapDifference = useRef<number>(0);
23-
const tabsClassPrefix = `${classPrefix}-tabs`;
24-
const navClassPrefix = `${tabsClassPrefix}__nav`;
25-
26-
const { panels, tabPosition, size, activeId, theme, onClick, addable, onClose, onAdd = noop } = props;
10+
export interface TabNavProps extends TdTabsProps {
11+
itemList: TdTabPanelProps[];
12+
tabClick: (s: TabValue) => void;
13+
activeValue: TabValue;
14+
size?: 'medium' | 'large';
15+
}
2716

28-
const [isScroll, setIsScroll] = useState<boolean>(false);
17+
const TabNav: React.FC<TabNavProps> = (props) => {
18+
const {
19+
placement = 'top',
20+
itemList,
21+
activeValue,
22+
tabClick = noop,
23+
theme,
24+
addable,
25+
onAdd,
26+
size = 'medium',
27+
disabled = false,
28+
} = props;
2929

30-
const tabNavClick = useCallback(
31-
(event, idx: number) => {
32-
onClick(event, idx);
33-
},
34-
[onClick],
35-
);
30+
const { tdTabsClassGenerator, tdClassGenerator, tdSizeClassGenerator } = useTabClass();
3631

37-
const handleScroll = useCallback(
38-
({ position }: { position: 'left' | 'right' }) => {
39-
if (!isScroll) return;
40-
const absWrapTranslateX = Math.abs(wrapTranslateX);
41-
let delt = 0;
42-
if (position === 'left') {
43-
delt = absWrapTranslateX < 0 ? 0 : Math.min(absWrapTranslateX, 100);
44-
setWrapTranslateX(() => wrapTranslateX + delt);
45-
} else if (position === 'right') {
46-
// prettier-ignore
47-
delt = (
48-
absWrapTranslateX >= wrapDifference.current ? 0 : Math.min(wrapDifference.current - absWrapTranslateX, 100)
49-
);
50-
setWrapTranslateX(() => wrapTranslateX - delt);
32+
// :todo 兼容老版本 TabBar 的实现
33+
const navContainerRef = useRef<HTMLDivElement>(null);
34+
const getIndex = (value = activeValue) => {
35+
let index = 0;
36+
itemList.forEach((v, i) => {
37+
if (v.value === value) {
38+
index = i;
5139
}
52-
},
53-
[isScroll, wrapTranslateX],
54-
);
55-
56-
const wrapStyle = useMemo(
57-
() => ({
58-
transform: `translateX(${wrapTranslateX}px)`,
59-
}),
60-
[wrapTranslateX],
61-
);
40+
});
41+
return index;
42+
};
6243

63-
const checkScroll = useCallback(() => {
64-
if (theme === 'card' && ['bottom', 'top'].includes(tabPosition)) {
65-
if (navScrollRef.current && navContainerRef.current) {
66-
wrapDifference.current = navContainerRef.current.offsetWidth - navScrollRef.current.offsetWidth;
67-
if (wrapDifference.current > 0) {
68-
setIsScroll(true);
69-
}
70-
}
71-
} else {
72-
setIsScroll(false);
73-
}
74-
}, [theme, tabPosition]);
44+
const [activeIndex, setActiveIndex] = useState(getIndex());
7545

76-
const scrollToActiveItem = () => {
77-
if (!isScroll) return;
78-
const $navScroll = navScrollRef.current as any;
79-
const $navWrap = navContainerRef.current as any;
80-
const $tabActive = $navWrap.querySelector('.t-is-active');
81-
if (!$tabActive) return;
82-
const navScrollBounding = $navScroll.getBoundingClientRect();
83-
const tabActiveBounding = $tabActive.getBoundingClientRect();
84-
const currOffset = wrapTranslateX;
85-
let newOffset = currOffset;
46+
// 判断滚动条是否需要展示
47+
const [scrollBtnVisible, setScrollBtnVisible] = useState(true);
8648

87-
if (tabActiveBounding.left < navScrollBounding.left) {
88-
newOffset = currOffset + (navScrollBounding.left - tabActiveBounding.left);
49+
// 滚动条处理逻辑
50+
const scrollBarRef = useRef(null);
51+
const scrollClickHandler = (position: 'left' | 'right') => {
52+
const ref = scrollBarRef.current;
53+
if (ref) {
54+
ref.scrollTo({
55+
left: position === 'left' ? ref.scrollLeft - 200 : ref.scrollLeft + 200,
56+
behavior: 'smooth',
57+
});
8958
}
90-
if (tabActiveBounding.right > navScrollBounding.right) {
91-
newOffset = currOffset - (tabActiveBounding.right - navScrollBounding.right);
59+
};
60+
61+
// 检查当前内容区块是否超出滚动区块,判断左右滑动按钮是否展示
62+
const checkScrollBtnVisible = (): boolean => {
63+
if (!scrollBarRef.current || !navContainerRef.current) {
64+
// :todo 滚动条和内容区的 ref 任意一个不合法时,不执行此函数,暂时 console.error 打印错误
65+
console.error('[tdesign-tabs]滚动条和内容区 dom 结构异常');
66+
return false;
9267
}
93-
newOffset = Math.min(newOffset, 0);
9468

95-
setWrapTranslateX(newOffset);
69+
return scrollBarRef.current.clientWidth < navContainerRef.current.clientWidth;
9670
};
9771

98-
useEffect(() => {
99-
/**
100-
* scroll 处理逻辑
101-
*/
102-
checkScroll();
103-
}, [panels, tabPosition, theme, checkScroll]);
72+
// 调用检查函数,并设置左右滑动按钮的展示状态
73+
const setScrollBtnVisibleHandler = () => {
74+
setScrollBtnVisible(checkScrollBtnVisible());
75+
};
10476

77+
// TabBar 组件逻辑层抽象,卡片类型时无需展示,故将逻辑整合到此处
78+
// eslint-disable-next-line operator-linebreak
79+
const TabBarCom =
80+
theme === 'card' ? null : <TabBar tabPosition={placement} activeId={activeIndex} containerRef={navContainerRef} />;
81+
82+
// 组件初始化后判断当前是否需要展示滑动按钮
10583
useEffect(() => {
106-
let timer = null;
107-
// 处理变动
108-
if (theme === 'card') {
109-
timer = setInterval(() => {
110-
checkScroll();
111-
}, 500);
112-
}
113-
return () => {
114-
if (timer) {
115-
clearInterval(timer);
116-
}
117-
};
118-
}, [checkScroll, theme]);
84+
setScrollBtnVisibleHandler();
85+
});
11986

12087
return (
121-
<div
122-
className={classNames(`${tabsClassPrefix}__header`, {
123-
[`t-is-${tabPosition}`]: true,
124-
})}
125-
>
126-
<div className={classNames(`${navClassPrefix}`)}>
127-
{theme === 'card' && addable && (
128-
<span
129-
className="t-tabs__add-btn t-size-m"
130-
onClick={(e) => {
131-
scrollToActiveItem();
132-
onAdd(e);
133-
}}
88+
<div className={classNames(tdTabsClassGenerator('header'), tdClassGenerator(`is-${placement}`))}>
89+
<div className={classNames(tdTabsClassGenerator('nav'))}>
90+
{addable ? (
91+
<div
92+
className={classNames(tdTabsClassGenerator('add-btn'), tdSizeClassGenerator(size))}
93+
onClick={(e) => onAdd({ e })}
13494
>
135-
+
136-
</span>
137-
)}
95+
<AddIcon name={'add'} />
96+
</div>
97+
) : null}
13898
<div
139-
className={classNames({
140-
[`${navClassPrefix}-container`]: true,
141-
[`t-is-${tabPosition}`]: true,
142-
['t-is-addable']: addable,
143-
})}
144-
>
145-
{isScroll && (
146-
<span
147-
onClick={() => handleScroll({ position: 'left' })}
148-
className={classNames({
149-
['t-tabs__scroll-btn']: true,
150-
['t-tabs__scroll-btn--left']: true,
151-
['t-size-m']: size === 'middle',
152-
['t-size-l']: size === 'large',
153-
})}
154-
>
155-
<ChevronLeftIcon name={'chevron-left'} />
156-
</span>
157-
)}
158-
{isScroll && (
159-
<span
160-
onClick={() => handleScroll({ position: 'right' })}
161-
className={classNames({
162-
['t-tabs__scroll-btn']: true,
163-
['t-tabs__scroll-btn--right']: true,
164-
['t-size-m']: size === 'middle',
165-
['t-size-l']: size === 'large',
166-
})}
167-
>
168-
<ChevronRightIcon name={'chevron-right'} />
169-
</span>
99+
className={classNames(
100+
tdTabsClassGenerator('nav-container'),
101+
tdClassGenerator(`is-${placement}`),
102+
addable ? tdClassGenerator('is-addable') : '',
170103
)}
104+
>
105+
{addable && scrollBtnVisible ? (
106+
<>
107+
<span
108+
onClick={() => {
109+
scrollClickHandler('left');
110+
}}
111+
className={classNames(
112+
tdTabsClassGenerator('scroll-btn'),
113+
tdTabsClassGenerator('scroll-btn--left'),
114+
tdSizeClassGenerator(size),
115+
)}
116+
>
117+
<ChevronLeftIcon />
118+
</span>
119+
<span
120+
onClick={() => {
121+
scrollClickHandler('right');
122+
}}
123+
className={classNames(
124+
tdTabsClassGenerator('scroll-btn'),
125+
tdTabsClassGenerator('scroll-btn--right'),
126+
tdSizeClassGenerator(size),
127+
)}
128+
>
129+
<ChevronRightIcon />
130+
</span>
131+
</>
132+
) : null}
171133
<div
172-
className={classNames({
173-
['t-tabs__nav-scroll']: true,
174-
['t-is-scrollable']: isScroll,
175-
})}
176-
ref={navScrollRef}
134+
className={classNames(
135+
tdTabsClassGenerator('nav-scroll'),
136+
scrollBtnVisible ? tdClassGenerator('is-scrollable') : '',
137+
)}
138+
ref={scrollBarRef}
177139
>
178-
<div className={classNames(`${tabsClassPrefix}__nav-wrap`)} style={wrapStyle} ref={navContainerRef}>
179-
<TabBar tabPosition={tabPosition} activeId={activeId} containerRef={navContainerRef} />
180-
{panels.map((panel, index) => (
181-
<div
182-
key={index}
183-
onClick={(event) => {
184-
if (panel.disabled) {
185-
return;
186-
}
187-
tabNavClick(event, index);
140+
<div className={classNames(tdTabsClassGenerator('nav-wrap'))} ref={navContainerRef}>
141+
{placement !== 'bottom' ? TabBarCom : null}
142+
<div className={classNames(tdTabsClassGenerator('bar'), tdClassGenerator(`is-${placement}`))} />
143+
{itemList.map((v) => (
144+
<TabNavItem
145+
{...props}
146+
{...v}
147+
key={v.value}
148+
label={v.label}
149+
isActive={activeValue === v.value}
150+
theme={theme}
151+
placement={placement}
152+
disabled={disabled || v.disabled}
153+
onClick={() => {
154+
tabClick(v.value);
155+
setActiveIndex(getIndex(v.value));
188156
}}
189-
className={classNames({
190-
[`${navClassPrefix}-item`]: true,
191-
[`${navClassPrefix}--card`]: theme === 'card',
192-
['t-is-disabled']: panel.disabled,
193-
['t-is-active']: activeId === index,
194-
['t-is-left']: tabPosition === 'left',
195-
['t-is-right']: tabPosition === 'right',
196-
['t-size-m']: size === 'middle',
197-
['t-size-l']: size === 'large',
198-
})}
199-
>
200-
{panel.label}
201-
{panel.closable && theme === 'card' && (
202-
<CloseIcon
203-
onClick={(e) => {
204-
e.stopPropagation();
205-
onClose(e, String(panel.name));
206-
}}
207-
/>
208-
)}
209-
</div>
157+
/>
210158
))}
159+
{placement === 'bottom' ? TabBarCom : null}
211160
</div>
212161
</div>
213162
</div>

0 commit comments

Comments
 (0)