Skip to content

Commit be8c952

Browse files
committed
feat(TabBar): support zIndex and placeholder props
1 parent 523d88d commit be8c952

File tree

6 files changed

+150
-24
lines changed

6 files changed

+150
-24
lines changed

src/hooks/useElementHeight.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { useRef, useState, useEffect, useCallback } from 'react';
2+
3+
export interface UseElementHeightOptions {
4+
/** 是否立即计算高度 */
5+
immediate?: boolean;
6+
/** 监听窗口大小变化 */
7+
resizeObserver?: boolean;
8+
}
9+
10+
/**
11+
* 用于计算元素高度的 hook
12+
* @param targetRef 目标元素的 ref
13+
* @param options 配置选项
14+
* @returns 返回高度和重新计算的方法
15+
*/
16+
export default function useElementHeight(
17+
targetRef: React.RefObject<HTMLElement | null>,
18+
options: UseElementHeightOptions = {},
19+
) {
20+
const { immediate = true, resizeObserver = false } = options;
21+
const [elementHeight, setElementHeight] = useState(0);
22+
const resizeObserverInstance = useRef<ResizeObserver | null>(null);
23+
24+
const calculateHeight = useCallback(() => {
25+
const currentElement = targetRef.current;
26+
if (currentElement) {
27+
// 使用 getBoundingClientRect 获取精确高度
28+
const { height } = currentElement.getBoundingClientRect();
29+
setElementHeight(height);
30+
}
31+
}, [targetRef]);
32+
33+
const setupResizeObserver = useCallback(() => {
34+
if (!resizeObserver) return;
35+
36+
const currentElement = targetRef.current;
37+
if (currentElement && window.ResizeObserver) {
38+
resizeObserverInstance.current = new ResizeObserver(() => {
39+
calculateHeight();
40+
});
41+
resizeObserverInstance.current.observe(currentElement);
42+
}
43+
}, [resizeObserver, calculateHeight, targetRef]);
44+
45+
const cleanupResizeObserver = useCallback(() => {
46+
if (resizeObserverInstance.current) {
47+
resizeObserverInstance.current.disconnect();
48+
resizeObserverInstance.current = null;
49+
}
50+
}, []);
51+
52+
useEffect(() => {
53+
if (immediate) {
54+
// 使用 setTimeout 确保在 DOM 更新后计算高度
55+
const timeoutId = setTimeout(calculateHeight, 0);
56+
return () => clearTimeout(timeoutId);
57+
}
58+
}, [immediate, calculateHeight]);
59+
60+
useEffect(() => {
61+
setupResizeObserver();
62+
return cleanupResizeObserver;
63+
}, [setupResizeObserver, cleanupResizeObserver]);
64+
65+
return {
66+
height: elementHeight,
67+
calculateHeight,
68+
};
69+
}

src/tab-bar/TabBar.tsx

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { forwardRef, memo, useMemo, useRef } from 'react';
1+
import React, { forwardRef, memo, useMemo, useRef, useCallback } from 'react';
22
import classNames from 'classnames';
33
import useDefault from '../_util/useDefault';
44
import type { StyledProps } from '../common';
@@ -8,6 +8,7 @@ import parseTNode from '../_util/parseTNode';
88
import useDefaultProps from '../hooks/useDefaultProps';
99
import { usePrefixClass } from '../hooks/useClass';
1010
import { tabBarDefaultProps } from './defaultProps';
11+
import useElementHeight from '../hooks/useElementHeight';
1112

1213
export interface TabBarProps extends TdTabBarProps, StyledProps {}
1314

