@janbox/storefront-ui 4.0.17 → 5.0.1
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/components/scroll-view.js +1 -1
- package/dist/index.js +5 -2
- package/dist/lib/accordion/accordion-content/accordion-content.js +1 -1
- 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/count-up/count-up.js +1 -1
- package/dist/lib/date-picker/date-picker.js +3 -3
- package/dist/lib/drawer/drawer/drawer.js +1 -1
- package/dist/lib/floating/floating-content/floating-content.js +1 -1
- 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/lightbox/lightbox.js +1 -1
- package/dist/lib/loading/image-skeleton/image-skeleton.js +1 -1
- package/dist/lib/notifications/notifications.js +1 -1
- package/dist/lib/star-rating/star-rating.js +1 -1
- package/dist/lib/swiper/swiper-dots-paging/swiper-dots-paging.js +1 -1
- 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 +90 -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 +3 -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 +72 -41
- 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 +1 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Tab
|
|
2
|
+
|
|
3
|
+
Nút của một tab trong [TabList](../tab-list/README.md). Render `<button type="button">` với `aria-selected` phản ánh trạng thái active, cùng slot cho icon/nội dung phụ ở hai đầu.
|
|
4
|
+
|
|
5
|
+
`Tab` phải nằm bên trong `TabList` (để lấy index qua `TabIndexContext`) và [Tabs](../README.md) (để lấy `activeIndex` + `focusTab` qua `TabsContext`).
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { Tab } from '@janbox/storefront-ui';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Props
|
|
14
|
+
|
|
15
|
+
| Prop | Type | Default | Description |
|
|
16
|
+
| ------ | ------ | ------- | ----------- |
|
|
17
|
+
| `startIcon` | `IconProps` | - | Icon render trước `children`. Tự động thêm `flexShrink: 0`. |
|
|
18
|
+
| `endIcon` | `IconProps` | - | Icon render sau `children`. Tự động thêm `flexShrink: 0`. |
|
|
19
|
+
| `prefix` | `React.ReactNode` | - | Node render ở đầu, **trước** `startIcon`. Dùng cho badge, dot, counter… |
|
|
20
|
+
| `suffix` | `React.ReactNode` | - | Node render ở cuối, **sau** `endIcon`. |
|
|
21
|
+
| `states` | `{ active?: PropsWithSx }` | Xem [TabList](../tab-list/README.md#default-tabconfig) | `states.active.sx` được apply qua selector `&[aria-selected=true]`. |
|
|
22
|
+
| `onClick` | `React.MouseEventHandler<HTMLButtonElement>` | - | Được gọi **sau** khi tab đã chuyển. Xem [Click handling](#click-handling). |
|
|
23
|
+
| `sx` | `ToResponsiveProps<StyledCSS>` | `{ flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }` | Responsive Emotion CSS overrides. |
|
|
24
|
+
| `children` | `React.ReactNode` | - | Nội dung label của tab. |
|
|
25
|
+
| `ref` | `React.Ref<HTMLButtonElement>` | - | Forwarded ref tới `<button>`. |
|
|
26
|
+
|
|
27
|
+
Kế thừa toàn bộ `JSX.IntrinsicElements['button']` attributes.
|
|
28
|
+
|
|
29
|
+
### Click handling
|
|
30
|
+
|
|
31
|
+
`Tab` tự gắn `onClick` gọi `focusTab(tabIndex)` từ `TabsContext`, rồi mới gọi `onClick` của bạn. Nghĩa là:
|
|
32
|
+
|
|
33
|
+
- `onClick` của bạn luôn chạy, và chạy **sau** khi state đã cập nhật.
|
|
34
|
+
- `e.preventDefault()` hay `e.stopPropagation()` trong `onClick` **không** ngăn được tab chuyển, vì việc chuyển đã xảy ra trước đó.
|
|
35
|
+
- Nếu `preventActiveTabChange` (trên `Tabs`) là `true` — mặc định — click vào tab đang active không re-fire `onTabChange`, nhưng `onClick` của bạn vẫn được gọi.
|
|
36
|
+
|
|
37
|
+
### Thứ tự merge props
|
|
38
|
+
|
|
39
|
+
`Tab` merge theo thứ tự tăng dần độ ưu tiên:
|
|
40
|
+
|
|
41
|
+
1. `defaultProps` của `Tab` (layout flex cơ bản)
|
|
42
|
+
2. `tabConfig` từ `TabList` (qua `TabListContext`)
|
|
43
|
+
3. Props truyền trực tiếp vào `Tab`
|
|
44
|
+
|
|
45
|
+
Nên prop trên từng `Tab` luôn thắng `tabConfig`, và cả hai đều là deep merge — chỉ khai báo phần muốn đổi.
|
|
46
|
+
|
|
47
|
+
### Attributes được render
|
|
48
|
+
|
|
49
|
+
| Attribute | Giá trị |
|
|
50
|
+
| ------ | ------ |
|
|
51
|
+
| `aria-selected` | `true` khi index của tab bằng `activeIndex` |
|
|
52
|
+
| `type` | Luôn `"button"` để không submit form khi đặt trong `<form>` |
|
|
53
|
+
|
|
54
|
+
## Examples
|
|
55
|
+
|
|
56
|
+
### Tab cơ bản
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
<TabList>
|
|
60
|
+
<Tab>Overview</Tab>
|
|
61
|
+
<Tab>Details</Tab>
|
|
62
|
+
</TabList>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Tab với icon
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import HomeIcon from '@/assets/svg/home.svg?raw';
|
|
69
|
+
import ChevronIcon from '@/assets/svg/chevron-down.svg?raw';
|
|
70
|
+
|
|
71
|
+
<TabList>
|
|
72
|
+
<Tab startIcon={{ source: HomeIcon, size: 20 }}>Home</Tab>
|
|
73
|
+
<Tab endIcon={{ source: ChevronIcon, size: 20 }}>More</Tab>
|
|
74
|
+
</TabList>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Tab với counter ở suffix
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<TabList>
|
|
81
|
+
<Tab suffix={<span>(3)</span>}>Notifications</Tab>
|
|
82
|
+
<Tab>Settings</Tab>
|
|
83
|
+
</TabList>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Override style cho một Tab riêng lẻ
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
<TabList>
|
|
90
|
+
<Tab>Normal</Tab>
|
|
91
|
+
<Tab
|
|
92
|
+
sx={{ color: getColorVar('red.600') }}
|
|
93
|
+
states={{ active: { sx: { color: getColorVar('red.700') } } }}
|
|
94
|
+
>
|
|
95
|
+
Danger
|
|
96
|
+
</Tab>
|
|
97
|
+
</TabList>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Disabled tab
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
<TabList>
|
|
104
|
+
<Tab>Available</Tab>
|
|
105
|
+
<Tab disabled sx={{ opacity: 0.5, cursor: 'not-allowed' }}>
|
|
106
|
+
Coming soon
|
|
107
|
+
</Tab>
|
|
108
|
+
</TabList>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
> ⚠️ `disabled` ngăn tab chuyển vì `<button disabled>` không phát click event — `Tab` không kiểm tra `disabled` trước khi gọi `focusTab`. Style cho trạng thái disabled phải tự khai báo, không có style mặc định.
|
|
112
|
+
|
|
113
|
+
## Accessibility
|
|
114
|
+
|
|
115
|
+
- `aria-selected` được set tự động theo trạng thái active.
|
|
116
|
+
- `Tab` render `<button>` nên đã focusable và activate được bằng `Enter`/`Space`.
|
|
117
|
+
- `TabList` set `role="tablist"` trên wrapper, nhưng `Tab` **không** tự set `role="tab"` hay `aria-controls`, và không xử lý điều hướng bằng arrow key. Nếu cần đầy đủ ARIA tabs pattern, truyền `role="tab"` và `aria-controls` thủ công cùng `id` tương ứng trên `TabPanel`.
|
|
118
|
+
|
|
119
|
+
## Related Components
|
|
120
|
+
|
|
121
|
+
- [Tabs](../README.md)
|
|
122
|
+
- [TabList](../tab-list/README.md)
|
|
123
|
+
- [Icon](../../icon/README.md)
|
|
124
|
+
- [Primitive](../../primitive/README.md)
|
|
125
|
+
|
|
126
|
+
## Dependencies
|
|
127
|
+
|
|
128
|
+
- @emotion/react
|
|
129
|
+
|
|
130
|
+
## See Also
|
|
131
|
+
|
|
132
|
+
- [Component Source](./tab.tsx)
|
|
133
|
+
- [Storybook Stories](../tabs.stories.tsx)
|
|
134
|
+
- [Main README](../../../README.md)
|
|
@@ -1,9 +1,26 @@
|
|
|
1
|
+
import { mergeComponentProps } from "../../../utils/index.js";
|
|
1
2
|
import { createContext, useContext } from "react";
|
|
3
|
+
import { useTabListContext } from "../tab-list/helpers.js";
|
|
2
4
|
const TabIndexContext = createContext(-1);
|
|
3
5
|
const useTabIndexContext = () => {
|
|
4
6
|
return useContext(TabIndexContext);
|
|
5
7
|
};
|
|
8
|
+
const defaultProps = {
|
|
9
|
+
sx: {
|
|
10
|
+
flexShrink: 0,
|
|
11
|
+
display: "flex",
|
|
12
|
+
alignItems: "center",
|
|
13
|
+
justifyContent: "center"
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const useGetTabProps = () => {
|
|
17
|
+
const { tabConfig = {} } = useTabListContext();
|
|
18
|
+
return (extended) => {
|
|
19
|
+
return mergeComponentProps(defaultProps, tabConfig, extended);
|
|
20
|
+
};
|
|
21
|
+
};
|
|
6
22
|
export {
|
|
7
23
|
TabIndexContext,
|
|
24
|
+
useGetTabProps,
|
|
8
25
|
useTabIndexContext
|
|
9
26
|
};
|
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,90 @@
|
|
|
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
|
+
| `as` | `HTMLElementType` | `'div'` | Tag HTML của container. |
|
|
18
|
+
| `sx` | `ToResponsiveProps<StyledCSS>` | - | Style responsive cho container. |
|
|
19
|
+
| `ref` | `React.Ref<HTMLElement>` | - | Trỏ tới DOM node của container. |
|
|
20
|
+
|
|
21
|
+
Kiểu prop là `PrimitiveProps<JSX.IntrinsicElements['div'], 'div'>` — kế thừa toàn bộ div attributes cùng `as` và `sx` từ [Primitive](../../primitive/README.md).
|
|
22
|
+
|
|
23
|
+
> React 19 coi `ref` là prop thường, nên `ref` được `Primitive` spread thẳng vào DOM node của container.
|
|
24
|
+
|
|
25
|
+
*Required props
|
|
26
|
+
|
|
27
|
+
## Cách hoạt động
|
|
28
|
+
|
|
29
|
+
- Children được `flattenChildren` trước khi chọn, nên bọc panel trong Fragment vẫn giữ index đúng.
|
|
30
|
+
- 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.
|
|
31
|
+
- Nếu `activeIndex` không khớp child nào (vd: `defaultActiveIndex` vượt số panel), `TabContent` render rỗng.
|
|
32
|
+
|
|
33
|
+
## Examples
|
|
34
|
+
|
|
35
|
+
### Cơ bản
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
<Tabs defaultActiveIndex={0}>
|
|
39
|
+
<TabList>
|
|
40
|
+
<Tab>Tab 1</Tab>
|
|
41
|
+
<Tab>Tab 2</Tab>
|
|
42
|
+
</TabList>
|
|
43
|
+
<TabContent>
|
|
44
|
+
<TabPanel>Content for Tab 1</TabPanel>
|
|
45
|
+
<TabPanel>Content for Tab 2</TabPanel>
|
|
46
|
+
</TabContent>
|
|
47
|
+
</Tabs>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Style container
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<TabContent style={{ paddingTop: 16, minHeight: 200 }}>
|
|
54
|
+
<TabPanel>Panel 1</TabPanel>
|
|
55
|
+
<TabPanel>Panel 2</TabPanel>
|
|
56
|
+
</TabContent>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Số panel ít hơn số tab
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
{/* Tab 3 sẽ hiển thị content rỗng */}
|
|
63
|
+
<Tabs>
|
|
64
|
+
<TabList>
|
|
65
|
+
<Tab>Tab 1</Tab>
|
|
66
|
+
<Tab>Tab 2</Tab>
|
|
67
|
+
<Tab>Tab 3</Tab>
|
|
68
|
+
</TabList>
|
|
69
|
+
<TabContent>
|
|
70
|
+
<TabPanel>Panel 1</TabPanel>
|
|
71
|
+
<TabPanel>Panel 2</TabPanel>
|
|
72
|
+
</TabContent>
|
|
73
|
+
</Tabs>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Related Components
|
|
77
|
+
|
|
78
|
+
- [Tabs](../README.md)
|
|
79
|
+
- [TabPanel](../tab-panel/README.md)
|
|
80
|
+
- [Primitive](../../primitive/README.md)
|
|
81
|
+
|
|
82
|
+
## Dependencies
|
|
83
|
+
|
|
84
|
+
- @emotion/react
|
|
85
|
+
|
|
86
|
+
## See Also
|
|
87
|
+
|
|
88
|
+
- [Component Source](./tab-content.tsx)
|
|
89
|
+
- [Storybook Stories](../tabs.stories.tsx)
|
|
90
|
+
- [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
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}>;
|
|
1
|
+
import { JSX } from 'react';
|
|
2
|
+
import { PrimitiveProps } from '../../primitive';
|
|
3
|
+
export type TabContentProps = PrimitiveProps<JSX.IntrinsicElements['div'], 'div'>;
|
|
@@ -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
|
-
import
|
|
4
|
+
import "lodash-es";
|
|
5
|
+
import "../../../theme/palette.js";
|
|
6
|
+
import { isNumber } from "../../../utils/common.js";
|
|
5
7
|
import { useTabsContext } from "../tabs/helpers.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
import { Stylesheet } from "../../stylesheet/stylesheet.js";
|
|
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>>;
|