@compa11y/react 0.1.5 → 0.1.8
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 +17 -709
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +100 -1
- package/dist/index.d.ts +100 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @compa11y/react
|
|
2
2
|
|
|
3
|
-
Accessible React components that just work.
|
|
3
|
+
Accessible React components that just work. Every component handles ARIA attributes, keyboard navigation, focus management, and screen reader announcements under the hood.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -10,724 +10,32 @@ npm install @compa11y/react
|
|
|
10
10
|
|
|
11
11
|
## Components
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
```tsx
|
|
16
|
-
import { Dialog } from '@compa11y/react';
|
|
17
|
-
|
|
18
|
-
function ConfirmDialog({ open, onClose, onConfirm }) {
|
|
19
|
-
return (
|
|
20
|
-
<Dialog open={open} onOpenChange={onClose}>
|
|
21
|
-
<Dialog.Title>Confirm Delete</Dialog.Title>
|
|
22
|
-
<Dialog.Description>
|
|
23
|
-
Are you sure you want to delete this item?
|
|
24
|
-
</Dialog.Description>
|
|
25
|
-
<Dialog.Actions>
|
|
26
|
-
<button onClick={onClose}>Cancel</button>
|
|
27
|
-
<button onClick={onConfirm}>Delete</button>
|
|
28
|
-
</Dialog.Actions>
|
|
29
|
-
</Dialog>
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
### Menu
|
|
35
|
-
|
|
36
|
-
```tsx
|
|
37
|
-
import { Menu } from '@compa11y/react';
|
|
38
|
-
|
|
39
|
-
function ActionMenu() {
|
|
40
|
-
return (
|
|
41
|
-
<Menu>
|
|
42
|
-
<Menu.Trigger>Actions</Menu.Trigger>
|
|
43
|
-
<Menu.Content>
|
|
44
|
-
<Menu.Item onSelect={() => console.log('Edit')}>Edit</Menu.Item>
|
|
45
|
-
<Menu.Item onSelect={() => console.log('Copy')}>Copy</Menu.Item>
|
|
46
|
-
<Menu.Separator />
|
|
47
|
-
<Menu.Item onSelect={() => console.log('Delete')}>Delete</Menu.Item>
|
|
48
|
-
</Menu.Content>
|
|
49
|
-
</Menu>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### Tabs
|
|
55
|
-
|
|
56
|
-
```tsx
|
|
57
|
-
import { Tabs } from '@compa11y/react';
|
|
58
|
-
|
|
59
|
-
function SettingsTabs() {
|
|
60
|
-
return (
|
|
61
|
-
<Tabs defaultValue="general">
|
|
62
|
-
<Tabs.List>
|
|
63
|
-
<Tabs.Tab value="general">General</Tabs.Tab>
|
|
64
|
-
<Tabs.Tab value="security">Security</Tabs.Tab>
|
|
65
|
-
<Tabs.Tab value="notifications">Notifications</Tabs.Tab>
|
|
66
|
-
</Tabs.List>
|
|
67
|
-
<Tabs.Panel value="general">General settings...</Tabs.Panel>
|
|
68
|
-
<Tabs.Panel value="security">Security settings...</Tabs.Panel>
|
|
69
|
-
<Tabs.Panel value="notifications">Notification settings...</Tabs.Panel>
|
|
70
|
-
</Tabs>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### Toast
|
|
76
|
-
|
|
77
|
-
```tsx
|
|
78
|
-
import { ToastProvider, ToastViewport, useToastHelpers } from '@compa11y/react';
|
|
79
|
-
|
|
80
|
-
function App() {
|
|
81
|
-
return (
|
|
82
|
-
<ToastProvider>
|
|
83
|
-
<Content />
|
|
84
|
-
<ToastViewport position="bottom-right" />
|
|
85
|
-
</ToastProvider>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function Content() {
|
|
90
|
-
const { success, error } = useToastHelpers();
|
|
91
|
-
|
|
92
|
-
return <button onClick={() => success('Settings saved!')}>Save</button>;
|
|
93
|
-
}
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### Combobox
|
|
97
|
-
|
|
98
|
-
```tsx
|
|
99
|
-
import { Combobox } from '@compa11y/react';
|
|
100
|
-
|
|
101
|
-
const countries = [
|
|
102
|
-
{ value: 'us', label: 'United States' },
|
|
103
|
-
{ value: 'uk', label: 'United Kingdom' },
|
|
104
|
-
{ value: 'ca', label: 'Canada' },
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
function CountrySelect() {
|
|
108
|
-
const [value, setValue] = useState(null);
|
|
109
|
-
|
|
110
|
-
return (
|
|
111
|
-
<Combobox
|
|
112
|
-
options={countries}
|
|
113
|
-
value={value}
|
|
114
|
-
onValueChange={setValue}
|
|
115
|
-
aria-label="Select country"
|
|
116
|
-
>
|
|
117
|
-
<Combobox.Input placeholder="Choose a country..." clearable />
|
|
118
|
-
<Combobox.Listbox emptyMessage="No countries found" />
|
|
119
|
-
</Combobox>
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### Select
|
|
125
|
-
|
|
126
|
-
```tsx
|
|
127
|
-
import { Select } from '@compa11y/react';
|
|
128
|
-
|
|
129
|
-
const fruits = [
|
|
130
|
-
{ value: 'apple', label: 'Apple' },
|
|
131
|
-
{ value: 'banana', label: 'Banana' },
|
|
132
|
-
{ value: 'cherry', label: 'Cherry' },
|
|
133
|
-
{ value: 'dragonfruit', label: 'Dragon Fruit', disabled: true },
|
|
134
|
-
{ value: 'elderberry', label: 'Elderberry' },
|
|
135
|
-
];
|
|
136
|
-
|
|
137
|
-
function FruitPicker() {
|
|
138
|
-
const [value, setValue] = useState(null);
|
|
139
|
-
|
|
140
|
-
return (
|
|
141
|
-
<Select
|
|
142
|
-
options={fruits}
|
|
143
|
-
value={value}
|
|
144
|
-
onValueChange={setValue}
|
|
145
|
-
aria-label="Choose a fruit"
|
|
146
|
-
>
|
|
147
|
-
<Select.Trigger placeholder="Pick a fruit..." />
|
|
148
|
-
<Select.Listbox />
|
|
149
|
-
</Select>
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
**Keyboard Navigation:**
|
|
155
|
-
|
|
156
|
-
| Key | Action |
|
|
157
|
-
| ----------------- | ----------------------------------------- |
|
|
158
|
-
| `Enter` / `Space` | Open listbox or select highlighted option |
|
|
159
|
-
| `ArrowDown` | Open listbox / move highlight down |
|
|
160
|
-
| `ArrowUp` | Open listbox / move highlight up |
|
|
161
|
-
| `Home` / `End` | Jump to first / last option |
|
|
162
|
-
| `Escape` | Close listbox |
|
|
163
|
-
| `Tab` | Close listbox and move focus |
|
|
164
|
-
| Type characters | Jump to matching option (type-ahead) |
|
|
165
|
-
|
|
166
|
-
**Props:**
|
|
167
|
-
|
|
168
|
-
| Prop | Type | Default | Description |
|
|
169
|
-
| ----------------- | --------------------------------- | ----------------------- | ---------------------------- |
|
|
170
|
-
| `options` | `SelectOption[]` | — | List of options |
|
|
171
|
-
| `value` | `string \| null` | — | Controlled selected value |
|
|
172
|
-
| `defaultValue` | `string` | — | Default value (uncontrolled) |
|
|
173
|
-
| `onValueChange` | `(value: string \| null) => void` | — | Change handler |
|
|
174
|
-
| `placeholder` | `string` | `'Select an option...'` | Trigger placeholder text |
|
|
175
|
-
| `disabled` | `boolean` | `false` | Disable the select |
|
|
176
|
-
| `aria-label` | `string` | — | Accessible label |
|
|
177
|
-
| `aria-labelledby` | `string` | — | ID of labelling element |
|
|
178
|
-
|
|
179
|
-
### Switch
|
|
180
|
-
|
|
181
|
-
```tsx
|
|
182
|
-
import { Switch } from '@compa11y/react';
|
|
183
|
-
|
|
184
|
-
function NotificationSettings() {
|
|
185
|
-
const [enabled, setEnabled] = useState(false);
|
|
186
|
-
|
|
187
|
-
return (
|
|
188
|
-
<Switch checked={enabled} onCheckedChange={setEnabled}>
|
|
189
|
-
Enable notifications
|
|
190
|
-
</Switch>
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
**Customization:**
|
|
196
|
-
|
|
197
|
-
```css
|
|
198
|
-
.my-switch {
|
|
199
|
-
--compa11y-switch-bg: #d1d5db;
|
|
200
|
-
--compa11y-switch-checked-bg: #10b981;
|
|
201
|
-
--compa11y-switch-thumb-bg: white;
|
|
202
|
-
--compa11y-switch-width: 3rem;
|
|
203
|
-
--compa11y-switch-height: 1.75rem;
|
|
204
|
-
--compa11y-focus-color: #10b981;
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
### Listbox
|
|
209
|
-
|
|
210
|
-
```tsx
|
|
211
|
-
import { Listbox } from '@compa11y/react';
|
|
212
|
-
|
|
213
|
-
// Single select (selection follows focus)
|
|
214
|
-
function FruitPicker() {
|
|
215
|
-
const [fruit, setFruit] = useState('apple');
|
|
216
|
-
|
|
217
|
-
return (
|
|
218
|
-
<Listbox value={fruit} onValueChange={setFruit} aria-label="Favorite fruit">
|
|
219
|
-
<Listbox.Group label="Citrus">
|
|
220
|
-
<Listbox.Option value="orange">Orange</Listbox.Option>
|
|
221
|
-
<Listbox.Option value="lemon">Lemon</Listbox.Option>
|
|
222
|
-
<Listbox.Option value="grapefruit">Grapefruit</Listbox.Option>
|
|
223
|
-
</Listbox.Group>
|
|
224
|
-
<Listbox.Option value="apple">Apple</Listbox.Option>
|
|
225
|
-
<Listbox.Option value="banana" disabled>
|
|
226
|
-
Banana (sold out)
|
|
227
|
-
</Listbox.Option>
|
|
228
|
-
</Listbox>
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Multi select (focus independent of selection)
|
|
233
|
-
function ToppingsPicker() {
|
|
234
|
-
const [toppings, setToppings] = useState(['cheese']);
|
|
235
|
-
|
|
236
|
-
return (
|
|
237
|
-
<Listbox
|
|
238
|
-
multiple
|
|
239
|
-
value={toppings}
|
|
240
|
-
onValueChange={setToppings}
|
|
241
|
-
aria-label="Pizza toppings"
|
|
242
|
-
>
|
|
243
|
-
<Listbox.Option value="cheese">Cheese</Listbox.Option>
|
|
244
|
-
<Listbox.Option value="pepperoni">Pepperoni</Listbox.Option>
|
|
245
|
-
<Listbox.Option value="mushrooms">Mushrooms</Listbox.Option>
|
|
246
|
-
<Listbox.Option value="olives">Olives</Listbox.Option>
|
|
247
|
-
</Listbox>
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
**Props (Listbox):**
|
|
253
|
-
|
|
254
|
-
| Prop | Type | Default | Description |
|
|
255
|
-
| ----------------- | ------------------------------------- | ------------ | ----------------------------------------------------- |
|
|
256
|
-
| `value` | `string \| string[]` | — | Controlled value (string for single, array for multi) |
|
|
257
|
-
| `defaultValue` | `string \| string[]` | — | Default value (uncontrolled) |
|
|
258
|
-
| `onValueChange` | `(value: string \| string[]) => void` | — | Change handler |
|
|
259
|
-
| `multiple` | `boolean` | `false` | Enable multi-select mode |
|
|
260
|
-
| `disabled` | `boolean` | `false` | Disable all options |
|
|
261
|
-
| `discoverable` | `boolean` | `true` | Keep disabled listbox in tab order |
|
|
262
|
-
| `orientation` | `'horizontal' \| 'vertical'` | `'vertical'` | Layout orientation |
|
|
263
|
-
| `unstyled` | `boolean` | `false` | Remove default styles |
|
|
264
|
-
| `aria-label` | `string` | — | Accessible label |
|
|
265
|
-
| `aria-labelledby` | `string` | — | ID of labelling element |
|
|
266
|
-
|
|
267
|
-
**Props (Listbox.Option):**
|
|
268
|
-
|
|
269
|
-
| Prop | Type | Default | Description |
|
|
270
|
-
| -------------- | --------- | ------- | ------------------------------------------ |
|
|
271
|
-
| `value` | `string` | — | Value for this option (required) |
|
|
272
|
-
| `disabled` | `boolean` | `false` | Disable this option |
|
|
273
|
-
| `discoverable` | `boolean` | `true` | Keep disabled option discoverable |
|
|
274
|
-
| `unstyled` | `boolean` | — | Remove default styles (inherits from root) |
|
|
275
|
-
| `aria-label` | `string` | — | Accessible label override |
|
|
276
|
-
|
|
277
|
-
**Props (Listbox.Group):**
|
|
278
|
-
|
|
279
|
-
| Prop | Type | Default | Description |
|
|
280
|
-
| ---------- | --------- | ------- | ------------------------------------------ |
|
|
281
|
-
| `label` | `string` | — | Group label (required, visible) |
|
|
282
|
-
| `disabled` | `boolean` | `false` | Disable all options in group |
|
|
283
|
-
| `unstyled` | `boolean` | — | Remove default styles (inherits from root) |
|
|
284
|
-
|
|
285
|
-
**Keyboard Navigation:**
|
|
286
|
-
|
|
287
|
-
| Key | Single Select | Multi Select |
|
|
288
|
-
| ----------------------- | ---------------------------- | -------------------------- |
|
|
289
|
-
| `ArrowDown` / `ArrowUp` | Move focus and select | Move focus only |
|
|
290
|
-
| `Home` / `End` | First/last option and select | Move focus only |
|
|
291
|
-
| `Space` | — | Toggle focused option |
|
|
292
|
-
| `Shift+Arrow` | — | Move focus and toggle |
|
|
293
|
-
| `Ctrl+Shift+Home/End` | — | Select range to first/last |
|
|
294
|
-
| `Ctrl+A` | — | Toggle select all |
|
|
295
|
-
| Type characters | Jump to match and select | Jump to match |
|
|
296
|
-
|
|
297
|
-
**Customization:**
|
|
298
|
-
|
|
299
|
-
```css
|
|
300
|
-
[data-compa11y-listbox] {
|
|
301
|
-
--compa11y-listbox-bg: white;
|
|
302
|
-
--compa11y-listbox-border: 1px solid #ccc;
|
|
303
|
-
--compa11y-listbox-radius: 6px;
|
|
304
|
-
--compa11y-listbox-max-height: 300px;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
[data-compa11y-listbox-option] {
|
|
308
|
-
--compa11y-option-hover-bg: #f5f5f5;
|
|
309
|
-
--compa11y-option-focused-bg: #e6f0ff;
|
|
310
|
-
--compa11y-option-selected-bg: #e6f0ff;
|
|
311
|
-
--compa11y-option-selected-color: #10b981;
|
|
312
|
-
--compa11y-option-check-color: #10b981;
|
|
313
|
-
--compa11y-option-disabled-color: #999;
|
|
314
|
-
--compa11y-focus-color: #10b981;
|
|
315
|
-
}
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
### Input
|
|
319
|
-
|
|
320
|
-
```tsx
|
|
321
|
-
import { Input } from '@compa11y/react';
|
|
322
|
-
|
|
323
|
-
function ContactForm() {
|
|
324
|
-
const [name, setName] = useState('');
|
|
325
|
-
const [nameError, setNameError] = useState('');
|
|
326
|
-
|
|
327
|
-
const validate = () => {
|
|
328
|
-
if (!name.trim()) setNameError('Name is required');
|
|
329
|
-
else setNameError('');
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
return (
|
|
333
|
-
<Input
|
|
334
|
-
label="Full Name"
|
|
335
|
-
hint="Enter your first and last name"
|
|
336
|
-
error={nameError || undefined}
|
|
337
|
-
required
|
|
338
|
-
placeholder="John Doe"
|
|
339
|
-
value={name}
|
|
340
|
-
onValueChange={setName}
|
|
341
|
-
onBlur={validate}
|
|
342
|
-
/>
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Compound mode for custom layouts
|
|
347
|
-
function CustomInput() {
|
|
348
|
-
const [value, setValue] = useState('');
|
|
349
|
-
|
|
350
|
-
return (
|
|
351
|
-
<Input value={value} onValueChange={setValue}>
|
|
352
|
-
<Input.Label>Email</Input.Label>
|
|
353
|
-
<Input.Field type="email" placeholder="you@example.com" />
|
|
354
|
-
<Input.Hint>We'll never share your email</Input.Hint>
|
|
355
|
-
<Input.Error>{/* error message here */}</Input.Error>
|
|
356
|
-
</Input>
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
**Props:**
|
|
362
|
-
|
|
363
|
-
| Prop | Type | Default | Description |
|
|
364
|
-
| ----------------- | ------------------------- | -------- | ------------------------------------------------------------ |
|
|
365
|
-
| `label` | `ReactNode` | — | Visible label text |
|
|
366
|
-
| `hint` | `ReactNode` | — | Hint/description text |
|
|
367
|
-
| `error` | `ReactNode` | — | Error message (enables `aria-invalid`) |
|
|
368
|
-
| `value` | `string` | — | Controlled value |
|
|
369
|
-
| `defaultValue` | `string` | `''` | Default value (uncontrolled) |
|
|
370
|
-
| `onValueChange` | `(value: string) => void` | — | Change handler |
|
|
371
|
-
| `type` | `string` | `'text'` | Input type (text, email, password, number, tel, url, search) |
|
|
372
|
-
| `placeholder` | `string` | — | Placeholder text |
|
|
373
|
-
| `required` | `boolean` | `false` | Required field |
|
|
374
|
-
| `disabled` | `boolean` | `false` | Disable the input |
|
|
375
|
-
| `readOnly` | `boolean` | `false` | Read-only input |
|
|
376
|
-
| `unstyled` | `boolean` | `false` | Remove default styles |
|
|
377
|
-
| `aria-label` | `string` | — | Accessible label (when no visible label) |
|
|
378
|
-
| `aria-labelledby` | `string` | — | ID of labelling element |
|
|
379
|
-
|
|
380
|
-
**Customization:**
|
|
381
|
-
|
|
382
|
-
```css
|
|
383
|
-
.my-input {
|
|
384
|
-
--compa11y-input-border: 1px solid #ccc;
|
|
385
|
-
--compa11y-input-border-focus: #10b981;
|
|
386
|
-
--compa11y-input-border-error: #ef4444;
|
|
387
|
-
--compa11y-input-radius: 8px;
|
|
388
|
-
--compa11y-input-label-weight: 600;
|
|
389
|
-
--compa11y-input-error-color: #ef4444;
|
|
390
|
-
--compa11y-input-hint-color: #666;
|
|
391
|
-
--compa11y-focus-color: #10b981;
|
|
392
|
-
}
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
### Button
|
|
396
|
-
|
|
397
|
-
```tsx
|
|
398
|
-
import { Button } from '@compa11y/react';
|
|
399
|
-
|
|
400
|
-
function Actions() {
|
|
401
|
-
return (
|
|
402
|
-
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
403
|
-
<Button variant="primary" onClick={handleSave}>
|
|
404
|
-
Save
|
|
405
|
-
</Button>
|
|
406
|
-
<Button variant="outline" onClick={handleCancel}>
|
|
407
|
-
Cancel
|
|
408
|
-
</Button>
|
|
409
|
-
<Button variant="danger" onClick={handleDelete}>
|
|
410
|
-
Delete
|
|
411
|
-
</Button>
|
|
412
|
-
</div>
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Loading state
|
|
417
|
-
function SaveButton() {
|
|
418
|
-
const [loading, setLoading] = useState(false);
|
|
419
|
-
|
|
420
|
-
const handleSave = async () => {
|
|
421
|
-
setLoading(true);
|
|
422
|
-
await saveData();
|
|
423
|
-
setLoading(false);
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
return (
|
|
427
|
-
<Button variant="primary" loading={loading} onClick={handleSave}>
|
|
428
|
-
{loading ? 'Saving...' : 'Save Changes'}
|
|
429
|
-
</Button>
|
|
430
|
-
);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Disabled but discoverable (stays in tab order)
|
|
434
|
-
<Button variant="primary" disabled discoverable>
|
|
435
|
-
Unavailable
|
|
436
|
-
</Button>;
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
**Props:**
|
|
440
|
-
|
|
441
|
-
| Prop | Type | Default | Description |
|
|
442
|
-
| -------------- | -------------------------------------------------------------- | ------------- | ------------------------------------------------------ |
|
|
443
|
-
| `variant` | `'primary' \| 'secondary' \| 'danger' \| 'outline' \| 'ghost'` | `'secondary'` | Visual variant |
|
|
444
|
-
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Button size |
|
|
445
|
-
| `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | HTML button type |
|
|
446
|
-
| `disabled` | `boolean` | `false` | Disable the button |
|
|
447
|
-
| `discoverable` | `boolean` | `false` | Keep disabled button in tab order with `aria-disabled` |
|
|
448
|
-
| `loading` | `boolean` | `false` | Loading state (shows spinner, sets `aria-busy`) |
|
|
449
|
-
| `unstyled` | `boolean` | `false` | Remove default styles |
|
|
450
|
-
| `aria-label` | `string` | — | Accessible label |
|
|
451
|
-
|
|
452
|
-
**Customization:**
|
|
453
|
-
|
|
454
|
-
```css
|
|
455
|
-
[data-compa11y-button] {
|
|
456
|
-
--compa11y-button-radius: 8px;
|
|
457
|
-
--compa11y-button-font-weight: 600;
|
|
458
|
-
--compa11y-button-primary-bg: #10b981;
|
|
459
|
-
--compa11y-button-primary-color: white;
|
|
460
|
-
--compa11y-button-danger-bg: #ef4444;
|
|
461
|
-
--compa11y-button-danger-color: white;
|
|
462
|
-
--compa11y-button-disabled-opacity: 0.5;
|
|
463
|
-
--compa11y-focus-color: #10b981;
|
|
464
|
-
}
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
### Textarea
|
|
468
|
-
|
|
469
|
-
```tsx
|
|
470
|
-
import { Textarea } from '@compa11y/react';
|
|
471
|
-
|
|
472
|
-
function FeedbackForm() {
|
|
473
|
-
const [desc, setDesc] = useState('');
|
|
474
|
-
const [descError, setDescError] = useState('');
|
|
475
|
-
|
|
476
|
-
const validate = () => {
|
|
477
|
-
if (!desc.trim()) setDescError('Description is required');
|
|
478
|
-
else if (desc.trim().length < 10)
|
|
479
|
-
setDescError('Must be at least 10 characters');
|
|
480
|
-
else setDescError('');
|
|
481
|
-
};
|
|
482
|
-
|
|
483
|
-
return (
|
|
484
|
-
<Textarea
|
|
485
|
-
label="Description"
|
|
486
|
-
hint="Provide at least 10 characters"
|
|
487
|
-
error={descError || undefined}
|
|
488
|
-
required
|
|
489
|
-
rows={4}
|
|
490
|
-
placeholder="Enter a description..."
|
|
491
|
-
value={desc}
|
|
492
|
-
onValueChange={setDesc}
|
|
493
|
-
onBlur={validate}
|
|
494
|
-
/>
|
|
495
|
-
);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Compound mode for custom layouts
|
|
499
|
-
function CustomTextarea() {
|
|
500
|
-
const [value, setValue] = useState('');
|
|
501
|
-
|
|
502
|
-
return (
|
|
503
|
-
<Textarea value={value} onValueChange={setValue}>
|
|
504
|
-
<Textarea.Label>Bio</Textarea.Label>
|
|
505
|
-
<Textarea.Field rows={5} placeholder="Tell us about yourself..." />
|
|
506
|
-
<Textarea.Hint>Markdown is supported</Textarea.Hint>
|
|
507
|
-
<Textarea.Error>{/* error message here */}</Textarea.Error>
|
|
508
|
-
</Textarea>
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
**Props:**
|
|
514
|
-
|
|
515
|
-
| Prop | Type | Default | Description |
|
|
516
|
-
| ----------------- | ------------------------- | ------------ | -------------------------------------------------- |
|
|
517
|
-
| `label` | `ReactNode` | — | Visible label text |
|
|
518
|
-
| `hint` | `ReactNode` | — | Hint/description text |
|
|
519
|
-
| `error` | `ReactNode` | — | Error message (enables `aria-invalid`) |
|
|
520
|
-
| `value` | `string` | — | Controlled value |
|
|
521
|
-
| `defaultValue` | `string` | `''` | Default value (uncontrolled) |
|
|
522
|
-
| `onValueChange` | `(value: string) => void` | — | Change handler |
|
|
523
|
-
| `rows` | `number` | `3` | Number of visible text rows |
|
|
524
|
-
| `resize` | `string` | `'vertical'` | Resize behavior (none, both, horizontal, vertical) |
|
|
525
|
-
| `placeholder` | `string` | — | Placeholder text |
|
|
526
|
-
| `required` | `boolean` | `false` | Required field |
|
|
527
|
-
| `disabled` | `boolean` | `false` | Disable the textarea |
|
|
528
|
-
| `readOnly` | `boolean` | `false` | Read-only textarea |
|
|
529
|
-
| `unstyled` | `boolean` | `false` | Remove default styles |
|
|
530
|
-
| `aria-label` | `string` | — | Accessible label (when no visible label) |
|
|
531
|
-
| `aria-labelledby` | `string` | — | ID of labelling element |
|
|
532
|
-
|
|
533
|
-
**Customization:**
|
|
534
|
-
|
|
535
|
-
```css
|
|
536
|
-
.my-textarea {
|
|
537
|
-
--compa11y-textarea-border: 1px solid #ccc;
|
|
538
|
-
--compa11y-textarea-border-focus: #10b981;
|
|
539
|
-
--compa11y-textarea-border-error: #ef4444;
|
|
540
|
-
--compa11y-textarea-radius: 8px;
|
|
541
|
-
--compa11y-textarea-label-weight: 600;
|
|
542
|
-
--compa11y-textarea-error-color: #ef4444;
|
|
543
|
-
--compa11y-textarea-hint-color: #666;
|
|
544
|
-
--compa11y-focus-color: #10b981;
|
|
545
|
-
}
|
|
546
|
-
```
|
|
13
|
+
Dialog, ActionMenu, Tabs, Toast, Combobox, Select, Listbox, Checkbox, RadioGroup, Switch, Input, Textarea, Button
|
|
547
14
|
|
|
548
15
|
## Hooks
|
|
549
16
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
```tsx
|
|
553
|
-
import { useFocusTrap } from '@compa11y/react';
|
|
554
|
-
|
|
555
|
-
function Modal({ isOpen }) {
|
|
556
|
-
const trapRef = useFocusTrap({ active: isOpen });
|
|
557
|
-
|
|
558
|
-
return (
|
|
559
|
-
<div ref={trapRef} role="dialog">
|
|
560
|
-
<button>Close</button>
|
|
561
|
-
</div>
|
|
562
|
-
);
|
|
563
|
-
}
|
|
564
|
-
```
|
|
565
|
-
|
|
566
|
-
### useAnnouncer
|
|
567
|
-
|
|
568
|
-
```tsx
|
|
569
|
-
import { useAnnouncer } from '@compa11y/react';
|
|
570
|
-
|
|
571
|
-
function SearchResults({ count }) {
|
|
572
|
-
const { announce } = useAnnouncer();
|
|
17
|
+
`useFocusTrap`, `useFocusVisible`, `useFocusNeighbor`, `useFocusReturn`, `useKeyboard`, `useMenuKeyboard`, `useTabsKeyboard`, `useGridKeyboard`, `useTypeAhead`, `useAnnouncer`, `useRovingTabindex`, and more.
|
|
573
18
|
|
|
574
|
-
|
|
575
|
-
announce(`Found ${count} results`);
|
|
576
|
-
}, [count, announce]);
|
|
577
|
-
}
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
### useKeyboard
|
|
19
|
+
## Quick start
|
|
581
20
|
|
|
582
21
|
```tsx
|
|
583
|
-
import {
|
|
584
|
-
|
|
585
|
-
function CustomList() {
|
|
586
|
-
const keyboardProps = useKeyboard({
|
|
587
|
-
ArrowDown: () => focusNext(),
|
|
588
|
-
ArrowUp: () => focusPrevious(),
|
|
589
|
-
Enter: () => selectItem(),
|
|
590
|
-
});
|
|
22
|
+
import { Dialog, Select, useToastHelpers } from '@compa11y/react';
|
|
591
23
|
|
|
592
|
-
|
|
593
|
-
}
|
|
24
|
+
// Fully accessible dialog — focus trap, Escape to close, screen reader announcements
|
|
25
|
+
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
26
|
+
<Dialog.Trigger>Open</Dialog.Trigger>
|
|
27
|
+
<Dialog.Title>Confirm</Dialog.Title>
|
|
28
|
+
<Dialog.Content>Are you sure?</Dialog.Content>
|
|
29
|
+
<Dialog.Actions>
|
|
30
|
+
<button onClick={() => setIsOpen(false)}>Cancel</button>
|
|
31
|
+
<button onClick={handleConfirm}>Confirm</button>
|
|
32
|
+
</Dialog.Actions>
|
|
33
|
+
</Dialog>
|
|
594
34
|
```
|
|
595
35
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
```tsx
|
|
599
|
-
import { useFocusVisible } from '@compa11y/react';
|
|
600
|
-
|
|
601
|
-
function Button({ children }) {
|
|
602
|
-
const { isFocusVisible, focusProps } = useFocusVisible();
|
|
603
|
-
|
|
604
|
-
return (
|
|
605
|
-
<button {...focusProps} className={isFocusVisible ? 'focus-ring' : ''}>
|
|
606
|
-
{children}
|
|
607
|
-
</button>
|
|
608
|
-
);
|
|
609
|
-
}
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
### useRovingTabindex
|
|
613
|
-
|
|
614
|
-
```tsx
|
|
615
|
-
import { useRovingTabindex } from '@compa11y/react';
|
|
616
|
-
|
|
617
|
-
function Toolbar() {
|
|
618
|
-
const { getItemProps } = useRovingTabindex({
|
|
619
|
-
itemCount: 3,
|
|
620
|
-
orientation: 'horizontal',
|
|
621
|
-
});
|
|
36
|
+
## Documentation
|
|
622
37
|
|
|
623
|
-
|
|
624
|
-
<div role="toolbar">
|
|
625
|
-
<button {...getItemProps(0)}>Cut</button>
|
|
626
|
-
<button {...getItemProps(1)}>Copy</button>
|
|
627
|
-
<button {...getItemProps(2)}>Paste</button>
|
|
628
|
-
</div>
|
|
629
|
-
);
|
|
630
|
-
}
|
|
631
|
-
```
|
|
632
|
-
|
|
633
|
-
## Styling
|
|
634
|
-
|
|
635
|
-
All components are unstyled. Use `data-*` attributes for state-based styling:
|
|
636
|
-
|
|
637
|
-
```css
|
|
638
|
-
/* Dialog */
|
|
639
|
-
[data-compa11y-dialog-overlay] {
|
|
640
|
-
background: rgba(0, 0, 0, 0.5);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
[data-compa11y-dialog] {
|
|
644
|
-
background: white;
|
|
645
|
-
padding: 1.5rem;
|
|
646
|
-
border-radius: 8px;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
/* Menu */
|
|
650
|
-
[data-compa11y-menu-content] {
|
|
651
|
-
background: white;
|
|
652
|
-
border: 1px solid #e0e0e0;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
[data-compa11y-menu-item][data-highlighted='true'] {
|
|
656
|
-
background: #f0f0f0;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/* Tabs */
|
|
660
|
-
[data-compa11y-tab][data-selected='true'] {
|
|
661
|
-
border-bottom: 2px solid blue;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/* Combobox */
|
|
665
|
-
[data-compa11y-combobox-option][data-highlighted='true'] {
|
|
666
|
-
background: #f0f0f0;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
/* Listbox */
|
|
670
|
-
[data-compa11y-listbox] {
|
|
671
|
-
border: 1px solid #e0e0e0;
|
|
672
|
-
border-radius: 6px;
|
|
673
|
-
max-height: 300px;
|
|
674
|
-
overflow-y: auto;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
[data-compa11y-listbox-option][data-focused='true'] {
|
|
678
|
-
background: #e6f0ff;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
[data-compa11y-listbox-option][data-selected='true'] {
|
|
682
|
-
background: #e6f0ff;
|
|
683
|
-
font-weight: 600;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/* Select */
|
|
687
|
-
[data-compa11y-select] {
|
|
688
|
-
position: relative;
|
|
689
|
-
width: 300px;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
[data-compa11y-select-trigger] {
|
|
693
|
-
width: 100%;
|
|
694
|
-
display: flex;
|
|
695
|
-
align-items: center;
|
|
696
|
-
justify-content: space-between;
|
|
697
|
-
padding: 0.5rem 2rem 0.5rem 0.75rem;
|
|
698
|
-
border: 1px solid #ccc;
|
|
699
|
-
border-radius: 4px;
|
|
700
|
-
background: white;
|
|
701
|
-
cursor: pointer;
|
|
702
|
-
text-align: left;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
[data-compa11y-select-listbox] {
|
|
706
|
-
position: absolute;
|
|
707
|
-
top: 100%;
|
|
708
|
-
left: 0;
|
|
709
|
-
right: 0;
|
|
710
|
-
margin-top: 4px;
|
|
711
|
-
background: white;
|
|
712
|
-
border: 1px solid #e0e0e0;
|
|
713
|
-
border-radius: 4px;
|
|
714
|
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
715
|
-
max-height: 200px;
|
|
716
|
-
overflow-y: auto;
|
|
717
|
-
z-index: 1000;
|
|
718
|
-
list-style: none;
|
|
719
|
-
padding: 0;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
[data-compa11y-select-option][data-highlighted='true'] {
|
|
723
|
-
background: #f0f0f0;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
[data-compa11y-select-option][data-selected='true'] {
|
|
727
|
-
background: #e6f0ff;
|
|
728
|
-
font-weight: 600;
|
|
729
|
-
}
|
|
730
|
-
```
|
|
38
|
+
Full documentation, live examples, props reference, and accessibility details at **[compa11y.org](https://compa11y.org)**.
|
|
731
39
|
|
|
732
40
|
## License
|
|
733
41
|
|