diff --git a/site/test-coverage.js b/site/test-coverage.js index 8886ceac..6c9afe28 100644 --- a/site/test-coverage.js +++ b/site/test-coverage.js @@ -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%' }, diff --git a/src/tab-bar/TabBar.tsx b/src/tab-bar/TabBar.tsx index 348c7883..bbd00c37 100644 --- a/src/tab-bar/TabBar.tsx +++ b/src/tab-bar/TabBar.tsx @@ -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'; @@ -13,9 +13,32 @@ export interface TabBarProps extends TdTabBarProps, StyledProps {} const TabBar = forwardRef((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); @@ -38,19 +61,7 @@ const TabBar = forwardRef((originProps, ref) => { ); return ( -
+
{parseTNode(children)}
); diff --git a/src/tab-bar/TabBarItem.tsx b/src/tab-bar/TabBarItem.tsx index 4147c4ea..c6c527a2 100644 --- a/src/tab-bar/TabBarItem.tsx +++ b/src/tab-bar/TabBarItem.tsx @@ -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'; @@ -18,7 +18,7 @@ const defaultBadgeMaxCount = 99; const TabBarItem = forwardRef((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); @@ -27,6 +27,8 @@ const TabBarItem = forwardRef((originProps, ref const textNode = useRef(null); + const menuRef = useRef(null); + const [iconOnly, setIconOnly] = useState(false); // 组件每次 render 生成一个临时的当前组件唯一值 @@ -101,6 +103,7 @@ const TabBarItem = forwardRef((originProps, ref const tabItemCls = cls( tabBarItemClass, + className, { [`${tabBarItemClass}--split`]: split, [`${tabBarItemClass}--text-only`]: !icon, @@ -130,7 +133,7 @@ const TabBarItem = forwardRef((originProps, ref icon && React.cloneElement(icon, { style: { fontSize: iconSize } }); return ( -
+
((originProps, ref )}
- +
    {subTabBar?.map((child, index) => (
    }, + { value: 2, label: '应用', icon: }, + { value: 3, label: '聊天', icon: }, + { + value: 4, + label: '我的', + icon: , + subTabBar: [ + { + value: '4_1', + label: '基本信息', + }, + { + value: '4_2', + label: '个人主页', + }, + { + value: '4_3', + label: '设置', + }, + ], + }, +]; + +describe('TabBarItem', () => { + describe('props', () => { + it(': className', () => { + const { container } = render( + + {list.map((item) => ( + + {item.label} + + ))} + , + ); + + expect(container.querySelectorAll('.custom-class')).toHaveLength(list.length); + }); + + it(': style', () => { + const { container } = render( + + {list.map((item) => ( + + {item.label} + + ))} + , + ); + const tabBarItems = container.querySelectorAll(`.${prefixClass}`); + expect(tabBarItems).toHaveLength(list.length); + expect(tabBarItems[0]).toHaveStyle({ color: '#0052d9' }); + }); + + it(': badgeProps', () => { + const { container, rerender } = render( + + {list.map((item) => ( + + {item.label} + + ))} + , + ); + expect(container.querySelectorAll(`.${prefix}-badge--dot`)).toHaveLength(list.length); + + rerender( + + {list.map((item) => ( + + {item.label} + + ))} + , + ); + expect(container.querySelectorAll(`.${prefix}-badge__content`)).toHaveLength(list.length); + }); + + it(': badgeProps', () => { + const { container, rerender } = render( + + {list.map((item) => ( + + {item.label} + + ))} + , + ); + expect(container.querySelectorAll(`.${prefix}-badge--dot`)).toHaveLength(list.length); + + rerender( + + {list.map((item) => ( + + {item.label} + + ))} + , + ); + expect(container.querySelectorAll(`.${prefix}-badge__content`)).toHaveLength(list.length); + }); + + it(': icon', () => { + const { container } = render( + + {list.map((item) => ( + + {item.label} + + ))} + , + ); + + expect(container.querySelector(`.${prefixClass}__icon`)).toBeTruthy(); + expect(container.querySelectorAll(`.${prefixClass}__icon`)).toHaveLength(list.length); + }); + + // tabBarItem 无value属性时,通过一个递增的 defaultIndex 生成一个临时的当前组件唯一值 + it(': value', async () => { + const { container } = render( + + {list.map((item) => ( + {item.label} + ))} + , + ); + 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( + + {list.map((item) => ( + + {item.label} + + ))} + , + ); + + 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( + + {list.map((item) => ( + + {item.label} + + ))} + , + ); + + const subTabBarItem = getByText('我的'); + fireEvent.click(subTabBarItem); + expect(container.querySelector(`.${prefixClass}__spread`)).toBeTruthy(); + fireEvent.click(subTabBarItem); + expect(container.querySelector(`.${prefixClass}__spread`)).toBeTruthy(); + }); + }); +}); diff --git a/src/tab-bar/__tests__/tab-bar.test.tsx b/src/tab-bar/__tests__/tab-bar.test.tsx new file mode 100644 index 00000000..befc9aad --- /dev/null +++ b/src/tab-bar/__tests__/tab-bar.test.tsx @@ -0,0 +1,113 @@ +import React from 'react'; +import { describe, it, expect, render, vi, fireEvent } from '@test/utils'; +import { TabBar, TabBarItem } from '..'; + +const prefix = 't'; +const prefixClass = `${prefix}-tab-bar`; +const list = [ + { value: 1, label: '首页' }, + { value: 2, label: '应用' }, + { value: 3, label: '聊天' }, + { value: 4, label: '我的' }, +]; + +const renderTabBar = () => + list.map((item) => ( + + {item.label} + + )); + +describe('TabBar', () => { + describe('props', () => { + it(': className', () => { + const { container } = render(); + expect(container.querySelector('.custom-class')).toBeTruthy(); + }); + + it(': style', () => { + const { container } = render(); + expect(container.querySelector(`.${prefixClass}`)).toBeTruthy(); + expect(container.querySelector(`.${prefixClass}`)).toHaveStyle({ color: '#0052d9' }); + }); + + it(': bordered', async () => { + const { container, rerender } = render(); + expect(container.querySelector(`.${prefixClass}--bordered`)).toBeTruthy(); + rerender(); + expect(container.querySelector(`.${prefixClass}--bordered`)).toBeFalsy(); + }); + + it(': fixed', async () => { + const { container, rerender } = render(); + expect(container.querySelector(`.${prefixClass}--fixed`)).toBeTruthy(); + rerender(); + expect(container.querySelector(`.${prefixClass}--fixed`)).toBeFalsy(); + }); + + it(': safeAreaInsetBottom', async () => { + const { container, rerender } = render(); + expect(container.querySelector(`.${prefixClass}--safe`)).toBeTruthy(); + rerender(); + expect(container.querySelector(`.${prefixClass}--safe`)).toBeFalsy(); + }); + + it(': shape', () => { + const testShape = (shape, target) => { + const { container } = render(); + expect(container.querySelector(target)).toBeTruthy(); + }; + testShape(undefined, `.${prefixClass}--normal`); + testShape('normal', `.${prefixClass}--normal`); + testShape('round', `.${prefixClass}--round`); + }); + + it(': split', () => { + const { container } = render({renderTabBar()}); + const splits = container.querySelectorAll(`.${prefixClass}-item--split`); + expect(splits[0]).toBeTruthy(); + expect(splits).toHaveLength(list.length); + }); + + it(': theme', () => { + const testTheme = (theme, target) => { + const { container } = render( {renderTabBar()} ); + expect(container.querySelector(target)).toBeTruthy(); + }; + testTheme(undefined, `.${prefixClass}-item__content--normal`); + testTheme('normal', `.${prefixClass}-item__content--normal`); + testTheme('tag', `.${prefixClass}-item__content--tag`); + }); + + it(': value', async () => { + const { container, rerender } = render( {renderTabBar()} ); + expect(container.querySelectorAll(`.${prefixClass}-item`)).toHaveLength(list.length); + + rerender( {renderTabBar()} ); + const activeItem = container.querySelector(`.${prefixClass}-item__content--checked`); + expect(activeItem).toBeTruthy(); + expect(activeItem).toHaveTextContent(list[0].label); + }); + + it(': defaultValue', async () => { + const { container } = render( {renderTabBar()} ); + expect(container.querySelectorAll(`.${prefixClass}-item`)).toHaveLength(list.length); + const activeItem = container.querySelector(`.${prefixClass}-item__content--checked`); + expect(activeItem).toBeTruthy(); + expect(activeItem).toHaveTextContent(list[0].label); + }); + }); +}); + +describe('events', () => { + it(': onChange', async () => { + const onChange = vi.fn(); + const { container } = render( {renderTabBar()} ); + const tabBarItems = container.querySelectorAll(`.${prefixClass}-item__content`); + + expect(onChange).toHaveBeenCalledTimes(0); + fireEvent.click(tabBarItems[1]); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith(expect.any(Number)); + }); +}); diff --git a/src/tab-bar/useTabBarCssTransition.ts b/src/tab-bar/hooks/useTabBarCssTransition.ts similarity index 100% rename from src/tab-bar/useTabBarCssTransition.ts rename to src/tab-bar/hooks/useTabBarCssTransition.ts