@@ -25,6 +26,8 @@ const TabBar = forwardRef<HTMLDivElement, TabBarProps>((originProps, ref) => {
2526
shape,
2627
split,
2728
theme,
29+
zIndex,
30+
placeholder,
2831
children,
2932
} = props;
3033

@@ -39,6 +42,35 @@ const TabBar = forwardRef<HTMLDivElement, TabBarProps>((originProps, ref) => {
3942
},
4043
`${tabBarClass}--${props.shape}`,
4144
);
45+
46+
const styles = useMemo<React.CSSProperties>(
47+
() => ({
48+
zIndex,
49+
...style,
50+
}),
51+
[style, zIndex],
52+
);
53+
54+
const internalRef = useRef<HTMLDivElement>(null);
55+
const { height: tabBarHeight } = useElementHeight(internalRef, {
56+
immediate: fixed && placeholder,
57+
});
58+
59+
// 创建合并的 ref callback,保持用户 ref 指向 role="tablist" 元素
60+
const mergedRef = useCallback(
61+
(element: HTMLDivElement | null) => {
62+
internalRef.current = element;
63+
64+
const userRef = ref;
65+
if (typeof userRef === 'function') {
66+
userRef(element);
67+
} else if (userRef && 'current' in userRef) {
68+
userRef.current = element;
69+
}
70+
},
71+
[ref],
72+
);
73+
4274
const [activeValue, onToggleActiveValue] = useDefault(value, defaultValue, onChange);
4375

4476
const defaultIndex = useRef(-1);
@@ -60,11 +92,21 @@ const TabBar = forwardRef<HTMLDivElement, TabBarProps>((originProps, ref) => {
6092
[defaultIndex, activeValue, updateChild, shape, split, theme, itemCount],
6193
);
6294

63-
return (
64-
<div className={tabBarClasses} style={style} ref={ref} role="tablist">
95+
const tabBarElement = (
96+
<div className={tabBarClasses} style={styles} ref={mergedRef} role="tablist">
6597
<TabBarProvider value={memoProviderValues}>{parseTNode(children)}</TabBarProvider>
6698
</div>
6799
);
100+
101+
if (fixed && placeholder) {
102+
return (
103+
<div className={`${tabBarClass}__placeholder`} style={{ height: `${tabBarHeight}px` }}>
104+
{tabBarElement}
105+
</div>
106+
);
107+
}
108+
109+
return tabBarElement;
68110
});
69111

70112
export default memo(TabBar);

src/tab-bar/defaultProps.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import { TdTabBarProps } from './type';
77
export const tabBarDefaultProps: TdTabBarProps = {
88
bordered: true,
99
fixed: true,
10+
placeholder: false,
1011
safeAreaInsetBottom: true,
1112
shape: 'normal',
1213
split: true,
1314
theme: 'normal',
14-
defaultValue: undefined,
15+
zIndex: 1,
1516
};

src/tab-bar/tab-bar.en-US.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,31 @@
77
name | type | default | description | required
88
-- | -- | -- | -- | --
99
className | String | - | className of component | N
10-
style | Object | - | CSS(Cascading Style Sheets),Typescript`React.CSSProperties` | N
10+
style | Object | - | CSS(Cascading Style Sheets),Typescript: `React.CSSProperties` | N
1111
bordered | Boolean | true | \- | N
12-
children | TNode | - | Typescript`TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
12+
children | TNode | - | Typescript: `TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
1313
fixed | Boolean | true | \- | N
14+
placeholder | Boolean | false | `0.21.1` | N
1415
safeAreaInsetBottom | Boolean | true | \- | N
15-
shape | String | 'normal' | options: normal/round | N
16+
shape | String | normal | options: normal/round | N
1617
split | Boolean | true | \- | N
17-
theme | String | 'normal' | options: normal/tag | N
18-
value | String / Number / Array | undefined | Typescript:`string \| number \| Array<string \| number>` | N
19-
defaultValue | String / Number / Array | undefined | uncontrolled property。Typescript:`string \| number \| Array<string \| number>` | N
20-
onChange | Function | | Typescript:`(value: string \| number) => void`<br/> | N
18+
theme | String | normal | options: normal/tag | N
19+
value | String / Number / Array | - | Typescript: `string \| number \| Array<string \| number>` | N
20+
defaultValue | String / Number / Array | - | uncontrolled property。Typescript: `string \| number \| Array<string \| number>` | N
21+
zIndex | Number | 1 | \- | N
22+
onChange | Function | | Typescript: `(value: string \| number) => void`<br/> | N
2123

2224

2325
### TabBarItem Props
2426

2527
name | type | default | description | required
2628
-- | -- | -- | -- | --
2729
className | String | - | className of component | N
28-
style | Object | - | CSS(Cascading Style Sheets),Typescript`React.CSSProperties` | N
29-
badgeProps | Object | - | Typescript`BadgeProps`[Badge API Documents](./badge?tab=api)[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/tab-bar/type.ts) | N
30-
children | TNode | - | Typescript`TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
31-
icon | TElement | - | Typescript`TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
32-
subTabBar | Array | - | Typescript`SubTabBarItem[] ` `interface SubTabBarItem { value: string; label: string }`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/tab-bar/type.ts) | N
30+
style | Object | - | CSS(Cascading Style Sheets),Typescript: `React.CSSProperties` | N
31+
badgeProps | Object | - | Typescript: `BadgeProps`[Badge API Documents](./badge?tab=api)[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/tab-bar/type.ts) | N
32+
children | TNode | - | Typescript: `TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
33+
icon | TElement | - | Typescript: `TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
34+
subTabBar | Array | - | Typescript: `SubTabBarItem[] ` `interface SubTabBarItem { value: string; label: string }`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/tab-bar/type.ts) | N
3335
value | String / Number | - | \- | N
3436

