mirror of
https://github.com/Tencent/tdesign-react.git
synced 2024-10-23 08:33:49 +08:00
重构 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,禁用选项卡。
This commit is contained in:
parent
b5194e956f
commit
939f11648d
@ -23,7 +23,6 @@ module.exports = {
|
||||
arrowParens: 'always',
|
||||
// 每个文件格式化的范围是文件的全部内容
|
||||
rangeStart: 0,
|
||||
// 每个文件格式化的范围是文件的全部内容
|
||||
rangeEnd: Infinity,
|
||||
// 不需要写文件开头的 @prettier
|
||||
requirePragma: false,
|
||||
|
2
common
2
common
@ -1 +1 @@
|
||||
Subproject commit 8def2d7c31cc3576ef9ed77c5d0f68ce3b2ad8ed
|
||||
Subproject commit 91bc5689f7c597d0c83def80806ee9188374d789
|
@ -1,213 +1,162 @@
|
||||
import React, { useCallback, useRef, useState, useEffect, useMemo } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Combine } from '../_type';
|
||||
import useConfig from '../_util/useConfig';
|
||||
import { CloseIcon, ChevronRightIcon, ChevronLeftIcon } from '../icon';
|
||||
import { AddIcon, ChevronLeftIcon, ChevronRightIcon } from '@tencent/tdesign-react';
|
||||
import { TdTabsProps, TdTabPanelProps, TabValue } from '../_type/components/tabs';
|
||||
import noop from '../_util/noop';
|
||||
import { TabsProps, TabPanelProps } from './TabProps';
|
||||
import { useTabClass } from './useTabClass';
|
||||
import TabNavItem from './TabNavItem';
|
||||
import TabBar from './TabBar';
|
||||
|
||||
const TabNav: React.FC<Combine<
|
||||
TabsProps,
|
||||
{
|
||||
panels: Combine<TabPanelProps, { key: string }>[];
|
||||
activeId: any;
|
||||
onClick: (e, idx: number) => any;
|
||||
}
|
||||
>> = (props) => {
|
||||
const { classPrefix } = useConfig();
|
||||
const [wrapTranslateX, setWrapTranslateX] = useState<number>(0);
|
||||
export interface TabNavProps extends TdTabsProps {
|
||||
itemList: TdTabPanelProps[];
|
||||
tabClick: (s: TabValue) => void;
|
||||
activeValue: TabValue;
|
||||
size?: 'medium' | 'large';
|
||||
}
|
||||
|
||||
const TabNav: React.FC<TabNavProps> = (props) => {
|
||||
const {
|
||||
placement = 'top',
|
||||
itemList,
|
||||
activeValue,
|
||||
tabClick = noop,
|
||||
theme,
|
||||
addable,
|
||||
onAdd,
|
||||
size = 'medium',
|
||||
disabled = false,
|
||||
} = props;
|
||||
|
||||
const { tdTabsClassGenerator, tdClassGenerator, tdSizeClassGenerator } = useTabClass();
|
||||
|
||||
// :todo 兼容老版本 TabBar 的实现
|
||||
const navContainerRef = useRef<HTMLDivElement>(null);
|
||||
const navScrollRef = useRef<HTMLDivElement>(null);
|
||||
const wrapDifference = useRef<number>(0);
|
||||
const tabsClassPrefix = `${classPrefix}-tabs`;
|
||||
const navClassPrefix = `${tabsClassPrefix}__nav`;
|
||||
|
||||
const { panels, tabPosition, size, activeId, theme, onClick, addable, onClose, onAdd = noop } = props;
|
||||
|
||||
const [isScroll, setIsScroll] = useState<boolean>(false);
|
||||
|
||||
const tabNavClick = useCallback(
|
||||
(event, idx: number) => {
|
||||
onClick(event, idx);
|
||||
},
|
||||
[onClick],
|
||||
);
|
||||
|
||||
const handleScroll = useCallback(
|
||||
({ position }: { position: 'left' | 'right' }) => {
|
||||
if (!isScroll) return;
|
||||
const absWrapTranslateX = Math.abs(wrapTranslateX);
|
||||
let delt = 0;
|
||||
if (position === 'left') {
|
||||
delt = absWrapTranslateX < 0 ? 0 : Math.min(absWrapTranslateX, 100);
|
||||
setWrapTranslateX(() => wrapTranslateX + delt);
|
||||
} else if (position === 'right') {
|
||||
// prettier-ignore
|
||||
delt = (
|
||||
absWrapTranslateX >= wrapDifference.current ? 0 : Math.min(wrapDifference.current - absWrapTranslateX, 100)
|
||||
);
|
||||
setWrapTranslateX(() => wrapTranslateX - delt);
|
||||
const getIndex = (value = activeValue) => {
|
||||
let index = 0;
|
||||
itemList.forEach((v, i) => {
|
||||
if (v.value === value) {
|
||||
index = i;
|
||||
}
|
||||
},
|
||||
[isScroll, wrapTranslateX],
|
||||
);
|
||||
|
||||
const wrapStyle = useMemo(
|
||||
() => ({
|
||||
transform: `translateX(${wrapTranslateX}px)`,
|
||||
}),
|
||||
[wrapTranslateX],
|
||||
);
|
||||
|
||||
const checkScroll = useCallback(() => {
|
||||
if (theme === 'card' && ['bottom', 'top'].includes(tabPosition)) {
|
||||
if (navScrollRef.current && navContainerRef.current) {
|
||||
wrapDifference.current = navContainerRef.current.offsetWidth - navScrollRef.current.offsetWidth;
|
||||
if (wrapDifference.current > 0) {
|
||||
setIsScroll(true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setIsScroll(false);
|
||||
}
|
||||
}, [theme, tabPosition]);
|
||||
|
||||
const scrollToActiveItem = () => {
|
||||
if (!isScroll) return;
|
||||
const $navScroll = navScrollRef.current as any;
|
||||
const $navWrap = navContainerRef.current as any;
|
||||
const $tabActive = $navWrap.querySelector('.t-is-active');
|
||||
if (!$tabActive) return;
|
||||
const navScrollBounding = $navScroll.getBoundingClientRect();
|
||||
const tabActiveBounding = $tabActive.getBoundingClientRect();
|
||||
const currOffset = wrapTranslateX;
|
||||
let newOffset = currOffset;
|
||||
|
||||
if (tabActiveBounding.left < navScrollBounding.left) {
|
||||
newOffset = currOffset + (navScrollBounding.left - tabActiveBounding.left);
|
||||
}
|
||||
if (tabActiveBounding.right > navScrollBounding.right) {
|
||||
newOffset = currOffset - (tabActiveBounding.right - navScrollBounding.right);
|
||||
}
|
||||
newOffset = Math.min(newOffset, 0);
|
||||
|
||||
setWrapTranslateX(newOffset);
|
||||
});
|
||||
return index;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* scroll 处理逻辑
|
||||
*/
|
||||
checkScroll();
|
||||
}, [panels, tabPosition, theme, checkScroll]);
|
||||
const [activeIndex, setActiveIndex] = useState(getIndex());
|
||||
|
||||
useEffect(() => {
|
||||
let timer = null;
|
||||
// 处理变动
|
||||
if (theme === 'card') {
|
||||
timer = setInterval(() => {
|
||||
checkScroll();
|
||||
}, 500);
|
||||
// 判断滚动条是否需要展示
|
||||
const [scrollBtnVisible, setScrollBtnVisible] = useState(true);
|
||||
|
||||
// 滚动条处理逻辑
|
||||
const scrollBarRef = useRef(null);
|
||||
const scrollClickHandler = (position: 'left' | 'right') => {
|
||||
const ref = scrollBarRef.current;
|
||||
if (ref) {
|
||||
ref.scrollTo({
|
||||
left: position === 'left' ? ref.scrollLeft - 200 : ref.scrollLeft + 200,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
};
|
||||
}, [checkScroll, theme]);
|
||||
};
|
||||
|
||||
// 检查当前内容区块是否超出滚动区块,判断左右滑动按钮是否展示
|
||||
const checkScrollBtnVisible = (): boolean => {
|
||||
if (!scrollBarRef.current || !navContainerRef.current) {
|
||||
// :todo 滚动条和内容区的 ref 任意一个不合法时,不执行此函数,暂时 console.error 打印错误
|
||||
console.error('[tdesign-tabs]滚动条和内容区 dom 结构异常');
|
||||
return false;
|
||||
}
|
||||
|
||||
return scrollBarRef.current.clientWidth < navContainerRef.current.clientWidth;
|
||||
};
|
||||
|
||||
// 调用检查函数,并设置左右滑动按钮的展示状态
|
||||
const setScrollBtnVisibleHandler = () => {
|
||||
setScrollBtnVisible(checkScrollBtnVisible());
|
||||
};
|
||||
|
||||
// TabBar 组件逻辑层抽象,卡片类型时无需展示,故将逻辑整合到此处
|
||||
// eslint-disable-next-line operator-linebreak
|
||||
const TabBarCom =
|
||||
theme === 'card' ? null : <TabBar tabPosition={placement} activeId={activeIndex} containerRef={navContainerRef} />;
|
||||
|
||||
// 组件初始化后判断当前是否需要展示滑动按钮
|
||||
useEffect(() => {
|
||||
setScrollBtnVisibleHandler();
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(`${tabsClassPrefix}__header`, {
|
||||
[`t-is-${tabPosition}`]: true,
|
||||
})}
|
||||
>
|
||||
<div className={classNames(`${navClassPrefix}`)}>
|
||||
{theme === 'card' && addable && (
|
||||
<span
|
||||
className="t-tabs__add-btn t-size-m"
|
||||
onClick={(e) => {
|
||||
scrollToActiveItem();
|
||||
onAdd(e);
|
||||
}}
|
||||
>
|
||||
+
|
||||
</span>
|
||||
)}
|
||||
<div
|
||||
className={classNames({
|
||||
[`${navClassPrefix}-container`]: true,
|
||||
[`t-is-${tabPosition}`]: true,
|
||||
['t-is-addable']: addable,
|
||||
})}
|
||||
>
|
||||
{isScroll && (
|
||||
<span
|
||||
onClick={() => handleScroll({ position: 'left' })}
|
||||
className={classNames({
|
||||
['t-tabs__scroll-btn']: true,
|
||||
['t-tabs__scroll-btn--left']: true,
|
||||
['t-size-m']: size === 'middle',
|
||||
['t-size-l']: size === 'large',
|
||||
})}
|
||||
>
|
||||
<ChevronLeftIcon name={'chevron-left'} />
|
||||
</span>
|
||||
)}
|
||||
{isScroll && (
|
||||
<span
|
||||
onClick={() => handleScroll({ position: 'right' })}
|
||||
className={classNames({
|
||||
['t-tabs__scroll-btn']: true,
|
||||
['t-tabs__scroll-btn--right']: true,
|
||||
['t-size-m']: size === 'middle',
|
||||
['t-size-l']: size === 'large',
|
||||
})}
|
||||
>
|
||||
<ChevronRightIcon name={'chevron-right'} />
|
||||
</span>
|
||||
)}
|
||||
<div className={classNames(tdTabsClassGenerator('header'), tdClassGenerator(`is-${placement}`))}>
|
||||
<div className={classNames(tdTabsClassGenerator('nav'))}>
|
||||
{addable ? (
|
||||
<div
|
||||
className={classNames({
|
||||
['t-tabs__nav-scroll']: true,
|
||||
['t-is-scrollable']: isScroll,
|
||||
})}
|
||||
ref={navScrollRef}
|
||||
className={classNames(tdTabsClassGenerator('add-btn'), tdSizeClassGenerator(size))}
|
||||
onClick={(e) => onAdd({ e })}
|
||||
>
|
||||
<div className={classNames(`${tabsClassPrefix}__nav-wrap`)} style={wrapStyle} ref={navContainerRef}>
|
||||
<TabBar tabPosition={tabPosition} activeId={activeId} containerRef={navContainerRef} />
|
||||
{panels.map((panel, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={(event) => {
|
||||
if (panel.disabled) {
|
||||
return;
|
||||
}
|
||||
tabNavClick(event, index);
|
||||
<AddIcon name={'add'} />
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className={classNames(
|
||||
tdTabsClassGenerator('nav-container'),
|
||||
tdClassGenerator(`is-${placement}`),
|
||||
addable ? tdClassGenerator('is-addable') : '',
|
||||
)}
|
||||
>
|
||||
{addable && scrollBtnVisible ? (
|
||||
<>
|
||||
<span
|
||||
onClick={() => {
|
||||
scrollClickHandler('left');
|
||||
}}
|
||||
className={classNames(
|
||||
tdTabsClassGenerator('scroll-btn'),
|
||||
tdTabsClassGenerator('scroll-btn--left'),
|
||||
tdSizeClassGenerator(size),
|
||||
)}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
</span>
|
||||
<span
|
||||
onClick={() => {
|
||||
scrollClickHandler('right');
|
||||
}}
|
||||
className={classNames(
|
||||
tdTabsClassGenerator('scroll-btn'),
|
||||
tdTabsClassGenerator('scroll-btn--right'),
|
||||
tdSizeClassGenerator(size),
|
||||
)}
|
||||
>
|
||||
<ChevronRightIcon />
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
<div
|
||||
className={classNames(
|
||||
tdTabsClassGenerator('nav-scroll'),
|
||||
scrollBtnVisible ? tdClassGenerator('is-scrollable') : '',
|
||||
)}
|
||||
ref={scrollBarRef}
|
||||
>
|
||||
<div className={classNames(tdTabsClassGenerator('nav-wrap'))} ref={navContainerRef}>
|
||||
{placement !== 'bottom' ? TabBarCom : null}
|
||||
<div className={classNames(tdTabsClassGenerator('bar'), tdClassGenerator(`is-${placement}`))} />
|
||||
{itemList.map((v) => (
|
||||
<TabNavItem
|
||||
{...props}
|
||||
{...v}
|
||||
key={v.value}
|
||||
label={v.label}
|
||||
isActive={activeValue === v.value}
|
||||
theme={theme}
|
||||
placement={placement}
|
||||
disabled={disabled || v.disabled}
|
||||
onClick={() => {
|
||||
tabClick(v.value);
|
||||
setActiveIndex(getIndex(v.value));
|
||||
}}
|
||||
className={classNames({
|
||||
[`${navClassPrefix}-item`]: true,
|
||||
[`${navClassPrefix}--card`]: theme === 'card',
|
||||
['t-is-disabled']: panel.disabled,
|
||||
['t-is-active']: activeId === index,
|
||||
['t-is-left']: tabPosition === 'left',
|
||||
['t-is-right']: tabPosition === 'right',
|
||||
['t-size-m']: size === 'middle',
|
||||
['t-size-l']: size === 'large',
|
||||
})}
|
||||
>
|
||||
{panel.label}
|
||||
{panel.closable && theme === 'card' && (
|
||||
<CloseIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose(e, String(panel.name));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
/>
|
||||
))}
|
||||
{placement === 'bottom' ? TabBarCom : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
65
src/tabs/TabNavItem.tsx
Normal file
65
src/tabs/TabNavItem.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import React, { MouseEvent } from 'react';
|
||||
import { CloseIcon } from '@tencent/tdesign-react';
|
||||
import classNames from 'classnames';
|
||||
import { TdTabPanelProps } from '../_type/components/tabs';
|
||||
import noop from '../_util/noop';
|
||||
import { useTabClass } from './useTabClass';
|
||||
|
||||
export interface TabNavItemProps extends TdTabPanelProps {
|
||||
// 当前 item 是否处于激活态
|
||||
isActive: boolean;
|
||||
// 点击事件
|
||||
onClick: (e: MouseEvent) => void;
|
||||
theme: 'normal' | 'card';
|
||||
placement: string;
|
||||
size?: 'medium' | 'large';
|
||||
}
|
||||
|
||||
const TabNavItem: React.FC<TabNavItemProps> = (props) => {
|
||||
const {
|
||||
label,
|
||||
removable,
|
||||
isActive,
|
||||
onClick = noop,
|
||||
theme,
|
||||
placement,
|
||||
onRemove = noop,
|
||||
value,
|
||||
size = 'medium',
|
||||
disabled = false,
|
||||
} = props;
|
||||
|
||||
// 样式变量和常量定义
|
||||
const { tdTabsClassGenerator, tdClassGenerator, tdSizeClassGenerator } = useTabClass();
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={disabled ? noop : onClick}
|
||||
className={classNames(
|
||||
tdTabsClassGenerator('nav-item'),
|
||||
theme === 'card' ? tdTabsClassGenerator('nav--card') : '',
|
||||
tdSizeClassGenerator(size),
|
||||
isActive ? tdClassGenerator('is-active') : '',
|
||||
tdClassGenerator(`is-${placement}`),
|
||||
disabled ? tdClassGenerator('is-disabled') : '',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
{removable ? (
|
||||
<CloseIcon
|
||||
name={'close'}
|
||||
className={classNames('remove-btn')}
|
||||
onClick={(e) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
onRemove({ value, e });
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabNavItem;
|
@ -1,21 +1,14 @@
|
||||
import React from 'react';
|
||||
import { TabPanelProps } from './TabProps';
|
||||
import classNames from 'classnames';
|
||||
import { TdTabPanelProps } from '../_type/components/tabs';
|
||||
import { useTabClass } from './useTabClass';
|
||||
|
||||
export interface TabPanelProps extends TdTabPanelProps {}
|
||||
|
||||
const TabPanel: React.FC<TabPanelProps> = (props) => {
|
||||
const { active, forceRender } = props;
|
||||
if (forceRender) {
|
||||
return (
|
||||
<div
|
||||
className={'t-tab-panel'}
|
||||
style={{
|
||||
display: active ? 'block' : 'none',
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return active && <div className={'t-tab-panel'}>{props.children}</div>;
|
||||
const { tdTabPanelClassPrefix } = useTabClass();
|
||||
|
||||
return <div className={classNames(tdTabPanelClassPrefix)}>{props.children}</div>;
|
||||
};
|
||||
|
||||
TabPanel.displayName = 'TabPanel';
|
||||
|
@ -1,149 +1,63 @@
|
||||
import React, { forwardRef, useEffect, useState, useCallback, useRef } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Combine } from '../_type';
|
||||
import useConfig from '../_util/useConfig';
|
||||
import noop from '../_util/noop';
|
||||
import TabPanel from './TabPanel';
|
||||
import { TabValue, TdTabsProps } from '../_type/components/tabs';
|
||||
import TabNav from './TabNav';
|
||||
import { TabsProps, TabPanelProps } from './TabProps';
|
||||
import { useTabClass } from './useTabClass';
|
||||
import TabPanel from './TabPanel';
|
||||
|
||||
const Tabs: React.FC<TabsProps> = forwardRef((props, ref: React.Ref<HTMLDivElement>) => {
|
||||
const { classPrefix } = useConfig();
|
||||
const {
|
||||
children,
|
||||
disabled,
|
||||
closable,
|
||||
activeName,
|
||||
className,
|
||||
defaultActiveName,
|
||||
tabPosition,
|
||||
addable,
|
||||
onChange = noop,
|
||||
onAdd = noop,
|
||||
onClose = noop,
|
||||
style = {},
|
||||
} = props;
|
||||
const tabsClassPrefix = `${classPrefix}-tabs`;
|
||||
export interface TabsProps extends TdTabsProps {}
|
||||
|
||||
const [tabPanels, setTabPanels] = useState<Combine<TabPanelProps, { key: string }>[]>([]);
|
||||
const [activeId, setActiveId] = useState<string | number>('');
|
||||
const [parsedChildren, setParsedChildren] = useState<React.ReactNode>(null);
|
||||
const Tabs: React.FC<TabsProps> = (props) => {
|
||||
const { children, defaultValue, placement } = props;
|
||||
|
||||
// 判断是否 init ,如果 init ,active Tab不应再改变
|
||||
const isInit = useRef<boolean>(false);
|
||||
// 样式工具引入
|
||||
const { tdTabPanelClassPrefix, tdTabsClassPrefix, tdTabsClassGenerator, tdClassGenerator } = useTabClass();
|
||||
|
||||
const parseTabs = useCallback(
|
||||
(children: React.ReactNode) => {
|
||||
const panelList = [];
|
||||
React.Children.forEach(children, (child) => {
|
||||
if (!React.isValidElement(child)) {
|
||||
console.error('Tabs的children应为合法React节点');
|
||||
return null;
|
||||
}
|
||||
const { type, props, key } = child;
|
||||
if (type !== TabPanel) {
|
||||
console.error('Tabs的children类型应为TabPanel');
|
||||
}
|
||||
const panel = {
|
||||
...props,
|
||||
key,
|
||||
};
|
||||
const [value, setValue] = useState<TabValue>(defaultValue);
|
||||
|
||||
// 如果设定 tab 的 disabled,那么所有的 panel 都应该 disabled
|
||||
if (disabled) {
|
||||
Object.assign(panel, {
|
||||
disabled: true,
|
||||
});
|
||||
}
|
||||
panelList.push(panel);
|
||||
});
|
||||
setTabPanels(panelList);
|
||||
},
|
||||
[disabled],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
parseTabs(children);
|
||||
}, [children, parseTabs]);
|
||||
|
||||
// 设定 activeId
|
||||
useEffect(() => {
|
||||
const targetName = activeName || defaultActiveName;
|
||||
if (isInit.current && !activeName) return;
|
||||
if (!isInit.current) isInit.current = true;
|
||||
if (targetName && tabPanels.filter((panel) => panel.name === targetName).length > 0) {
|
||||
const idx = tabPanels.indexOf(tabPanels.filter((panel) => panel.name === targetName)[0]);
|
||||
setActiveId(idx);
|
||||
return;
|
||||
const itemList = React.Children.map(children, (child: any) => {
|
||||
if (child && child.type === TabPanel) {
|
||||
return child.props;
|
||||
}
|
||||
// 如果没有设定,那么默认 0
|
||||
setActiveId(0);
|
||||
}, [activeName, tabPanels, defaultActiveName]);
|
||||
|
||||
// 为 active 的 panel 设定 props
|
||||
useEffect(() => {
|
||||
setParsedChildren(
|
||||
React.Children.map(children, (child, idx) => {
|
||||
if (React.isValidElement(child)) {
|
||||
let active = false;
|
||||
if (idx === activeId) {
|
||||
active = true;
|
||||
}
|
||||
|
||||
return React.cloneElement(child, {
|
||||
...child.props,
|
||||
key: child.key,
|
||||
active,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}, [activeId, tabPanels, children]);
|
||||
|
||||
const handleChange = (event, index) => {
|
||||
if (!activeName) {
|
||||
setActiveId(index);
|
||||
}
|
||||
onChange(event, String(tabPanels[index].name));
|
||||
};
|
||||
|
||||
const handleClose = (event, an) => {
|
||||
if (!activeName) {
|
||||
if (tabPanels.length === 1) {
|
||||
setActiveId('');
|
||||
} else if (activeId >= tabPanels.length - 1) {
|
||||
// close 时的处理
|
||||
const nextActiveId = tabPanels.length - 2 >= 0 ? tabPanels.length - 2 : '';
|
||||
setActiveId(nextActiveId);
|
||||
} else {
|
||||
setActiveId(activeId);
|
||||
}
|
||||
}
|
||||
onClose(event, an);
|
||||
};
|
||||
|
||||
const tabNav = (
|
||||
<TabNav
|
||||
{...props}
|
||||
tabPosition={tabPosition}
|
||||
panels={tabPanels}
|
||||
activeId={activeId}
|
||||
addable={addable}
|
||||
onAdd={onAdd}
|
||||
onClose={handleClose}
|
||||
closable={closable}
|
||||
onClick={handleChange}
|
||||
/>
|
||||
);
|
||||
return null;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classNames(className, tabsClassPrefix)} style={style} ref={ref}>
|
||||
{tabPosition !== 'bottom' && tabNav}
|
||||
<div className={`${tabsClassPrefix}__content`}>{parsedChildren}</div>
|
||||
{tabPosition === 'bottom' && tabNav}
|
||||
<div className={classNames(tdTabsClassPrefix)}>
|
||||
{placement !== 'bottom' ? (
|
||||
<TabNav
|
||||
{...props}
|
||||
activeValue={value}
|
||||
itemList={itemList}
|
||||
tabClick={(v) => {
|
||||
setValue(v);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{/* :todo 后续做 children 的结构校验,保证一定是 tabPanel 类型 */}
|
||||
<div className={classNames(tdTabsClassGenerator('content'), tdClassGenerator(`is-${placement}`))}>
|
||||
<div className={classNames(tdTabPanelClassPrefix)}>
|
||||
{React.Children.map(children, (child: any) => {
|
||||
if (child && child.type === TabPanel && child.props.value === value) {
|
||||
return child;
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{placement === 'bottom' ? (
|
||||
<TabNav
|
||||
{...props}
|
||||
activeValue={value}
|
||||
itemList={itemList}
|
||||
tabClick={(v) => {
|
||||
setValue(v);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
Tabs.displayName = 'Tabs';
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -10,11 +10,11 @@ describe('Tabs 组件测试', () => {
|
||||
const testId = 'tab bar test id';
|
||||
const { getByTestId } = render(
|
||||
<div data-testid={testId}>
|
||||
<Tabs tabPosition={'top'} data-testid={testId} size={'middle'}>
|
||||
<TabPanel name={'a'} label={'a'}>
|
||||
<Tabs placement={'top'} data-testid={testId} size={'medium'}>
|
||||
<TabPanel value={'a'} label={'a'}>
|
||||
<div>a</div>
|
||||
</TabPanel>
|
||||
<TabPanel name={'b'} label={'b'}>
|
||||
<TabPanel value={'b'} label={'b'}>
|
||||
<div>b</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
@ -30,11 +30,11 @@ describe('Tabs 组件测试', () => {
|
||||
const testId = 'tab card theme test id';
|
||||
const { getByTestId } = render(
|
||||
<div data-testid={testId}>
|
||||
<Tabs tabPosition={'top'} size={'middle'}>
|
||||
<TabPanel name={'a'} label={'a'}>
|
||||
<Tabs placement={'top'} size={'medium'}>
|
||||
<TabPanel value={'a'} label={'a'}>
|
||||
<div>a</div>
|
||||
</TabPanel>
|
||||
<TabPanel name={'b'} label={'b'}>
|
||||
<TabPanel value={'b'} label={'b'}>
|
||||
<div>b</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
@ -50,11 +50,11 @@ describe('Tabs 组件测试', () => {
|
||||
const testId = 'tab position test id';
|
||||
const { getByTestId } = render(
|
||||
<div data-testid={testId}>
|
||||
<Tabs tabPosition={'top'} size={'middle'}>
|
||||
<TabPanel name={'a'} label={'a'}>
|
||||
<Tabs placement={'top'} size={'medium'}>
|
||||
<TabPanel value={'a'} label={'a'}>
|
||||
<div>a</div>
|
||||
</TabPanel>
|
||||
<TabPanel name={'b'} label={'b'}>
|
||||
<TabPanel value={'b'} label={'b'}>
|
||||
<div>b</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
@ -2,17 +2,17 @@ import React, { useState } from 'react';
|
||||
import { Tabs, TabPanel, Button } from '@tencent/tdesign-react';
|
||||
|
||||
export default function ThemeTabs() {
|
||||
const [theme, setTheme] = useState('default');
|
||||
const [theme, setTheme] = useState('normal');
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Button onClick={() => setTheme('default')}>default</Button>
|
||||
<Button onClick={() => setTheme('normal')}>default</Button>
|
||||
<Button onClick={() => setTheme('card')}>card</Button>
|
||||
<Tabs tabPosition={'top'} size={'middle'} theme={theme} disabled={false}>
|
||||
<TabPanel name={'1'} label={'1'}>
|
||||
<Tabs placement={'top'} size={'medium'} theme={theme} disabled={false}>
|
||||
<TabPanel value={'1'} label={'1'}>
|
||||
<div style={{ margin: 20 }}>这是一个Tabs</div>
|
||||
</TabPanel>
|
||||
<TabPanel name={'2'} label={<div>2</div>}>
|
||||
<TabPanel value={'2'} label={<div>2</div>}>
|
||||
<div style={{ margin: 20 }}>这是一个Tabs</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
@ -5,14 +5,14 @@ export default function BasicTabs() {
|
||||
return (
|
||||
<>
|
||||
<div className="tdegsin-demo-tabs">
|
||||
<Tabs tabPosition={'top'} size={'middle'}>
|
||||
<TabPanel name="a" label="选项卡1">
|
||||
<Tabs placement={'top'} size={'medium'} defaultValue={'a'}>
|
||||
<TabPanel value="a" label="选项卡1">
|
||||
<div className="tabs-content">选项卡1</div>
|
||||
</TabPanel>
|
||||
<TabPanel name="b" label="选项卡2">
|
||||
<TabPanel value="b" label="选项卡2">
|
||||
<div className="tabs-content">选项卡2</div>
|
||||
</TabPanel>
|
||||
<TabPanel name="c" label="选项卡3">
|
||||
<TabPanel value="c" label="选项卡3">
|
||||
<div className="tabs-content">选项卡3</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
@ -4,7 +4,7 @@ import { Tabs, TabPanel } from '@tencent/tdesign-react';
|
||||
export default function AddTabs() {
|
||||
const [panels, setPanels] = useState([
|
||||
{
|
||||
name: 1,
|
||||
value: 1,
|
||||
label: '选项卡1',
|
||||
},
|
||||
]);
|
||||
@ -12,24 +12,22 @@ export default function AddTabs() {
|
||||
<>
|
||||
<div className="tdegsin-demo-tabs">
|
||||
<Tabs
|
||||
tabPosition={'top'}
|
||||
size={'middle'}
|
||||
placement={'top'}
|
||||
size={'medium'}
|
||||
disabled={false}
|
||||
theme={'card'}
|
||||
defaultActiveName={'2'}
|
||||
addable={true}
|
||||
defaultValue={1}
|
||||
addable
|
||||
onAdd={() => {
|
||||
setPanels((panels) => {
|
||||
panels.push({
|
||||
name: panels.length + 1,
|
||||
label: `选项卡${panels.length + 1}`,
|
||||
});
|
||||
return [...panels];
|
||||
const newPanels = panels.concat({
|
||||
value: panels.length + 1,
|
||||
label: `选项卡${panels.length + 1}`,
|
||||
});
|
||||
setPanels(newPanels);
|
||||
}}
|
||||
>
|
||||
{panels.map(({ name, label }) => (
|
||||
<TabPanel key={name} name={name} label={label}>
|
||||
{panels.map(({ value, label }) => (
|
||||
<TabPanel key={value} value={value} label={label}>
|
||||
<div className="tabs-content">{label}</div>
|
||||
</TabPanel>
|
||||
))}
|
||||
|
@ -4,7 +4,7 @@ import { Tabs, TabPanel } from '@tencent/tdesign-react';
|
||||
export default function AddTabs() {
|
||||
const [panels, setPanels] = useState([
|
||||
{
|
||||
name: 1,
|
||||
value: 0,
|
||||
label: '选项卡1',
|
||||
},
|
||||
]);
|
||||
@ -12,24 +12,33 @@ export default function AddTabs() {
|
||||
<>
|
||||
<div className="tdegsin-demo-tabs">
|
||||
<Tabs
|
||||
tabPosition={'top'}
|
||||
size={'middle'}
|
||||
placement={'top'}
|
||||
size={'medium'}
|
||||
disabled={false}
|
||||
theme={'card'}
|
||||
defaultActiveName={'2'}
|
||||
addable={true}
|
||||
defaultValue={0}
|
||||
addable
|
||||
onAdd={() => {
|
||||
setPanels((panels) => {
|
||||
panels.push({
|
||||
name: panels.length + 1,
|
||||
label: `选项卡${panels.length + 1}`,
|
||||
});
|
||||
return [...panels];
|
||||
const newPanels = panels.concat({
|
||||
value: panels.length + 1,
|
||||
label: `选项卡${panels.length + 1}`,
|
||||
});
|
||||
setPanels(newPanels);
|
||||
}}
|
||||
>
|
||||
{panels.map(({ name, label }) => (
|
||||
<TabPanel key={name} name={name} label={label}>
|
||||
{panels.map(({ value, label }, index) => (
|
||||
<TabPanel
|
||||
key={value}
|
||||
value={value}
|
||||
label={label}
|
||||
removable={panels.length > 1}
|
||||
onRemove={() => {
|
||||
setPanels((panels) => {
|
||||
panels.splice(index, 1);
|
||||
return panels;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="tabs-content">{label}</div>
|
||||
</TabPanel>
|
||||
))}
|
||||
|
19
src/tabs/_example/disabledTabs.jsx
Normal file
19
src/tabs/_example/disabledTabs.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Tabs, TabPanel } from '@tencent/tdesign-react';
|
||||
|
||||
export default function ThemeTabs() {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Tabs placement={'top'} defaultValue={'2'} size={'medium'} disabled={false}>
|
||||
<TabPanel value={'1'} label={'选项卡一'} disabled>
|
||||
<div style={{ margin: 20 }}>这是一个Tabs</div>
|
||||
</TabPanel>
|
||||
<TabPanel value={'2'} label={'选项卡二'}>
|
||||
<div style={{ margin: 20 }}>这是一个Tabs</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -20,14 +20,14 @@ export default function IconTabs() {
|
||||
<Button variant="outline" onClick={toggle}>
|
||||
{desc}
|
||||
</Button>
|
||||
<Tabs tabPosition={'top'} size={'middle'} theme={theme}>
|
||||
<TabPanel name="a" label={label}>
|
||||
<Tabs placement={'top'} defaultValue={'a'} theme={theme}>
|
||||
<TabPanel value="a" label={label}>
|
||||
<div className="tabs-content">选项卡1</div>
|
||||
</TabPanel>
|
||||
<TabPanel name="b" label={label}>
|
||||
<TabPanel value="b" label={label}>
|
||||
<div className="tabs-content">选项卡2</div>
|
||||
</TabPanel>
|
||||
<TabPanel name="c" label={label}>
|
||||
<TabPanel value="c" label={label}>
|
||||
<div className="tabs-content">选项卡3</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
@ -4,43 +4,34 @@ import { Tabs, TabPanel } from '@tencent/tdesign-react';
|
||||
export default function CloseableTabs() {
|
||||
const [panels, setPanels] = useState([
|
||||
{
|
||||
name: 1,
|
||||
value: 1,
|
||||
label: '选项卡1',
|
||||
},
|
||||
]);
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '400px',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Tabs
|
||||
tabPosition={'top'}
|
||||
size={'middle'}
|
||||
placement={'top'}
|
||||
size={'medium'}
|
||||
disabled={false}
|
||||
theme={'card'}
|
||||
defaultActiveName={'2'}
|
||||
addable={true}
|
||||
onClose={(event, activeName) => {
|
||||
const targetPanelIndex = panels.findIndex((panel) => String(panel.name) === activeName);
|
||||
if (targetPanelIndex !== -1) {
|
||||
panels.splice(targetPanelIndex, 1);
|
||||
setPanels([...panels]);
|
||||
}
|
||||
defaultValue={1}
|
||||
addable
|
||||
onRemove={({ value }) => {
|
||||
const newPanels = panels.filter((panel) => panel.value !== value);
|
||||
setPanels(newPanels);
|
||||
}}
|
||||
onAdd={() => {
|
||||
setPanels((panels) => {
|
||||
panels.push({
|
||||
name: panels.length + 1,
|
||||
label: `选项卡${panels.length + 1}`,
|
||||
});
|
||||
return [...panels];
|
||||
const newPanels = panels.concat({
|
||||
value: panels.length + 1,
|
||||
label: `选项卡${panels.length + 1}`,
|
||||
});
|
||||
setPanels(newPanels);
|
||||
}}
|
||||
>
|
||||
{panels.map(({ name, label }) => (
|
||||
<TabPanel closable={panels.length > 1} key={name} name={name} label={label}>
|
||||
{panels.map(({ value, label }) => (
|
||||
<TabPanel removable key={value} value={value} label={label}>
|
||||
<div style={{ margin: 20 }}>{label}</div>
|
||||
</TabPanel>
|
||||
))}
|
||||
|
@ -6,30 +6,38 @@ export default function PositionTabs() {
|
||||
return (
|
||||
<>
|
||||
<div className="tdegsin-demo-tabs">
|
||||
<Button onClick={() => setPosition('top')}>top</Button>
|
||||
<Button onClick={() => setPosition('bottom')}>bottom</Button>
|
||||
<Button onClick={() => setPosition('left')}>left</Button>
|
||||
<Button onClick={() => setPosition('right')}>right</Button>
|
||||
<Tabs tabPosition={position} size={'middle'} theme={'default'} disabled={false} addable>
|
||||
<TabPanel name={'1'} label="选项卡1" forceRender={true}>
|
||||
<Button variant="outline" onClick={() => setPosition('top')}>
|
||||
top
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setPosition('bottom')}>
|
||||
bottom
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setPosition('left')}>
|
||||
left
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setPosition('right')}>
|
||||
right
|
||||
</Button>
|
||||
<Tabs placement={position} defaultValue={'1'} theme={'normal'} disabled={false}>
|
||||
<TabPanel value={'1'} label="选项卡1">
|
||||
<div className="tabs-content">这是一个Tabs</div>
|
||||
</TabPanel>
|
||||
<TabPanel name={'2'} label={<div>选项卡2</div>}>
|
||||
<TabPanel value={'2'} label={<div>选项卡2</div>}>
|
||||
<div className="tabs-content">这是一个Tabs</div>
|
||||
</TabPanel>
|
||||
<TabPanel name={'3'} label="选项卡3">
|
||||
<TabPanel value={'3'} label="选项卡3">
|
||||
<div className="tabs-content">这是一个Tabs</div>
|
||||
</TabPanel>
|
||||
<TabPanel name={'4'} label="选项卡4">
|
||||
<TabPanel value={'4'} label="选项卡4">
|
||||
<div className="tabs-content">这是一个Tabs</div>
|
||||
</TabPanel>
|
||||
<TabPanel name={'5'} label={<div>选项卡5</div>}>
|
||||
<TabPanel value={'5'} label={<div>选项卡5</div>}>
|
||||
<div className="tabs-content">这是一个Tabs</div>
|
||||
</TabPanel>
|
||||
<TabPanel name={'6'} label={<div>选项卡6</div>}>
|
||||
<TabPanel value={'6'} label={<div>选项卡6</div>}>
|
||||
<div className="tabs-content">这是一个Tabs</div>
|
||||
</TabPanel>
|
||||
<TabPanel name={'7'} label={<div>选项卡7</div>}>
|
||||
<TabPanel value={'7'} label={<div>选项卡7</div>}>
|
||||
<div className="tabs-content">这是一个Tabs</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
34
src/tabs/_example/size.jsx
Normal file
34
src/tabs/_example/size.jsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Tabs, TabPanel, Button } from '@tencent/tdesign-react';
|
||||
|
||||
export default function SizeTabs() {
|
||||
const [size, setSize] = useState('medium');
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Button variant="outline" onClick={() => setSize('medium')}>
|
||||
middle
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setSize('large')}>
|
||||
large
|
||||
</Button>
|
||||
<Tabs placement={'top'} size={size} theme="normal" disabled={false} defaultValue={'1'}>
|
||||
<TabPanel value={'1'} label={'选项卡一'}>
|
||||
<div style={{ margin: 20 }}>这是一个Tabs</div>
|
||||
</TabPanel>
|
||||
<TabPanel value={'2'} label={'选项卡二'}>
|
||||
<div style={{ margin: 20 }}>这是一个Tabs</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
<Tabs placement={'top'} size={size} theme="card" disabled={false} defaultValue={'1'}>
|
||||
<TabPanel value={'1'} label={'选项卡一'}>
|
||||
<div style={{ margin: 20 }}>这是一个Tabs</div>
|
||||
</TabPanel>
|
||||
<TabPanel value={'2'} label={'选项卡二'}>
|
||||
<div style={{ margin: 20 }}>这是一个Tabs</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,2 +1,5 @@
|
||||
export { default as Tabs } from './Tabs';
|
||||
export { default as TabPanel } from './TabPanel';
|
||||
|
||||
export type { TabsProps } from './Tabs';
|
||||
export type { TabPanelProps } from './TabPanel';
|
||||
|
26
src/tabs/useTabClass.ts
Normal file
26
src/tabs/useTabClass.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/* eslint-disable implicit-arrow-linebreak */
|
||||
import useConfig from '../_util/useConfig';
|
||||
|
||||
/**
|
||||
* @author kenzyyang
|
||||
* @date 2021-04-07 17:40:06
|
||||
* @desc tabs 相关的所有样式常量和样式生成器
|
||||
*/
|
||||
export const useTabClass = () => {
|
||||
const { classPrefix } = useConfig();
|
||||
const tdTabsClassPrefix = `${classPrefix}-tabs`;
|
||||
const tdTabPanelClassPrefix = `${classPrefix}-tab-panel`;
|
||||
const tdClassGenerator = (append: string) => `${classPrefix}-${append}`;
|
||||
const tdTabsClassGenerator = (append: string) => `${tdTabsClassPrefix}__${append}`;
|
||||
const tdTabPanelClassGenerator = (append: string) => `${tdTabPanelClassPrefix}__${append}`;
|
||||
const tdSizeClassGenerator = (size: 'medium' | 'large') => `${classPrefix}-size-${size === 'large' ? 'l' : 'm'}`;
|
||||
|
||||
return {
|
||||
tdTabsClassPrefix,
|
||||
tdTabPanelClassPrefix,
|
||||
tdClassGenerator,
|
||||
tdTabsClassGenerator,
|
||||
tdTabPanelClassGenerator,
|
||||
tdSizeClassGenerator,
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user