@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,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;
|
|
@@ -1,8 +1,51 @@
|
|
|
1
|
-
import { mergeComponentProps } from "../../../utils/index.js";
|
|
2
|
-
|
|
1
|
+
import { mergeComponentProps, getSizeVariantState } from "../../../utils/index.js";
|
|
2
|
+
import "lodash-es";
|
|
3
|
+
import { getColorVar } from "../../../theme/palette.js";
|
|
4
|
+
import { createContext, useContext } from "react";
|
|
5
|
+
const { inputPaddingInline, inputSize } = getSizeVariantState("md");
|
|
6
|
+
const defaultProps = {
|
|
7
|
+
indicator: {
|
|
8
|
+
size: 2,
|
|
9
|
+
sx: {
|
|
10
|
+
backgroundColor: getColorVar("primary.600")
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
overflowBehavior: "scroll",
|
|
14
|
+
align: "start",
|
|
15
|
+
tabConfig: {
|
|
16
|
+
sx: {
|
|
17
|
+
columnGap: 8,
|
|
18
|
+
paddingLeft: inputPaddingInline,
|
|
19
|
+
paddingRight: inputPaddingInline,
|
|
20
|
+
color: getColorVar("neutral.600"),
|
|
21
|
+
height: inputSize,
|
|
22
|
+
borderTopLeftRadius: 8,
|
|
23
|
+
borderTopRightRadius: 8,
|
|
24
|
+
":hover": {
|
|
25
|
+
backgroundColor: getColorVar("neutral.100")
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
states: {
|
|
29
|
+
active: {
|
|
30
|
+
sx: {
|
|
31
|
+
color: getColorVar("primary.600"),
|
|
32
|
+
":hover": {
|
|
33
|
+
backgroundColor: getColorVar("primary.100")
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
3
40
|
const getTabListProps = (extended) => {
|
|
4
41
|
return mergeComponentProps(defaultProps, extended);
|
|
5
42
|
};
|
|
43
|
+
const TabListContext = createContext({});
|
|
44
|
+
const useTabListContext = () => {
|
|
45
|
+
return useContext(TabListContext);
|
|
46
|
+
};
|
|
6
47
|
export {
|
|
7
|
-
|
|
48
|
+
TabListContext,
|
|
49
|
+
getTabListProps,
|
|
50
|
+
useTabListContext
|
|
8
51
|
};
|
|
@@ -1,37 +1,74 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getTabListProps } from "./helpers.js";
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { getTabListProps, TabListContext } from "./helpers.js";
|
|
3
3
|
import { TabIndexContext } from "../tab/helpers.js";
|
|
4
4
|
import { flattenChildren } from "../../../utils/index.js";
|
|
5
5
|
import { scrollbarNoneStyle } from "../../../constants/emotion.js";
|
|
6
|
+
import { useMemo } from "react";
|
|
6
7
|
import { TabIndicator } from "../tab-indicator/tab-indicator.js";
|
|
7
8
|
import { Primitive } from "../../primitive/primitive.js";
|
|
8
9
|
const TabList = ({ ref, children, ..._props }) => {
|
|
9
|
-
const {
|
|
10
|
-
|
|
10
|
+
const { tabConfig, overflowBehavior, indicator, align, containerProps, ...props } = getTabListProps(_props);
|
|
11
|
+
const alignStyle = (() => {
|
|
12
|
+
switch (align) {
|
|
13
|
+
case "end":
|
|
14
|
+
return {
|
|
15
|
+
justifyContent: "flex-end"
|
|
16
|
+
};
|
|
17
|
+
case "start":
|
|
18
|
+
return {
|
|
19
|
+
justifyContent: "flex-start"
|
|
20
|
+
};
|
|
21
|
+
case "center":
|
|
22
|
+
return {
|
|
23
|
+
justifyContent: "center"
|
|
24
|
+
};
|
|
25
|
+
default:
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
})();
|
|
29
|
+
const contextValue = useMemo(
|
|
30
|
+
() => ({
|
|
31
|
+
tabConfig
|
|
32
|
+
}),
|
|
33
|
+
[tabConfig]
|
|
34
|
+
);
|
|
35
|
+
return /* @__PURE__ */ jsx(TabListContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
|
|
11
36
|
Primitive,
|
|
12
37
|
{
|
|
38
|
+
...containerProps,
|
|
13
39
|
sx: {
|
|
14
40
|
display: "flex",
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
alignItems: "center",
|
|
18
|
-
gap: 16,
|
|
19
|
-
position: "relative",
|
|
20
|
-
overflow: "auto",
|
|
21
|
-
"& > *": {
|
|
22
|
-
flexShrink: 0
|
|
23
|
-
},
|
|
24
|
-
...scrollbarNoneStyle,
|
|
25
|
-
...sx
|
|
41
|
+
...alignStyle,
|
|
42
|
+
...containerProps == null ? void 0 : containerProps.sx
|
|
26
43
|
},
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
children: /* @__PURE__ */ jsxs(
|
|
45
|
+
Primitive,
|
|
46
|
+
{
|
|
47
|
+
...props,
|
|
48
|
+
role: "tablist",
|
|
49
|
+
sx: {
|
|
50
|
+
display: "flex",
|
|
51
|
+
width: "fit-content",
|
|
52
|
+
maxWidth: "100%",
|
|
53
|
+
alignItems: "center",
|
|
54
|
+
position: "relative",
|
|
55
|
+
...overflowBehavior === "scroll" ? {
|
|
56
|
+
overflow: "auto",
|
|
57
|
+
...scrollbarNoneStyle
|
|
58
|
+
} : {
|
|
59
|
+
flexWrap: "wrap"
|
|
60
|
+
},
|
|
61
|
+
...props.sx
|
|
62
|
+
},
|
|
63
|
+
ref,
|
|
64
|
+
children: [
|
|
65
|
+
flattenChildren(children).map((child, index) => /* @__PURE__ */ jsx(TabIndexContext.Provider, { value: index, children: child }, index)),
|
|
66
|
+
indicator && /* @__PURE__ */ jsx(TabIndicator, { ...indicator })
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
)
|
|
33
70
|
}
|
|
34
|
-
);
|
|
71
|
+
) });
|
|
35
72
|
};
|
|
36
73
|
export {
|
|
37
74
|
TabList
|
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
import { PrimitiveProps } from '../../primitive';
|
|
2
|
+
import { ShallowMerge } from '../../../types';
|
|
2
3
|
import { JSX } from 'react';
|
|
3
|
-
|
|
4
|
+
import { TabIndicatorProps } from '../tab-indicator/types';
|
|
5
|
+
import { TabProps } from '../tab/types';
|
|
6
|
+
export type TabConfig = Pick<TabProps, 'sx' | 'states'>;
|
|
7
|
+
export type TabListProps = PrimitiveProps<ShallowMerge<JSX.IntrinsicElements['div'], {
|
|
8
|
+
overflowBehavior?: 'scroll' | 'wrap';
|
|
9
|
+
indicator?: false | TabIndicatorProps;
|
|
10
|
+
align?: 'start' | 'center' | 'end';
|
|
11
|
+
tabConfig?: TabConfig;
|
|
12
|
+
containerProps?: PrimitiveProps<JSX.IntrinsicElements['div'], 'div'>;
|
|
13
|
+
}>, 'div'>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# TabPanel
|
|
2
|
+
|
|
3
|
+
Wrapper cho nội dung của một tab. Là child trực tiếp của [TabContent](../tab-content/README.md), và chỉ được render khi index của nó khớp `activeIndex`.
|
|
4
|
+
|
|
5
|
+
`TabPanel` không chứa logic — nó chỉ render một `div` với các props bạn truyền vào. Việc chọn panel nào hiển thị do `TabContent` quyết định.
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { TabPanel } from '@janbox/storefront-ui';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Props
|
|
14
|
+
|
|
15
|
+
| Prop | Type | Default | Description |
|
|
16
|
+
| ------ | ------ | ------- | ----------- |
|
|
17
|
+
| `children` | `React.ReactNode` | - | Nội dung của panel. |
|
|
18
|
+
| `className` | `string` | - | Class cho `div`. |
|
|
19
|
+
| `ref` | `React.Ref<HTMLDivElement>` | - | Forwarded ref tới `div`. |
|
|
20
|
+
|
|
21
|
+
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 panel.
|
|
22
|
+
|
|
23
|
+
## Examples
|
|
24
|
+
|
|
25
|
+
### Cơ bản
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
<TabContent>
|
|
29
|
+
<TabPanel>Content for Tab 1</TabPanel>
|
|
30
|
+
<TabPanel>Content for Tab 2</TabPanel>
|
|
31
|
+
</TabContent>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Panel với nội dung phức tạp
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
<TabContent>
|
|
38
|
+
<TabPanel style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
39
|
+
<h3>Overview</h3>
|
|
40
|
+
<p>Mô tả sản phẩm…</p>
|
|
41
|
+
</TabPanel>
|
|
42
|
+
<TabPanel>
|
|
43
|
+
<ul>
|
|
44
|
+
<li>Review 1</li>
|
|
45
|
+
<li>Review 2</li>
|
|
46
|
+
</ul>
|
|
47
|
+
</TabPanel>
|
|
48
|
+
</TabContent>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Panel kèm ARIA attributes
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
<Tabs>
|
|
55
|
+
<TabList>
|
|
56
|
+
<Tab role="tab" aria-controls="panel-overview">Overview</Tab>
|
|
57
|
+
<Tab role="tab" aria-controls="panel-reviews">Reviews</Tab>
|
|
58
|
+
</TabList>
|
|
59
|
+
<TabContent>
|
|
60
|
+
<TabPanel id="panel-overview" role="tabpanel">Overview content</TabPanel>
|
|
61
|
+
<TabPanel id="panel-reviews" role="tabpanel">Reviews content</TabPanel>
|
|
62
|
+
</TabContent>
|
|
63
|
+
</Tabs>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> ⚠️ Panel không active bị unmount, không phải ẩn bằng CSS. Mọi state nội bộ (form input, scroll position, data đã fetch) sẽ mất khi đổi tab. Nếu cần giữ, lift state lên component cha của `Tabs`.
|
|
67
|
+
|
|
68
|
+
## Accessibility
|
|
69
|
+
|
|
70
|
+
`TabPanel` không tự set `role="tabpanel"` hay `aria-labelledby`. Nếu cần đầy đủ ARIA tabs pattern, truyền các attribute này thủ công và ghép với `id`/`aria-controls` trên [Tab](../tab/README.md) tương ứng.
|
|
71
|
+
|
|
72
|
+
## Related Components
|
|
73
|
+
|
|
74
|
+
- [Tabs](../README.md)
|
|
75
|
+
- [TabContent](../tab-content/README.md)
|
|
76
|
+
- [Primitive](../../primitive/README.md)
|
|
77
|
+
|
|
78
|
+
## Dependencies
|
|
79
|
+
|
|
80
|
+
- @emotion/react
|
|
81
|
+
|
|
82
|
+
## See Also
|
|
83
|
+
|
|
84
|
+
- [Component Source](./tab-panel.tsx)
|
|
85
|
+
- [Storybook Stories](../tabs.stories.tsx)
|
|
86
|
+
- [Main README](../../../README.md)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { TabPanelProps } from './types';
|
|
2
|
-
export declare const getTabPanelProps: (extended: TabPanelProps) =>
|
|
2
|
+
export declare const getTabPanelProps: (extended: TabPanelProps) => import('react').DetailedHTMLProps<import('react').HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import { useTabsContext } from "../tabs/helpers.js";
|
|
2
|
+
import { getTabPanelProps } from "./helpers.js";
|
|
4
3
|
import { Primitive } from "../../primitive/primitive.js";
|
|
5
4
|
const TabPanel = ({ children, ref, ..._props }) => {
|
|
6
|
-
const {
|
|
7
|
-
|
|
8
|
-
return /* @__PURE__ */ jsx(Primitive, { ...props, ref, children });
|
|
5
|
+
const { ...props } = getTabPanelProps(_props);
|
|
6
|
+
return /* @__PURE__ */ jsx(Primitive, { ...props, role: "tabpanel", ref, children });
|
|
9
7
|
};
|
|
10
8
|
export {
|
|
11
9
|
TabPanel
|
|
@@ -2,11 +2,7 @@ import { TabsProps } from './types';
|
|
|
2
2
|
export declare const getTabsProps: (extended: TabsProps) => TabsProps;
|
|
3
3
|
export type TabsContextValue = {
|
|
4
4
|
activeIndex: number;
|
|
5
|
-
focusTab: (
|
|
6
|
-
tabProps?: TabsProps['tabProps'];
|
|
7
|
-
tabIndicatorProps?: TabsProps['tabIndicatorProps'];
|
|
8
|
-
tabPanelProps?: TabsProps['tabPanelProps'];
|
|
9
|
-
preventActiveTabChange: TabsProps['preventActiveTabChange'];
|
|
5
|
+
focusTab: (nextIndex: number) => void;
|
|
10
6
|
};
|
|
11
7
|
export declare const TabsContext: import('react').Context<TabsContextValue>;
|
|
12
8
|
export declare const useTabsContext: () => TabsContextValue;
|
|
@@ -1,25 +1,6 @@
|
|
|
1
1
|
import { createContext, useContext } from "react";
|
|
2
|
-
import "lodash-es";
|
|
3
|
-
import { getColorVar } from "../../../theme/palette.js";
|
|
4
2
|
import { mergeComponentProps } from "../../../utils/index.js";
|
|
5
|
-
const defaultProps = {
|
|
6
|
-
tabProps: {
|
|
7
|
-
color: "neutral",
|
|
8
|
-
variant: "text",
|
|
9
|
-
states: {
|
|
10
|
-
active: {
|
|
11
|
-
color: "primary",
|
|
12
|
-
variant: "text"
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
tabIndicatorProps: {
|
|
17
|
-
sx: {
|
|
18
|
-
height: 2,
|
|
19
|
-
backgroundColor: getColorVar("primary.600")
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
};
|
|
3
|
+
const defaultProps = {};
|
|
23
4
|
const getTabsProps = (extended) => {
|
|
24
5
|
return mergeComponentProps(defaultProps, extended);
|
|
25
6
|
};
|
|
@@ -1,39 +1,35 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useRef, useMemo } from "react";
|
|
2
|
+
import { useRef, useEffect, useCallback, useMemo } from "react";
|
|
3
3
|
import { getTabsProps, TabsContext } from "./helpers.js";
|
|
4
4
|
import { useControllableState } from "../../../hooks/use-controllable-state.js";
|
|
5
5
|
import { Primitive } from "../../primitive/primitive.js";
|
|
6
6
|
const Tabs = ({ ref, children, ..._props }) => {
|
|
7
|
-
const {
|
|
8
|
-
defaultActiveIndex,
|
|
9
|
-
activeIndex,
|
|
10
|
-
onTabChange,
|
|
11
|
-
tabProps,
|
|
12
|
-
tabIndicatorProps,
|
|
13
|
-
tabPanelProps,
|
|
14
|
-
preventActiveTabChange = true,
|
|
15
|
-
...rest
|
|
16
|
-
} = getTabsProps(_props);
|
|
7
|
+
const { defaultActiveIndex, activeIndex, onTabChange, preventActiveTabChange = true, ...rest } = getTabsProps(_props);
|
|
17
8
|
const [activeTabIndex, setActiveTabIndex] = useControllableState({
|
|
18
9
|
defaultValue: defaultActiveIndex ?? 0,
|
|
19
10
|
value: activeIndex
|
|
20
11
|
});
|
|
21
12
|
const onTabChangeRef = useRef(onTabChange);
|
|
22
|
-
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
onTabChangeRef.current = onTabChange;
|
|
15
|
+
}, [onTabChange]);
|
|
16
|
+
const focusTab = useCallback(
|
|
17
|
+
(nextIndex) => {
|
|
18
|
+
var _a;
|
|
19
|
+
const isChanged = preventActiveTabChange ? nextIndex !== activeTabIndex : true;
|
|
20
|
+
if (isChanged) {
|
|
21
|
+
setActiveTabIndex(nextIndex);
|
|
22
|
+
(_a = onTabChangeRef.current) == null ? void 0 : _a.call(onTabChangeRef, nextIndex);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
[activeTabIndex, preventActiveTabChange, setActiveTabIndex]
|
|
26
|
+
);
|
|
23
27
|
const contextValue = useMemo(() => {
|
|
24
28
|
return {
|
|
25
29
|
activeIndex: activeTabIndex,
|
|
26
|
-
focusTab
|
|
27
|
-
var _a;
|
|
28
|
-
setActiveTabIndex(tabIndex);
|
|
29
|
-
(_a = onTabChangeRef.current) == null ? void 0 : _a.call(onTabChangeRef, tabIndex);
|
|
30
|
-
},
|
|
31
|
-
tabProps,
|
|
32
|
-
tabIndicatorProps,
|
|
33
|
-
tabPanelProps,
|
|
34
|
-
preventActiveTabChange
|
|
30
|
+
focusTab
|
|
35
31
|
};
|
|
36
|
-
}, [activeTabIndex,
|
|
32
|
+
}, [activeTabIndex, focusTab]);
|
|
37
33
|
return /* @__PURE__ */ jsx(TabsContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(Primitive, { ...rest, ref, children }) });
|
|
38
34
|
};
|
|
39
35
|
export {
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import { JSX } from 'react';
|
|
2
|
-
import { TabProps } from '../tab';
|
|
3
|
-
import { TabIndicatorProps } from '../tab-indicator';
|
|
4
|
-
import { TabPanelProps } from '../tab-panel';
|
|
5
2
|
import { PropsWithSx, ShallowMerge } from '../../../types';
|
|
6
3
|
export type TabsExtendedProps = PropsWithSx<{
|
|
7
4
|
activeIndex?: number;
|
|
8
5
|
defaultActiveIndex?: number;
|
|
9
|
-
onTabChange?: (
|
|
10
|
-
tabProps?: Partial<TabProps>;
|
|
11
|
-
tabIndicatorProps?: TabIndicatorProps;
|
|
12
|
-
tabPanelProps?: Partial<TabPanelProps>;
|
|
6
|
+
onTabChange?: (nextIndex: number) => void;
|
|
13
7
|
preventActiveTabChange?: boolean;
|
|
14
8
|
}>;
|
|
15
9
|
export type TabsProps = ShallowMerge<JSX.IntrinsicElements['div'], TabsExtendedProps>;
|
|
@@ -162,10 +162,10 @@ const Textarea = ({ ref, ..._props }) => {
|
|
|
162
162
|
);
|
|
163
163
|
};
|
|
164
164
|
const createStyle = ({ size } = {}) => {
|
|
165
|
-
const {
|
|
165
|
+
const { inputPaddingInline, textVariant } = getSizeVariantState(size);
|
|
166
166
|
return {
|
|
167
|
-
paddingLeft:
|
|
168
|
-
paddingRight:
|
|
167
|
+
paddingLeft: inputPaddingInline,
|
|
168
|
+
paddingRight: inputPaddingInline,
|
|
169
169
|
fontSize: textVariant ? getTypographyVar(textVariant).fontSize : void 0,
|
|
170
170
|
lineHeight: textVariant ? getTypographyVar(textVariant).lineHeight : void 0
|
|
171
171
|
};
|
package/dist/utils/common.d.ts
CHANGED
|
@@ -16,3 +16,4 @@ export declare const parseNumber: (text?: string | number, type?: "int" | "float
|
|
|
16
16
|
export declare function omitEmpty<T extends object>(obj?: T): T;
|
|
17
17
|
export declare const getEnumValues: <T extends Dict<string | number>>(enumObject: T) => Array<T[keyof T]>;
|
|
18
18
|
export declare function parseSvgString(source: string, uniqueId: string): ReactElement<import('react').SVGProps<SVGSVGElement>, "svg"> | null;
|
|
19
|
+
export declare const isNumber: (value: any) => value is number;
|
package/dist/utils/common.js
CHANGED
|
@@ -108,11 +108,13 @@ function parseSvgString(source, uniqueId) {
|
|
|
108
108
|
const validType = "svg";
|
|
109
109
|
return isValidElement(parsed) && parsed.type === validType ? parsed : null;
|
|
110
110
|
}
|
|
111
|
+
const isNumber = (value) => typeof value === "number";
|
|
111
112
|
export {
|
|
112
113
|
ensureArray,
|
|
113
114
|
ensureBoolean,
|
|
114
115
|
getDecimalDigits,
|
|
115
116
|
getEnumValues,
|
|
117
|
+
isNumber,
|
|
116
118
|
matchSearch,
|
|
117
119
|
normalizeSearchText,
|
|
118
120
|
omitEmpty,
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export declare const flattenChildren: (children: React.ReactNode) => ReactElemen
|
|
|
31
31
|
export type SizeVariantState = Partial<{
|
|
32
32
|
iconSize: number;
|
|
33
33
|
inputSize: number;
|
|
34
|
-
|
|
34
|
+
inputPaddingInline: number;
|
|
35
35
|
textVariant: TypographySizeVariant;
|
|
36
36
|
}>;
|
|
37
37
|
export declare const getSizeVariantState: (size?: SizeVariant) => SizeVariantState;
|
package/dist/utils/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Children, isValidElement, Fragment } from "react";
|
|
|
2
2
|
import { isString, isNumber, isUndefined, mergeWith, get } from "lodash-es";
|
|
3
3
|
import { getSmallerScreens } from "../theme/screens.js";
|
|
4
4
|
import { getColorVar } from "../theme/palette.js";
|
|
5
|
-
import { ensureArray, ensureBoolean, getDecimalDigits, getEnumValues, matchSearch, normalizeSearchText, omitEmpty, parseNumber, parseSvgString } from "./common.js";
|
|
5
|
+
import { ensureArray, ensureBoolean, getDecimalDigits, getEnumValues, isNumber as isNumber2, matchSearch, normalizeSearchText, omitEmpty, parseNumber, parseSvgString } from "./common.js";
|
|
6
6
|
import { ArrayParamParser, BooleanParamParser, DateParamParser, EnumParamParser, NumberParamParser, StringParamParser, simplifyQueryParams } from "./query-params.js";
|
|
7
7
|
import { createUnderlineEffect } from "./stylesheet.js";
|
|
8
8
|
const getColorShadesByVariant = (variant) => {
|
|
@@ -127,28 +127,28 @@ const getSizeVariantState = (size) => {
|
|
|
127
127
|
return {
|
|
128
128
|
iconSize: 20,
|
|
129
129
|
inputSize: 24,
|
|
130
|
-
|
|
130
|
+
inputPaddingInline: 8,
|
|
131
131
|
textVariant: "xs"
|
|
132
132
|
};
|
|
133
133
|
case "sm":
|
|
134
134
|
return {
|
|
135
135
|
iconSize: 20,
|
|
136
136
|
inputSize: 32,
|
|
137
|
-
|
|
137
|
+
inputPaddingInline: 12,
|
|
138
138
|
textVariant: "sm"
|
|
139
139
|
};
|
|
140
140
|
case "md":
|
|
141
141
|
return {
|
|
142
142
|
iconSize: 24,
|
|
143
143
|
inputSize: 40,
|
|
144
|
-
|
|
144
|
+
inputPaddingInline: 12,
|
|
145
145
|
textVariant: "sm"
|
|
146
146
|
};
|
|
147
147
|
case "lg":
|
|
148
148
|
return {
|
|
149
149
|
iconSize: 24,
|
|
150
150
|
inputSize: 48,
|
|
151
|
-
|
|
151
|
+
inputPaddingInline: 16,
|
|
152
152
|
textVariant: "base"
|
|
153
153
|
};
|
|
154
154
|
default:
|
|
@@ -253,6 +253,7 @@ export {
|
|
|
253
253
|
getLanguageDisplayName,
|
|
254
254
|
getResponsiveProp,
|
|
255
255
|
getSizeVariantState,
|
|
256
|
+
isNumber2 as isNumber,
|
|
256
257
|
matchSearch,
|
|
257
258
|
mergeComponentProps,
|
|
258
259
|
normalizeSearchText,
|