@openway/ui 1.0.0 → 1.0.2
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 +203 -181
- package/dist/chunk-X4LIYOS5.js +3 -0
- package/dist/chunk-X4LIYOS5.js.map +1 -0
- package/dist/index.cjs +5 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2740 -1473
- package/dist/index.d.ts +2740 -1473
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/query.cjs +3 -0
- package/dist/query.cjs.map +1 -0
- package/dist/query.d.cts +431 -0
- package/dist/query.d.ts +431 -0
- package/dist/query.js +3 -0
- package/dist/query.js.map +1 -0
- package/dist/useInfiniteScroll-D9AW6cQV.d.cts +598 -0
- package/dist/useInfiniteScroll-D9AW6cQV.d.ts +598 -0
- package/docs/AGENTS.md +144 -0
- package/docs/README.md +150 -0
- package/docs/SKILL.md +54 -0
- package/docs/components/alert.md +246 -0
- package/docs/components/badge.md +238 -0
- package/docs/components/button.md +262 -0
- package/docs/components/carousel.md +354 -0
- package/docs/components/checkbox.md +252 -0
- package/docs/components/collapse.md +318 -0
- package/docs/components/confirm.md +322 -0
- package/docs/components/datepicker.md +259 -0
- package/docs/components/daterangepicker.md +260 -0
- package/docs/components/datetimepicker.md +226 -0
- package/docs/components/datetimerangepicker.md +222 -0
- package/docs/components/dropdown.md +275 -0
- package/docs/components/empty.md +200 -0
- package/docs/components/file-preview.md +180 -0
- package/docs/components/input.md +559 -0
- package/docs/components/modal.md +293 -0
- package/docs/components/popover.md +255 -0
- package/docs/components/radio.md +245 -0
- package/docs/components/select.md +254 -0
- package/docs/components/skeleton.md +150 -0
- package/docs/components/slider.md +346 -0
- package/docs/components/table.md +316 -0
- package/docs/components/tabs.md +432 -0
- package/docs/components/textarea.md +193 -0
- package/docs/components/timepicker.md +242 -0
- package/docs/components/timerangepicker.md +210 -0
- package/docs/components/toast.md +282 -0
- package/docs/components/toggle.md +211 -0
- package/docs/components/tooltip.md +213 -0
- package/docs/components/upload-avatar.md +318 -0
- package/docs/components/upload-file.md +245 -0
- package/docs/components/upload-image.md +126 -0
- package/docs/hooks/useDebounce.md +92 -0
- package/docs/hooks/useInfiniteScroll.md +95 -0
- package/docs/hooks/useMutationApp.md +242 -0
- package/docs/hooks/useSelectInfiniteQuery.md +123 -0
- package/docs/hooks/useTableQuery.md +124 -0
- package/package.json +31 -12
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
# 📑 Tabs Component (`@owa/ui`)
|
|
2
|
+
|
|
3
|
+
Bộ component **Tabs** tương tác cao, thiết kế chuẩn **Compound Components Pattern** (`<Tabs>`, `<TabList>`, `<Tab>`, `<TabPanels>`, `<TabPanel>`), tích hợp **Sliding Animated Indicator**, **Overflow Scroll**, **Custom Hook `useTabIndicator`**, **Safe Config Fallback** và tuân thủ đầy đủ tiêu chuẩn **WAI-ARIA Accessibility**.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🌟 Điểm nổi bật
|
|
8
|
+
|
|
9
|
+
- **Compound Components Pattern chuẩn chỉ**: Tách biệt rõ ràng các thành phần `<Tabs>`, `<TabList>`, `<Tab>`, `<TabPanels>`, `<TabPanel>`, tối đa hóa khả năng tùy biến layout và composition.
|
|
10
|
+
- **Sliding Animated Indicator mượt mà**: Tự động tính toán vị trí, kích thước và chuyển động trượt mượt mà cho mọi variant (`line`, `solid`, `bordered`, `flat`), hỗ trợ tự động căn chỉnh khi resize màn hình thông qua `ResizeObserver`.
|
|
11
|
+
- **Custom Hook `useTabIndicator` độc lập**: Tách biệt toàn bộ logic tính toán toạ độ indicator, kiểm tra tràn viền và điều hướng bàn phím thành hook riêng để dễ bảo trì và tái sử dụng.
|
|
12
|
+
- **Controlled & Uncontrolled Mode**: Hỗ trợ linh hoạt cả `activeKey` + `onChange` (Controlled) và `defaultActiveKey` (Uncontrolled).
|
|
13
|
+
- **3 Kích thước tiêu chuẩn (`size`)**: `sm`, `md` (*mặc định*), `lg`.
|
|
14
|
+
- **5 Biến thể giao diện (`variant`)**:
|
|
15
|
+
- `line` *(mặc định)*: Thanh gạch dưới/bên trượt mượt mà.
|
|
16
|
+
- `solid`: Khối pill nền đậm nổi bật.
|
|
17
|
+
- `bordered`: Bao khung viền quanh tab.
|
|
18
|
+
- `flat`: Khối pill nền mềm nhạt (soft pill).
|
|
19
|
+
- `other`: Tự do tùy biến 100% qua `className`.
|
|
20
|
+
- **7 Chủ đề màu sắc (`color`)**: `primary`, `secondary`, `neutral`, `error`, `success`, `warning`, `info`.
|
|
21
|
+
- **6 Tùy chỉnh bo góc (`radius`)**: `none`, `sm`, `md` (*mặc định*), `lg`, `xl`, `full`.
|
|
22
|
+
- **Hướng & Vị trí đa dạng (`orientation` & `placement`)**:
|
|
23
|
+
- Ngang (`horizontal`): `top` (*mặc định*), `bottom`.
|
|
24
|
+
- Dọc (`vertical`): `left`, `right`.
|
|
25
|
+
- **Cuộn khi tràn viền (Scrollable Overflow & Chevrons)**: Tự động hiển thị 2 nút chevron cuộn trái/phải khi danh sách tabs vượt quá chiều rộng container, tự động cuộn `scrollIntoView` tab active.
|
|
26
|
+
- **Đóng / Xóa tab tiện lợi (`closable`)**: Cho phép click nút đóng hoặc nhấn phím `Delete` / `Backspace` khi đang focus vào tab.
|
|
27
|
+
- **WAI-ARIA Accessibility & Keyboard Navigation**: Hỗ trợ đầy đủ `role="tablist"`, `role="tab"`, `role="tabpanel"`, `aria-selected`, `aria-controls`, phím mũi tên `ArrowLeft` / `ArrowRight` / `ArrowUp` / `ArrowDown`, `Home`, `End`.
|
|
28
|
+
- **Safe Config Fallback**: Tích hợp hàm `getSafeConfig` giúp component luôn an toàn, không bị crash kể cả khi truyền prop không hợp lệ.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 🚀 Cài đặt & Import
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import {
|
|
36
|
+
Tabs,
|
|
37
|
+
TabList,
|
|
38
|
+
Tab,
|
|
39
|
+
TabPanels,
|
|
40
|
+
TabPanel,
|
|
41
|
+
useTabsContext,
|
|
42
|
+
useTabIndicator,
|
|
43
|
+
} from "@owa/ui";
|
|
44
|
+
|
|
45
|
+
import type {
|
|
46
|
+
TabsProps,
|
|
47
|
+
TabListProps,
|
|
48
|
+
TabProps,
|
|
49
|
+
TabPanelsProps,
|
|
50
|
+
TabPanelProps,
|
|
51
|
+
TabSize,
|
|
52
|
+
TabVariant,
|
|
53
|
+
TabColor,
|
|
54
|
+
TabRadius,
|
|
55
|
+
TabOrientation,
|
|
56
|
+
TabPlacement,
|
|
57
|
+
UseTabIndicatorOptions,
|
|
58
|
+
} from "@owa/ui";
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 📖 Hướng dẫn sử dụng
|
|
64
|
+
|
|
65
|
+
### 1. Cách sử dụng cơ bản (Compound Components)
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
import { Tabs, TabList, Tab, TabPanels, TabPanel } from "@owa/ui";
|
|
69
|
+
|
|
70
|
+
export function BasicTabsExample() {
|
|
71
|
+
return (
|
|
72
|
+
<Tabs defaultActiveKey="overview" variant="line" color="primary">
|
|
73
|
+
<TabList>
|
|
74
|
+
<Tab value="overview" label="Tổng quan" />
|
|
75
|
+
<Tab value="profile" label="Hồ sơ" />
|
|
76
|
+
<Tab value="settings" label="Cài đặt" />
|
|
77
|
+
</TabList>
|
|
78
|
+
|
|
79
|
+
<TabPanels>
|
|
80
|
+
<TabPanel value="overview">
|
|
81
|
+
<p className="p-4 text-neutral-700">Nội dung trang tổng quan</p>
|
|
82
|
+
</TabPanel>
|
|
83
|
+
<TabPanel value="profile">
|
|
84
|
+
<p className="p-4 text-neutral-700">Thông tin hồ sơ người dùng</p>
|
|
85
|
+
</TabPanel>
|
|
86
|
+
<TabPanel value="settings">
|
|
87
|
+
<p className="p-4 text-neutral-700">Cấu hình cài đặt hệ thống</p>
|
|
88
|
+
</TabPanel>
|
|
89
|
+
</TabPanels>
|
|
90
|
+
</Tabs>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
### 2. Chế độ điều khiển (Controlled vs Uncontrolled)
|
|
98
|
+
|
|
99
|
+
#### a) Controlled Mode (Quản lý state từ bên ngoài)
|
|
100
|
+
```tsx
|
|
101
|
+
import { useState } from "react";
|
|
102
|
+
import { Tabs, TabList, Tab, TabPanels, TabPanel } from "@owa/ui";
|
|
103
|
+
|
|
104
|
+
export function ControlledTabs() {
|
|
105
|
+
const [activeKey, setActiveKey] = useState<string | number>("tab-1");
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Tabs activeKey={activeKey} onChange={(key) => setActiveKey(key)}>
|
|
109
|
+
<TabList>
|
|
110
|
+
<Tab value="tab-1" label="Tab 1" />
|
|
111
|
+
<Tab value="tab-2" label="Tab 2" />
|
|
112
|
+
</TabList>
|
|
113
|
+
<TabPanels>
|
|
114
|
+
<TabPanel value="tab-1">Nội dung 1</TabPanel>
|
|
115
|
+
<TabPanel value="tab-2">Nội dung 2</TabPanel>
|
|
116
|
+
</TabPanels>
|
|
117
|
+
</Tabs>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### b) Uncontrolled Mode (Tự quản lý nội bộ với `defaultActiveKey`)
|
|
123
|
+
```tsx
|
|
124
|
+
<Tabs defaultActiveKey="tab-2">
|
|
125
|
+
<TabList>
|
|
126
|
+
<Tab value="tab-1" label="Tab 1" />
|
|
127
|
+
<Tab value="tab-2" label="Tab 2" />
|
|
128
|
+
</TabList>
|
|
129
|
+
<TabPanels>
|
|
130
|
+
<TabPanel value="tab-1">Nội dung 1</TabPanel>
|
|
131
|
+
<TabPanel value="tab-2">Nội dung 2</TabPanel>
|
|
132
|
+
</TabPanels>
|
|
133
|
+
</Tabs>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
### 3. Các kích thước (`size`)
|
|
139
|
+
|
|
140
|
+
Hỗ trợ 3 kích cỡ: `sm`, `md` (*mặc định*), `lg`:
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
<Tabs size="sm" defaultActiveKey="1">
|
|
144
|
+
<TabList>
|
|
145
|
+
<Tab value="1" label="Small Tab" />
|
|
146
|
+
<Tab value="2" label="Tab 2" />
|
|
147
|
+
</TabList>
|
|
148
|
+
<TabPanels>
|
|
149
|
+
<TabPanel value="1">Nội dung kích thước nhỏ</TabPanel>
|
|
150
|
+
<TabPanel value="2">Nội dung 2</TabPanel>
|
|
151
|
+
</TabPanels>
|
|
152
|
+
</Tabs>
|
|
153
|
+
|
|
154
|
+
<Tabs size="md" defaultActiveKey="1">
|
|
155
|
+
<TabList>
|
|
156
|
+
<Tab value="1" label="Medium Tab (Mặc định)" />
|
|
157
|
+
<Tab value="2" label="Tab 2" />
|
|
158
|
+
</TabList>
|
|
159
|
+
<TabPanels>
|
|
160
|
+
<TabPanel value="1">Nội dung kích thước vừa</TabPanel>
|
|
161
|
+
<TabPanel value="2">Nội dung 2</TabPanel>
|
|
162
|
+
</TabPanels>
|
|
163
|
+
</Tabs>
|
|
164
|
+
|
|
165
|
+
<Tabs size="lg" defaultActiveKey="1">
|
|
166
|
+
<TabList>
|
|
167
|
+
<Tab value="1" label="Large Tab" />
|
|
168
|
+
<Tab value="2" label="Tab 2" />
|
|
169
|
+
</TabList>
|
|
170
|
+
<TabPanels>
|
|
171
|
+
<TabPanel value="1">Nội dung kích thước lớn</TabPanel>
|
|
172
|
+
<TabPanel value="2">Nội dung 2</TabPanel>
|
|
173
|
+
</TabPanels>
|
|
174
|
+
</Tabs>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
### 4. Các biến thể giao diện (`variant`)
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
// 1. Line (Mặc định): Thanh gạch dưới/bên trượt
|
|
183
|
+
<Tabs variant="line" defaultActiveKey="1">...</Tabs>
|
|
184
|
+
|
|
185
|
+
// 2. Solid: Khối pill nền nổi bật
|
|
186
|
+
<Tabs variant="solid" defaultActiveKey="1">...</Tabs>
|
|
187
|
+
|
|
188
|
+
// 3. Bordered: Bao viền khung
|
|
189
|
+
<Tabs variant="bordered" defaultActiveKey="1">...</Tabs>
|
|
190
|
+
|
|
191
|
+
// 4. Flat: Khối pill nền mềm nhạt
|
|
192
|
+
<Tabs variant="flat" defaultActiveKey="1">...</Tabs>
|
|
193
|
+
|
|
194
|
+
// 5. Other: Tự do tùy biến class hoàn toàn
|
|
195
|
+
<Tabs variant="other" defaultActiveKey="1">...</Tabs>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### 5. Các chủ đề màu sắc (`color`)
|
|
201
|
+
|
|
202
|
+
Cung cấp 7 màu sắc theo chuẩn Design System:
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
<Tabs color="primary" variant="solid" defaultActiveKey="1">...</Tabs>
|
|
206
|
+
<Tabs color="secondary" variant="solid" defaultActiveKey="1">...</Tabs>
|
|
207
|
+
<Tabs color="neutral" variant="solid" defaultActiveKey="1">...</Tabs>
|
|
208
|
+
<Tabs color="error" variant="solid" defaultActiveKey="1">...</Tabs>
|
|
209
|
+
<Tabs color="success" variant="solid" defaultActiveKey="1">...</Tabs>
|
|
210
|
+
<Tabs color="warning" variant="solid" defaultActiveKey="1">...</Tabs>
|
|
211
|
+
<Tabs color="info" variant="solid" defaultActiveKey="1">...</Tabs>
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
### 6. Hướng và Vị trí (`orientation` & `placement`)
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
// Dọc bên trái (Vertical Left)
|
|
220
|
+
<Tabs orientation="vertical" placement="left" defaultActiveKey="1">
|
|
221
|
+
<TabList>
|
|
222
|
+
<Tab value="1" label="Menu 1" />
|
|
223
|
+
<Tab value="2" label="Menu 2" />
|
|
224
|
+
</TabList>
|
|
225
|
+
<TabPanels>
|
|
226
|
+
<TabPanel value="1">Nội dung menu 1</TabPanel>
|
|
227
|
+
<TabPanel value="2">Nội dung menu 2</TabPanel>
|
|
228
|
+
</TabPanels>
|
|
229
|
+
</Tabs>
|
|
230
|
+
|
|
231
|
+
// Dọc bên phải (Vertical Right)
|
|
232
|
+
<Tabs orientation="vertical" placement="right" defaultActiveKey="1">...</Tabs>
|
|
233
|
+
|
|
234
|
+
// Ngang phía dưới (Horizontal Bottom)
|
|
235
|
+
<Tabs orientation="horizontal" placement="bottom" defaultActiveKey="1">...</Tabs>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
### 7. Icon, Badge & Tab có thể đóng (`closable` & `onClose`)
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
import { Tabs, TabList, Tab, TabPanels, TabPanel } from "@owa/ui";
|
|
244
|
+
import { HomeIcon, UserIcon, SettingsIcon } from "@/components/icons";
|
|
245
|
+
|
|
246
|
+
export function RichTabsExample() {
|
|
247
|
+
const handleClose = (key: string | number) => {
|
|
248
|
+
console.log("Đóng tab:", key);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<Tabs defaultActiveKey="tab-1" onClose={handleClose}>
|
|
253
|
+
<TabList>
|
|
254
|
+
{/* Tab có icon đầu */}
|
|
255
|
+
<Tab value="tab-1" startIcon={<HomeIcon />} label="Trang chủ" />
|
|
256
|
+
|
|
257
|
+
{/* Tab có badge số lượng */}
|
|
258
|
+
<Tab value="tab-2" startIcon={<UserIcon />} badge={5} label="Thông báo" />
|
|
259
|
+
|
|
260
|
+
{/* Tab có thể đóng (closable) & vô hiệu hóa */}
|
|
261
|
+
<Tab value="tab-3" startIcon={<SettingsIcon />} label="Tạm thời" closable />
|
|
262
|
+
<Tab value="tab-4" label="Bị khóa" disabled />
|
|
263
|
+
</TabList>
|
|
264
|
+
|
|
265
|
+
<TabPanels>
|
|
266
|
+
<TabPanel value="tab-1">Nội dung trang chủ</TabPanel>
|
|
267
|
+
<TabPanel value="tab-2">Nội dung thông báo</TabPanel>
|
|
268
|
+
<TabPanel value="tab-3">Nội dung tab tạm thời</TabPanel>
|
|
269
|
+
<TabPanel value="tab-4">Nội dung bị khóa</TabPanel>
|
|
270
|
+
</TabPanels>
|
|
271
|
+
</Tabs>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
### 8. Cuộn ngang khi tràn viền (Scrollable Overflow) & Canh giữa (`centered`)
|
|
279
|
+
|
|
280
|
+
Khi danh sách tab có độ dài lớn vượt quá container, `TabList` sẽ tự động hiển thị 2 nút chevron trái/phải để người dùng cuộn mượt mà:
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
// Canh giữa các tab trong container
|
|
284
|
+
<Tabs defaultActiveKey="1">
|
|
285
|
+
<TabList centered>
|
|
286
|
+
<Tab value="1" label="Tab 1" />
|
|
287
|
+
<Tab value="2" label="Tab 2" />
|
|
288
|
+
</TabList>
|
|
289
|
+
<TabPanels>...</TabPanels>
|
|
290
|
+
</Tabs>
|
|
291
|
+
|
|
292
|
+
// Tabs fullWidth tràn đều 100%
|
|
293
|
+
<Tabs fullWidth defaultActiveKey="1">
|
|
294
|
+
<TabList>
|
|
295
|
+
<Tab value="1" label="Tab 1" />
|
|
296
|
+
<Tab value="2" label="Tab 2" />
|
|
297
|
+
</TabList>
|
|
298
|
+
<TabPanels>...</TabPanels>
|
|
299
|
+
</Tabs>
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
### 9. Nội dung mở rộng (`extra`) & Tối ưu DOM (`destroyInactiveTabPane`)
|
|
305
|
+
|
|
306
|
+
```tsx
|
|
307
|
+
<Tabs defaultActiveKey="1" destroyInactiveTabPane>
|
|
308
|
+
<TabList
|
|
309
|
+
extra={
|
|
310
|
+
<button className="px-3 py-1.5 text-xs bg-primary-50 text-primary-700 rounded-md font-medium hover:bg-primary-100">
|
|
311
|
+
+ Thêm mới
|
|
312
|
+
</button>
|
|
313
|
+
}
|
|
314
|
+
>
|
|
315
|
+
<Tab value="1" label="Tab 1" />
|
|
316
|
+
<Tab value="2" label="Tab 2" />
|
|
317
|
+
</TabList>
|
|
318
|
+
<TabPanels>
|
|
319
|
+
{/* Khi tab không active, DOM của TabPanel sẽ được unmount hoàn toàn để tiết kiệm bộ nhớ */}
|
|
320
|
+
<TabPanel value="1">Nội dung 1</TabPanel>
|
|
321
|
+
<TabPanel value="2">Nội dung 2</TabPanel>
|
|
322
|
+
</TabPanels>
|
|
323
|
+
</Tabs>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## 🛠️ API Reference
|
|
329
|
+
|
|
330
|
+
### 1. `TabsProps`
|
|
331
|
+
|
|
332
|
+
Kế thừa các thuộc tính HTML `HTMLAttributes<HTMLDivElement>` ngoại trừ `onChange`:
|
|
333
|
+
|
|
334
|
+
| Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
335
|
+
| :--- | :--- | :--- | :--- |
|
|
336
|
+
| `activeKey` | `string \| number` | `undefined` | Khóa của tab đang active (Controlled mode). |
|
|
337
|
+
| `defaultActiveKey` | `string \| number` | `undefined` | Khóa của tab active mặc định ban đầu (Uncontrolled mode). |
|
|
338
|
+
| `onChange` | `(key: string \| number) => void` | `undefined` | Callback kích hoạt khi thay đổi tab được chọn. |
|
|
339
|
+
| `onClose` | `(key: string \| number) => void` | `undefined` | Callback kích hoạt khi người dùng bấm nút đóng tab (`closable`). |
|
|
340
|
+
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Kích thước giao diện chung cho các tab con. |
|
|
341
|
+
| `variant` | `"line" \| "solid" \| "bordered" \| "flat" \| "other"` | `"line"` | Biến thể kiểu dáng thanh tab & indicator. |
|
|
342
|
+
| `color` | `"primary" \| "secondary" \| "neutral" \| "error" \| "success" \| "warning" \| "info"` | `"primary"` | Chủ đề màu sắc theo Design System. |
|
|
343
|
+
| `radius` | `"none" \| "sm" \| "md" \| "lg" \| "xl" \| "full"` | `"md"` | Độ bo góc của tab và sliding indicator. |
|
|
344
|
+
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Chiều hiển thị danh sách tabs (ngang hoặc dọc). |
|
|
345
|
+
| `placement` | `"top" \| "bottom" \| "left" \| "right"` | `"top"` | Vị trí đặt `TabList` so với `TabPanels`. |
|
|
346
|
+
| `fullWidth` | `boolean` | `false` | Tự động giãn đều chiều rộng các tab vừa khớp 100% container. |
|
|
347
|
+
| `disabled` | `boolean` | `false` | Vô hiệu hóa toàn bộ tabs trong nhóm. |
|
|
348
|
+
| `destroyInactiveTabPane` | `boolean` | `false` | Tự động unmount nội dung khỏi DOM khi tab không active. |
|
|
349
|
+
| `children` | `ReactNode` | `undefined` | Các component con (`<TabList>`, `<TabPanels>`). |
|
|
350
|
+
| `className` | `string` | `""` | Class CSS tùy biến cho container bao ngoài. |
|
|
351
|
+
| `ref` | `Ref<HTMLDivElement>` | `undefined` | Ref chuyển tiếp đến container Tabs. |
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
### 2. `TabListProps`
|
|
356
|
+
|
|
357
|
+
Kế thừa `HTMLAttributes<HTMLDivElement>`:
|
|
358
|
+
|
|
359
|
+
| Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
360
|
+
| :--- | :--- | :--- | :--- |
|
|
361
|
+
| `children` | `ReactNode` | `undefined` | Danh sách các phần tử `<Tab>` con. |
|
|
362
|
+
| `extra` | `ReactNode` | `undefined` | Nội dung hoặc nút hành động phụ đặt ở góc thanh tab list. |
|
|
363
|
+
| `centered` | `boolean` | `false` | Canh giữa danh sách tabs trong container (`orientation="horizontal"`). |
|
|
364
|
+
| `className` | `string` | `""` | Class CSS tùy biến bổ sung cho TabList. |
|
|
365
|
+
| `ref` | `Ref<HTMLDivElement>` | `undefined` | Ref chuyển tiếp đến thẻ tablist. |
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
### 3. `TabProps`
|
|
370
|
+
|
|
371
|
+
Kế thừa `ButtonHTMLAttributes<HTMLButtonElement>` ngoại trừ `value`:
|
|
372
|
+
|
|
373
|
+
| Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
374
|
+
| :--- | :--- | :--- | :--- |
|
|
375
|
+
| `value` | `string \| number` | *(Bắt buộc)* | Khóa định danh duy nhất của tab để liên kết với `TabPanel`. |
|
|
376
|
+
| `label` | `ReactNode` | `undefined` | Tiêu đề hiển thị của tab. |
|
|
377
|
+
| `startIcon` | `ReactNode` | `undefined` | Icon hiển thị trước tiêu đề. |
|
|
378
|
+
| `endIcon` | `ReactNode` | `undefined` | Icon hiển thị sau tiêu đề. |
|
|
379
|
+
| `badge` | `ReactNode` | `undefined` | Huy hiệu hoặc số lượng hiển thị trên tab. |
|
|
380
|
+
| `disabled` | `boolean` | `false` | Vô hiệu hóa riêng tab này. |
|
|
381
|
+
| `closable` | `boolean` | `false` | Cho phép hiển thị nút đóng tab (hỗ trợ phím Delete/Backspace). |
|
|
382
|
+
| `onClose` | `(e: MouseEvent) => void` | `undefined` | Callback khi bấm nút đóng trên tab này. |
|
|
383
|
+
| `children` | `ReactNode` | `undefined` | Nội dung tùy biến thay thế cho prop `label`. |
|
|
384
|
+
| `className` | `string` | `""` | Class CSS tùy biến cho nút tab. |
|
|
385
|
+
| `ref` | `Ref<HTMLButtonElement>` | `undefined` | Ref chuyển tiếp đến thẻ `<button>` của tab. |
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
### 4. `TabPanelsProps` & `TabPanelProps`
|
|
390
|
+
|
|
391
|
+
#### `TabPanelsProps`
|
|
392
|
+
| Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
393
|
+
| :--- | :--- | :--- | :--- |
|
|
394
|
+
| `children` | `ReactNode` | `undefined` | Danh sách các component `<TabPanel>` con. |
|
|
395
|
+
| `className` | `string` | `""` | Class CSS tùy biến cho khung chứa TabPanels. |
|
|
396
|
+
| `ref` | `Ref<HTMLDivElement>` | `undefined` | Ref chuyển tiếp đến container TabPanels. |
|
|
397
|
+
|
|
398
|
+
#### `TabPanelProps`
|
|
399
|
+
| Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
400
|
+
| :--- | :--- | :--- | :--- |
|
|
401
|
+
| `value` | `string \| number` | *(Bắt buộc)* | Khóa liên kết trực tiếp với `value` của `<Tab>` tương ứng. |
|
|
402
|
+
| `destroyInactiveTabPane` | `boolean` | `undefined` | Ghi đè cấu hình unmount khỏi DOM khi tab không active riêng cho panel này. |
|
|
403
|
+
| `children` | `ReactNode` | `undefined` | Nội dung hiển thị khi tab tương ứng đang active. |
|
|
404
|
+
| `className` | `string` | `""` | Class CSS tùy biến cho panel nội dung. |
|
|
405
|
+
| `ref` | `Ref<HTMLDivElement>` | `undefined` | Ref chuyển tiếp đến thẻ tabpanel. |
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
### 5. Hook `useTabIndicator`
|
|
410
|
+
|
|
411
|
+
```tsx
|
|
412
|
+
import { useTabIndicator } from "@owa/ui";
|
|
413
|
+
|
|
414
|
+
const {
|
|
415
|
+
listRef,
|
|
416
|
+
indicatorStyle,
|
|
417
|
+
canScrollLeft,
|
|
418
|
+
canScrollRight,
|
|
419
|
+
checkScroll,
|
|
420
|
+
updateIndicator,
|
|
421
|
+
handleScrollLeft,
|
|
422
|
+
handleScrollRight,
|
|
423
|
+
handleKeyDown,
|
|
424
|
+
} = useTabIndicator({
|
|
425
|
+
activeKey,
|
|
426
|
+
orientation,
|
|
427
|
+
placement,
|
|
428
|
+
variant,
|
|
429
|
+
color,
|
|
430
|
+
radius,
|
|
431
|
+
});
|
|
432
|
+
```
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# 📝 TextArea Component (`@owa/ui`)
|
|
2
|
+
|
|
3
|
+
Component **TextArea** đa năng, hỗ trợ **tự động co giãn chiều cao (AutoResize)** bằng thư viện `react-textarea-autosize`, tích hợp **Safe Config Fallback**, **quản lý Ref tối ưu** qua `@floating-ui/react`, **bộ đếm ký tự (Character Counter)** và tuân thủ đầy đủ tiêu chuẩn **WAI-ARIA Accessibility**.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🌟 Điểm nổi bật
|
|
8
|
+
|
|
9
|
+
- **Tích hợp `react-textarea-autosize`**: Tự động tăng giảm chiều cao mượt mà theo nội dung nhập mà không làm giật khung hình (`layout shift`). Hỗ trợ cấu hình `minRows`, `maxRows`, `cacheMeasurements` và callback `onHeightChange`.
|
|
10
|
+
- **Tùy chọn chuyển đổi linh hoạt**: Dễ dàng tắt chế độ co giãn tự động bằng `autoResize={false}` để chuyển về thẻ `<textarea>` truyền thống.
|
|
11
|
+
- **5 Kích thước tiêu chuẩn (`size`)**: `xs`, `sm`, `md` (*mặc định*), `lg`, `xl` với font chữ, padding, kích thước nhãn và thông báo phụ được đồng bộ tỉ lệ.
|
|
12
|
+
- **3 Biến thể giao diện (`variant`)**:
|
|
13
|
+
- `outline` *(mặc định)*: Viền nét rõ ràng quanh khung soạn thảo, hover/focus đổi màu viền chủ đề.
|
|
14
|
+
- `filled`: Nền pastel nhạt (`bg-{color}-50/60`), có viền bao quanh.
|
|
15
|
+
- `ghost`: Nền trong suốt, chỉ nổi bật khi hover hoặc focus.
|
|
16
|
+
- `other`: Bỏ qua các class màu mặc định, tự do áp dụng custom style qua `textareaWrapperClassName`.
|
|
17
|
+
- **7 Chủ đề màu sắc (`color`)**: `primary`, `secondary`, `error`, `success`, `warning`, `info`, `neutral`.
|
|
18
|
+
- **6 Mức độ bo góc (`radius`)**: `none`, `sm`, `md` (*mặc định*), `lg`, `xl`, `full`.
|
|
19
|
+
- **3 Vị trí đặt nhãn (`labelPlacement`)**:
|
|
20
|
+
- `top` *(mặc định)*: Nhãn nằm phía trên ô textarea.
|
|
21
|
+
- `left`: Nhãn nằm ngang bên trái ô textarea (tự động căn chỉnh theo dạng flex-row).
|
|
22
|
+
- `floating`: Nhãn nổi bật vắt ngang viền trên của khung textarea.
|
|
23
|
+
- **Trạng thái Loading & Xoay Spinner (`isLoading` & `showSpinner`)**:
|
|
24
|
+
- `isLoading={true}`: Tự động vô hiệu hóa ô nhập liệu (`disabled`), kích hoạt `aria-busy="true"` và `aria-disabled="true"`.
|
|
25
|
+
- `showSpinner`: Mặc định là `false`. Đặt `showSpinner={true}` khi muốn hiển thị icon xoay vòng ở góc phải trên.
|
|
26
|
+
- **Nút xóa nhanh (`isClearable` & `onClear`)**: Hiển thị nút bấm xóa sạch nội dung ở góc phải trên khi ô có văn bản.
|
|
27
|
+
- **Bộ đếm số lượng ký tự (`showCount` & `maxLength`)**:
|
|
28
|
+
- Hiển thị số lượng ký tự trực tiếp ở góc dưới bên phải (ví dụ: `45/500`).
|
|
29
|
+
- Tự động chặn nhập vượt quá số ký tự cho phép cả khi gõ và khi dán (paste).
|
|
30
|
+
- **Quản lý Ref nâng cao**: Sử dụng `useMergeRefs` từ `@floating-ui/react` giúp gộp và chuyển tiếp `ref` mượt mà trong React 19.
|
|
31
|
+
- **Safe Config Fallback**: Tích hợp hàm `getSafeConfig` đảm bảo component hoạt động an toàn, không bị crash kể cả khi truyền prop không hợp lệ.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 🚀 Cài đặt & Import
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { TextArea } from "@owa/ui";
|
|
39
|
+
import type {
|
|
40
|
+
TextAreaProps,
|
|
41
|
+
TextAreaConfig,
|
|
42
|
+
TextAreaSize,
|
|
43
|
+
TextAreaVariant,
|
|
44
|
+
TextAreaColor,
|
|
45
|
+
TextAreaRadius,
|
|
46
|
+
TextAreaLabelPlacement,
|
|
47
|
+
TextAreaResize,
|
|
48
|
+
} from "@owa/ui";
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 📖 Hướng dẫn sử dụng
|
|
54
|
+
|
|
55
|
+
### 1. Cách sử dụng cơ bản
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { TextArea } from "@owa/ui";
|
|
59
|
+
|
|
60
|
+
export function BasicTextAreaExample() {
|
|
61
|
+
return (
|
|
62
|
+
<div className="flex flex-col gap-4 max-w-md">
|
|
63
|
+
<TextArea
|
|
64
|
+
label="Mô tả chi tiết"
|
|
65
|
+
placeholder="Nhập mô tả sản phẩm của bạn..."
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### 2. Tự động co giãn chiều cao (AutoResize)
|
|
75
|
+
|
|
76
|
+
Mặc định `autoResize={true}` với số dòng tối thiểu `minRows={3}`:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
// Co giãn tự do từ 3 dòng trở lên
|
|
80
|
+
<TextArea
|
|
81
|
+
label="Phản hồi ý kiến"
|
|
82
|
+
autoResize={true}
|
|
83
|
+
minRows={3}
|
|
84
|
+
maxRows={8}
|
|
85
|
+
onHeightChange={(height) => console.log("Chiều cao hiện tại:", height)}
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
// Tắt autoResize để dùng chiều cao cố định
|
|
89
|
+
<TextArea
|
|
90
|
+
label="Ghi chú cố định"
|
|
91
|
+
autoResize={false}
|
|
92
|
+
rows={4}
|
|
93
|
+
resize="vertical"
|
|
94
|
+
/>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### 3. Vị trí đặt nhãn (`labelPlacement`)
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
// 1. Top (Mặc định)
|
|
103
|
+
<TextArea label="Nhãn phía trên" labelPlacement="top" placeholder="Nhập văn bản..." />
|
|
104
|
+
|
|
105
|
+
// 2. Left (Ngang bên trái)
|
|
106
|
+
<TextArea label="Nhãn bên trái" labelPlacement="left" placeholder="Nhập văn bản..." />
|
|
107
|
+
|
|
108
|
+
// 3. Floating (Nổi trên viền)
|
|
109
|
+
<TextArea label="Nhãn nổi viền" labelPlacement="floating" placeholder="Nhập văn bản..." />
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### 4. Giới hạn ký tự & Bộ đếm (`showCount` & `maxLength`)
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
<TextArea
|
|
118
|
+
label="Đánh giá sản phẩm"
|
|
119
|
+
maxLength={200}
|
|
120
|
+
showCount={true}
|
|
121
|
+
placeholder="Tối đa 200 ký tự (tự động chặn khi đạt giới hạn)..."
|
|
122
|
+
/>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
### 5. Xóa nhanh & Trạng thái Loading
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
<TextArea
|
|
131
|
+
label="Tìm kiếm nội dung"
|
|
132
|
+
isClearable={true}
|
|
133
|
+
onClear={() => console.log("Đã xóa sạch nội dung")}
|
|
134
|
+
defaultValue="Nội dung ban đầu"
|
|
135
|
+
/>
|
|
136
|
+
|
|
137
|
+
<TextArea
|
|
138
|
+
label="Đang đồng bộ dữ liệu"
|
|
139
|
+
isLoading={true}
|
|
140
|
+
showSpinner={true}
|
|
141
|
+
defaultValue="Vui lòng đợi..."
|
|
142
|
+
/>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### 6. Trạng thái Báo lỗi & Hướng dẫn (`errorMessage` & `helperText`)
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
<TextArea
|
|
151
|
+
label="Địa chỉ giao hàng"
|
|
152
|
+
isRequired={true}
|
|
153
|
+
errorMessage="Địa chỉ không được để trống!"
|
|
154
|
+
isInvalid={true}
|
|
155
|
+
/>
|
|
156
|
+
|
|
157
|
+
<TextArea
|
|
158
|
+
label="Tiểu sử bản thân"
|
|
159
|
+
helperText="Hãy viết ngắn gọn 1-2 câu về kinh nghiệm của bạn."
|
|
160
|
+
/>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 🛠 Bảng thông số Props (`TextAreaProps`)
|
|
166
|
+
|
|
167
|
+
| Tên Prop | Kiểu dữ liệu | Giá trị mặc định | Mô tả |
|
|
168
|
+
| :--- | :--- | :---: | :--- |
|
|
169
|
+
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Kích cỡ giao diện (font chữ, padding, kích thước nhãn). |
|
|
170
|
+
| `variant` | `'outline' \| 'filled' \| 'ghost' \| 'other'` | `'outline'` | Biến thể hiển thị giao diện. |
|
|
171
|
+
| `color` | `'primary' \| 'secondary' \| 'error' \| 'success' \| 'warning' \| 'info' \| 'neutral'` | `'primary'` | Chủ đề màu sắc theo Design System. |
|
|
172
|
+
| `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | Độ bo góc của khung viền textarea. |
|
|
173
|
+
| `label` | `ReactNode` | — | Nhãn tiêu đề hiển thị cho ô nhập liệu. |
|
|
174
|
+
| `labelPlacement` | `'top' \| 'left' \| 'floating'` | `'top'` | Vị trí hiển thị của nhãn. |
|
|
175
|
+
| `config` | `TextAreaConfig` | — | Cấu hình tập trung các cờ trạng thái / tính năng (`isRequired`, `isInvalid`, `isLoading`, `showSpinner`, `isClearable`, `autoResize`, `showCount`, `isFullWidth`). |
|
|
176
|
+
| `isRequired` | `boolean` | `false` | Hiển thị dấu sao đỏ `*` và đánh dấu `aria-required="true"`. |
|
|
177
|
+
| `helperText` | `ReactNode` | — | Đoạn văn bản hướng dẫn/trợ giúp bên dưới ô. |
|
|
178
|
+
| `errorMessage` | `ReactNode` | — | Thông báo lỗi khi nhập sai (tự kích hoạt trạng thái báo lỗi). |
|
|
179
|
+
| `isInvalid` | `boolean` | `false` | Bật trạng thái viền đỏ báo lỗi và `aria-invalid="true"`. |
|
|
180
|
+
| `isLoading` | `boolean` | `false` | Khóa tương tác, bật `aria-busy="true"` và `aria-disabled="true"`. |
|
|
181
|
+
| `showSpinner` | `boolean` | `false` | Hiển thị biểu tượng xoay spinner khi `isLoading={true}`. |
|
|
182
|
+
| `isClearable` | `boolean` | `false` | Hiển thị nút xóa nhanh nội dung khi có văn bản. |
|
|
183
|
+
| `onClear` | `() => void` | — | Callback được gọi khi bấm nút xóa nhanh. |
|
|
184
|
+
| `autoResize` | `boolean` | `true` | Tự động co giãn chiều cao theo nội dung nhập. |
|
|
185
|
+
| `minRows` | `number` | `3` | Số dòng hiển thị tối thiểu khi `autoResize` bật. |
|
|
186
|
+
| `maxRows` | `number` | — | Số dòng hiển thị tối đa trước khi xuất hiện thanh cuộn. |
|
|
187
|
+
| `onHeightChange` | `(height: number, meta: { rowHeight: number }) => void` | — | Callback khi chiều cao thay đổi do autoResize. |
|
|
188
|
+
| `cacheMeasurements` | `boolean` | — | Bật bộ nhớ đệm kết quả đo chiều cao để tối ưu render. |
|
|
189
|
+
| `showCount` | `boolean` | `false` | Hiển thị bộ đếm số lượng ký tự ở góc phải dưới. |
|
|
190
|
+
| `maxLength` | `number` | — | Số ký tự tối đa cho phép nhập (chặn tự động khi đạt giới hạn). |
|
|
191
|
+
| `resize` | `'none' \| 'vertical' \| 'horizontal' \| 'both'` | `'none'` | Tùy chọn kéo giãn khung thủ công. |
|
|
192
|
+
| `isFullWidth` | `boolean` | `false` | Mở rộng chiếm toàn bộ 100% chiều ngang container cha. |
|
|
193
|
+
| `ref` | `Ref<HTMLTextAreaElement>` | — | Ref chuyển tiếp đến thẻ `<textarea>` HTML bên dưới. |
|