@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,346 @@
|
|
|
1
|
+
# 🎚️ Slider Component (`@owa/ui`)
|
|
2
|
+
|
|
3
|
+
Component **Slider** hiện đại, mượt mà và toàn diện, được xây dựng trên nền tảng logic headless của **`@radix-ui/react-slider`**, tuân thủ cấu trúc thiết kế chuẩn **Design System (`styleCode`)** của `@owa/ui`, tích hợp **Safe Config Fallback**, **Floating UI Tooltip**, hỗ trợ cả thanh trượt đơn (**Single**) lẫn thanh trượt dải (**Range**) và đáp ứng đầy đủ tiêu chuẩn **WAI-ARIA Accessibility**.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🌟 Điểm nổi bật
|
|
8
|
+
|
|
9
|
+
- **Engine Headless Radix UI**: Quản lý cử chỉ kéo thả (Pointer Capture, Touch, Multi-touch), tính toán giá trị và điều hướng bàn phím mượt mà, chính xác, không bị giật lag khi kéo ra ngoài track.
|
|
10
|
+
- **Hỗ trợ Single & Range Slider**:
|
|
11
|
+
- **Single Slider**: Truyền `number` vào `value` / `defaultValue`.
|
|
12
|
+
- **Range Slider (Dual Thumbs)**: Truyền `[number, number]` vào `value` / `defaultValue` để điều chỉnh khoảng giá trị (giá cả, bộ lọc...).
|
|
13
|
+
- **Cấu hình tập trung chuẩn (`config?: SliderConfig`)**: Toàn bộ cờ trạng thái (`isRequired`, `isInvalid`, `isLoading`, `showSpinner`, `showSteps`, `showValue`, `isFullWidth`) được quản lý thống nhất qua đối tượng `config`.
|
|
14
|
+
- **5 Kích thước tiêu chuẩn (`size`)**: `xs`, `sm`, `md` (*mặc định*), `lg`, `xl` với kích thước track, thumb, nấc bước và font chữ được cân chỉnh theo tỷ lệ chuẩn.
|
|
15
|
+
- **4 Biến thể giao diện (`variant`)**:
|
|
16
|
+
- `filled` *(mặc định)*: Nền track rõ nét, thanh fill màu chủ đề tương phản cao.
|
|
17
|
+
- `soft`: Nền track màu pastel dịu mắt theo tone màu chủ đề.
|
|
18
|
+
- `outline`: Track và Thumb dạng viền viền theo màu chủ đề.
|
|
19
|
+
- `other`: Bỏ qua các class màu mặc định, tự do áp dụng custom style qua `trackClassName`, `fillerClassName`, `thumbClassName`.
|
|
20
|
+
- **7 Chủ đề màu sắc (`color`)**: `primary`, `secondary`, `error`, `success`, `warning`, `info`, `neutral`.
|
|
21
|
+
- **6 Mức độ bo góc (`radius` & `thumbRadius`)**: `none`, `sm`, `md`, `lg`, `xl`, `full` (*mặc định*). Có thể tùy biến riêng bo góc cho thanh track và nút kéo thumb.
|
|
22
|
+
- **Tích hợp Tooltip (@floating-ui/react)**: Hỗ trợ 4 chế độ hiển thị tooltip nổi trên thumb: `"always"`, `"active"` (khi kéo / focus), `"hover"`, và `"none"`.
|
|
23
|
+
- **Nấc bước & Đánh dấu (Marks & Step Dots)**:
|
|
24
|
+
- `showSteps`: Tự động hiển thị các chấm tròn tại từng nấc bước trên thanh trượt.
|
|
25
|
+
- `marks`: Hiển thị nhãn mốc giá trị bên dưới hoặc bên cạnh thanh trượt.
|
|
26
|
+
- **Tích hợp Form HTML chuẩn**: Tự động render `<input type="hidden" />` khi truyền `name` để hỗ trợ submit form HTML native / FormData.
|
|
27
|
+
- **Đạt chuẩn Accessibility (WAI-ARIA)**: Tích hợp đầy đủ `role="slider"`, `aria-valuenow`, `aria-valuemin`, `aria-valuemax`, `aria-valuetext`, `aria-invalid`, `aria-required`, `aria-errormessage`, `aria-describedby`.
|
|
28
|
+
- **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ệ.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 🚀 Cài đặt & Import
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import {
|
|
36
|
+
Slider,
|
|
37
|
+
SliderThumb,
|
|
38
|
+
SliderMarks,
|
|
39
|
+
SliderStepDots,
|
|
40
|
+
clamp,
|
|
41
|
+
getPercentage,
|
|
42
|
+
} from "@owa/ui";
|
|
43
|
+
import type {
|
|
44
|
+
SliderProps,
|
|
45
|
+
SliderConfig,
|
|
46
|
+
SliderMark,
|
|
47
|
+
SliderSize,
|
|
48
|
+
SliderVariant,
|
|
49
|
+
SliderColor,
|
|
50
|
+
SliderRadius,
|
|
51
|
+
SliderOrientation,
|
|
52
|
+
SliderTooltipMode,
|
|
53
|
+
SliderTooltipPlacement,
|
|
54
|
+
SliderValue,
|
|
55
|
+
} from "@owa/ui";
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 📖 Hướng dẫn sử dụng
|
|
61
|
+
|
|
62
|
+
### 1. Slider đơn cơ bản (Single Value)
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { useState } from "react";
|
|
66
|
+
import { Slider } from "@owa/ui";
|
|
67
|
+
|
|
68
|
+
export function BasicSingleSlider() {
|
|
69
|
+
const [volume, setVolume] = useState<number>(50);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="max-w-md space-y-6">
|
|
73
|
+
{/* Uncontrolled Slider */}
|
|
74
|
+
<Slider
|
|
75
|
+
label="Âm lượng mặc định"
|
|
76
|
+
defaultValue={40}
|
|
77
|
+
config={{ showValue: true }}
|
|
78
|
+
/>
|
|
79
|
+
|
|
80
|
+
{/* Controlled Slider */}
|
|
81
|
+
<Slider
|
|
82
|
+
label="Âm lượng tùy chỉnh"
|
|
83
|
+
value={volume}
|
|
84
|
+
onChange={(val) => setVolume(val as number)}
|
|
85
|
+
formatValue={(val) => `${val}%`}
|
|
86
|
+
config={{ showValue: true }}
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### 2. Slider khoảng dải (Range Slider - Dual Thumbs)
|
|
96
|
+
|
|
97
|
+
Chỉ cần truyền một mảng 2 phần tử `[minVal, maxVal]` vào `value` hoặc `defaultValue`:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { useState } from "react";
|
|
101
|
+
import { Slider } from "@owa/ui";
|
|
102
|
+
|
|
103
|
+
export function PriceRangeSlider() {
|
|
104
|
+
const [priceRange, setPriceRange] = useState<[number, number]>([200, 800]);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="max-w-md">
|
|
108
|
+
<Slider
|
|
109
|
+
label="Khoảng giá (USD)"
|
|
110
|
+
min={0}
|
|
111
|
+
max={1000}
|
|
112
|
+
step={10}
|
|
113
|
+
value={priceRange}
|
|
114
|
+
onChange={(val) => setPriceRange(val as [number, number])}
|
|
115
|
+
formatValue={(val) => {
|
|
116
|
+
const [min, max] = val as [number, number];
|
|
117
|
+
return `$${min} - $${max}`;
|
|
118
|
+
}}
|
|
119
|
+
config={{ showValue: true }}
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### 3. Kích thước (`size`)
|
|
129
|
+
|
|
130
|
+
Hỗ trợ 5 kích thước: `xs`, `sm`, `md` (*mặc định*), `lg`, `xl`:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
<Slider size="xs" label="Size XS" defaultValue={20} config={{ showValue: true }} />
|
|
134
|
+
<Slider size="sm" label="Size SM" defaultValue={40} config={{ showValue: true }} />
|
|
135
|
+
<Slider size="md" label="Size MD" defaultValue={60} config={{ showValue: true }} />
|
|
136
|
+
<Slider size="lg" label="Size LG" defaultValue={80} config={{ showValue: true }} />
|
|
137
|
+
<Slider size="xl" label="Size XL" defaultValue={100} config={{ showValue: true }} />
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### 4. Biến thể (`variant`) & Màu sắc (`color`)
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
// 7 Bảng màu chủ đề
|
|
146
|
+
<Slider color="primary" label="Primary" defaultValue={50} />
|
|
147
|
+
<Slider color="secondary" label="Secondary" defaultValue={50} />
|
|
148
|
+
<Slider color="success" label="Success" defaultValue={50} />
|
|
149
|
+
<Slider color="warning" label="Warning" defaultValue={50} />
|
|
150
|
+
<Slider color="error" label="Error" defaultValue={50} />
|
|
151
|
+
<Slider color="info" label="Info" defaultValue={50} />
|
|
152
|
+
<Slider color="neutral" label="Neutral" defaultValue={50} />
|
|
153
|
+
|
|
154
|
+
// 4 Biến thể hiển thị
|
|
155
|
+
<Slider variant="filled" color="primary" label="Filled" defaultValue={60} />
|
|
156
|
+
<Slider variant="soft" color="primary" label="Soft" defaultValue={60} />
|
|
157
|
+
<Slider variant="outline" color="primary" label="Outline" defaultValue={60} />
|
|
158
|
+
<Slider
|
|
159
|
+
variant="other"
|
|
160
|
+
label="Custom Other"
|
|
161
|
+
defaultValue={60}
|
|
162
|
+
trackClassName="bg-purple-200 dark:bg-purple-900/40"
|
|
163
|
+
fillerClassName="bg-purple-600 dark:bg-purple-500"
|
|
164
|
+
thumbClassName="border-purple-600"
|
|
165
|
+
/>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### 5. Bo góc thanh trượt & Nút kéo (`radius` & `thumbRadius`)
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
<Slider radius="none" thumbRadius="none" label="Square" defaultValue={40} />
|
|
174
|
+
<Slider radius="md" thumbRadius="md" label="Medium Rounded" defaultValue={50} />
|
|
175
|
+
<Slider radius="full" thumbRadius="full" label="Fully Rounded" defaultValue={60} />
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
### 6. Tích hợp Tooltip (`showTooltip` & `formatTooltip`)
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
// 1. Luôn hiển thị Tooltip
|
|
184
|
+
<Slider
|
|
185
|
+
label="Always Open"
|
|
186
|
+
defaultValue={45}
|
|
187
|
+
showTooltip="always"
|
|
188
|
+
formatTooltip={(val) => `${val}%`}
|
|
189
|
+
/>
|
|
190
|
+
|
|
191
|
+
// 2. Hiển thị khi đang kéo hoặc focus bàn phím
|
|
192
|
+
<Slider
|
|
193
|
+
label="Active On Drag/Focus"
|
|
194
|
+
defaultValue={75}
|
|
195
|
+
showTooltip="active"
|
|
196
|
+
color="success"
|
|
197
|
+
formatTooltip={(val) => `${val} kW/h`}
|
|
198
|
+
/>
|
|
199
|
+
|
|
200
|
+
// 3. Hiển thị khi hover chuột
|
|
201
|
+
<Slider
|
|
202
|
+
label="Hover"
|
|
203
|
+
defaultValue={30}
|
|
204
|
+
showTooltip="hover"
|
|
205
|
+
color="warning"
|
|
206
|
+
formatTooltip={(val) => `$${val}`}
|
|
207
|
+
/>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
### 7. Nấc bước & Đánh dấu mốc (`showSteps` & `marks`)
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
<Slider
|
|
216
|
+
label="Nhiệt độ phòng"
|
|
217
|
+
defaultValue={25}
|
|
218
|
+
min={10}
|
|
219
|
+
max={40}
|
|
220
|
+
step={5}
|
|
221
|
+
config={{ showSteps: true }}
|
|
222
|
+
marks={[
|
|
223
|
+
{ value: 10, label: "10°C" },
|
|
224
|
+
{ value: 20, label: "20°C" },
|
|
225
|
+
{ value: 25, label: "25°C" },
|
|
226
|
+
{ value: 37, label: "37°C" },
|
|
227
|
+
{ value: 40, label: "40°C" },
|
|
228
|
+
]}
|
|
229
|
+
/>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
### 8. Hướng dọc (`orientation="vertical"`)
|
|
235
|
+
|
|
236
|
+
Rất thích hợp cho bộ chỉnh âm thanh (Equalizer), thanh chỉnh độ sáng:
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
<div className="flex items-center gap-6 h-64">
|
|
240
|
+
<Slider orientation="vertical" defaultValue={40} color="primary" showTooltip="active" />
|
|
241
|
+
<Slider orientation="vertical" defaultValue={75} color="secondary" showTooltip="active" />
|
|
242
|
+
<Slider orientation="vertical" defaultValue={60} color="success" showTooltip="active" />
|
|
243
|
+
</div>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
### 9. Trạng thái Loading, Validation & Form Integration
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
// Trạng thái Loading kèm Spinner
|
|
252
|
+
<Slider
|
|
253
|
+
label="Đang đồng bộ dữ liệu"
|
|
254
|
+
defaultValue={50}
|
|
255
|
+
helperText="Vui lòng chờ..."
|
|
256
|
+
config={{ isLoading: true, showSpinner: true }}
|
|
257
|
+
/>
|
|
258
|
+
|
|
259
|
+
// Trạng thái Báo lỗi (Invalid)
|
|
260
|
+
<Slider
|
|
261
|
+
label="Công suất máy"
|
|
262
|
+
defaultValue={10}
|
|
263
|
+
errorMessage="Giá trị công suất tối thiểu là 20!"
|
|
264
|
+
config={{ isInvalid: true, isRequired: true }}
|
|
265
|
+
/>
|
|
266
|
+
|
|
267
|
+
// Tích hợp Form HTML (FormData)
|
|
268
|
+
<form onSubmit={(e) => {
|
|
269
|
+
e.preventDefault();
|
|
270
|
+
const data = new FormData(e.currentTarget);
|
|
271
|
+
console.log(data.get("volume")); // Output: giá trị số
|
|
272
|
+
}}>
|
|
273
|
+
<Slider name="volume" defaultValue={80} label="Volume" />
|
|
274
|
+
<button type="submit">Submit</button>
|
|
275
|
+
</form>
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## 📋 Bảng thuộc tính (Props Reference)
|
|
281
|
+
|
|
282
|
+
### `SliderProps`
|
|
283
|
+
|
|
284
|
+
| Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
285
|
+
| :--- | :--- | :---: | :--- |
|
|
286
|
+
| **`config`** | `SliderConfig` | `{}` | Đối tượng gom nhóm các cờ cấu hình tính năng/trạng thái. |
|
|
287
|
+
| **`value`** | `number \| [number, number]` | `undefined` | Giá trị controlled của thanh trượt. |
|
|
288
|
+
| **`defaultValue`** | `number \| [number, number]` | `undefined` | Giá trị khởi tạo mặc định (uncontrolled). |
|
|
289
|
+
| **`min`** | `number` | `0` | Giá trị tối thiểu. |
|
|
290
|
+
| **`max`** | `number` | `100` | Giá trị tối đa. |
|
|
291
|
+
| **`step`** | `number` | `1` | Bước nhảy giữa các giá trị. |
|
|
292
|
+
| **`minStepsBetweenThumbs`** | `number` | `0` | Số bước nhảy tối thiểu giữa 2 thumbs ở Range mode. |
|
|
293
|
+
| **`inverted`** | `boolean` | `false` | Đảo ngược chiều thanh trượt. |
|
|
294
|
+
| **`size`** | `"xs" \| "sm" \| "md" \| "lg" \| "xl"` | `"md"` | Kích thước component. |
|
|
295
|
+
| **`variant`** | `"filled" \| "soft" \| "outline" \| "other"` | `"filled"` | Biến thể giao diện của thanh trượt. |
|
|
296
|
+
| **`color`** | `"primary" \| "secondary" \| "error" \| "success" \| "warning" \| "info" \| "neutral"` | `"primary"` | Chủ đề màu sắc. |
|
|
297
|
+
| **`radius`** | `"none" \| "sm" \| "md" \| "lg" \| "xl" \| "full"` | `"full"` | Bo góc của track. |
|
|
298
|
+
| **`thumbRadius`** | `"none" \| "sm" \| "md" \| "lg" \| "xl" \| "full"` | `"full"` | Bo góc của thumb. |
|
|
299
|
+
| **`orientation`** | `"horizontal" \| "vertical"` | `"horizontal"` | Chiều hướng trượt. |
|
|
300
|
+
| **`label`** | `ReactNode` | `undefined` | Nhãn tiêu đề của slider. |
|
|
301
|
+
| **`labelPlacement`** | `"top" \| "left" \| "right"` | `"top"` | Vị trí đặt nhãn tiêu đề. |
|
|
302
|
+
| **`formatValue`** | `(val: SliderValue) => ReactNode` | `undefined` | Hàm format hiển thị giá trị cạnh label. |
|
|
303
|
+
| **`showTooltip`** | `"none" \| "hover" \| "active" \| "always"` | `"none"` | Chế độ hiển thị Tooltip nổi trên thumb. |
|
|
304
|
+
| **`tooltipPlacement`** | `"top" \| "bottom" \| "left" \| "right"` | `undefined` | Vị trí đặt tooltip. |
|
|
305
|
+
| **`formatTooltip`** | `(val: number) => ReactNode` | `undefined` | Hàm format nội dung hiển thị trong tooltip. |
|
|
306
|
+
| **`marks`** | `SliderMark[] \| boolean` | `false` | Danh sách mốc đánh dấu hoặc `true` để tự sinh theo step. |
|
|
307
|
+
| **`startContent`** | `ReactNode` | `undefined` | Icon hoặc nội dung đầu thanh trượt. |
|
|
308
|
+
| **`endContent`** | `ReactNode` | `undefined` | Icon hoặc nội dung cuối thanh trượt. |
|
|
309
|
+
| **`disabled`** | `boolean` | `false` | Vô hiệu hóa toàn bộ tương tác. |
|
|
310
|
+
| **`readOnly`** | `boolean` | `false` | Chế độ chỉ đọc (không kéo được nhưng vẫn submit form). |
|
|
311
|
+
| **`helperText`** | `ReactNode` | `undefined` | Văn bản hướng dẫn phía dưới. |
|
|
312
|
+
| **`errorMessage`** | `ReactNode` | `undefined` | Thông báo lỗi phía dưới (tự động bật `hasError`). |
|
|
313
|
+
| **`name`** | `string` | `undefined` | Tên input ẩn dùng cho form submit. |
|
|
314
|
+
| **`onChange`** | `(val: SliderValue) => void` | `undefined` | Callback gọi liên tục khi đang kéo thay đổi giá trị. |
|
|
315
|
+
| **`onChangeEnd`** | `(val: SliderValue) => void` | `undefined` | Callback chỉ gọi khi thả tay/kết thúc kéo. |
|
|
316
|
+
| **`trackClassName`** | `string` | `""` | Tùy biến class của thanh track. |
|
|
317
|
+
| **`fillerClassName`** | `string` | `""` | Tùy biến class của thanh đã fill. |
|
|
318
|
+
| **`thumbClassName`** | `string` | `""` | Tùy biến class của nút kéo thumb. |
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
### `SliderConfig`
|
|
323
|
+
|
|
324
|
+
| Cờ cấu hình | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
325
|
+
| :--- | :--- | :---: | :--- |
|
|
326
|
+
| **`isRequired`** | `boolean` | `false` | Đánh dấu trường bắt buộc (hiển thị dấu `*` đỏ cạnh label). |
|
|
327
|
+
| **`isInvalid`** | `boolean` | `false` | Bật trạng thái lỗi (chuyển màu track, thumb, label sang tone error). |
|
|
328
|
+
| **`isLoading`** | `boolean` | `false` | Khóa tương tác và kích hoạt trạng thái loading. |
|
|
329
|
+
| **`showSpinner`** | `boolean` | `false` | Hiển thị biểu tượng xoay Spinner bên trong nút thumb khi đang loading. |
|
|
330
|
+
| **`showSteps`** | `boolean` | `false` | Hiển thị các chấm nấc bước (Step Dots) trên track. |
|
|
331
|
+
| **`showValue`** | `boolean` | `false` | Hiển thị giá trị dạng text bên cạnh nhãn. |
|
|
332
|
+
| **`isFullWidth`** | `boolean` | `true` | Mở rộng 100% chiều rộng khung chứa. |
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## ♿ Khả năng tiếp cận (Accessibility / WAI-ARIA)
|
|
337
|
+
|
|
338
|
+
- **Tuân thủ WAI-ARIA Slider Pattern**: Tự động gán `role="slider"`, `tabIndex={0}`, `aria-valuenow`, `aria-valuemin`, `aria-valuemax` trên từng thumb.
|
|
339
|
+
- **Screen Reader Friendly**: Hỗ trợ `aria-valuetext` để phát âm các định dạng giá trị tùy biến (tiền tệ, phần trăm, độ C).
|
|
340
|
+
- **Hỗ trợ đầy đủ bàn phím**:
|
|
341
|
+
- `←` / `↓`: Giảm 1 bước (`-step`).
|
|
342
|
+
- `→` / `↑`: Tăng 1 bước (`+step`).
|
|
343
|
+
- `Page Down` / `Page Up`: Giảm/Tăng một khoảng lớn ($10\%$).
|
|
344
|
+
- `Home`: Về giá trị nhỏ nhất (`min`).
|
|
345
|
+
- `End`: Lên giá trị lớn nhất (`max`).
|
|
346
|
+
- **Liên kết ngữ cảnh lỗi**: Tự động gán `aria-invalid`, `aria-errormessage`, `aria-describedby` trỏ tới phần tử thông báo lỗi.
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# 📊 Table & DataTable Component (`@openway/ui`)
|
|
2
|
+
|
|
3
|
+
Bộ component **Table** và **DataTable** chuyên nghiệp, tích hợp sâu với **TanStack Table v9** (`@tanstack/react-table@^9.2.4`), thiết kế chuẩn **Design System**, hỗ trợ **Kiến trúc module 3 tầng**, **Sorting**, **Fuzzy Search / Filtering**, **Pagination**, **Row Selection**, **Column Visibility**, **Loading Skeleton**, **Empty State**, và tuân thủ tiêu chuẩn tiếp cận **WAI-ARIA Accessibility**.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🌟 Điểm nổi bật
|
|
8
|
+
|
|
9
|
+
- **Tích hợp TanStack Table v9 mới nhất**:
|
|
10
|
+
- Tận dụng kiến trúc module hóa (`tableFeatures`) giúp tối ưu bundle size và tree-shaking.
|
|
11
|
+
- Reactive state hiệu năng cao trên nền tảng **TanStack Store**, tương thích hoàn toàn với **React 19** và **React Compiler**.
|
|
12
|
+
- **Kiến trúc linh hoạt 3 tầng**:
|
|
13
|
+
- **Tầng 1 - Low-level UI Primitives**: `<Table>`, `<TableHeader>`, `<TableBody>`, `<TableFooter>`, `<TableRow>`, `<TableHead>`, `<TableCell>`, `<TableCaption>`. Dùng để dựng bảng HTML tùy biến theo phong cách Tailwind đồng bộ.
|
|
14
|
+
- **Tầng 2 - Headless Hook & Helpers**: `useDataTable()`, `createTableColumnHelper()`, `defaultTableFeatures` cung cấp type inference mạnh mẽ khi cần xây dựng custom datagrid.
|
|
15
|
+
- **Tầng 3 - High-level `<DataTable>`**: Component hoàn chỉnh "Plug & Play", sẵn sàng sử dụng với đầy đủ toolbar tìm kiếm, phân trang, chọn dòng, hiển thị cột.
|
|
16
|
+
- **Tính năng mạnh mẽ sẵn có**:
|
|
17
|
+
- ↕️ **Sắp xếp (Sorting)**: Đa chế độ (Tăng dần, Giảm dần, Mặc định), hỗ trợ multi-sort và header tương tác trực quan.
|
|
18
|
+
- 🔍 **Tìm kiếm & Lọc (Filtering)**: Thanh tìm kiếm toàn bảng (Global filter) mượt mà với nút xóa nhanh.
|
|
19
|
+
- 📄 **Phân trang chuyên nghiệp (Pagination)**: Điều hướng trang đầu, trang trước, trang sau, trang cuối; chọn số dòng mỗi trang (10, 20, 50, 100).
|
|
20
|
+
- ☑️ **Chọn dòng (Row Selection)**: Tự động render checkbox chọn từng dòng hoặc chọn toàn bộ dòng trên trang, kèm thanh hiển thị số lượng dòng đã chọn và khu vực Bulk Actions.
|
|
21
|
+
- 👁️ **Ẩn/hiện cột (Column Visibility)**: Dropdown menu popover cho phép người dùng chủ động chọn cột cần xem.
|
|
22
|
+
- ⏳ **Trạng thái tải & Rỗng thông minh**: Tự động render Skeleton rows khi `isLoading={true}` và hiển thị `<Empty>` minh họa khi bảng không có dữ liệu hoặc không tìm thấy kết quả tìm kiếm.
|
|
23
|
+
- 🌐 **Hỗ trợ cả Client-side & Server-side (Manual Mode)**: Linh hoạt kết nối API phân trang, tìm kiếm từ máy chủ với `manualPagination`, `manualSorting`, `pageCount`, `rowCount`.
|
|
24
|
+
- **3 Kích cỡ hiển thị (`size`)**: `sm` (gọn gàng, dense), `md` (chuẩn - *mặc định*), `lg` (thoáng đãng).
|
|
25
|
+
- **3 Biến thể giao diện (`variant`)**: `default` (viền thanh lịch), `striped` (xen kẽ màu dòng), `bordered` (viền ô đầy đủ).
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 🚀 Cài đặt & Import
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import {
|
|
33
|
+
DataTable,
|
|
34
|
+
Table,
|
|
35
|
+
TableHeader,
|
|
36
|
+
TableBody,
|
|
37
|
+
TableFooter,
|
|
38
|
+
TableRow,
|
|
39
|
+
TableHead,
|
|
40
|
+
TableCell,
|
|
41
|
+
TableCaption,
|
|
42
|
+
TableColumnHeader,
|
|
43
|
+
DraggableTableHead,
|
|
44
|
+
TablePagination,
|
|
45
|
+
TableToolbar,
|
|
46
|
+
useDataTable,
|
|
47
|
+
createTableColumnHelper,
|
|
48
|
+
defaultTableFeatures,
|
|
49
|
+
} from "@openway/ui";
|
|
50
|
+
|
|
51
|
+
import type {
|
|
52
|
+
DataTableProps,
|
|
53
|
+
TableProps,
|
|
54
|
+
TableSize,
|
|
55
|
+
TableVariant,
|
|
56
|
+
TableColumnHeaderProps,
|
|
57
|
+
DraggableTableHeadProps,
|
|
58
|
+
ColumnOrderState,
|
|
59
|
+
TablePaginationProps,
|
|
60
|
+
TableToolbarProps,
|
|
61
|
+
} from "@openway/ui";
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 📖 Hướng dẫn sử dụng
|
|
67
|
+
|
|
68
|
+
### 1. Dựng DataTable hoàn chỉnh với TanStack Table v9
|
|
69
|
+
|
|
70
|
+
Sử dụng `createTableColumnHelper` để tạo định nghĩa cột có type-safety và truyền vào component `<DataTable>`:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
import { DataTable, createTableColumnHelper } from "@openway/ui";
|
|
74
|
+
|
|
75
|
+
interface Product {
|
|
76
|
+
id: string;
|
|
77
|
+
name: string;
|
|
78
|
+
category: string;
|
|
79
|
+
price: number;
|
|
80
|
+
stock: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const columnHelper = createTableColumnHelper<Product>();
|
|
84
|
+
|
|
85
|
+
const columns = [
|
|
86
|
+
columnHelper.accessor("name", {
|
|
87
|
+
header: "Tên sản phẩm",
|
|
88
|
+
}),
|
|
89
|
+
columnHelper.accessor("category", {
|
|
90
|
+
header: "Danh mục",
|
|
91
|
+
}),
|
|
92
|
+
columnHelper.accessor("price", {
|
|
93
|
+
header: "Đơn giá",
|
|
94
|
+
cell: (info) => `${info.getValue().toLocaleString("vi-VN")} đ`,
|
|
95
|
+
}),
|
|
96
|
+
columnHelper.accessor("stock", {
|
|
97
|
+
header: "Tồn kho",
|
|
98
|
+
}),
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const mockData: Product[] = [
|
|
102
|
+
{ id: "1", name: "Bàn làm việc thông minh", category: "Nội thất", price: 3500000, stock: 12 },
|
|
103
|
+
{ id: "2", name: "Ghế công thái học", category: "Nội thất", price: 2800000, stock: 25 },
|
|
104
|
+
{ id: "3", name: "Đèn LED chống cận", category: "Thiết bị", price: 450000, stock: 80 },
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
export function ProductTableExample() {
|
|
108
|
+
return (
|
|
109
|
+
<DataTable
|
|
110
|
+
columns={columns}
|
|
111
|
+
data={mockData}
|
|
112
|
+
enableSorting={true}
|
|
113
|
+
enableFiltering={true}
|
|
114
|
+
enablePagination={true}
|
|
115
|
+
enableRowSelection={true}
|
|
116
|
+
initialPageSize={10}
|
|
117
|
+
searchPlaceholder="Tìm kiếm sản phẩm..."
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### 2. Sử dụng Low-level Primitives (Dựng bảng HTML thuần)
|
|
126
|
+
|
|
127
|
+
Nếu bạn chỉ cần một bảng dữ liệu đơn giản không cần engine TanStack Table:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
import {
|
|
131
|
+
Table,
|
|
132
|
+
TableHeader,
|
|
133
|
+
TableBody,
|
|
134
|
+
TableFooter,
|
|
135
|
+
TableRow,
|
|
136
|
+
TableHead,
|
|
137
|
+
TableCell,
|
|
138
|
+
TableCaption,
|
|
139
|
+
} from "@openway/ui";
|
|
140
|
+
|
|
141
|
+
export function SimpleTableExample() {
|
|
142
|
+
return (
|
|
143
|
+
<Table variant="striped" size="md">
|
|
144
|
+
<TableCaption>Bảng kê hóa đơn gần nhất</TableCaption>
|
|
145
|
+
<TableHeader>
|
|
146
|
+
<TableRow>
|
|
147
|
+
<TableHead>Mã HĐ</TableHead>
|
|
148
|
+
<TableHead>Khách hàng</TableHead>
|
|
149
|
+
<TableHead align="right">Tổng tiền</TableHead>
|
|
150
|
+
</TableRow>
|
|
151
|
+
</TableHeader>
|
|
152
|
+
<TableBody>
|
|
153
|
+
<TableRow>
|
|
154
|
+
<TableCell>#INV-001</TableCell>
|
|
155
|
+
<TableCell>Nguyễn Văn A</TableCell>
|
|
156
|
+
<TableCell align="right">1.200.000 đ</TableCell>
|
|
157
|
+
</TableRow>
|
|
158
|
+
<TableRow>
|
|
159
|
+
<TableCell>#INV-002</TableCell>
|
|
160
|
+
<TableCell>Trần Thị B</TableCell>
|
|
161
|
+
<TableCell align="right">850.000 đ</TableCell>
|
|
162
|
+
</TableRow>
|
|
163
|
+
</TableBody>
|
|
164
|
+
<TableFooter>
|
|
165
|
+
<TableRow>
|
|
166
|
+
<TableCell colSpan={2}>Tổng cộng</TableCell>
|
|
167
|
+
<TableCell align="right" className="font-bold">2.050.000 đ</TableCell>
|
|
168
|
+
</TableRow>
|
|
169
|
+
</TableFooter>
|
|
170
|
+
</Table>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### 3. Bulk Actions (Hành động hàng loạt khi chọn dòng)
|
|
178
|
+
|
|
179
|
+
`<DataTable>` hỗ trợ `renderBulkActions` để hiển thị các nút thao tác khi có dòng được chọn:
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
import { DataTable, Button, createTableColumnHelper } from "@openway/ui";
|
|
183
|
+
|
|
184
|
+
export function BulkActionsExample() {
|
|
185
|
+
return (
|
|
186
|
+
<DataTable
|
|
187
|
+
columns={columns}
|
|
188
|
+
data={mockData}
|
|
189
|
+
enableRowSelection={true}
|
|
190
|
+
renderBulkActions={(selectedRows) => (
|
|
191
|
+
<div className="flex items-center gap-2">
|
|
192
|
+
<Button
|
|
193
|
+
size="sm"
|
|
194
|
+
color="error"
|
|
195
|
+
variant="soft"
|
|
196
|
+
onClick={() => {
|
|
197
|
+
console.log("Xóa các dòng:", selectedRows.map((r) => r.original));
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
Xóa ({selectedRows.length})
|
|
201
|
+
</Button>
|
|
202
|
+
<Button size="sm" color="neutral" variant="outline">
|
|
203
|
+
Xuất Excel
|
|
204
|
+
</Button>
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
/>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
### 4. Chế độ Server-Side (Manual Mode)
|
|
215
|
+
|
|
216
|
+
Khi làm việc với API phân trang và lọc dữ liệu từ Backend:
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
import { useState } from "react";
|
|
220
|
+
import { DataTable } from "@openway/ui";
|
|
221
|
+
import type { PaginationState, SortingState } from "@openway/ui";
|
|
222
|
+
|
|
223
|
+
export function ServerSideTableExample() {
|
|
224
|
+
const [pagination, setPagination] = useState<PaginationState>({
|
|
225
|
+
pageIndex: 0,
|
|
226
|
+
pageSize: 10,
|
|
227
|
+
});
|
|
228
|
+
const [sorting, setSorting] = useState<SortingState>([]);
|
|
229
|
+
const [search, setSearch] = useState("");
|
|
230
|
+
|
|
231
|
+
// Gọi hook API fetch dữ liệu từ backend (ví dụ TanStack Query)
|
|
232
|
+
// const { data, isLoading } = useQuery(...)
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<DataTable
|
|
236
|
+
columns={columns}
|
|
237
|
+
data={apiData?.items ?? []}
|
|
238
|
+
isLoading={isFetching}
|
|
239
|
+
manualPagination={true}
|
|
240
|
+
manualSorting={true}
|
|
241
|
+
manualFiltering={true}
|
|
242
|
+
pageCount={apiData?.totalPages ?? 1}
|
|
243
|
+
rowCount={apiData?.totalItems ?? 0}
|
|
244
|
+
pagination={pagination}
|
|
245
|
+
onPaginationChange={setPagination}
|
|
246
|
+
sorting={sorting}
|
|
247
|
+
onSortingChange={setSorting}
|
|
248
|
+
globalFilter={search}
|
|
249
|
+
onGlobalFilterChange={setSearch}
|
|
250
|
+
/>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## ⚙️ Bảng thuộc tính (Props Table)
|
|
258
|
+
|
|
259
|
+
### `<DataTable />`
|
|
260
|
+
|
|
261
|
+
| Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
262
|
+
| :--- | :--- | :--- | :--- |
|
|
263
|
+
| `columns` | `ColumnDef<DefaultTableFeatures, TData, unknown>[]` | **Bắt buộc** | Mảng định nghĩa các cột hiển thị trong bảng |
|
|
264
|
+
| `data` | `TData[]` | **Bắt buộc** | Mảng dữ liệu hiển thị |
|
|
265
|
+
| `variant` | `'default' \| 'striped' \| 'bordered'` | `'default'` | Biến thể giao diện của bảng |
|
|
266
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Kích cỡ padding và font chữ của bảng |
|
|
267
|
+
| `isLoading` | `boolean` | `false` | Bật trạng thái đang tải dữ liệu với Skeleton rows |
|
|
268
|
+
| `isRefresh` | `boolean` | `false` | Bật nút làm mới dữ liệu (Refresh button) trên thanh công cụ |
|
|
269
|
+
| `onRefresh` | `() => void` | `undefined` | Callback được gọi khi người dùng bấm nút làm mới |
|
|
270
|
+
| `loadingRowsCount` | `number` | `5` | Số dòng skeleton hiển thị khi `isLoading={true}` |
|
|
271
|
+
| `emptyText` | `string` | `undefined` | Văn bản hiển thị khi bảng không có dữ liệu |
|
|
272
|
+
| `emptyIllustration` | `ReactNode` | `undefined` | Tùy biến illustration khi bảng rỗng |
|
|
273
|
+
| `enableSorting` | `boolean` | `true` | Bật tính năng sắp xếp khi click vào tiêu đề cột |
|
|
274
|
+
| `enableFiltering` | `boolean` | `true` | Hiển thị ô tìm kiếm toàn bảng trên toolbar |
|
|
275
|
+
| `enablePagination` | `boolean` | `true` | Bật thanh phân trang ở chân bảng |
|
|
276
|
+
| `enableRowSelection`| `boolean` | `false` | Tự động thêm cột Checkbox chọn dòng |
|
|
277
|
+
| `enableColumnVisibility` | `boolean` | `true` | Hiển thị menu bật/tắt ẩn hiện từng cột |
|
|
278
|
+
| `enableColumnOrdering` | `boolean` | `false` | Bật tính năng kéo thả thay đổi thứ tự các cột (Drag-and-Drop Column Ordering) |
|
|
279
|
+
| `columnOrder` | `string[]` | `undefined` | Trạng thái mảng thứ tự các cột điều khiển ngoài |
|
|
280
|
+
| `onColumnOrderChange` | `(order) => void` | `undefined` | Callback khi thứ tự cột thay đổi do kéo thả |
|
|
281
|
+
| `searchPlaceholder`| `string` | `"Tìm kiếm trong bảng..."` | Placeholder cho ô tìm kiếm |
|
|
282
|
+
| `toolbarActions` | `ReactNode` | `undefined` | Các nút hành động thêm ở góc phải toolbar |
|
|
283
|
+
| `renderBulkActions`| `(selectedRows) => ReactNode` | `undefined` | Render các nút hành động hàng loạt khi chọn dòng |
|
|
284
|
+
| `pageSizeOptions` | `number[]` | `[10, 20, 50, 100]` | Danh sách tùy chọn số dòng hiển thị mỗi trang |
|
|
285
|
+
| `initialPageSize` | `number` | `10` | Số lượng dòng hiển thị mặc định mỗi trang |
|
|
286
|
+
| `manualPagination` | `boolean` | `false` | Bật chế độ phân trang từ máy chủ (Server-side) |
|
|
287
|
+
| `manualSorting` | `boolean` | `false` | Bật chế độ sắp xếp từ máy chủ (Server-side) |
|
|
288
|
+
| `manualFiltering` | `boolean` | `false` | Bật chế độ tìm kiếm từ máy chủ (Server-side) |
|
|
289
|
+
| `pageCount` | `number` | `undefined` | Tổng số trang (khi dùng `manualPagination`) |
|
|
290
|
+
| `rowCount` | `number` | `undefined` | Tổng số dòng dữ liệu thực tế (khi dùng `manualPagination`) |
|
|
291
|
+
| `onRowClick` | `(row) => void` | `undefined` | Sự kiện click chuột vào một dòng |
|
|
292
|
+
| `className` | `string` | `""` | Class tùy biến cho wrapper bên ngoài |
|
|
293
|
+
| `containerClassName`| `string` | `""` | Class tùy biến cho khối bao quanh thẻ table |
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
### `<DraggableTableHead />`
|
|
298
|
+
|
|
299
|
+
| Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
300
|
+
| :--- | :--- | :--- | :--- |
|
|
301
|
+
| `header` | `Header<DefaultTableFeatures, TData, TValue>` | **Bắt buộc** | Đối tượng Header TanStack Table v9 đại diện cho cột |
|
|
302
|
+
| `canSort` | `boolean` | `false` | Cho phép click để sắp xếp dữ liệu của cột |
|
|
303
|
+
| `headerContent` | `ReactNode` | `undefined` | Nội dung phần tử bên trong tiêu đề |
|
|
304
|
+
| `ariaSort` | `'ascending' \| 'descending' \| 'none'` | `undefined` | Trạng thái WAI-ARIA aria-sort trên thẻ `<th>` |
|
|
305
|
+
| `disabled` | `boolean` | `false` | Vô hiệu hóa tính năng kéo thả cho cột chỉ định |
|
|
306
|
+
| `className` | `string` | `""` | Class tùy biến cho thẻ `<th>` |
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## ♿ Tiêu chuẩn tiếp cận (WAI-ARIA Accessibility)
|
|
311
|
+
|
|
312
|
+
- Sử dụng đầy đủ các semantic tags: `<table>`, `<thead>`, `<tbody>`, `<tfoot>`, `<tr>`, `<th>`, `<td>`, `<caption>`.
|
|
313
|
+
- `aria-sort="ascending" | "descending" | "none"` cập nhật trực tiếp trên thẻ tiêu đề cột `<th>` (`<TableHead />`) theo chuẩn WAI-ARIA (thay vì đặt trên thẻ `<button>`).
|
|
314
|
+
- `aria-selected="true"` và `data-state="selected"` cập nhật trên các `<tr>` được chọn.
|
|
315
|
+
- Điều hướng bàn phím: Tiêu đề cột và các nút phân trang có focus ring nổi bật (`focus-visible:ring-2`), hỗ trợ phím `Enter` và `Space`.
|
|
316
|
+
- Thẻ `<button>` sắp xếp và icon đều có nhãn `aria-label` đầy đủ cho các công cụ đọc màn hình (Screen Reader).
|