@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,318 @@
|
|
|
1
|
+
# 🗂️ Collapse Component (`@owa/ui`)
|
|
2
|
+
|
|
3
|
+
Component **Collapse** (Accordion / Collapsible panel) hiện đại, hiệu năng cao, thiết kế chuẩn **Declarative Compound Components Pattern** (`<Collapse>`, `<CollapsePanel>`, `<CollapseHeader>`, `<CollapseContent>`, `<Collapsible>`), tích hợp **CSS Grid Height Transition**, **Accordion Mode**, **Custom Slots & Subcomponents**, **Safe Config Fallback** (`getSafeConfig`) 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 `<Collapse>`, `<CollapsePanel>`, `<CollapseHeader>`, `<CollapseContent>`, hỗ trợ 2 phong cách viết linh hoạt (khai báo nhanh qua Props hoặc tùy biến sâu qua Subcomponents).
|
|
10
|
+
- **Hiệu ứng gập mở siêu mượt (CSS Grid Transition)**: Sử dụng kỹ thuật chuyển đổi `transition-[grid-template-rows]` (`grid-rows-[1fr]` khi mở và `grid-rows-[0fr]` khi đóng), tự động tính toán chiều cao nội dung động mà không cần đo đạc DOM bằng JavaScript.
|
|
11
|
+
- **Chế độ Accordion & Multiple Open**:
|
|
12
|
+
- `accordion={true}`: Tự động đóng các panel khác khi mở một panel mới.
|
|
13
|
+
- `accordion={false}`: Cho phép mở nhiều panel đồng thời.
|
|
14
|
+
- **Chế độ Controlled & Uncontrolled**:
|
|
15
|
+
- **Controlled**: Quản lý bằng `activeKey` + callback `onChange`.
|
|
16
|
+
- **Uncontrolled**: Tự quản lý nội bộ với `defaultActiveKey`.
|
|
17
|
+
- **5 Biến thể giao diện (`variant`)**:
|
|
18
|
+
- `outlined` (*mặc định*): Khung viền ngoài và các đường kẻ phân cách mang màu sắc chủ đề.
|
|
19
|
+
- `filled`: Khối nền mang màu sắc chủ đề nhẹ tạo cảm giác liền mạch.
|
|
20
|
+
- `ghost`: Trong suốt, không viền ngoài tối giản.
|
|
21
|
+
- `separated`: Mỗi panel là một thẻ card riêng biệt cách nhau bởi khoảng trống `space-y-3`.
|
|
22
|
+
- `other`: Không áp dụng style mặc định, tự do tùy biến qua `className`.
|
|
23
|
+
- **3 Kích thước tiêu chuẩn (`size`)**: `sm`, `md` (*mặc định*), `lg`.
|
|
24
|
+
- **7 Chủ đề màu sắc (`color`)**: `primary`, `secondary`, `neutral`, `error`, `success`, `warning`, `info`.
|
|
25
|
+
- **6 Kiểu bo góc (`radius`)**: `none`, `sm`, `md` (*mặc định*), `lg`, `xl`, `full`.
|
|
26
|
+
- **Tùy biến icon & vị trí mũi tên (`expandIconPosition`)**:
|
|
27
|
+
- `expandIconPosition`: `"right"` (*mặc định*), `"left"`, hoặc `"none"`.
|
|
28
|
+
- `expandIcon`: Tùy biến icon hoặc render function `({ isActive, disabled }) => ReactNode`.
|
|
29
|
+
- **Extra Slot & Hành động phụ**: Hỗ trợ truyền badge, nút bấm, icon action (`extra`) vào header mà không kích hoạt sự kiện toggle của header.
|
|
30
|
+
- **Giải phóng bộ nhớ (`destroyInactivePanel`)**: Tự động unmount nội dung khỏi DOM khi panel bị đóng.
|
|
31
|
+
- **Component độc lập `<Collapsible>`**: Cung cấp khung gập mở độc lập nhẹ nhàng cho bất kỳ nội dung nào.
|
|
32
|
+
- **WAI-ARIA Accessibility & Bàn phím**: Tự động gắn `role="region"`, `aria-expanded`, `aria-controls`, `aria-labelledby`, hỗ trợ đầy đủ phím `Enter` và `Space`.
|
|
33
|
+
- **Safe Config Fallback**: Tích hợp `getSafeConfig` từ `@/utils/function` đảm bảo an toàn tuyệt đối, không crash ứng dụng khi truyền prop không hợp lệ.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 🚀 Cài đặt & Import
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import {
|
|
41
|
+
Collapse,
|
|
42
|
+
CollapsePanel,
|
|
43
|
+
CollapseHeader,
|
|
44
|
+
CollapseContent,
|
|
45
|
+
Collapsible,
|
|
46
|
+
useCollapseContext,
|
|
47
|
+
useCollapsePanelContext,
|
|
48
|
+
collapseSizeConfig,
|
|
49
|
+
collapseRadiusConfig,
|
|
50
|
+
collapseVariantContainerConfig,
|
|
51
|
+
collapseVariantPanelConfig,
|
|
52
|
+
collapseVariantHeaderConfig,
|
|
53
|
+
collapseColorConfig,
|
|
54
|
+
} from "@owa/ui";
|
|
55
|
+
|
|
56
|
+
import type {
|
|
57
|
+
CollapseProps,
|
|
58
|
+
CollapsePanelProps,
|
|
59
|
+
CollapseHeaderProps,
|
|
60
|
+
CollapseContentProps,
|
|
61
|
+
CollapsibleProps,
|
|
62
|
+
CollapseSize,
|
|
63
|
+
CollapseVariant,
|
|
64
|
+
CollapseColor,
|
|
65
|
+
CollapseRadius,
|
|
66
|
+
CollapseExpandIconPosition,
|
|
67
|
+
CollapseActiveKey,
|
|
68
|
+
} from "@owa/ui";
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 📖 Hướng dẫn sử dụng
|
|
74
|
+
|
|
75
|
+
### 1. Cách 1: Khai báo nhanh qua Props (Declarative Panel Props)
|
|
76
|
+
|
|
77
|
+
Phù hợp cho hầu hết các trường hợp thông dụng:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import { Collapse, CollapsePanel } from "@owa/ui";
|
|
81
|
+
|
|
82
|
+
export function BasicCollapse() {
|
|
83
|
+
return (
|
|
84
|
+
<Collapse defaultActiveKey={["1"]} variant="outlined" color="primary">
|
|
85
|
+
<CollapsePanel
|
|
86
|
+
value="1"
|
|
87
|
+
label="1. Giới thiệu tổng quan"
|
|
88
|
+
description="Thông tin nền tảng và kiến trúc hệ thống"
|
|
89
|
+
>
|
|
90
|
+
<p className="text-neutral-600">
|
|
91
|
+
Đây là nội dung chi tiết của panel đầu tiên.
|
|
92
|
+
</p>
|
|
93
|
+
</CollapsePanel>
|
|
94
|
+
|
|
95
|
+
<CollapsePanel value="2" label="2. Hướng dẫn cài đặt nhanh">
|
|
96
|
+
<p className="text-neutral-600">
|
|
97
|
+
Chạy lệnh <code>pnpm add @owa/ui</code> để bắt đầu sử dụng.
|
|
98
|
+
</p>
|
|
99
|
+
</CollapsePanel>
|
|
100
|
+
|
|
101
|
+
<CollapsePanel value="3" label="3. Câu hỏi thường gặp" disabled>
|
|
102
|
+
<p className="text-neutral-600">
|
|
103
|
+
Panel này đang bị vô hiệu hóa.
|
|
104
|
+
</p>
|
|
105
|
+
</CollapsePanel>
|
|
106
|
+
</Collapse>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### 2. Cách 2: Tùy biến sâu qua Subcomponents (`<CollapseHeader>` & `<CollapseContent>`)
|
|
114
|
+
|
|
115
|
+
Cho phép bạn tự do composition cấu trúc giao diện phức tạp:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
import { Collapse, CollapsePanel, CollapseHeader, CollapseContent } from "@owa/ui";
|
|
119
|
+
import { Badge, Button } from "@owa/ui";
|
|
120
|
+
|
|
121
|
+
export function CustomSlotCollapse() {
|
|
122
|
+
return (
|
|
123
|
+
<Collapse variant="separated">
|
|
124
|
+
<CollapsePanel value="order-101">
|
|
125
|
+
<CollapseHeader
|
|
126
|
+
extra={
|
|
127
|
+
<div className="flex items-center gap-2">
|
|
128
|
+
<Badge color="success">Đã thanh toán</Badge>
|
|
129
|
+
<Button size="sm" variant="ghost">In hóa đơn</Button>
|
|
130
|
+
</div>
|
|
131
|
+
}
|
|
132
|
+
>
|
|
133
|
+
<span className="font-bold text-neutral-900">
|
|
134
|
+
Đơn hàng #101 - $149.00
|
|
135
|
+
</span>
|
|
136
|
+
</CollapseHeader>
|
|
137
|
+
<CollapseContent>
|
|
138
|
+
<div className="space-y-2">
|
|
139
|
+
<p>Khách hàng: Nguyễn Văn A</p>
|
|
140
|
+
<p>Địa chỉ: 123 Đường Lê Lợi, Q.1, TP.HCM</p>
|
|
141
|
+
</div>
|
|
142
|
+
</CollapseContent>
|
|
143
|
+
</CollapsePanel>
|
|
144
|
+
</Collapse>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### 3. Chế độ Accordion (Chỉ mở 1 panel tại một thời điểm)
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
<Collapse accordion defaultActiveKey="faq-1" color="primary">
|
|
155
|
+
<CollapsePanel value="faq-1" label="Làm sao để đổi mật khẩu?">
|
|
156
|
+
<p>Truy cập mục Cài đặt tài khoản và chọn Đổi mật khẩu.</p>
|
|
157
|
+
</CollapsePanel>
|
|
158
|
+
<CollapsePanel value="faq-2" label="Chính sách hoàn tiền như thế nào?">
|
|
159
|
+
<p>Chúng tôi hoàn tiền 100% trong vòng 30 ngày đầu tiên.</p>
|
|
160
|
+
</CollapsePanel>
|
|
161
|
+
</Collapse>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### 4. Các biến thể giao diện (`variant`)
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
{/* 1. Outlined (Mặc định) */}
|
|
170
|
+
<Collapse variant="outlined">...</Collapse>
|
|
171
|
+
|
|
172
|
+
{/* 2. Filled */}
|
|
173
|
+
<Collapse variant="filled">...</Collapse>
|
|
174
|
+
|
|
175
|
+
{/* 3. Ghost (Tối giản) */}
|
|
176
|
+
<Collapse variant="ghost">...</Collapse>
|
|
177
|
+
|
|
178
|
+
{/* 4. Separated (Thẻ card rời rạc) */}
|
|
179
|
+
<Collapse variant="separated">...</Collapse>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### 5. Chế độ điều khiển chủ động (Controlled Mode)
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
import { useState } from "react";
|
|
188
|
+
import { Collapse, CollapsePanel } from "@owa/ui";
|
|
189
|
+
|
|
190
|
+
export function ControlledCollapse() {
|
|
191
|
+
const [activeKeys, setActiveKeys] = useState<string | number | (string | number)[]>(["1"]);
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<div>
|
|
195
|
+
<div className="flex gap-2 mb-3">
|
|
196
|
+
<button onClick={() => setActiveKeys(["1"])} className="px-3 py-1 bg-primary-600 text-white rounded">
|
|
197
|
+
Mở Panel 1
|
|
198
|
+
</button>
|
|
199
|
+
<button onClick={() => setActiveKeys(["1", "2"])} className="px-3 py-1 bg-primary-600 text-white rounded">
|
|
200
|
+
Mở cả 1 và 2
|
|
201
|
+
</button>
|
|
202
|
+
<button onClick={() => setActiveKeys([])} className="px-3 py-1 bg-neutral-600 text-white rounded">
|
|
203
|
+
Đóng tất cả
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<Collapse activeKey={activeKeys} onChange={setActiveKeys}>
|
|
208
|
+
<CollapsePanel value="1" label="Panel 1">Nội dung 1</CollapsePanel>
|
|
209
|
+
<CollapsePanel value="2" label="Panel 2">Nội dung 2</CollapsePanel>
|
|
210
|
+
</Collapse>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
### 6. Sử dụng component gập mở độc lập `<Collapsible>`
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
import { useState } from "react";
|
|
222
|
+
import { Collapsible, Button } from "@owa/ui";
|
|
223
|
+
|
|
224
|
+
export function CollapsibleDemo() {
|
|
225
|
+
const [open, setOpen] = useState(false);
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<div className="border p-4 rounded-xl">
|
|
229
|
+
<Button onClick={() => setOpen(!open)}>
|
|
230
|
+
{open ? "Thu gọn chi tiết" : "Xem thêm chi tiết"}
|
|
231
|
+
</Button>
|
|
232
|
+
|
|
233
|
+
<Collapsible open={open} className="mt-3">
|
|
234
|
+
<div className="p-3 bg-neutral-50 rounded-lg">
|
|
235
|
+
Nội dung mở rộng linh hoạt không cần nằm trong Collapse list.
|
|
236
|
+
</div>
|
|
237
|
+
</Collapsible>
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## 📊 Bảng thuộc tính Props
|
|
246
|
+
|
|
247
|
+
### `<Collapse>`
|
|
248
|
+
|
|
249
|
+
| Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
250
|
+
| :--- | :--- | :--- | :--- |
|
|
251
|
+
| `activeKey` | `string \| number \| (string \| number)[]` | - | Khóa các panel đang mở (Controlled mode) |
|
|
252
|
+
| `defaultActiveKey` | `string \| number \| (string \| number)[]` | - | Khóa các panel mở ban đầu (Uncontrolled mode) |
|
|
253
|
+
| `onChange` | `(activeKey) => void` | - | Callback kích hoạt khi trạng thái mở/đóng thay đổi |
|
|
254
|
+
| `accordion` | `boolean` | `false` | Chế độ chỉ mở tối đa 1 panel |
|
|
255
|
+
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Kích thước hiển thị |
|
|
256
|
+
| `variant` | `"outlined" \| "filled" \| "ghost" \| "separated" \| "other"` | `"outlined"` | Biến thể kiểu dáng giao diện |
|
|
257
|
+
| `color` | `"primary" \| "secondary" \| "neutral" \| "error" \| "success" \| "warning" \| "info"` | `"primary"` | Chủ đề màu sắc |
|
|
258
|
+
| `radius` | `"none" \| "sm" \| "md" \| "lg" \| "xl" \| "full"` | `"md"` | Tùy chỉnh bo góc |
|
|
259
|
+
| `expandIconPosition` | `"left" \| "right" \| "none"` | `"right"` | Vị trí icon mũi tên |
|
|
260
|
+
| `expandIcon` | `ReactNode \| ((props) => ReactNode)` | - | Custom icon mũi tên |
|
|
261
|
+
| `destroyInactivePanel`| `boolean` | `false` | Tự động unmount nội dung DOM khi panel đóng |
|
|
262
|
+
| `children` | `ReactNode` | - | Danh sách các `<CollapsePanel>` |
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
### `<CollapsePanel>`
|
|
267
|
+
|
|
268
|
+
| Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
269
|
+
| :--- | :--- | :--- | :--- |
|
|
270
|
+
| `value` | `string \| number` | **Bắt buộc** | Khóa định danh duy nhất của panel |
|
|
271
|
+
| `label` | `ReactNode` | - | Tiêu đề panel (khi dùng cách khai báo nhanh) |
|
|
272
|
+
| `description` | `ReactNode` | - | Phụ đề mô tả ngắn |
|
|
273
|
+
| `startIcon` | `ReactNode` | - | Icon đặt phía trước tiêu đề |
|
|
274
|
+
| `extra` | `ReactNode` | - | Nội dung phụ bên phải (badge, action) |
|
|
275
|
+
| `disabled` | `boolean` | `false` | Vô hiệu hóa panel |
|
|
276
|
+
| `showArrow` | `boolean` | `true` | Hiển thị icon mũi tên |
|
|
277
|
+
| `destroyInactivePanel`| `boolean` | - | Ghi đè cấu hình unmount cho riêng panel này |
|
|
278
|
+
| `children` | `ReactNode` | - | Nội dung body hoặc `<CollapseHeader>` & `<CollapseContent>` |
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
### `<CollapseHeader>`
|
|
283
|
+
|
|
284
|
+
| Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
285
|
+
| :--- | :--- | :--- | :--- |
|
|
286
|
+
| `startIcon` | `ReactNode` | - | Icon đặt phía trước tiêu đề |
|
|
287
|
+
| `description` | `ReactNode` | - | Phụ đề mô tả |
|
|
288
|
+
| `extra` | `ReactNode` | - | Vùng hành động phụ bên phải |
|
|
289
|
+
| `showArrow` | `boolean` | `true` | Hiển thị mũi tên mở rộng |
|
|
290
|
+
| `children` | `ReactNode` | - | Nội dung tiêu đề JSX |
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
### `<CollapseContent>`
|
|
295
|
+
|
|
296
|
+
| Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
297
|
+
| :--- | :--- | :--- | :--- |
|
|
298
|
+
| `destroyInactivePanel`| `boolean` | - | Tự động gỡ nội dung khỏi DOM khi đóng |
|
|
299
|
+
| `children` | `ReactNode` | - | Nội dung chi tiết bên trong panel |
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## ⌨️ Phím tắt & Trợ năng (Accessibility)
|
|
304
|
+
|
|
305
|
+
- **`role="region"`**: Thẻ nội dung được đánh dấu là vùng nội dung bổ sung.
|
|
306
|
+
- **`aria-expanded` & `aria-controls`**: Nút Header tự động đồng bộ trạng thái đóng/mở với ID của Content panel.
|
|
307
|
+
- **Phím `Enter` / `Space`**: Nhấn để đóng hoặc mở panel khi đang focus vào tiêu đề.
|
|
308
|
+
- **Ngăn chặn xung đột sự kiện**: Vùng `extra` được tách biệt bên ngoài thẻ `<button>` trigger để tránh lỗi nested interactive element.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## 🧪 Kiểm thử Component (Cypress Testing)
|
|
313
|
+
|
|
314
|
+
Component được kiểm thử 100% bằng **Cypress Component Testing** tại [`Collapse.cy.tsx`](Collapse.cy.tsx):
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
pnpm --filter @owa/ui cypress:run --spec "src/components/collapse/Collapse.cy.tsx"
|
|
318
|
+
```
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# 📦 Confirm Component (`@owa/ui`)
|
|
2
|
+
|
|
3
|
+
Bộ component **Confirm** (Hộp thoại xác nhận) hiệu năng cao, chuẩn **HTML5 Native `<dialog>`** và thiết kế theo mô hình **Pure Compound Pattern** linh hoạt.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🌟 Điểm nổi bật
|
|
8
|
+
|
|
9
|
+
- **Native Top Layer**: Sử dụng thẻ `<dialog>` kết hợp phương thức `dialog.showModal()` của trình duyệt. Confirm tự động nổi lên tầng cao nhất (`#top-layer`), không bao giờ bị cắt xén bởi phần tử cha có `overflow: hidden`, `overflow: auto` hay xung đột `z-index`.
|
|
10
|
+
- **Zero Global Store**: Loại bỏ hoàn toàn Zustand / Redux / React Portal. Bạn có thể render Confirm ở bất kỳ đâu trong cây JSX.
|
|
11
|
+
- **Mượt mà với Exit Animation**: Tích hợp sẵn hiệu ứng mở và thoát (**Exit Animation 300ms**) cho cả hộp thoại lẫn nền mờ backdrop.
|
|
12
|
+
- **Pure Compound Architecture**: Tách rời hoàn toàn giữa tầng Container (`<ConfirmContainer>`) và tầng giao diện (`<Confirm>`, `<ConfirmHeader>`, `<ConfirmBody>`, `<ConfirmFooter>`, `<ConfirmClose>`).
|
|
13
|
+
- **Tự động liên kết Context**: Nút đóng `(X)` trong `<ConfirmHeader>` và component `<ConfirmClose>` tự động kích hoạt đóng kèm Exit Animation mà không cần truyền lặp lại state `open/onClose`.
|
|
14
|
+
- **Hỗ trợ Loading an toàn (`isLoading`)**: Tự động khóa đóng modal (chặn click backdrop, chặn ESC, tự động disable nút đóng `(X)`, nút Hủy và nút bọc `<ConfirmClose>`) khi đang trong tiến trình xử lý API/tải dữ liệu.
|
|
15
|
+
- **Hỗ trợ đầy đủ Accessibility (a11y)**: Tự động khóa cuộn trang (`body scroll lock`), bắt phím `ESC`, focus trap của trình duyệt, liên kết `aria-labelledby` và `aria-describedby`.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🚀 Cài đặt & Import
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import {
|
|
23
|
+
ConfirmContainer,
|
|
24
|
+
Confirm,
|
|
25
|
+
ConfirmHeader,
|
|
26
|
+
ConfirmBody,
|
|
27
|
+
ConfirmFooter,
|
|
28
|
+
ConfirmClose,
|
|
29
|
+
useConfirmContext,
|
|
30
|
+
} from "@owa/ui";
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 📖 Hướng dẫn sử dụng
|
|
36
|
+
|
|
37
|
+
### 1. Cách sử dụng cơ bản (Pure Compound Pattern)
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { useState } from "react";
|
|
41
|
+
import {
|
|
42
|
+
Button,
|
|
43
|
+
ConfirmContainer,
|
|
44
|
+
Confirm,
|
|
45
|
+
ConfirmHeader,
|
|
46
|
+
ConfirmBody,
|
|
47
|
+
ConfirmFooter,
|
|
48
|
+
} from "@owa/ui";
|
|
49
|
+
|
|
50
|
+
export function BasicConfirmExample() {
|
|
51
|
+
const [open, setOpen] = useState(false);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div>
|
|
55
|
+
<Button color="error" onClick={() => setOpen(true)}>
|
|
56
|
+
Xóa mục này
|
|
57
|
+
</Button>
|
|
58
|
+
|
|
59
|
+
<ConfirmContainer open={open} size="md" color="error" onClose={() => setOpen(false)}>
|
|
60
|
+
<Confirm>
|
|
61
|
+
<ConfirmHeader title="Xác nhận xóa dữ liệu?" />
|
|
62
|
+
<ConfirmBody>
|
|
63
|
+
Hành động này không thể hoàn tác. Bạn có chắc chắn muốn xóa không?
|
|
64
|
+
</ConfirmBody>
|
|
65
|
+
<ConfirmFooter
|
|
66
|
+
confirmText="Xóa vĩnh viễn"
|
|
67
|
+
cancelText="Hủy bỏ"
|
|
68
|
+
onConfirm={() => {
|
|
69
|
+
console.log("Đã xóa!");
|
|
70
|
+
setOpen(false);
|
|
71
|
+
}}
|
|
72
|
+
onCancel={() => setOpen(false)}
|
|
73
|
+
/>
|
|
74
|
+
</Confirm>
|
|
75
|
+
</ConfirmContainer>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### 2. Xử lý bất đồng bộ & Khóa an toàn (`isLoading`)
|
|
84
|
+
|
|
85
|
+
Khi xử lý logic bất đồng bộ (API call) trong `onConfirm`, bạn có thể truyền hàm async cho `onConfirm` trên `<ConfirmFooter>` hoặc kiểm soát prop `isLoading={loading}` trên `<ConfirmContainer>`:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import { useState } from "react";
|
|
89
|
+
import {
|
|
90
|
+
Button,
|
|
91
|
+
ConfirmContainer,
|
|
92
|
+
Confirm,
|
|
93
|
+
ConfirmHeader,
|
|
94
|
+
ConfirmBody,
|
|
95
|
+
ConfirmFooter,
|
|
96
|
+
} from "@owa/ui";
|
|
97
|
+
|
|
98
|
+
export function AsyncConfirmExample() {
|
|
99
|
+
const [open, setOpen] = useState(false);
|
|
100
|
+
|
|
101
|
+
const handleConfirmAction = async () => {
|
|
102
|
+
// Tự động bật Spinner và khóa tương tác trong suốt thời gian API chạy
|
|
103
|
+
await apiDeleteResource();
|
|
104
|
+
setOpen(false);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div>
|
|
109
|
+
<Button onClick={() => setOpen(true)}>Xóa tài khoản</Button>
|
|
110
|
+
|
|
111
|
+
<ConfirmContainer open={open} color="error" onClose={() => setOpen(false)}>
|
|
112
|
+
<Confirm>
|
|
113
|
+
<ConfirmHeader title="Xác nhận xóa tài khoản" />
|
|
114
|
+
<ConfirmBody>
|
|
115
|
+
Quá trình này có thể mất vài giây để hoàn tất dọn dẹp dữ liệu.
|
|
116
|
+
</ConfirmBody>
|
|
117
|
+
<ConfirmFooter
|
|
118
|
+
confirmText="Xóa tài khoản"
|
|
119
|
+
onConfirm={handleConfirmAction}
|
|
120
|
+
onCancel={() => setOpen(false)}
|
|
121
|
+
/>
|
|
122
|
+
</Confirm>
|
|
123
|
+
</ConfirmContainer>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Khi đang loading:
|
|
130
|
+
- Người dùng bấm phím `ESC` sẽ **bị chặn**.
|
|
131
|
+
- Click ra ngoài vùng backdrop overlay sẽ **bị chặn**.
|
|
132
|
+
- Nút đóng `(X)` và nút `Hủy` tự động bị **vô hiệu hóa (`disabled`)**.
|
|
133
|
+
- Nút `Xác nhận` hiển thị Spinner loading.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### 3. Thiết kế Compound Components tùy biến linh hoạt
|
|
138
|
+
|
|
139
|
+
Bạn có thể tự do tùy biến giao diện bên trong Confirm bằng các sub-component:
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
import {
|
|
143
|
+
ConfirmContainer,
|
|
144
|
+
Confirm,
|
|
145
|
+
ConfirmHeader,
|
|
146
|
+
ConfirmBody,
|
|
147
|
+
ConfirmFooter,
|
|
148
|
+
ConfirmClose,
|
|
149
|
+
Button,
|
|
150
|
+
} from "@owa/ui";
|
|
151
|
+
|
|
152
|
+
export function CompoundConfirmExample({ open, onClose }) {
|
|
153
|
+
return (
|
|
154
|
+
<ConfirmContainer open={open} size="lg" color="warning" onClose={onClose}>
|
|
155
|
+
<Confirm>
|
|
156
|
+
<ConfirmHeader title="Cảnh báo cập nhật hệ thống" showCloseButton />
|
|
157
|
+
<ConfirmBody>
|
|
158
|
+
<div className="space-y-3">
|
|
159
|
+
<p className="text-sm text-neutral-600">
|
|
160
|
+
Một số dịch vụ có thể tạm ngưng trong ít phút khi bảo trì.
|
|
161
|
+
</p>
|
|
162
|
+
<div className="p-3 bg-warning-50 border border-warning-200 rounded-md text-xs text-warning-800">
|
|
163
|
+
⚠️ Vui lòng lưu lại công việc hiện tại trước khi tiếp tục.
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</ConfirmBody>
|
|
167
|
+
<ConfirmFooter>
|
|
168
|
+
{/* ConfirmClose tự động đóng kèm Exit Animation */}
|
|
169
|
+
<ConfirmClose>
|
|
170
|
+
<Button variant="outline">Để sau</Button>
|
|
171
|
+
</ConfirmClose>
|
|
172
|
+
<Button color="warning" onClick={() => proceedUpdate()}>
|
|
173
|
+
Bắt đầu cập nhật
|
|
174
|
+
</Button>
|
|
175
|
+
</ConfirmFooter>
|
|
176
|
+
</Confirm>
|
|
177
|
+
</ConfirmContainer>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### 4. Đóng Confirm lập trình qua Hook (`useConfirmContext`)
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
import { useConfirmContext, Button } from "@owa/ui";
|
|
188
|
+
|
|
189
|
+
function CustomChildAction() {
|
|
190
|
+
const { onClose, isLoading } = useConfirmContext();
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<Button onClick={onClose} disabled={isLoading}>
|
|
194
|
+
Đóng hộp thoại
|
|
195
|
+
</Button>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
### 5. Tùy biến Kích thước (`size`), Chủ đề màu (`color`) & Bo góc (`radius`)
|
|
203
|
+
|
|
204
|
+
Confirm hỗ trợ 5 kích thước chiều rộng và 7 chủ đề màu sắc:
|
|
205
|
+
|
|
206
|
+
| Kích thước (`size`) | Chiều rộng (`max-w`) |
|
|
207
|
+
| :--- | :--- |
|
|
208
|
+
| `xs` | `320px` |
|
|
209
|
+
| `sm` | `380px` |
|
|
210
|
+
| `md` *(mặc định)* | `440px` |
|
|
211
|
+
| `lg` | `520px` |
|
|
212
|
+
| `xl` | `600px` |
|
|
213
|
+
|
|
214
|
+
Chủ đề màu (`color`):
|
|
215
|
+
- `primary`, `secondary`, `neutral`, `error`, `success`, `warning` *(mặc định)*, `info`.
|
|
216
|
+
|
|
217
|
+
Độ ưu tiên truyền props:
|
|
218
|
+
> **`Prop truyền trực tiếp vào component con`** > **`Prop size / color từ ConfirmContainer qua Context`** > **`Mặc định ("md" / "warning")`**.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 📚 Bảng tra cứu Props (API Reference)
|
|
223
|
+
|
|
224
|
+
### `<ConfirmContainer>`
|
|
225
|
+
|
|
226
|
+
Tầng Wrapper quản lý Overlay Backdrop, Native Dialog, Animation thoát và phím ESC.
|
|
227
|
+
|
|
228
|
+
| Prop | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
229
|
+
| :--- | :--- | :--- | :--- |
|
|
230
|
+
| `open` | `boolean` | `false` | Trạng thái hiển thị mở/đóng Confirm |
|
|
231
|
+
| `onClose` | `() => void` | `undefined` | Callback khi Confirm đóng (click backdrop, bấm ESC, bấm nút X) |
|
|
232
|
+
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Kích thước tổng thể của Confirm (truyền xuống context để đồng bộ kích thước cho Confirm, Header, Body, Footer) |
|
|
233
|
+
| `color` | `'primary' \| 'secondary' \| 'neutral' \| 'error' \| 'success' \| 'warning' \| 'info'` | `'warning'` | Chủ đề màu sắc tổng thể (truyền xuống context để đồng bộ cho Confirm, Header, Footer) |
|
|
234
|
+
| `isLoading` | `boolean` | `false` | Trạng thái đang tải/xử lý. Khi `true`, sẽ chặn đóng Confirm và tự động `disabled` các nút đóng |
|
|
235
|
+
| `closeOnOverlayClick` | `boolean` | `true` | Cho phép đóng Confirm khi click vào lớp nền backdrop bên ngoài (bị chặn khi `isLoading = true`) |
|
|
236
|
+
| `closeOnEsc` | `boolean` | `true` | Cho phép đóng Confirm khi nhấn phím `ESC` trên bàn phím (bị chặn khi `isLoading = true`) |
|
|
237
|
+
| `lockScroll` | `boolean` | `true` | Tự động khóa cuộn trang (`body overflow: hidden`) khi mở Confirm |
|
|
238
|
+
| `overlayClassName` | `string` | `""` | Class CSS tùy biến cho lớp nền backdrop mờ toàn màn hình |
|
|
239
|
+
| `className` | `string` | `""` | Class CSS cho container bao ngoài dialog |
|
|
240
|
+
| `children` | `ReactNode` | — | Nội dung bên trong container (thường là `<Confirm>`) |
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### `<Confirm>`
|
|
245
|
+
|
|
246
|
+
Khung giao diện hộp thoại xác nhận (Pure Compound Dialog Box).
|
|
247
|
+
|
|
248
|
+
| Prop | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
249
|
+
| :--- | :--- | :--- | :--- |
|
|
250
|
+
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | *Kế thừa từ ConfirmContainer* | Kích thước chiều rộng của hộp thoại |
|
|
251
|
+
| `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'rounded-lg'` | Mức độ bo góc |
|
|
252
|
+
| `className` | `string` | `""` | Class CSS tùy biến cho hộp thoại |
|
|
253
|
+
| `ref` | `Ref<HTMLDivElement>` | `undefined` | Ref trỏ trực tiếp đến thẻ div hộp thoại |
|
|
254
|
+
| `children` | `ReactNode` | — | Các sub-component con (`<ConfirmHeader>`, `<ConfirmBody>`, `<ConfirmFooter>`) |
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
### `<ConfirmHeader>`
|
|
259
|
+
|
|
260
|
+
Phần đầu của hộp thoại Confirm (hiển thị icon badge, tiêu đề và nút đóng X).
|
|
261
|
+
|
|
262
|
+
| Prop | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
263
|
+
| :--- | :--- | :--- | :--- |
|
|
264
|
+
| `title` | `ReactNode` | `undefined` | Tiêu đề chính của Confirm |
|
|
265
|
+
| `icon` | `ReactNode \| boolean` | `true` | Icon hiển thị cạnh tiêu đề (`true` để lấy theo màu, `false` để ẩn) |
|
|
266
|
+
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | *Kế thừa từ Context* | Kích thước áp dụng cho tiêu đề, icon và padding |
|
|
267
|
+
| `color` | `'primary' \| 'secondary' \| ...` | *Kế thừa từ Context* | Chủ đề màu sắc cho icon badge |
|
|
268
|
+
| `showCloseButton` | `boolean` | `false` | Hiển thị nút đóng `(X)` ở góc trên bên phải (tự động `disabled` khi `isLoading = true`) |
|
|
269
|
+
| `onClose` | `() => void` | `undefined` | Callback khi click nút `(X)` (tự động kích hoạt Exit Animation của ConfirmContainer) |
|
|
270
|
+
| `iconClassName` | `string` | `""` | Class CSS riêng cho container icon badge |
|
|
271
|
+
| `titleClassName` | `string` | `""` | Class CSS riêng cho text tiêu đề |
|
|
272
|
+
| `closeButtonClassName`| `string` | `""` | Class CSS tùy biến nút đóng `(X)` |
|
|
273
|
+
| `className` | `string` | `""` | Class CSS cho toàn bộ khối header |
|
|
274
|
+
| `children` | `ReactNode` | `undefined` | Nội dung tùy biến bên trong header |
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
### `<ConfirmBody>`
|
|
279
|
+
|
|
280
|
+
Phần thân chứa nội dung mô tả hoặc nội dung tùy biến của Confirm.
|
|
281
|
+
|
|
282
|
+
| Prop | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
283
|
+
| :--- | :--- | :--- | :--- |
|
|
284
|
+
| `description` | `ReactNode` | `undefined` | Đoạn văn bản mô tả ngắn (tự động bọc trong thẻ `<p>` với typography chuẩn) |
|
|
285
|
+
| `descriptionClassName` | `string` | `""` | Class CSS riêng cho thẻ `<p>` mô tả |
|
|
286
|
+
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | *Kế thừa từ Context* | Kích thước áp dụng cho cỡ chữ và padding |
|
|
287
|
+
| `className` | `string` | `""` | Class CSS cho toàn bộ khối body |
|
|
288
|
+
| `children` | `ReactNode` | `undefined` | Nội dung tùy biến bên trong body |
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
### `<ConfirmFooter>`
|
|
293
|
+
|
|
294
|
+
Phần chân chứa các nút hành động xác nhận và hủy.
|
|
295
|
+
|
|
296
|
+
| Prop | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
297
|
+
| :--- | :--- | :--- | :--- |
|
|
298
|
+
| `confirmText` | `ReactNode` | `'Xác nhận'` | Nhãn nút Xác nhận |
|
|
299
|
+
| `cancelText` | `ReactNode \| false` | `'Hủy'` | Nhãn nút Hủy (`false` để ẩn nút Hủy) |
|
|
300
|
+
| `confirmVariant` | `ButtonVariant` | `'filled'` | Biến thể hiển thị của nút Xác nhận |
|
|
301
|
+
| `cancelVariant` | `ButtonVariant` | `'outline'` | Biến thể hiển thị của nút Hủy |
|
|
302
|
+
| `confirmColor` | `ButtonColor` | *Kế thừa theo `color`* | Màu nút Xác nhận |
|
|
303
|
+
| `cancelColor` | `ButtonColor` | `'secondary'` | Màu nút Hủy |
|
|
304
|
+
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | *Kế thừa từ Context* | Kích thước áp dụng cho nút bấm và padding |
|
|
305
|
+
| `onConfirm` | `() => void \| Promise<unknown>` | `undefined` | Callback khi bấm nút Xác nhận (hỗ trợ hàm Async tự động bật Loading Spinner) |
|
|
306
|
+
| `onCancel` | `() => void` | `undefined` | Callback khi bấm nút Hủy |
|
|
307
|
+
| `onClose` | `() => void` | `undefined` | Callback khi đóng |
|
|
308
|
+
| `confirmButtonProps` | `Partial<ButtonProps>` | `undefined` | Props bổ sung truyền trực tiếp cho Button Xác nhận |
|
|
309
|
+
| `cancelButtonProps` | `Partial<ButtonProps>` | `undefined` | Props bổ sung truyền trực tiếp cho Button Hủy |
|
|
310
|
+
| `className` | `string` | `""` | Class CSS cho toàn bộ khối footer |
|
|
311
|
+
| `children` | `ReactNode` | `undefined` | Các nút bấm tùy chỉnh khi tự định nghĩa footer |
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
### `<ConfirmClose>`
|
|
316
|
+
|
|
317
|
+
Wrapper bọc quanh bất kỳ nút bấm hoặc phần tử nào bên trong Confirm. Khi click, `<ConfirmClose>` sẽ tự động kích hoạt hiệu ứng đóng mượt mà trước khi unmount (tự động `disabled` khi `isLoading = true`).
|
|
318
|
+
|
|
319
|
+
| Prop | Kiểu dữ liệu | Mặc định | Mô tả |
|
|
320
|
+
| :--- | :--- | :--- | :--- |
|
|
321
|
+
| `children` | `ReactNode` | — | Nút bấm hoặc phần tử con cần kích hoạt sự kiện đóng |
|
|
322
|
+
| `asChild` | `boolean` | `true` | Truyền thẳng sự kiện `onClick` và prop `disabled` vào phần tử con thay vì bọc ngoài bằng thẻ `div` |
|