Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(TabBar): TabBar组件对齐 mobile-vue #482

Merged
merged 25 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6fe4780
fix: 更新样式文件
tobytovi Aug 20, 2024
a1c8480
Merge remote-tracking branch 'origin/develop' into feat/tabbar
tobytovi Aug 21, 2024
fec4466
feat(Demo): 升级TabBar示例
tobytovi Aug 21, 2024
14f71a6
fix(TabBar-type): 修复TabBar类型错误
tobytovi Aug 21, 2024
d5e1f13
fix(TabBar-type): 同步新类型
tobytovi Aug 21, 2024
779df19
feat(TabBarItem): 增加tab-bar-item/样式
tobytovi Aug 21, 2024
6349202
feat(Demo): 升级TabBar示例
tobytovi Aug 21, 2024
ee1c5d3
feat(Demo): 组件TabBar自定义示例升级
tobytovi Aug 21, 2024
32eebae
feat(TabBar-type): 增加TabBarContext类型
tobytovi Aug 21, 2024
ee15027
feat(TabBar): 完成TabBar逻辑迁移
tobytovi Aug 21, 2024
291465d
feat(TabBar): 加上parseTNode
tobytovi Aug 21, 2024
5db30b4
chore: 更新snap
tobytovi Aug 21, 2024
6c14361
feat(TabBar-API): 同步TabBar的API文档
tobytovi Aug 22, 2024
4d321da
chore: update common
github-actions[bot] Aug 23, 2024
f515ea8
chore: merge develop
github-actions[bot] Aug 23, 2024
e5e2474
chore(TabBar): update snap
tobytovi Aug 26, 2024
70e5eb3
chore(TabBar): sync common file
tobytovi Aug 26, 2024
5984513
chore: 变更common分支
tobytovi Aug 26, 2024
c9a6b41
chore(TabBar): update tdesign-api
tobytovi Aug 26, 2024
81e5772
fix: 去掉旧的默认值定义
tobytovi Aug 26, 2024
ce06565
refactor(TabBar): 改用usePrefixClass、useDefaultProps
tobytovi Aug 26, 2024
87bcb2e
chore(TabBar): update api
tobytovi Aug 26, 2024
8af9a1c
chore: 更改common到develop
tobytovi Aug 26, 2024
4b33e27
refactor(TabBar): 简化逻辑
tobytovi Aug 26, 2024
cfe4c88
Merge branch 'develop' into feat/tabbar
tobytovi Aug 26, 2024
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/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
Loading