Skip to content

Commit

Permalink
feat(TabBar): TabBar组件对齐 mobile-vue (#482)
Browse files Browse the repository at this point in the history
* fix: 更新样式文件

* feat(Demo): 升级TabBar示例

* fix(TabBar-type): 修复TabBar类型错误

* fix(TabBar-type): 同步新类型

* feat(TabBarItem): 增加tab-bar-item/样式

* feat(Demo): 升级TabBar示例

* feat(Demo): 组件TabBar自定义示例升级

* feat(TabBar-type): 增加TabBarContext类型

* feat(TabBar): 完成TabBar逻辑迁移

* feat(TabBar): 加上parseTNode

* chore: 更新snap

* feat(TabBar-API): 同步TabBar的API文档

* chore: update common

* chore(TabBar): update snap

* chore(TabBar): sync common file

* chore: 变更common分支

* chore(TabBar):  update tdesign-api

* fix: 去掉旧的默认值定义

* refactor(TabBar): 改用usePrefixClass、useDefaultProps

* chore(TabBar): update api

* chore: 更改common到develop

* refactor(TabBar): 简化逻辑

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
tobytovi and github-actions[bot] authored Aug 28, 2024
1 parent b346021 commit 8a52e88
Show file tree
Hide file tree
Showing 22 changed files with 2,235 additions and 177 deletions.
2 changes: 1 addition & 1 deletion site/mobile/mobile.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export default {
{
title: 'TabBar 标签栏',
name: 'tab-bar',
component: () => import('tdesign-mobile-react/tab-bar/_example/mobile.jsx'),
component: () => import('tdesign-mobile-react/tab-bar/_example/mobile.tsx'),
},
{
title: 'Fab 悬浮按钮',
Expand Down
2 changes: 1 addition & 1 deletion src/_common
46 changes: 29 additions & 17 deletions src/tab-bar/TabBar.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,59 @@
import React, { forwardRef, memo, useMemo, useRef } from 'react';
import cls from 'classnames';
import useConfig from '../_util/useConfig';
import useDefault from '../_util/useDefault';
import type { StyledProps } from '../common';
import type { TdTabBarProps } from './type';
import { TabBarProvider } from './TabBarContext';
import parseTNode from '../_util/parseTNode';
import useDefaultProps from '../hooks/useDefaultProps';
import { usePrefixClass } from '../hooks/useClass';
import { tabBarDefaultProps } from './defaultProps';

export interface TabBarProps extends TdTabBarProps, StyledProps {}

const TabBar = forwardRef<HTMLDivElement, TabBarProps>((props, ref) => {
const { bordered, fixed, onChange, value, defaultValue } = props;
const { classPrefix } = useConfig();
const name = `${classPrefix}-tab-bar`;
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 tabBarClass = usePrefixClass('tab-bar');
const [activeValue, onToggleActiveValue] = useDefault(value, defaultValue, onChange);

const defaultIndex = useRef(-1);

const updateChild = onToggleActiveValue;

const tabBarClass = cls(name, {
[`${name}--bordered`]: bordered,
[`${name}--fixed`]: fixed,
});
const itemCount = React.Children.count(parseTNode(children));

const memoProviderValues = useMemo(
() => ({
defaultIndex,
activeValue,
updateChild,
shape,
split,
theme,
itemCount,
}),
[defaultIndex, activeValue, updateChild],
[defaultIndex, activeValue, updateChild, shape, split, theme, itemCount],
);

return (
<div className={tabBarClass} ref={ref}>
<TabBarProvider value={memoProviderValues}>{props.children}</TabBarProvider>
<div
className={cls(
tabBarClass,
{
[`${tabBarClass}--bordered`]: bordered,
[`${tabBarClass}--fixed`]: fixed,
[`${tabBarClass}--safe`]: safeAreaInsetBottom,
},
`${tabBarClass}--${props.shape}`,
)}
ref={ref}
role="tablist"
>
<TabBarProvider value={memoProviderValues}>{parseTNode(children)}</TabBarProvider>
</div>
);
});

TabBar.defaultProps = {
bordered: true,
fixed: true,
};

export default memo(TabBar);
14 changes: 9 additions & 5 deletions src/tab-bar/TabBarContext.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import React, { createContext, MutableRefObject, useMemo } from 'react';
import { ChangeHandler } from '../_util/useDefault';
import { TdTabBarProps } from './type';

export const TabBarContext = createContext<{
defaultIndex: MutableRefObject<number>;
activeValue: number | string | (number | string)[];
updateChild: ChangeHandler<number | string | (number | string)[], any[]>;
}>(null);
export const TabBarContext = createContext<
{
defaultIndex: MutableRefObject<number>;
activeValue: number | string | (number | string)[];
updateChild: ChangeHandler<number | string | (number | string)[], any[]>;
itemCount: number;
} & Pick<TdTabBarProps, 'shape' | 'split' | 'theme'>
>(null);

export function TabBarProvider({ children, value }) {
const memoValue = useMemo(() => value, [value]);
Expand Down
145 changes: 91 additions & 54 deletions src/tab-bar/TabBarItem.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
import cls from 'classnames';
import React, { forwardRef, memo, useContext, useEffect, useMemo, useState } from 'react';

import React, { forwardRef, memo, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { CSSTransition } from 'react-transition-group';
import { Icon } from 'tdesign-icons-react';
import type { StyledProps } from '../common';
import type { TdTabBarItemProps } from './type';
import { TabBarContext } from './TabBarContext';
import Badge from '../badge';
import useConfig from '../_util/useConfig';
import useTabBarCssTransition from './useTabBarCssTransition';
import parseTNode from '../_util/parseTNode';
import useDefaultProps from '../hooks/useDefaultProps';
import { usePrefixClass } from '../hooks/useClass';

export interface TabBarItemProps extends TdTabBarItemProps, StyledProps {}

const defaultBadgeOffset = [0, 5];
const defaultBadgeOffset = [0, 0];
const defaultBadgeMaxCount = 99;

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

const hasSubTabBar = useMemo(() => !!subTabBar, [subTabBar]);
const { defaultIndex, activeValue, updateChild } = useContext(TabBarContext);
const { defaultIndex, activeValue, updateChild, shape, split, theme, itemCount } = useContext(TabBarContext);

const tabBarItemClass = usePrefixClass('tab-bar-item');

const textNode = useRef<HTMLDivElement>(null);

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

// 组件每次 render 生成一个临时的当前组件唯一值
const [currentName] = useState<undefined | number | string>(() => {
if (value) {
Expand All @@ -27,9 +37,10 @@ const TabBarItem = forwardRef<HTMLDivElement, TabBarItemProps>((props, ref) => {
return (defaultIndex.current += 1);
});

const { classPrefix } = useConfig();

const componentName = `${classPrefix}-tab-bar-item`;
useEffect(() => {
const height = textNode?.current?.clientHeight;
setIconOnly(Number(height) === 0);
}, [textNode]);

const [isSpread, setIsSpread] = useState(false);

Expand All @@ -42,6 +53,8 @@ const TabBarItem = forwardRef<HTMLDivElement, TabBarItemProps>((props, ref) => {

const mergedBadgeProps = useMemo(
() => ({
count: 0,
dot: false,
offset: defaultBadgeOffset,
maxCount: defaultBadgeMaxCount,
...badgeProps,
Expand Down Expand Up @@ -83,68 +96,92 @@ const TabBarItem = forwardRef<HTMLDivElement, TabBarItemProps>((props, ref) => {
setIsSpread(() => false);
};

const tabItemCls = cls(componentName, {
[`${classPrefix}-no-border`]: icon,
});
const tabItemInnerCls = cls(`${componentName}__content`, {
[`${classPrefix}-is-checked`]: isChecked,
[`${componentName}--onlytext`]: !icon,
/** 拥挤否 */
const crowded = itemCount > 3;

const tabItemCls = cls(
tabBarItemClass,
{
[`${tabBarItemClass}--split`]: split,
[`${tabBarItemClass}--text-only`]: !icon,
[`${tabBarItemClass}--crowded`]: crowded,
},
`${tabBarItemClass}--${shape}`,
);
const tabItemInnerCls = cls(
`${tabBarItemClass}__content`,
{
[`${tabBarItemClass}__content--checked`]: isChecked,
},
`${tabBarItemClass}__content--${theme}`,
);
const tabItemTextCls = cls(`${tabBarItemClass}__text`, {
[`${tabBarItemClass}__text--small`]: icon,
});
const tabItemIconCls = cls(`${componentName}__icon`);
const tabItemSpreadCls = cls(`${componentName}__spread`);
const tabItemSpreadItemCls = cls(`${componentName}__spread-item`);
const tabItemTextCls = cls(`${componentName}__text`);
const tabItemIconMenuCls = cls(`${componentName}__icon-menu`);

const transitionClsNames = useTabBarCssTransition({
name: 'spread',
});

const iconSize = `${iconOnly ? 24 : 20}px`;

const iconContent =
icon &&
React.cloneElement(icon, {
style: { fontSize: iconSize },
});

return (
<div
role="tab"
aria-label="TabBar"
aria-selected={isChecked}
aria-haspopup={shouldShowSubTabBar}
className={tabItemCls}
ref={ref}
>
<div className={tabItemInnerCls} onClick={toggle}>
<div className={tabItemCls} ref={ref}>
<div
role="tab"
aria-label="TabBar"
aria-selected={isChecked}
aria-haspopup={shouldShowSubTabBar}
className={tabItemInnerCls}
onClick={toggle}
>
{icon && (
<div className={tabItemIconCls}>
<div className={`${tabBarItemClass}__icon`} style={{ height: iconSize }}>
{badgeProps && (badgeProps?.dot || badgeProps?.count) ? (
<Badge content={icon} {...mergedBadgeProps} />
<Badge content={iconContent} {...mergedBadgeProps} />
) : (
icon
iconContent
)}
</div>
)}
{children && (
<div className={tabItemTextCls}>
{shouldShowSubTabBar && <div className={tabItemIconMenuCls} />}
{children}
<div ref={textNode} className={tabItemTextCls}>
{shouldShowSubTabBar && (
<>
<Icon name="view-list" size="16" />
<div className={`${tabBarItemClass}__icon-menu`} />
</>
)}
{parseTNode(children)}
</div>
)}

<CSSTransition timeout={200} in={showSubTabBar} classNames={transitionClsNames} mountOnEnter unmountOnExit>
<ul role="menu" className={tabItemSpreadCls}>
{subTabBar?.map((child, index) => (
<li
key={child.value || index}
role="menuitem"
aria-label={child.label}
className={tabItemSpreadItemCls}
onClick={(e) => {
e.stopPropagation();
selectChild(child.value || index);
}}
>
{child.label}
</li>
))}
</ul>
</CSSTransition>
</div>

<CSSTransition timeout={200} in={showSubTabBar} classNames={transitionClsNames} mountOnEnter unmountOnExit>
<ul role="menu" className={`${tabBarItemClass}__spread`}>
{subTabBar?.map((child, index) => (
<div
key={child.value || index}
role="menuitem"
aria-label={child.label}
className={`${tabBarItemClass}__spread-item`}
onClick={(e) => {
e.stopPropagation();
selectChild(child.value || index);
}}
>
{index !== 0 && <div className={`${tabBarItemClass}__spread-item-split`} />}
<div className={`${tabBarItemClass}__spread-item-text`}>{child.label}</div>
</div>
))}
</ul>
</CSSTransition>
</div>
);
});
Expand Down
41 changes: 41 additions & 0 deletions src/tab-bar/_example/badge-props.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { useState, useEffect } from 'react';
import { TabBar, TabBarItem } from 'tdesign-mobile-react';
import { Icon } from 'tdesign-icons-react';

function TabBarBaseDemo() {
const list = [
{ name: 'label_1', text: '首页', icon: 'home', badgeProps: { count: 16 }, ariaLabel: '首页,有16条消息' },
{ name: 'label_2', text: '软件', icon: 'app', badgeProps: { dot: true }, ariaLabel: '软件,有新的消息' },
{ name: 'label_3', text: '聊天', icon: 'chat', badgeProps: { count: 'New' }, ariaLabel: '聊天,New' },
{ name: 'label_4', text: '我的', icon: 'user', badgeProps: { count: '···' }, ariaLabel: '我的,有很多消息' },
];
const [value, setValue] = useState('label_1');

const change = (changeValue) => {
setValue(changeValue);
console.log('TabBar 值改变为:', changeValue);
};

useEffect(() => {
console.log('当前值:', value);
}, [value]);

return (
<div className="demo-tab-bar">
<TabBar value={value} onChange={change} split={false}>
{list.map((item, i) => (
<TabBarItem
key={item.name || i}
icon={<Icon name={item.icon} />}
value={item.name}
badgeProps={item.badgeProps}
>
{item.text}
</TabBarItem>
))}
</TabBar>
</div>
);
}

export default TabBarBaseDemo;
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React, { useState, useEffect } from 'react';
import { TabBar, TabBarItem } from 'tdesign-mobile-react';
import { AppIcon } from 'tdesign-icons-react';
import { Icon } from 'tdesign-icons-react';

function TabBarBaseDemo() {
const list = [
{ name: 'label_1', text: '文字', icon: <AppIcon />, badgeProps: { count: 16 } },
{ name: 'label_2', text: '文字', icon: <AppIcon />, badgeProps: { dot: true } },
{ name: 'label_3', text: '文字', icon: <AppIcon />, badgeProps: { count: 'New' } },
{ name: 'label_4', text: '文字', icon: <AppIcon />, badgeProps: { count: '···' } },
{ value: 'label_1', label: '首页', icon: 'home' },
{ value: 'label_2', label: '应用', icon: 'app' },
{ value: 'label_3', label: '聊天', icon: 'chat' },
{ value: 'label_4', label: '我的', icon: 'user' },
];
const [value, setValue] = useState('label_1');

Expand All @@ -22,10 +22,10 @@ function TabBarBaseDemo() {

return (
<div className="demo-tab-bar">
<TabBar value={value} onChange={change}>
<TabBar value={value} onChange={change} theme="tag" split={false}>
{list.map((item, i) => (
<TabBarItem key={item.name || i} icon={item.icon} value={item.name} badgeProps={item.badgeProps}>
{item.text}
<TabBarItem key={item.value || i} icon={<Icon name={item.icon} />} value={item.value}>
{item.label}
</TabBarItem>
))}
</TabBar>
Expand Down
Loading

0 comments on commit 8a52e88

Please sign in to comment.