@a13y/react 0.1.0
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/LICENSE +21 -0
- package/README.md +241 -0
- package/dist/components/index.d.ts +374 -0
- package/dist/components/index.js +849 -0
- package/dist/components/index.js.map +1 -0
- package/dist/hooks/index.d.ts +725 -0
- package/dist/hooks/index.js +648 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +1875 -0
- package/dist/index.js.map +1 -0
- package/dist/patterns/index.d.ts +539 -0
- package/dist/patterns/index.js +1170 -0
- package/dist/patterns/index.js.map +1 -0
- package/dist/use-accessible-button-B0syf-Az.d.ts +83 -0
- package/package.json +101 -0
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
export { A as AccessibleButtonProps, P as PressEvent, U as UseAccessibleButtonProps, a as UseAccessibleButtonReturn, u as useAccessibleButton } from '../use-accessible-button-B0syf-Az.js';
|
|
2
|
+
import { AriaRole } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @a13y/react - useAccessibleDialog
|
|
6
|
+
* Type-safe dialog/modal hook with full ARIA support
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Props for useAccessibleDialog
|
|
11
|
+
*/
|
|
12
|
+
interface UseAccessibleDialogProps {
|
|
13
|
+
/**
|
|
14
|
+
* Whether the dialog is open
|
|
15
|
+
*/
|
|
16
|
+
isOpen: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Callback when dialog should close
|
|
19
|
+
* Called on Escape key or backdrop click
|
|
20
|
+
*/
|
|
21
|
+
onClose: () => void;
|
|
22
|
+
/**
|
|
23
|
+
* Dialog title - REQUIRED for accessibility
|
|
24
|
+
* This becomes the aria-labelledby target
|
|
25
|
+
*/
|
|
26
|
+
title: string;
|
|
27
|
+
/**
|
|
28
|
+
* Optional dialog description
|
|
29
|
+
* This becomes the aria-describedby target
|
|
30
|
+
*/
|
|
31
|
+
description?: string;
|
|
32
|
+
/**
|
|
33
|
+
* ARIA role
|
|
34
|
+
* @default 'dialog'
|
|
35
|
+
*/
|
|
36
|
+
role?: Extract<AriaRole, 'dialog' | 'alertdialog'>;
|
|
37
|
+
/**
|
|
38
|
+
* Whether dialog is modal (blocking)
|
|
39
|
+
* @default true
|
|
40
|
+
*/
|
|
41
|
+
isModal?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Whether clicking backdrop closes dialog
|
|
44
|
+
* @default true
|
|
45
|
+
*/
|
|
46
|
+
closeOnBackdropClick?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Dialog container props
|
|
50
|
+
*/
|
|
51
|
+
interface DialogContainerProps {
|
|
52
|
+
ref: React.RefObject<HTMLElement | null>;
|
|
53
|
+
role: AriaRole;
|
|
54
|
+
'aria-labelledby': string;
|
|
55
|
+
'aria-describedby'?: string;
|
|
56
|
+
'aria-modal': boolean;
|
|
57
|
+
tabIndex: -1;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Title props
|
|
61
|
+
*/
|
|
62
|
+
interface DialogTitleProps {
|
|
63
|
+
id: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Description props
|
|
67
|
+
*/
|
|
68
|
+
interface DialogDescriptionProps {
|
|
69
|
+
id: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Backdrop props
|
|
73
|
+
*/
|
|
74
|
+
interface DialogBackdropProps {
|
|
75
|
+
onClick: () => void;
|
|
76
|
+
'aria-hidden': true;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Return type
|
|
80
|
+
*/
|
|
81
|
+
interface UseAccessibleDialogReturn {
|
|
82
|
+
dialogProps: DialogContainerProps;
|
|
83
|
+
titleProps: DialogTitleProps;
|
|
84
|
+
descriptionProps: DialogDescriptionProps | null;
|
|
85
|
+
backdropProps: DialogBackdropProps | null;
|
|
86
|
+
close: () => void;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Hook for creating accessible dialogs/modals
|
|
90
|
+
*
|
|
91
|
+
* Features:
|
|
92
|
+
* - Focus trap with Tab/Shift+Tab cycling
|
|
93
|
+
* - Escape key to close
|
|
94
|
+
* - Focus restoration on close
|
|
95
|
+
* - ARIA attributes (modal, labelledby, describedby)
|
|
96
|
+
* - Backdrop click handling
|
|
97
|
+
* - Development-time validation
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```tsx
|
|
101
|
+
* const { dialogProps, titleProps, descriptionProps, backdropProps } =
|
|
102
|
+
* useAccessibleDialog({
|
|
103
|
+
* isOpen,
|
|
104
|
+
* onClose: () => setIsOpen(false),
|
|
105
|
+
* title: 'Delete Item',
|
|
106
|
+
* description: 'This action cannot be undone',
|
|
107
|
+
* });
|
|
108
|
+
*
|
|
109
|
+
* if (!isOpen) return null;
|
|
110
|
+
*
|
|
111
|
+
* return (
|
|
112
|
+
* <>
|
|
113
|
+
* <div {...backdropProps} />
|
|
114
|
+
* <div {...dialogProps}>
|
|
115
|
+
* <h2 {...titleProps}>Delete Item</h2>
|
|
116
|
+
* <p {...descriptionProps}>This action cannot be undone</p>
|
|
117
|
+
* <button onClick={close}>Cancel</button>
|
|
118
|
+
* </div>
|
|
119
|
+
* </>
|
|
120
|
+
* );
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
declare const useAccessibleDialog: (props: UseAccessibleDialogProps) => UseAccessibleDialogReturn;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @a13y/react - useFocusTrap
|
|
127
|
+
* Focus trap hook for modals and dialogs
|
|
128
|
+
*/
|
|
129
|
+
/**
|
|
130
|
+
* Props for useFocusTrap
|
|
131
|
+
*/
|
|
132
|
+
interface UseFocusTrapProps {
|
|
133
|
+
/**
|
|
134
|
+
* Whether the focus trap is active
|
|
135
|
+
*/
|
|
136
|
+
isActive: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Callback when Escape key is pressed
|
|
139
|
+
*/
|
|
140
|
+
onEscape?: () => void;
|
|
141
|
+
/**
|
|
142
|
+
* Whether to restore focus when trap is deactivated
|
|
143
|
+
* @default true
|
|
144
|
+
*/
|
|
145
|
+
restoreFocus?: boolean;
|
|
146
|
+
/**
|
|
147
|
+
* Whether to auto-focus first element when activated
|
|
148
|
+
* @default true
|
|
149
|
+
*/
|
|
150
|
+
autoFocus?: boolean;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Return type
|
|
154
|
+
*/
|
|
155
|
+
interface UseFocusTrapReturn {
|
|
156
|
+
/**
|
|
157
|
+
* Ref to attach to the container element
|
|
158
|
+
*/
|
|
159
|
+
trapRef: React.RefObject<HTMLElement | null>;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Hook for creating focus traps
|
|
163
|
+
*
|
|
164
|
+
* Features:
|
|
165
|
+
* - Traps Tab/Shift+Tab within container
|
|
166
|
+
* - Handles Escape key
|
|
167
|
+
* - Restores focus on deactivation
|
|
168
|
+
* - Auto-focuses first element
|
|
169
|
+
* - Development-time validation
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```tsx
|
|
173
|
+
* const { trapRef } = useFocusTrap({
|
|
174
|
+
* isActive: isOpen,
|
|
175
|
+
* onEscape: () => setIsOpen(false),
|
|
176
|
+
* });
|
|
177
|
+
*
|
|
178
|
+
* return (
|
|
179
|
+
* <div ref={trapRef} role="dialog">
|
|
180
|
+
* <button>Close</button>
|
|
181
|
+
* </div>
|
|
182
|
+
* );
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
declare const useFocusTrap: (props: UseFocusTrapProps) => UseFocusTrapReturn;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @a13y/react - useKeyboardNavigation
|
|
189
|
+
* Roving tabindex keyboard navigation hook
|
|
190
|
+
*/
|
|
191
|
+
/**
|
|
192
|
+
* Navigation orientation
|
|
193
|
+
*/
|
|
194
|
+
type Orientation = 'horizontal' | 'vertical' | 'both';
|
|
195
|
+
/**
|
|
196
|
+
* Props for useKeyboardNavigation
|
|
197
|
+
*/
|
|
198
|
+
interface UseKeyboardNavigationProps {
|
|
199
|
+
/**
|
|
200
|
+
* Navigation orientation
|
|
201
|
+
* - 'horizontal': Arrow Left/Right
|
|
202
|
+
* - 'vertical': Arrow Up/Down
|
|
203
|
+
* - 'both': All arrow keys
|
|
204
|
+
*/
|
|
205
|
+
orientation: Orientation;
|
|
206
|
+
/**
|
|
207
|
+
* Whether to loop at boundaries
|
|
208
|
+
* @default false
|
|
209
|
+
*/
|
|
210
|
+
loop?: boolean;
|
|
211
|
+
/**
|
|
212
|
+
* Callback when navigation occurs
|
|
213
|
+
*/
|
|
214
|
+
onNavigate?: (index: number) => void;
|
|
215
|
+
/**
|
|
216
|
+
* Initial focused index
|
|
217
|
+
* @default 0
|
|
218
|
+
*/
|
|
219
|
+
defaultIndex?: number;
|
|
220
|
+
/**
|
|
221
|
+
* Controlled current index
|
|
222
|
+
*/
|
|
223
|
+
currentIndex?: number;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Item props for navigable items
|
|
227
|
+
*/
|
|
228
|
+
interface NavigableItemProps {
|
|
229
|
+
ref: (element: HTMLElement | null) => void;
|
|
230
|
+
tabIndex: number;
|
|
231
|
+
onKeyDown: (event: React.KeyboardEvent) => void;
|
|
232
|
+
'data-index': number;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Return type
|
|
236
|
+
*/
|
|
237
|
+
interface UseKeyboardNavigationReturn {
|
|
238
|
+
/**
|
|
239
|
+
* Current focused index
|
|
240
|
+
*/
|
|
241
|
+
currentIndex: number;
|
|
242
|
+
/**
|
|
243
|
+
* Navigate to specific index
|
|
244
|
+
*/
|
|
245
|
+
setCurrentIndex: (index: number) => void;
|
|
246
|
+
/**
|
|
247
|
+
* Get props for navigable item
|
|
248
|
+
*/
|
|
249
|
+
getItemProps: (index: number) => NavigableItemProps;
|
|
250
|
+
/**
|
|
251
|
+
* Container props
|
|
252
|
+
*/
|
|
253
|
+
containerProps: {
|
|
254
|
+
role: 'toolbar' | 'listbox' | 'menu';
|
|
255
|
+
'aria-orientation': Orientation;
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Hook for keyboard navigation with roving tabindex
|
|
260
|
+
*
|
|
261
|
+
* Features:
|
|
262
|
+
* - Arrow key navigation
|
|
263
|
+
* - Home/End navigation
|
|
264
|
+
* - Roving tabindex pattern
|
|
265
|
+
* - Automatic focus management
|
|
266
|
+
* - Development-time validation
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```tsx
|
|
270
|
+
* const { containerProps, getItemProps, currentIndex } =
|
|
271
|
+
* useKeyboardNavigation({
|
|
272
|
+
* orientation: 'horizontal',
|
|
273
|
+
* loop: true,
|
|
274
|
+
* });
|
|
275
|
+
*
|
|
276
|
+
* return (
|
|
277
|
+
* <div {...containerProps}>
|
|
278
|
+
* {items.map((item, index) => (
|
|
279
|
+
* <button key={index} {...getItemProps(index)}>
|
|
280
|
+
* {item.label}
|
|
281
|
+
* </button>
|
|
282
|
+
* ))}
|
|
283
|
+
* </div>
|
|
284
|
+
* );
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
declare const useKeyboardNavigation: (props: UseKeyboardNavigationProps) => UseKeyboardNavigationReturn;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* @a13y/react - useAccessibleForm Hook
|
|
291
|
+
* Form management with accessibility built-in
|
|
292
|
+
*/
|
|
293
|
+
/**
|
|
294
|
+
* Field-level validation function
|
|
295
|
+
*/
|
|
296
|
+
type FieldValidator<T> = (value: T) => string | true;
|
|
297
|
+
/**
|
|
298
|
+
* Form-level validation function (for cross-field validation)
|
|
299
|
+
*/
|
|
300
|
+
type FormValidator<T> = (values: T) => Record<string, string> | null;
|
|
301
|
+
/**
|
|
302
|
+
* Field configuration
|
|
303
|
+
*/
|
|
304
|
+
interface FieldConfig<T> {
|
|
305
|
+
/**
|
|
306
|
+
* Initial value
|
|
307
|
+
*/
|
|
308
|
+
initialValue: T;
|
|
309
|
+
/**
|
|
310
|
+
* Validation function (optional)
|
|
311
|
+
* Return true if valid, or error message if invalid
|
|
312
|
+
*/
|
|
313
|
+
validate?: FieldValidator<T>;
|
|
314
|
+
/**
|
|
315
|
+
* Required field
|
|
316
|
+
* @default false
|
|
317
|
+
*/
|
|
318
|
+
required?: boolean;
|
|
319
|
+
/**
|
|
320
|
+
* Custom required message
|
|
321
|
+
*/
|
|
322
|
+
requiredMessage?: string;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Form configuration
|
|
326
|
+
*/
|
|
327
|
+
interface FormConfig<T extends Record<string, unknown>> {
|
|
328
|
+
/**
|
|
329
|
+
* Field configurations
|
|
330
|
+
*/
|
|
331
|
+
fields: {
|
|
332
|
+
[K in keyof T]: FieldConfig<T[K]>;
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* Form-level validation (optional)
|
|
336
|
+
* For cross-field validation
|
|
337
|
+
*/
|
|
338
|
+
validate?: FormValidator<T>;
|
|
339
|
+
/**
|
|
340
|
+
* Called when form is successfully submitted
|
|
341
|
+
*/
|
|
342
|
+
onSubmit: (values: T) => void | Promise<void>;
|
|
343
|
+
/**
|
|
344
|
+
* Auto-focus first error on validation failure
|
|
345
|
+
* @default true
|
|
346
|
+
*/
|
|
347
|
+
autoFocusError?: boolean;
|
|
348
|
+
/**
|
|
349
|
+
* Announce errors to screen readers
|
|
350
|
+
* @default true
|
|
351
|
+
*/
|
|
352
|
+
announceErrors?: boolean;
|
|
353
|
+
/**
|
|
354
|
+
* Validate on blur
|
|
355
|
+
* @default true
|
|
356
|
+
*/
|
|
357
|
+
validateOnBlur?: boolean;
|
|
358
|
+
/**
|
|
359
|
+
* Validate on change (after first blur)
|
|
360
|
+
* @default true
|
|
361
|
+
*/
|
|
362
|
+
validateOnChange?: boolean;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Form state
|
|
366
|
+
*/
|
|
367
|
+
interface FormState<T extends Record<string, unknown>> {
|
|
368
|
+
/**
|
|
369
|
+
* Current form values
|
|
370
|
+
*/
|
|
371
|
+
values: T;
|
|
372
|
+
/**
|
|
373
|
+
* Field errors
|
|
374
|
+
*/
|
|
375
|
+
errors: Partial<Record<keyof T, string>>;
|
|
376
|
+
/**
|
|
377
|
+
* Fields that have been touched (blurred at least once)
|
|
378
|
+
*/
|
|
379
|
+
touched: Partial<Record<keyof T, boolean>>;
|
|
380
|
+
/**
|
|
381
|
+
* Is form submitting
|
|
382
|
+
*/
|
|
383
|
+
isSubmitting: boolean;
|
|
384
|
+
/**
|
|
385
|
+
* Is form valid (no errors)
|
|
386
|
+
*/
|
|
387
|
+
isValid: boolean;
|
|
388
|
+
/**
|
|
389
|
+
* Has form been submitted at least once
|
|
390
|
+
*/
|
|
391
|
+
hasSubmitted: boolean;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Field props for binding to input elements
|
|
395
|
+
*/
|
|
396
|
+
interface FieldProps<T> {
|
|
397
|
+
name: string;
|
|
398
|
+
value: T;
|
|
399
|
+
onChange: (value: T) => void;
|
|
400
|
+
onBlur: () => void;
|
|
401
|
+
'aria-invalid': boolean;
|
|
402
|
+
'aria-describedby'?: string;
|
|
403
|
+
'aria-required'?: boolean;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Form return value
|
|
407
|
+
*/
|
|
408
|
+
interface UseAccessibleFormReturn<T extends Record<string, unknown>> {
|
|
409
|
+
/**
|
|
410
|
+
* Form state
|
|
411
|
+
*/
|
|
412
|
+
state: FormState<T>;
|
|
413
|
+
/**
|
|
414
|
+
* Get props for a field
|
|
415
|
+
*/
|
|
416
|
+
getFieldProps: <K extends keyof T>(name: K, options?: {
|
|
417
|
+
'aria-describedby'?: string;
|
|
418
|
+
}) => FieldProps<T[K]>;
|
|
419
|
+
/**
|
|
420
|
+
* Set field value programmatically
|
|
421
|
+
*/
|
|
422
|
+
setFieldValue: <K extends keyof T>(name: K, value: T[K]) => void;
|
|
423
|
+
/**
|
|
424
|
+
* Set field error programmatically
|
|
425
|
+
*/
|
|
426
|
+
setFieldError: <K extends keyof T>(name: K, error: string) => void;
|
|
427
|
+
/**
|
|
428
|
+
* Set multiple field errors at once
|
|
429
|
+
*/
|
|
430
|
+
setErrors: (errors: Partial<Record<keyof T, string>>) => void;
|
|
431
|
+
/**
|
|
432
|
+
* Validate a single field
|
|
433
|
+
*/
|
|
434
|
+
validateField: <K extends keyof T>(name: K) => boolean;
|
|
435
|
+
/**
|
|
436
|
+
* Validate entire form
|
|
437
|
+
*/
|
|
438
|
+
validateForm: () => boolean;
|
|
439
|
+
/**
|
|
440
|
+
* Handle form submit
|
|
441
|
+
*/
|
|
442
|
+
handleSubmit: (e?: React.FormEvent) => void;
|
|
443
|
+
/**
|
|
444
|
+
* Reset form to initial values
|
|
445
|
+
*/
|
|
446
|
+
reset: () => void;
|
|
447
|
+
/**
|
|
448
|
+
* Clear all errors
|
|
449
|
+
*/
|
|
450
|
+
clearErrors: () => void;
|
|
451
|
+
/**
|
|
452
|
+
* Field refs for focus management
|
|
453
|
+
*/
|
|
454
|
+
fieldRefs: Map<keyof T, HTMLElement>;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* useAccessibleForm Hook
|
|
458
|
+
*
|
|
459
|
+
* Comprehensive form management with accessibility built-in:
|
|
460
|
+
* - Automatic error announcements to screen readers
|
|
461
|
+
* - Auto-focus first error field on validation failure
|
|
462
|
+
* - Required field validation
|
|
463
|
+
* - Field-level and form-level validation
|
|
464
|
+
* - aria-invalid and aria-describedby management
|
|
465
|
+
* - Touch tracking for better UX
|
|
466
|
+
*
|
|
467
|
+
* Pattern Explanation:
|
|
468
|
+
* - Each field gets automatic ARIA attributes
|
|
469
|
+
* - Errors are announced via screen reader
|
|
470
|
+
* - First error field receives focus on submit
|
|
471
|
+
* - Validation can run on blur or change
|
|
472
|
+
* - Required fields enforced via TypeScript
|
|
473
|
+
*
|
|
474
|
+
* @example
|
|
475
|
+
* ```tsx
|
|
476
|
+
* const form = useAccessibleForm({
|
|
477
|
+
* fields: {
|
|
478
|
+
* email: {
|
|
479
|
+
* initialValue: '',
|
|
480
|
+
* required: true,
|
|
481
|
+
* validate: (value) => {
|
|
482
|
+
* if (!value.includes('@')) return 'Invalid email';
|
|
483
|
+
* return true;
|
|
484
|
+
* },
|
|
485
|
+
* },
|
|
486
|
+
* password: {
|
|
487
|
+
* initialValue: '',
|
|
488
|
+
* required: true,
|
|
489
|
+
* validate: (value) => {
|
|
490
|
+
* if (value.length < 8) return 'Password must be at least 8 characters';
|
|
491
|
+
* return true;
|
|
492
|
+
* },
|
|
493
|
+
* },
|
|
494
|
+
* },
|
|
495
|
+
* onSubmit: (values) => {
|
|
496
|
+
* console.log('Form submitted:', values);
|
|
497
|
+
* },
|
|
498
|
+
* });
|
|
499
|
+
*
|
|
500
|
+
* return (
|
|
501
|
+
* <form onSubmit={form.handleSubmit}>
|
|
502
|
+
* <input {...form.getFieldProps('email')} type="email" />
|
|
503
|
+
* {form.state.errors.email && (
|
|
504
|
+
* <span id="email-error">{form.state.errors.email}</span>
|
|
505
|
+
* )}
|
|
506
|
+
* </form>
|
|
507
|
+
* );
|
|
508
|
+
* ```
|
|
509
|
+
*/
|
|
510
|
+
declare const useAccessibleForm: <T extends Record<string, unknown>>(config: FormConfig<T>) => UseAccessibleFormReturn<T>;
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* @a13y/react - useFormField Hook
|
|
514
|
+
* Individual form field management with accessibility built-in
|
|
515
|
+
*/
|
|
516
|
+
/**
|
|
517
|
+
* Props for useFormField
|
|
518
|
+
*/
|
|
519
|
+
interface UseFormFieldProps<T = string> {
|
|
520
|
+
/**
|
|
521
|
+
* Field label (required for accessibility)
|
|
522
|
+
* This will be used as the accessible name
|
|
523
|
+
*/
|
|
524
|
+
label: string;
|
|
525
|
+
/**
|
|
526
|
+
* Initial field value
|
|
527
|
+
*/
|
|
528
|
+
initialValue?: T;
|
|
529
|
+
/**
|
|
530
|
+
* Validation function
|
|
531
|
+
* Return true if valid, or error message if invalid
|
|
532
|
+
*/
|
|
533
|
+
validate?: (value: T) => string | true;
|
|
534
|
+
/**
|
|
535
|
+
* Is field required
|
|
536
|
+
* @default false
|
|
537
|
+
*/
|
|
538
|
+
required?: boolean;
|
|
539
|
+
/**
|
|
540
|
+
* Custom required message
|
|
541
|
+
*/
|
|
542
|
+
requiredMessage?: string;
|
|
543
|
+
/**
|
|
544
|
+
* Help text to display
|
|
545
|
+
*/
|
|
546
|
+
helpText?: string;
|
|
547
|
+
/**
|
|
548
|
+
* Validate on blur
|
|
549
|
+
* @default true
|
|
550
|
+
*/
|
|
551
|
+
validateOnBlur?: boolean;
|
|
552
|
+
/**
|
|
553
|
+
* Validate on change (after first blur)
|
|
554
|
+
* @default true
|
|
555
|
+
*/
|
|
556
|
+
validateOnChange?: boolean;
|
|
557
|
+
/**
|
|
558
|
+
* Announce errors to screen readers
|
|
559
|
+
* @default true
|
|
560
|
+
*/
|
|
561
|
+
announceErrors?: boolean;
|
|
562
|
+
/**
|
|
563
|
+
* Called when value changes
|
|
564
|
+
*/
|
|
565
|
+
onChange?: (value: T) => void;
|
|
566
|
+
/**
|
|
567
|
+
* Called when field is blurred
|
|
568
|
+
*/
|
|
569
|
+
onBlur?: () => void;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Return value from useFormField
|
|
573
|
+
*/
|
|
574
|
+
interface UseFormFieldReturn<T = string> {
|
|
575
|
+
/**
|
|
576
|
+
* Field ID (generated)
|
|
577
|
+
*/
|
|
578
|
+
id: string;
|
|
579
|
+
/**
|
|
580
|
+
* Label ID (generated)
|
|
581
|
+
*/
|
|
582
|
+
labelId: string;
|
|
583
|
+
/**
|
|
584
|
+
* Error message ID (generated)
|
|
585
|
+
*/
|
|
586
|
+
errorId: string;
|
|
587
|
+
/**
|
|
588
|
+
* Help text ID (generated)
|
|
589
|
+
*/
|
|
590
|
+
helpTextId: string;
|
|
591
|
+
/**
|
|
592
|
+
* Current field value
|
|
593
|
+
*/
|
|
594
|
+
value: T;
|
|
595
|
+
/**
|
|
596
|
+
* Current error message (if any)
|
|
597
|
+
*/
|
|
598
|
+
error: string | null;
|
|
599
|
+
/**
|
|
600
|
+
* Has field been touched (blurred at least once)
|
|
601
|
+
*/
|
|
602
|
+
isTouched: boolean;
|
|
603
|
+
/**
|
|
604
|
+
* Is field valid
|
|
605
|
+
*/
|
|
606
|
+
isValid: boolean;
|
|
607
|
+
/**
|
|
608
|
+
* Set field value
|
|
609
|
+
*/
|
|
610
|
+
setValue: (value: T) => void;
|
|
611
|
+
/**
|
|
612
|
+
* Set field error
|
|
613
|
+
*/
|
|
614
|
+
setError: (error: string | null) => void;
|
|
615
|
+
/**
|
|
616
|
+
* Validate field
|
|
617
|
+
*/
|
|
618
|
+
validate: () => boolean;
|
|
619
|
+
/**
|
|
620
|
+
* Clear error
|
|
621
|
+
*/
|
|
622
|
+
clearError: () => void;
|
|
623
|
+
/**
|
|
624
|
+
* Reset field to initial value
|
|
625
|
+
*/
|
|
626
|
+
reset: () => void;
|
|
627
|
+
/**
|
|
628
|
+
* Props for label element
|
|
629
|
+
*/
|
|
630
|
+
labelProps: {
|
|
631
|
+
id: string;
|
|
632
|
+
htmlFor: string;
|
|
633
|
+
};
|
|
634
|
+
/**
|
|
635
|
+
* Props for input element
|
|
636
|
+
*/
|
|
637
|
+
inputProps: {
|
|
638
|
+
id: string;
|
|
639
|
+
name: string;
|
|
640
|
+
value: T;
|
|
641
|
+
onChange: (value: T) => void;
|
|
642
|
+
onBlur: () => void;
|
|
643
|
+
'aria-labelledby': string;
|
|
644
|
+
'aria-describedby'?: string;
|
|
645
|
+
'aria-invalid': boolean;
|
|
646
|
+
'aria-required'?: boolean;
|
|
647
|
+
ref: React.RefObject<HTMLInputElement | null>;
|
|
648
|
+
};
|
|
649
|
+
/**
|
|
650
|
+
* Props for error message element
|
|
651
|
+
*/
|
|
652
|
+
errorProps: {
|
|
653
|
+
id: string;
|
|
654
|
+
role: 'alert';
|
|
655
|
+
'aria-live': 'polite';
|
|
656
|
+
};
|
|
657
|
+
/**
|
|
658
|
+
* Props for help text element
|
|
659
|
+
*/
|
|
660
|
+
helpTextProps: {
|
|
661
|
+
id: string;
|
|
662
|
+
};
|
|
663
|
+
/**
|
|
664
|
+
* Field ref for focus management
|
|
665
|
+
*/
|
|
666
|
+
fieldRef: React.RefObject<HTMLInputElement | null>;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* useFormField Hook
|
|
670
|
+
*
|
|
671
|
+
* Manages a single form field with accessibility built-in:
|
|
672
|
+
* - Required label via TypeScript
|
|
673
|
+
* - Automatic ARIA attributes
|
|
674
|
+
* - Error announcements to screen readers
|
|
675
|
+
* - Help text support
|
|
676
|
+
* - Validation on blur/change
|
|
677
|
+
* - ID generation for ARIA relationships
|
|
678
|
+
*
|
|
679
|
+
* Pattern Explanation:
|
|
680
|
+
* - Label is required (enforced at compile-time)
|
|
681
|
+
* - aria-labelledby automatically connects label to input
|
|
682
|
+
* - aria-describedby automatically connects errors and help text
|
|
683
|
+
* - aria-invalid automatically set when error exists
|
|
684
|
+
* - Errors announced to screen readers when they appear
|
|
685
|
+
*
|
|
686
|
+
* @example
|
|
687
|
+
* ```tsx
|
|
688
|
+
* const emailField = useFormField({
|
|
689
|
+
* label: 'Email Address',
|
|
690
|
+
* required: true,
|
|
691
|
+
* validate: (value) => {
|
|
692
|
+
* if (!value.includes('@')) return 'Invalid email address';
|
|
693
|
+
* return true;
|
|
694
|
+
* },
|
|
695
|
+
* helpText: 'We will never share your email',
|
|
696
|
+
* });
|
|
697
|
+
*
|
|
698
|
+
* return (
|
|
699
|
+
* <div>
|
|
700
|
+
* <label {...emailField.labelProps}>{emailField.label}</label>
|
|
701
|
+
* <input
|
|
702
|
+
* {...emailField.inputProps}
|
|
703
|
+
* type="email"
|
|
704
|
+
* onChange={(e) => emailField.inputProps.onChange(e.target.value)}
|
|
705
|
+
* />
|
|
706
|
+
* {emailField.helpText && (
|
|
707
|
+
* <span {...emailField.helpTextProps}>{emailField.helpText}</span>
|
|
708
|
+
* )}
|
|
709
|
+
* {emailField.error && (
|
|
710
|
+
* <span {...emailField.errorProps}>{emailField.error}</span>
|
|
711
|
+
* )}
|
|
712
|
+
* </div>
|
|
713
|
+
* );
|
|
714
|
+
* ```
|
|
715
|
+
*/
|
|
716
|
+
declare const useFormField: <T = string>(props: UseFormFieldProps<T>) => UseFormFieldReturn<T>;
|
|
717
|
+
/**
|
|
718
|
+
* Helper type to ensure label is provided
|
|
719
|
+
* Use this in component props to enforce accessible labels
|
|
720
|
+
*/
|
|
721
|
+
type RequireLabel<T> = T & {
|
|
722
|
+
label: string;
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
export { type DialogBackdropProps, type DialogContainerProps, type DialogDescriptionProps, type DialogTitleProps, type FieldConfig, type FieldProps, type FieldValidator, type FormConfig, type FormState, type FormValidator, type NavigableItemProps, type RequireLabel, type UseAccessibleDialogProps, type UseAccessibleDialogReturn, type UseAccessibleFormReturn, type UseFocusTrapProps, type UseFocusTrapReturn, type UseFormFieldProps, type UseFormFieldReturn, type UseKeyboardNavigationProps, type UseKeyboardNavigationReturn, useAccessibleDialog, useAccessibleForm, useFocusTrap, useFormField, useKeyboardNavigation };
|