3537
### CSS Variables

src/tab-bar/tab-bar.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ style | Object | - | 样式,TS 类型:`React.CSSProperties` | N
1111
bordered | Boolean | true | 是否显示外边框 | N
1212
children | TNode | - | 标签栏内容。TS 类型:`TNode`[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
1313
fixed | Boolean | true | 是否固定在底部 | N
14-
safeAreaInsetBottom | Boolean | true | 是否为 iPhoneX 留出底部安全距离 | N
15-
shape | String | 'normal' | 标签栏的形状。可选项:normal/round | N
14+
placeholder | Boolean | false | `0.21.1`。固定在底部时是否开启占位 | N
15+
safeAreaInsetBottom | Boolean | true | 是否开启底部安全区适配 | N
16+
shape | String | normal | 标签栏的形状。可选项:normal/round | N
1617
split | Boolean | true | 是否需要分割线 | N
17-
theme | String | 'normal' | 选项风格。可选项:normal/tag | N
18-
value | String / Number / Array | undefined | 当前选中标签的索引。TS 类型:`string \| number \| Array<string \| number>` | N
19-
defaultValue | String / Number / Array | undefined | 当前选中标签的索引。非受控属性。TS 类型:`string \| number \| Array<string \| number>` | N
18+
theme | String | normal | 选项风格。可选项:normal/tag | N
19+
value | String / Number / Array | - | 当前选中标签的索引。TS 类型:`string \| number \| Array<string \| number>` | N
20+
defaultValue | String / Number / Array | - | 当前选中标签的索引。非受控属性。TS 类型:`string \| number \| Array<string \| number>` | N
21+
zIndex | Number | 1 | 标签栏层级 | N
2022
onChange | Function | | TS 类型:`(value: string \| number) => void`<br/>选中标签切换时触发 | N
2123

2224

src/tab-bar/type.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,18 @@ export interface TdTabBarProps {
2323
*/
2424
fixed?: boolean;
2525
/**
26-
* 是否为 iPhoneX 留出底部安全距离
26+
* 固定在底部时是否开启占位
27+
* @default false
28+
*/
29+
placeholder?: boolean;
30+
/**
31+
* 是否开启底部安全区适配
2732
* @default true
2833
*/
2934
safeAreaInsetBottom?: boolean;
3035
/**
3136
* 标签栏的形状
32-
* @default 'normal'
37+
* @default normal
3338
*/
3439
shape?: 'normal' | 'round';
3540
/**
@@ -39,7 +44,7 @@ export interface TdTabBarProps {
3944
split?: boolean;
4045
/**
4146
* 选项风格
42-
* @default 'normal'
47+
* @default normal
4348
*/
4449
theme?: 'normal' | 'tag';
4550
/**
@@ -50,6 +55,11 @@ export interface TdTabBarProps {
5055
* 当前选中标签的索引,非受控属性
5156
*/
5257
defaultValue?: string | number | Array<string | number>;
58+
/**
59+
* 标签栏层级
60+
* @default 1
61+
*/
62+
zIndex?: number;
5363
/**
5464
* 选中标签切换时触发
5565
*/

0 commit comments

Comments
 (0)