@janbox/storefront-ui 4.0.16 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -12
- package/dist/index.js +5 -2
- package/dist/lib/button/button.js +3 -3
- package/dist/lib/button/helpers.js +3 -3
- package/dist/lib/cascader/cascader-floating-content.js +5 -5
- package/dist/lib/chip/chip.js +3 -3
- package/dist/lib/chip/helpers.d.ts +1 -1
- package/dist/lib/chip/helpers.js +4 -4
- package/dist/lib/date-picker/date-picker.js +3 -3
- package/dist/lib/icon-button/icon-button.js +2 -2
- package/dist/lib/input/input.js +3 -3
- package/dist/lib/input-mask/input-mask.js +5 -5
- package/dist/lib/input-range/input-range.js +8 -8
- package/dist/lib/tabs/README.md +92 -25
- package/dist/lib/tabs/tab/README.md +134 -0
- package/dist/lib/tabs/tab/helpers.d.ts +2 -0
- package/dist/lib/tabs/tab/helpers.js +17 -0
- package/dist/lib/tabs/tab/tab.js +51 -14
- package/dist/lib/tabs/tab/types.d.ts +12 -13
- package/dist/lib/tabs/tab-content/README.md +87 -0
- package/dist/lib/tabs/tab-content/tab-content.d.ts +1 -1
- package/dist/lib/tabs/tab-content/tab-content.js +2 -11
- package/dist/lib/tabs/tab-content/types.d.ts +2 -4
- package/dist/lib/tabs/tab-indicator/README.md +94 -0
- package/dist/lib/tabs/tab-indicator/index.d.ts +0 -1
- package/dist/lib/tabs/tab-indicator/tab-indicator.d.ts +2 -1
- package/dist/lib/tabs/tab-indicator/tab-indicator.js +71 -40
- package/dist/lib/tabs/tab-indicator/types.d.ts +5 -2
- package/dist/lib/tabs/tab-list/README.md +164 -0
- package/dist/lib/tabs/tab-list/helpers.d.ts +6 -1
- package/dist/lib/tabs/tab-list/helpers.js +46 -3
- package/dist/lib/tabs/tab-list/tab-list.js +59 -22
- package/dist/lib/tabs/tab-list/types.d.ts +11 -1
- package/dist/lib/tabs/tab-panel/README.md +86 -0
- package/dist/lib/tabs/tab-panel/helpers.d.ts +1 -1
- package/dist/lib/tabs/tab-panel/tab-panel.js +3 -5
- package/dist/lib/tabs/tab-panel/types.d.ts +1 -2
- package/dist/lib/tabs/tabs/helpers.d.ts +1 -5
- package/dist/lib/tabs/tabs/helpers.js +1 -20
- package/dist/lib/tabs/tabs/tabs.js +18 -22
- package/dist/lib/tabs/tabs/types.d.ts +1 -7
- package/dist/lib/textarea/textarea.js +3 -3
- package/dist/utils/common.d.ts +1 -0
- package/dist/utils/common.js +2 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +6 -5
- package/dist/utils/stylesheet.d.ts +1 -1
- package/dist/utils/stylesheet.js +2 -2
- package/package.json +2 -2
package/dist/lib/tabs/tab/tab.js
CHANGED
|
@@ -1,25 +1,62 @@
|
|
|
1
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useTabIndexContext } from "./helpers.js";
|
|
3
|
-
import {
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useTabIndexContext, useGetTabProps } from "./helpers.js";
|
|
3
|
+
import { createSxStyle } from "../../../utils/stylesheet.js";
|
|
4
4
|
import { useTabsContext } from "../tabs/helpers.js";
|
|
5
|
-
import {
|
|
5
|
+
import { Primitive } from "../../primitive/primitive.js";
|
|
6
|
+
import { Icon } from "../../icon/icon.js";
|
|
6
7
|
const Tab = ({ children, ref, ..._props }) => {
|
|
7
8
|
var _a;
|
|
8
|
-
const {
|
|
9
|
-
const { color, states, ...rest } = mergeComponentProps(defaultTabProps, _props);
|
|
9
|
+
const { activeIndex, focusTab } = useTabsContext();
|
|
10
10
|
const tabIndex = useTabIndexContext();
|
|
11
|
+
const getTabProps = useGetTabProps();
|
|
12
|
+
const { startIcon, endIcon, prefix, suffix, states, ...props } = getTabProps(_props);
|
|
11
13
|
const isTabActive = tabIndex === activeIndex;
|
|
12
14
|
const onClick = (e) => {
|
|
13
15
|
var _a2;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
!isTabActive && focusTab(tabIndex);
|
|
17
|
-
} else {
|
|
18
|
-
focusTab(tabIndex);
|
|
19
|
-
}
|
|
20
|
-
(_a2 = rest.onClick) == null ? void 0 : _a2.call(rest, e);
|
|
16
|
+
focusTab(tabIndex);
|
|
17
|
+
(_a2 = props.onClick) == null ? void 0 : _a2.call(props, e);
|
|
21
18
|
};
|
|
22
|
-
return /* @__PURE__ */
|
|
19
|
+
return /* @__PURE__ */ jsxs(
|
|
20
|
+
Primitive,
|
|
21
|
+
{
|
|
22
|
+
...props,
|
|
23
|
+
type: "button",
|
|
24
|
+
"aria-selected": isTabActive,
|
|
25
|
+
as: "button",
|
|
26
|
+
role: "tab",
|
|
27
|
+
ref,
|
|
28
|
+
sx: {
|
|
29
|
+
...props.sx,
|
|
30
|
+
"&[aria-selected=true]": createSxStyle((_a = states == null ? void 0 : states.active) == null ? void 0 : _a.sx)
|
|
31
|
+
},
|
|
32
|
+
onClick,
|
|
33
|
+
children: [
|
|
34
|
+
prefix,
|
|
35
|
+
!!startIcon && /* @__PURE__ */ jsx(
|
|
36
|
+
Icon,
|
|
37
|
+
{
|
|
38
|
+
...startIcon,
|
|
39
|
+
sx: {
|
|
40
|
+
flexShrink: 0,
|
|
41
|
+
...startIcon.sx
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
),
|
|
45
|
+
children,
|
|
46
|
+
!!endIcon && /* @__PURE__ */ jsx(
|
|
47
|
+
Icon,
|
|
48
|
+
{
|
|
49
|
+
...endIcon,
|
|
50
|
+
sx: {
|
|
51
|
+
flexShrink: 0,
|
|
52
|
+
...endIcon.sx
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
),
|
|
56
|
+
suffix
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
);
|
|
23
60
|
};
|
|
24
61
|
export {
|
|
25
62
|
Tab
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
export type TabProps = ShallowMerge<ButtonProps, TabExtendedProps>;
|
|
1
|
+
import { IconProps } from '../../icon';
|
|
2
|
+
import { PropsWithSx, ShallowMerge } from '../../../types';
|
|
3
|
+
import { JSX } from 'react';
|
|
4
|
+
export type TabProps = PropsWithSx<ShallowMerge<JSX.IntrinsicElements['button'], {
|
|
5
|
+
prefix?: React.ReactNode;
|
|
6
|
+
suffix?: React.ReactNode;
|
|
7
|
+
startIcon?: IconProps;
|
|
8
|
+
endIcon?: IconProps;
|
|
9
|
+
states?: {
|
|
10
|
+
active?: PropsWithSx;
|
|
11
|
+
};
|
|
12
|
+
}>>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# TabContent
|
|
2
|
+
|
|
3
|
+
Container cho các [TabPanel](../tab-panel/README.md). Đọc `activeIndex` từ context của [Tabs](../README.md) và chỉ render đúng một child tại index đó.
|
|
4
|
+
|
|
5
|
+
## Import
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { TabContent } from '@janbox/storefront-ui';
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Props
|
|
12
|
+
|
|
13
|
+
| Prop | Type | Default | Description |
|
|
14
|
+
| ------ | ------ | ------- | ----------- |
|
|
15
|
+
| `children` * | `React.ReactNode` | - | Danh sách `TabPanel`, thứ tự phải khớp với thứ tự các `Tab` trong `TabList`. |
|
|
16
|
+
| `className` | `string` | - | Class cho container `div`. |
|
|
17
|
+
|
|
18
|
+
Kiểu prop là `JSX.IntrinsicElements['div']` — kế thừa toàn bộ div attributes. Component **không** nhận `sx`; dùng `className` hoặc `style` nếu cần style container.
|
|
19
|
+
|
|
20
|
+
> ⚠️ `TabContent` **không** forward `ref`. `ref` trong props sẽ bị truyền vào `Primitive` như prop thường và không trỏ tới DOM node.
|
|
21
|
+
|
|
22
|
+
*Required props
|
|
23
|
+
|
|
24
|
+
## Cách hoạt động
|
|
25
|
+
|
|
26
|
+
- Children được `flattenChildren` trước khi chọn, nên bọc panel trong Fragment vẫn giữ index đúng.
|
|
27
|
+
- Chỉ child tại `activeIndex` được render — các panel khác **bị unmount**, state nội bộ và scroll position của chúng không được giữ lại. Nếu cần giữ state, lift state lên trên `Tabs` hoặc tự render tất cả panel và điều khiển `display` bằng CSS.
|
|
28
|
+
- Nếu `activeIndex` không khớp child nào (vd: `defaultActiveIndex` vượt số panel), `TabContent` render rỗng.
|
|
29
|
+
|
|
30
|
+
## Examples
|
|
31
|
+
|
|
32
|
+
### Cơ bản
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
<Tabs defaultActiveIndex={0}>
|
|
36
|
+
<TabList>
|
|
37
|
+
<Tab>Tab 1</Tab>
|
|
38
|
+
<Tab>Tab 2</Tab>
|
|
39
|
+
</TabList>
|
|
40
|
+
<TabContent>
|
|
41
|
+
<TabPanel>Content for Tab 1</TabPanel>
|
|
42
|
+
<TabPanel>Content for Tab 2</TabPanel>
|
|
43
|
+
</TabContent>
|
|
44
|
+
</Tabs>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Style container
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
<TabContent style={{ paddingTop: 16, minHeight: 200 }}>
|
|
51
|
+
<TabPanel>Panel 1</TabPanel>
|
|
52
|
+
<TabPanel>Panel 2</TabPanel>
|
|
53
|
+
</TabContent>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Số panel ít hơn số tab
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
{/* Tab 3 sẽ hiển thị content rỗng */}
|
|
60
|
+
<Tabs>
|
|
61
|
+
<TabList>
|
|
62
|
+
<Tab>Tab 1</Tab>
|
|
63
|
+
<Tab>Tab 2</Tab>
|
|
64
|
+
<Tab>Tab 3</Tab>
|
|
65
|
+
</TabList>
|
|
66
|
+
<TabContent>
|
|
67
|
+
<TabPanel>Panel 1</TabPanel>
|
|
68
|
+
<TabPanel>Panel 2</TabPanel>
|
|
69
|
+
</TabContent>
|
|
70
|
+
</Tabs>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Related Components
|
|
74
|
+
|
|
75
|
+
- [Tabs](../README.md)
|
|
76
|
+
- [TabPanel](../tab-panel/README.md)
|
|
77
|
+
- [Primitive](../../primitive/README.md)
|
|
78
|
+
|
|
79
|
+
## Dependencies
|
|
80
|
+
|
|
81
|
+
- @emotion/react
|
|
82
|
+
|
|
83
|
+
## See Also
|
|
84
|
+
|
|
85
|
+
- [Component Source](./tab-content.tsx)
|
|
86
|
+
- [Storybook Stories](../tabs.stories.tsx)
|
|
87
|
+
- [Main README](../../../README.md)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { TabContentProps } from './types';
|
|
2
|
-
export declare const TabContent: ({ children, ...
|
|
2
|
+
export declare const TabContent: ({ children, ...props }: TabContentProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -2,19 +2,10 @@ import { jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { flattenChildren } from "../../../utils/index.js";
|
|
3
3
|
import { useTabsContext } from "../tabs/helpers.js";
|
|
4
4
|
import { Primitive } from "../../primitive/primitive.js";
|
|
5
|
-
const TabContent = ({ children, ...
|
|
5
|
+
const TabContent = ({ children, ...props }) => {
|
|
6
6
|
const { activeIndex } = useTabsContext();
|
|
7
7
|
const flattenedChildren = flattenChildren(children);
|
|
8
|
-
return /* @__PURE__ */ jsx(
|
|
9
|
-
Primitive,
|
|
10
|
-
{
|
|
11
|
-
sx: {
|
|
12
|
-
display: "contents"
|
|
13
|
-
},
|
|
14
|
-
...rest,
|
|
15
|
-
children: flattenedChildren.find((_, index) => index === activeIndex)
|
|
16
|
-
}
|
|
17
|
-
);
|
|
8
|
+
return /* @__PURE__ */ jsx(Primitive, { ...props, children: flattenedChildren.find((_, index) => index === activeIndex) });
|
|
18
9
|
};
|
|
19
10
|
export {
|
|
20
11
|
TabContent
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# TabIndicator
|
|
2
|
+
|
|
3
|
+
Thanh gạch chân chạy theo tab đang active. Được [TabList](../tab-list/README.md) render tự động và animate bằng `motion/react` khi `activeIndex` đổi.
|
|
4
|
+
|
|
5
|
+
> ℹ️ Đây là component nội bộ — **không** được export từ `@janbox/storefront-ui`. Cấu hình nó qua prop `indicator` của `TabList`.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
<TabList indicator={{ size: 3, sx: { backgroundColor: getColorVar('green.600') } }}>
|
|
11
|
+
<Tab>Tab 1</Tab>
|
|
12
|
+
<Tab>Tab 2</Tab>
|
|
13
|
+
</TabList>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Truyền `indicator={false}` để không render indicator.
|
|
17
|
+
|
|
18
|
+
## Props
|
|
19
|
+
|
|
20
|
+
| Prop | Type | Default | Description |
|
|
21
|
+
| ------ | ------ | ------- | ----------- |
|
|
22
|
+
| `size` | `number` | `2` | Độ dày (px) của indicator. Được set qua CSS variable `--indicator-size` nên responsive theo breakpoint hoạt động đúng. |
|
|
23
|
+
| `sm` | `{ size?: number }` | - | Override `size` từ breakpoint sm (≥768px). |
|
|
24
|
+
| `md` | `{ size?: number }` | - | Override `size` từ breakpoint md (≥1280px). |
|
|
25
|
+
| `lg` | `{ size?: number }` | - | Override `size` từ breakpoint lg (≥1680px). |
|
|
26
|
+
| `sx` | `ToResponsiveProps<StyledCSS>` | `{ backgroundColor: getColorVar('primary.600') }` | Emotion CSS overrides. Apply sau `size` nên `height` trong `sx` sẽ thắng `size`. |
|
|
27
|
+
|
|
28
|
+
## Cách hoạt động
|
|
29
|
+
|
|
30
|
+
- Indicator định vị `position: absolute` bên trong element chứa các `Tab` của `TabList` (element đó luôn có `position: relative`).
|
|
31
|
+
- Tab đang active được tìm bằng `parentElement.children.item(activeIndex)` — tức là **theo thứ tự DOM**, không qua index từ context. Vì vậy đừng chèn element không phải `Tab` vào giữa các `Tab`, nếu không indicator sẽ trỏ sai.
|
|
32
|
+
- Vị trí (`left`, `top`, `width`) được tính từ `getBoundingClientRect()` của tab active so với container, có bù `scrollLeft`/`scrollTop` để đúng khi tab bar scroll ngang.
|
|
33
|
+
- Một `MutationObserver` theo dõi `childList` của `TabList` để cập nhật khi số lượng `Tab` thay đổi (thêm/bớt tab động).
|
|
34
|
+
- Một `ResizeObserver` observe cả tab active **và** container: resize container không đổi kích thước từng tab nhưng có thể làm tab xuống dòng khác khi `overflowBehavior="wrap"`, nên phải theo dõi cả hai để cập nhật vị trí dòng.
|
|
35
|
+
- `top` được tính là mép dưới của tab trừ đi chiều cao indicator, nên indicator luôn nằm sát đáy tab active — kể cả khi tab wrap sang dòng thứ hai.
|
|
36
|
+
|
|
37
|
+
## Examples
|
|
38
|
+
|
|
39
|
+
### Indicator dày hơn, bo góc
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
<TabList indicator={{ size: 4, sx: { backgroundColor: getColorVar('primary.600'), borderRadius: 2 } }}>
|
|
43
|
+
<Tab>Tab 1</Tab>
|
|
44
|
+
<Tab>Tab 2</Tab>
|
|
45
|
+
</TabList>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Responsive size
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
<TabList indicator={{ size: 2, md: { size: 3 } }}>
|
|
52
|
+
<Tab>Tab 1</Tab>
|
|
53
|
+
<Tab>Tab 2</Tab>
|
|
54
|
+
</TabList>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Không dùng indicator (dùng pill style cho tab active)
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
<TabList
|
|
61
|
+
indicator={false}
|
|
62
|
+
tabConfig={{
|
|
63
|
+
sx: { borderRadius: 999 },
|
|
64
|
+
states: {
|
|
65
|
+
active: {
|
|
66
|
+
sx: {
|
|
67
|
+
backgroundColor: getColorVar('primary.600'),
|
|
68
|
+
color: getColorVar('primary.contrast'),
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<Tab>All</Tab>
|
|
75
|
+
<Tab>Active</Tab>
|
|
76
|
+
</TabList>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Related Components
|
|
80
|
+
|
|
81
|
+
- [Tabs](../README.md)
|
|
82
|
+
- [TabList](../tab-list/README.md)
|
|
83
|
+
- [Tab](../tab/README.md)
|
|
84
|
+
|
|
85
|
+
## Dependencies
|
|
86
|
+
|
|
87
|
+
- motion/react
|
|
88
|
+
- @emotion/react
|
|
89
|
+
|
|
90
|
+
## See Also
|
|
91
|
+
|
|
92
|
+
- [Component Source](./tab-indicator.tsx)
|
|
93
|
+
- [Storybook Stories](../tabs.stories.tsx)
|
|
94
|
+
- [Main README](../../../README.md)
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { TabIndicatorProps } from './types';
|
|
2
|
+
export declare const TabIndicator: ({ size, sm, md, lg, ...props }: TabIndicatorProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useRef, useState, useEffect } from "react";
|
|
2
|
+
import { useRef, useState, useEffect, useCallback } from "react";
|
|
3
3
|
import { motion } from "motion/react";
|
|
4
4
|
import { Stylesheet } from "../../stylesheet/stylesheet.js";
|
|
5
|
+
import "lodash-es";
|
|
6
|
+
import "../../../theme/palette.js";
|
|
7
|
+
import { isNumber } from "../../../utils/common.js";
|
|
5
8
|
import { useTabsContext } from "../tabs/helpers.js";
|
|
6
|
-
const
|
|
7
|
-
|
|
9
|
+
const INDICATOR_SIZE_VAR_NAME = "--indicator-size";
|
|
10
|
+
const TabIndicator = ({ size, sm, md, lg, ...props }) => {
|
|
11
|
+
const { activeIndex } = useTabsContext();
|
|
8
12
|
const indicatorEl = useRef(null);
|
|
9
13
|
const [indicatorRect, setIndicatorRect] = useState({
|
|
10
14
|
left: 0,
|
|
15
|
+
top: 0,
|
|
11
16
|
width: 0
|
|
12
17
|
});
|
|
13
18
|
const [activeTabElement, setActiveTabElement] = useState();
|
|
@@ -31,50 +36,76 @@ const TabIndicator = () => {
|
|
|
31
36
|
mutationObserver.disconnect();
|
|
32
37
|
};
|
|
33
38
|
}, [activeIndex]);
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
width: targetRect.width
|
|
47
|
-
});
|
|
39
|
+
const syncIndicatorRect = useCallback(() => {
|
|
40
|
+
const indicator = indicatorEl.current;
|
|
41
|
+
const parentElement = indicator == null ? void 0 : indicator.parentElement;
|
|
42
|
+
if (!indicator || !parentElement || !activeTabElement) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const targetRect = activeTabElement.getBoundingClientRect();
|
|
46
|
+
const parentRect = parentElement.getBoundingClientRect();
|
|
47
|
+
setIndicatorRect({
|
|
48
|
+
left: targetRect.left - parentRect.left + parentElement.scrollLeft,
|
|
49
|
+
top: targetRect.bottom - parentRect.top + parentElement.scrollTop - indicator.offsetHeight,
|
|
50
|
+
width: targetRect.width
|
|
48
51
|
});
|
|
52
|
+
}, [activeTabElement]);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
var _a;
|
|
55
|
+
const resizeObserver = new ResizeObserver(syncIndicatorRect);
|
|
56
|
+
const parentElement = (_a = indicatorEl.current) == null ? void 0 : _a.parentElement;
|
|
49
57
|
if (activeTabElement) {
|
|
50
58
|
resizeObserver.observe(activeTabElement);
|
|
51
59
|
}
|
|
60
|
+
if (parentElement) {
|
|
61
|
+
resizeObserver.observe(parentElement);
|
|
62
|
+
}
|
|
52
63
|
return () => {
|
|
53
64
|
resizeObserver.disconnect();
|
|
54
65
|
};
|
|
55
|
-
}, [activeTabElement]);
|
|
56
|
-
return /* @__PURE__ */ jsx(Stylesheet, { children: ({ css }) =>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
66
|
+
}, [activeTabElement, syncIndicatorRect]);
|
|
67
|
+
return /* @__PURE__ */ jsx(Stylesheet, { children: ({ css }) => {
|
|
68
|
+
var _a, _b, _c;
|
|
69
|
+
return /* @__PURE__ */ jsx(
|
|
70
|
+
motion.div,
|
|
71
|
+
{
|
|
72
|
+
initial: {
|
|
73
|
+
transform: "translate(0, 0)",
|
|
74
|
+
width: 0
|
|
75
|
+
},
|
|
76
|
+
animate: {
|
|
77
|
+
width: indicatorRect.width,
|
|
78
|
+
transform: `translate(${indicatorRect.left}px, ${indicatorRect.top}px)`
|
|
79
|
+
},
|
|
80
|
+
ref: indicatorEl,
|
|
81
|
+
className: css({
|
|
82
|
+
height: `var(${INDICATOR_SIZE_VAR_NAME})`,
|
|
83
|
+
position: "absolute",
|
|
84
|
+
top: 0,
|
|
85
|
+
left: 0,
|
|
86
|
+
...createStyle({ size }),
|
|
87
|
+
...props.sx,
|
|
88
|
+
sm: {
|
|
89
|
+
...createStyle(sm),
|
|
90
|
+
...(_a = props.sx) == null ? void 0 : _a.sm
|
|
91
|
+
},
|
|
92
|
+
md: {
|
|
93
|
+
...createStyle(md),
|
|
94
|
+
...(_b = props.sx) == null ? void 0 : _b.md
|
|
95
|
+
},
|
|
96
|
+
lg: {
|
|
97
|
+
...createStyle(lg),
|
|
98
|
+
...(_c = props.sx) == null ? void 0 : _c.lg
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
} });
|
|
104
|
+
};
|
|
105
|
+
const createStyle = ({ size } = {}) => {
|
|
106
|
+
return isNumber(size) ? {
|
|
107
|
+
[INDICATOR_SIZE_VAR_NAME]: `${size}px`
|
|
108
|
+
} : {};
|
|
78
109
|
};
|
|
79
110
|
export {
|
|
80
111
|
TabIndicator
|
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
import { PropsWithSx } from '../../../types';
|
|
2
|
-
export type
|
|
1
|
+
import { PropsWithSx, ToResponsiveProps } from '../../../types';
|
|
2
|
+
export type TabIndicatorResponsiveProps = {
|
|
3
|
+
size?: number;
|
|
4
|
+
};
|
|
5
|
+
export type TabIndicatorProps = PropsWithSx<ToResponsiveProps<TabIndicatorResponsiveProps>>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# TabList
|
|
2
|
+
|
|
3
|
+
Container ngang chứa các `Tab`, đồng thời là nơi cấu hình style và hành vi overflow của tab bar. `TabList` gán index cho từng `Tab` con qua `TabIndexContext`, phân phối `tabConfig` xuống mọi `Tab` qua `TabListContext`, và render `TabIndicator` ở cuối.
|
|
4
|
+
|
|
5
|
+
Phải được dùng bên trong [Tabs](../README.md).
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { TabList } from '@janbox/storefront-ui';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Props
|
|
14
|
+
|
|
15
|
+
| Prop | Type | Default | Description |
|
|
16
|
+
| ------ | ------ | ------- | ----------- |
|
|
17
|
+
| `overflowBehavior` | `'scroll' \| 'wrap'` | `'scroll'` | `'scroll'` cho phép scroll ngang và ẩn scrollbar; `'wrap'` cho tab xuống dòng (`flex-wrap: wrap`). Apply trên inner element. |
|
|
18
|
+
| `align` | `'start' \| 'center' \| 'end'` | `'start'` | Căn tab bar trong outer element qua `justify-content`. Khi tab bar hẹp hơn container, `'center'` sẽ đưa nó vào giữa. |
|
|
19
|
+
| `tabConfig` | `Pick<TabProps, 'sx' \| 'states'>` | Xem [Default tabConfig](#default-tabconfig) | `sx` áp dụng cho mọi `Tab` con. `states.active.sx` override cho tab đang active (match qua `[aria-selected=true]`). |
|
|
20
|
+
| `indicator` | `false \| TabIndicatorProps` | `{ size: 2, sx: { backgroundColor: getColorVar('primary.600') } }` | Cấu hình [TabIndicator](../tab-indicator/README.md). Truyền `false` để không render indicator. |
|
|
21
|
+
| `sx` | `ToResponsiveProps<StyledCSS>` | - | Responsive Emotion CSS overrides cho **inner** element. Merge sau style mặc định nên override được `display`, `width`, v.v. |
|
|
22
|
+
| `as` | `HTMLElementType` | `'div'` | Tag HTML của inner element. |
|
|
23
|
+
| `children` * | `React.ReactNode` | - | Các `Tab`. Fragment được flatten nên index vẫn đúng khi bọc trong `<>…</>`. |
|
|
24
|
+
| `ref` | `React.Ref<HTMLDivElement>` | - | Forwarded ref tới inner element (element chứa các `Tab`). |
|
|
25
|
+
|
|
26
|
+
Kế thừa toàn bộ `JSX.IntrinsicElements['div']` attributes — tất cả được truyền vào **inner** element, không phải outer.
|
|
27
|
+
|
|
28
|
+
*Required props
|
|
29
|
+
|
|
30
|
+
### Cấu trúc DOM
|
|
31
|
+
|
|
32
|
+
`TabList` render hai element lồng nhau:
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<!-- outer: role=tablist, nhận `align` -->
|
|
36
|
+
<div role="tablist" style="display: flex; justify-content: …">
|
|
37
|
+
<!-- inner: nhận `sx`, `ref`, `as` và mọi div attribute khác -->
|
|
38
|
+
<div style="display: flex; width: fit-content; max-width: 100%; position: relative">
|
|
39
|
+
<button aria-selected="true">…</button>
|
|
40
|
+
<!-- các Tab -->
|
|
41
|
+
<div><!-- TabIndicator --></div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Inner element có `width: fit-content` để tab bar chỉ rộng bằng tổng các tab — nhờ đó `align` trên outer mới có tác dụng. Nó cũng có `position: relative` vì `TabIndicator` định vị absolute theo element này.
|
|
47
|
+
|
|
48
|
+
### Default tabConfig
|
|
49
|
+
|
|
50
|
+
Định nghĩa trong `tab-list/helpers.ts`, dựa trên `getSizeVariantState('md')`:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
{
|
|
54
|
+
sx: {
|
|
55
|
+
columnGap: 8,
|
|
56
|
+
paddingLeft: 12,
|
|
57
|
+
paddingRight: 12,
|
|
58
|
+
color: getColorVar('neutral.600'),
|
|
59
|
+
height: 40,
|
|
60
|
+
borderTopLeftRadius: 8,
|
|
61
|
+
borderTopRightRadius: 8,
|
|
62
|
+
':hover': { backgroundColor: getColorVar('neutral.100') },
|
|
63
|
+
},
|
|
64
|
+
states: {
|
|
65
|
+
active: {
|
|
66
|
+
sx: {
|
|
67
|
+
color: getColorVar('primary.600'),
|
|
68
|
+
':hover': { backgroundColor: getColorVar('primary.100') },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`tabConfig` bạn truyền vào được **deep merge** với default trên, nên chỉ cần khai báo phần muốn đổi. Muốn xoá hoàn toàn một style mặc định, override nó bằng giá trị khác (vd: `backgroundColor: 'transparent'`) chứ không xoá key được.
|
|
76
|
+
|
|
77
|
+
## Cách hoạt động
|
|
78
|
+
|
|
79
|
+
- Mỗi child được bọc trong `TabIndexContext.Provider value={index}` — index dựa trên thứ tự sau khi `flattenChildren`.
|
|
80
|
+
- `TabIndicator` được render như child **cuối cùng** của inner element. Vì `TabIndicator` tìm tab đang active bằng `parentElement.children.item(activeIndex)`, đừng chèn element không phải `Tab` vào giữa các `Tab` — index sẽ lệch.
|
|
81
|
+
- `role="tablist"` được set trên outer element. Nếu bạn truyền `role` qua props, nó vào inner element và **không** override được `role="tablist"` của outer.
|
|
82
|
+
|
|
83
|
+
> ⚠️ Cả hai element đang có `border` debug (`red 1px solid` ở outer, `blue 1px solid` ở inner) hardcode trong `tab-list.tsx`. Đây là style tạm khi phát triển, cần xoá trước khi release.
|
|
84
|
+
|
|
85
|
+
## Examples
|
|
86
|
+
|
|
87
|
+
### Căn giữa và cho tab xuống dòng
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
<Tabs sx={{ width: 300 }}>
|
|
91
|
+
<TabList align="center" overflowBehavior="wrap">
|
|
92
|
+
<Tab>Tab 1</Tab>
|
|
93
|
+
<Tab>Tab 2</Tab>
|
|
94
|
+
<Tab>Tab 3</Tab>
|
|
95
|
+
<Tab>Tab 4</Tab>
|
|
96
|
+
</TabList>
|
|
97
|
+
<TabContent>{/* … */}</TabContent>
|
|
98
|
+
</Tabs>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Custom style cho toàn bộ Tab
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
<TabList
|
|
105
|
+
tabConfig={{
|
|
106
|
+
sx: {
|
|
107
|
+
height: 36,
|
|
108
|
+
borderRadius: 999,
|
|
109
|
+
':hover': { backgroundColor: getColorVar('neutral.100') },
|
|
110
|
+
},
|
|
111
|
+
states: {
|
|
112
|
+
active: {
|
|
113
|
+
sx: {
|
|
114
|
+
color: getColorVar('primary.contrast'),
|
|
115
|
+
backgroundColor: getColorVar('primary.600'),
|
|
116
|
+
':hover': { backgroundColor: getColorVar('primary.700') },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
}}
|
|
121
|
+
indicator={false}
|
|
122
|
+
>
|
|
123
|
+
<Tab>All</Tab>
|
|
124
|
+
<Tab>Active</Tab>
|
|
125
|
+
<Tab>Archived</Tab>
|
|
126
|
+
</TabList>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Indicator dày hơn, màu khác
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
<TabList indicator={{ size: 3, sx: { backgroundColor: getColorVar('green.600'), borderRadius: 2 } }}>
|
|
133
|
+
<Tab>Tab 1</Tab>
|
|
134
|
+
<Tab>Tab 2</Tab>
|
|
135
|
+
</TabList>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Tab bar full-width, mỗi tab chia đều
|
|
139
|
+
|
|
140
|
+
`sx` apply lên inner element nên `width: '100%'` sẽ override `width: fit-content` mặc định:
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
<TabList sx={{ width: '100%' }} tabConfig={{ sx: { flex: 1, flexShrink: 1 } }}>
|
|
144
|
+
<Tab>Left</Tab>
|
|
145
|
+
<Tab>Right</Tab>
|
|
146
|
+
</TabList>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Related Components
|
|
150
|
+
|
|
151
|
+
- [Tabs](../README.md)
|
|
152
|
+
- [Tab](../tab/README.md)
|
|
153
|
+
- [TabIndicator](../tab-indicator/README.md)
|
|
154
|
+
- [Primitive](../../primitive/README.md)
|
|
155
|
+
|
|
156
|
+
## Dependencies
|
|
157
|
+
|
|
158
|
+
- @emotion/react
|
|
159
|
+
|
|
160
|
+
## See Also
|
|
161
|
+
|
|
162
|
+
- [Component Source](./tab-list.tsx)
|
|
163
|
+
- [Storybook Stories](../tabs.stories.tsx)
|
|
164
|
+
- [Main README](../../../README.md)
|
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
import { TabListProps } from './types';
|
|
1
|
+
import { TabConfig, TabListProps } from './types';
|
|
2
2
|
export declare const getTabListProps: (extended: TabListProps) => TabListProps;
|
|
3
|
+
export type TabListContextValue = {
|
|
4
|
+
tabConfig?: TabConfig;
|
|
5
|
+
};
|
|
6
|
+
export declare const TabListContext: import('react').Context<TabListContextValue>;
|
|
7
|
+
export declare const useTabListContext: () => TabListContextValue;
|