Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion site/test-coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ module.exports = {
swipeCell: { statements: '4.42%', branches: '0%', functions: '0%', lines: '4.67%' },
swiper: { statements: '3.77%', branches: '0.9%', functions: '1.4%', lines: '3.89%' },
switch: { statements: '100%', branches: '100%', functions: '100%', lines: '100%' },
tabBar: { statements: '10%', branches: '0%', functions: '0%', lines: '10.81%' },
tabBar: { statements: '100%', branches: '93.18%', functions: '100%', lines: '100%' },
table: { statements: '100%', branches: '90%', functions: '100%', lines: '100%' },
tabs: { statements: '43.22%', branches: '18.75%', functions: '56%', lines: '45.07%' },
tag: { statements: '100%', branches: '96.87%', functions: '100%', lines: '100%' },
Expand Down
41 changes: 26 additions & 15 deletions src/tab-bar/TabBar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { forwardRef, memo, useMemo, useRef } from 'react';
import cls from 'classnames';
import classNames from 'classnames';
import useDefault from '../_util/useDefault';
import type { StyledProps } from '../common';
import type { TdTabBarProps } from './type';
Expand All @@ -13,9 +13,32 @@ export interface TabBarProps extends TdTabBarProps, StyledProps {}

const TabBar = forwardRef<HTMLDivElement, TabBarProps>((originProps, ref) => {
const props = useDefaultProps(originProps, tabBarDefaultProps);
const { bordered, fixed, onChange, value, defaultValue, safeAreaInsetBottom, shape, split, theme, children } = props;
const {
className,
style,
bordered,
fixed,
onChange,
value,
defaultValue,
safeAreaInsetBottom,
shape,
split,
theme,
children,
} = props;

const tabBarClass = usePrefixClass('tab-bar');
const tabBarClasses = classNames(
tabBarClass,
className,
{
[`${tabBarClass}--bordered`]: bordered,
[`${tabBarClass}--fixed`]: fixed,
[`${tabBarClass}--safe`]: safeAreaInsetBottom,
},
`${tabBarClass}--${props.shape}`,
);
const [activeValue, onToggleActiveValue] = useDefault(value, defaultValue, onChange);

const defaultIndex = useRef(-1);
Expand All @@ -38,19 +61,7 @@ const TabBar = forwardRef<HTMLDivElement, TabBarProps>((originProps, ref) => {
);

return (
<div
className={cls(
tabBarClass,
{
[`${tabBarClass}--bordered`]: bordered,
[`${tabBarClass}--fixed`]: fixed,
[`${tabBarClass}--safe`]: safeAreaInsetBottom,
},
`${tabBarClass}--${props.shape}`,
)}
ref={ref}
role="tablist"
>
<div className={tabBarClasses} style={style} ref={ref} role="tablist">
<TabBarProvider value={memoProviderValues}>{parseTNode(children)}</TabBarProvider>
</div>
);
Expand Down
18 changes: 14 additions & 4 deletions src/tab-bar/TabBarItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { StyledProps } from '../common';
import type { TdTabBarItemProps } from './type';
import { TabBarContext } from './TabBarContext';
import Badge from '../badge';
import useTabBarCssTransition from './useTabBarCssTransition';
import useTabBarCssTransition from './hooks/useTabBarCssTransition';
import parseTNode from '../_util/parseTNode';
import useDefaultProps from '../hooks/useDefaultProps';
import { usePrefixClass } from '../hooks/useClass';
Expand All @@ -18,7 +18,7 @@ const defaultBadgeMaxCount = 99;

const TabBarItem = forwardRef<HTMLDivElement, TabBarItemProps>((originProps, ref) => {
const props = useDefaultProps(originProps, {});
const { subTabBar, icon, badgeProps, value, children } = props;
const { subTabBar, icon, badgeProps, value, children, className, style } = props;

const hasSubTabBar = useMemo(() => !!subTabBar, [subTabBar]);
const { defaultIndex, activeValue, updateChild, shape, split, theme, itemCount } = useContext(TabBarContext);
Expand All @@ -27,6 +27,8 @@ const TabBarItem = forwardRef<HTMLDivElement, TabBarItemProps>((originProps, ref

const textNode = useRef<HTMLDivElement>(null);

const menuRef = useRef<HTMLDivElement>(null);

const [iconOnly, setIconOnly] = useState(false);

// 组件每次 render 生成一个临时的当前组件唯一值
Expand Down Expand Up @@ -101,6 +103,7 @@ const TabBarItem = forwardRef<HTMLDivElement, TabBarItemProps>((originProps, ref

const tabItemCls = cls(
tabBarItemClass,
className,
{
[`${tabBarItemClass}--split`]: split,
[`${tabBarItemClass}--text-only`]: !icon,
Expand Down Expand Up @@ -130,7 +133,7 @@ const TabBarItem = forwardRef<HTMLDivElement, TabBarItemProps>((originProps, ref
icon && React.cloneElement(icon, { style: { fontSize: iconSize } });

return (
<div className={tabItemCls} ref={ref}>
<div className={tabItemCls} ref={ref} style={style}>
<div
role="tab"
aria-label="TabBar"
Expand Down Expand Up @@ -161,7 +164,14 @@ const TabBarItem = forwardRef<HTMLDivElement, TabBarItemProps>((originProps, ref
)}
</div>

<CSSTransition timeout={200} in={showSubTabBar} classNames={transitionClsNames} mountOnEnter unmountOnExit>
<CSSTransition
nodeRef={menuRef}
timeout={200}
in={showSubTabBar}
classNames={transitionClsNames}
mountOnEnter
unmountOnExit
>
<ul role="menu" className={`${tabBarItemClass}__spread`}>
{subTabBar?.map((child, index) => (
<div
Expand Down
189 changes: 189 additions & 0 deletions src/tab-bar/__tests__/tab-bar-item.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import React from 'react';
import { describe, it, expect, render, fireEvent, vi } from '@test/utils';
import { HomeIcon, AppIcon, ChatIcon, UserIcon } from 'tdesign-icons-react';
import { TabBar, TabBarItem } from '..';

const prefix = 't';
const prefixClass = `${prefix}-tab-bar-item`;
const list = [
{ value: 1, label: '首页', icon: <HomeIcon /> },
{ value: 2, label: '应用', icon: <AppIcon /> },
{ value: 3, label: '聊天', icon: <ChatIcon /> },
{
value: 4,
label: '我的',
icon: <UserIcon />,
subTabBar: [
{
value: '4_1',
label: '基本信息',
},
{
value: '4_2',
label: '个人主页',
},
{
value: '4_3',
label: '设置',
},
],
},
];

describe('TabBarItem', () => {
describe('props', () => {
it(': className', () => {
const { container } = render(
<TabBar>
{list.map((item) => (
<TabBarItem key={item.value} {...item} className="custom-class">
{item.label}
</TabBarItem>
))}
</TabBar>,
);

expect(container.querySelectorAll('.custom-class')).toHaveLength(list.length);
});

it(': style', () => {
const { container } = render(
<TabBar>
{list.map((item) => (
<TabBarItem key={item.value} {...item} style={{ color: '#0052d9' }}>
{item.label}
</TabBarItem>
))}
</TabBar>,
);
const tabBarItems = container.querySelectorAll(`.${prefixClass}`);
expect(tabBarItems).toHaveLength(list.length);
expect(tabBarItems[0]).toHaveStyle({ color: '#0052d9' });
});

it(': badgeProps', () => {
const { container, rerender } = render(
<TabBar>
{list.map((item) => (
<TabBarItem key={item.value} {...item} badgeProps={{ dot: true }}>
{item.label}
</TabBarItem>
))}
</TabBar>,
);
expect(container.querySelectorAll(`.${prefix}-badge--dot`)).toHaveLength(list.length);

rerender(
<TabBar>
{list.map((item) => (
<TabBarItem key={item.value} {...item} badgeProps={{ count: 16 }}>
{item.label}
</TabBarItem>
))}
</TabBar>,
);
expect(container.querySelectorAll(`.${prefix}-badge__content`)).toHaveLength(list.length);
});

it(': badgeProps', () => {
const { container, rerender } = render(
<TabBar>
{list.map((item) => (
<TabBarItem key={item.value} {...item} badgeProps={{ dot: true }}>
{item.label}
</TabBarItem>
))}
</TabBar>,
);
expect(container.querySelectorAll(`.${prefix}-badge--dot`)).toHaveLength(list.length);

rerender(
<TabBar>
{list.map((item) => (
<TabBarItem key={item.value} {...item} badgeProps={{ count: 16 }}>
{item.label}
</TabBarItem>
))}
</TabBar>,
);
expect(container.querySelectorAll(`.${prefix}-badge__content`)).toHaveLength(list.length);
});

it(': icon', () => {
const { container } = render(
<TabBar>
{list.map((item) => (
<TabBarItem key={item.value} {...item}>
{item.label}
</TabBarItem>
))}
</TabBar>,
);

expect(container.querySelector(`.${prefixClass}__icon`)).toBeTruthy();
expect(container.querySelectorAll(`.${prefixClass}__icon`)).toHaveLength(list.length);
});

// tabBarItem 无value属性时,通过一个递增的 defaultIndex 生成一个临时的当前组件唯一值
it(': value', async () => {
const { container } = render(
<TabBar value={1}>
{list.map((item) => (
<TabBarItem key={item.value}>{item.label}</TabBarItem>
))}
</TabBar>,
);
expect(container.querySelectorAll(`.${prefixClass}`)).toHaveLength(list.length);
const activeItem = container.querySelector(`.${prefixClass}__content--checked`);
expect(activeItem).toBeTruthy();
expect(activeItem).toHaveTextContent(list[1].label);
});

it(': subTabBar', async () => {
const onChange = vi.fn();
const { container, getByText } = render(
<TabBar onChange={onChange}>
{list.map((item) => (
<TabBarItem key={item.value} {...item}>
{item.label}
</TabBarItem>
))}
</TabBar>,
);

const subTabBarItem = getByText('我的');
fireEvent.click(subTabBarItem);
expect(container.querySelector(`.${prefixClass}__spread`)).toBeTruthy();
expect(onChange).toHaveBeenCalled();
// expect(onChange).toHaveBeenCalledWith([4]);

const subTabBarMenu = container.querySelectorAll(`.${prefixClass}__spread-item`);
fireEvent.click(subTabBarMenu[0]);
expect(onChange).toHaveBeenCalled();
// expect(onChange).toHaveBeenCalledWith([4, '4_1']);
// setTimeout(() => {
// expect(container.querySelector(`.${prefixClass}__spread`)).toBeFalsy();
// });
});
});

describe('event', () => {
it(': tabBar click', async () => {
const { container, getByText } = render(
<TabBar>
{list.map((item) => (
<TabBarItem key={item.value} {...item}>
{item.label}
</TabBarItem>
))}
</TabBar>,
);

const subTabBarItem = getByText('我的');
fireEvent.click(subTabBarItem);
expect(container.querySelector(`.${prefixClass}__spread`)).toBeTruthy();
fireEvent.click(subTabBarItem);
expect(container.querySelector(`.${prefixClass}__spread`)).toBeTruthy();
});
});
});
Loading
Loading