|
1 |
| -import React, { useCallback, useRef, useState, useEffect, useMemo } from 'react'; |
| 1 | +import React, { useEffect, useRef, useState } from 'react'; |
2 | 2 | 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'; |
6 | 5 | import noop from '../_util/noop';
|
7 |
| -import { TabsProps, TabPanelProps } from './TabProps'; |
| 6 | +import { useTabClass } from './useTabClass'; |
| 7 | +import TabNavItem from './TabNavItem'; |
8 | 8 | import TabBar from './TabBar';
|
9 | 9 |
|
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 | +} |
27 | 16 |
|
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; |
29 | 29 |
|
30 |
| - const tabNavClick = useCallback( |
31 |
| - (event, idx: number) => { |
32 |
| - onClick(event, idx); |
33 |
| - }, |
34 |
| - [onClick], |
35 |
| - ); |
| 30 | + const { tdTabsClassGenerator, tdClassGenerator, tdSizeClassGenerator } = useTabClass(); |
36 | 31 |
|
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; |
51 | 39 | }
|
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 | + }; |
62 | 43 |
|
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()); |
75 | 45 |
|
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); |
86 | 48 |
|
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 | + }); |
89 | 58 | }
|
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; |
92 | 67 | }
|
93 |
| - newOffset = Math.min(newOffset, 0); |
94 | 68 |
|
95 |
| - setWrapTranslateX(newOffset); |
| 69 | + return scrollBarRef.current.clientWidth < navContainerRef.current.clientWidth; |
96 | 70 | };
|
97 | 71 |
|
98 |
| - useEffect(() => { |
99 |
| - /** |
100 |
| - * scroll 处理逻辑 |
101 |
| - */ |
102 |
| - checkScroll(); |
103 |
| - }, [panels, tabPosition, theme, checkScroll]); |
| 72 | + // 调用检查函数,并设置左右滑动按钮的展示状态 |
| 73 | + const setScrollBtnVisibleHandler = () => { |
| 74 | + setScrollBtnVisible(checkScrollBtnVisible()); |
| 75 | + }; |
104 | 76 |
|
| 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 | + // 组件初始化后判断当前是否需要展示滑动按钮 |
105 | 83 | 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 | + }); |
119 | 86 |
|
120 | 87 | 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 })} |
134 | 94 | >
|
135 |
| - + |
136 |
| - </span> |
137 |
| - )} |
| 95 | + <AddIcon name={'add'} /> |
| 96 | + </div> |
| 97 | + ) : null} |
138 | 98 | <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') : '', |
170 | 103 | )}
|
| 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} |
171 | 133 | <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} |
177 | 139 | >
|
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)); |
188 | 156 | }}
|
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 | + /> |
210 | 158 | ))}
|
| 159 | + {placement === 'bottom' ? TabBarCom : null} |
211 | 160 | </div>
|
212 | 161 | </div>
|
213 | 162 | </div>
|
|
0 commit comments