重构 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:
HQ-Lin 2021-04-22 17:07:46 +08:00 committed by xiaosansiji
parent b5194e956f
commit 939f11648d
19 changed files with 766 additions and 886 deletions

View File

@ -23,7 +23,6 @@ module.exports = {
arrowParens: 'always',
// 每个文件格式化的范围是文件的全部内容
rangeStart: 0,
// 每个文件格式化的范围是文件的全部内容
rangeEnd: Infinity,
// 不需要写文件开头的 @prettier
requirePragma: false,

2
common

@ -1 +1 @@
Subproject commit 8def2d7c31cc3576ef9ed77c5d0f68ce3b2ad8ed
Subproject commit 91bc5689f7c597d0c83def80806ee9188374d789

View File

@ -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
View 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;

View File

@ -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';

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>
))}

View File

@ -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>
))}

View 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>
</>
);
}

View File

@ -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>

View File

@ -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>
))}

View File

@ -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>

View 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>
</>
);
}

View File

@ -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
View 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,
};
};