@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 Diego Aneli and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,241 @@
1
+ <div align="center">
2
+ <img src="https://raw.githubusercontent.com/DiegoAneli/a13y/main/assets/logo.svg" alt="A13Y Logo" width="150" />
3
+
4
+ <h1>@a13y/react</h1>
5
+
6
+ <p><strong>Type-safe React hooks for accessibility with compile-time guarantees</strong></p>
7
+ </div>
8
+
9
+ <br />
10
+
11
+ ## Features
12
+
13
+ - **Type-Safe**: Restrictive TypeScript signatures prevent misuse
14
+ - **Props Enforcement**: Required props enforced at compile-time
15
+ - **Automatic ARIA**: Correct ARIA attributes applied automatically
16
+ - **Keyboard Support**: Enter, Space, Arrow keys handled correctly
17
+ - **Focus Management**: Focus trap, restoration, roving tabindex
18
+ - **Dev Validation**: Runtime warnings in development (via @a13y/devtools)
19
+ - **Zero Overhead**: All dev code stripped in production
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install @a13y/react @a13y/core
25
+ ```
26
+
27
+ Optional devtools for development warnings:
28
+
29
+ ```bash
30
+ npm install -D @a13y/devtools
31
+ ```
32
+
33
+ ## Hooks
34
+
35
+ ### useAccessibleButton
36
+
37
+ Creates accessible buttons with keyboard support.
38
+
39
+ ```tsx
40
+ import { useAccessibleButton } from '@a13y/react';
41
+
42
+ function DeleteButton() {
43
+ const { buttonProps } = useAccessibleButton({
44
+ label: 'Delete item', // Required for icon-only buttons
45
+ onPress: (event) => {
46
+ console.log(event.type); // 'mouse' | 'keyboard'
47
+ console.log(event.key); // 'Enter' | ' ' | undefined
48
+ },
49
+ });
50
+
51
+ return <button {...buttonProps}>🗑️</button>;
52
+ }
53
+ ```
54
+
55
+ **Props:**
56
+ - `label?: string` - Accessible label (required for icon-only buttons)
57
+ - `onPress: (event: PressEvent) => void` - Called on click or Enter/Space
58
+ - `isDisabled?: boolean` - Whether button is disabled
59
+ - `role?: 'button' | 'link'` - ARIA role (default: 'button')
60
+ - `elementType?: 'button' | 'a'` - HTML element type (default: 'button')
61
+
62
+ ### useAccessibleDialog
63
+
64
+ Creates accessible modals/dialogs with focus trap.
65
+
66
+ ```tsx
67
+ import { useAccessibleDialog } from '@a13y/react';
68
+
69
+ function ConfirmDialog({ isOpen, onClose }) {
70
+ const { dialogProps, titleProps, descriptionProps, backdropProps } =
71
+ useAccessibleDialog({
72
+ isOpen,
73
+ onClose,
74
+ title: 'Confirm Action', // ✅ Required
75
+ description: 'This action cannot be undone',
76
+ });
77
+
78
+ if (!isOpen) return null;
79
+
80
+ return (
81
+ <>
82
+ {backdropProps && <div className="backdrop" {...backdropProps} />}
83
+ <div className="dialog" {...dialogProps}>
84
+ <h2 {...titleProps}>Confirm Action</h2>
85
+ <p {...descriptionProps}>This action cannot be undone</p>
86
+ <button onClick={onClose}>Cancel</button>
87
+ </div>
88
+ </>
89
+ );
90
+ }
91
+ ```
92
+
93
+ **Props:**
94
+ - `isOpen: boolean` - Whether dialog is open
95
+ - `onClose: () => void` - Called when dialog should close
96
+ - `title: string` - Dialog title (REQUIRED for accessibility)
97
+ - `description?: string` - Dialog description
98
+ - `role?: 'dialog' | 'alertdialog'` - ARIA role (default: 'dialog')
99
+ - `isModal?: boolean` - Whether dialog is modal/blocking (default: true)
100
+ - `closeOnBackdropClick?: boolean` - Close on backdrop click (default: true)
101
+
102
+ ### useFocusTrap
103
+
104
+ Creates a focus trap for modals and popups.
105
+
106
+ ```tsx
107
+ import { useFocusTrap } from '@a13y/react';
108
+
109
+ function Modal({ isOpen, onClose }) {
110
+ const { trapRef } = useFocusTrap({
111
+ isActive: isOpen,
112
+ onEscape: onClose,
113
+ restoreFocus: true, // Restore focus on close
114
+ autoFocus: true, // Auto-focus first element
115
+ });
116
+
117
+ if (!isOpen) return null;
118
+
119
+ return (
120
+ <div ref={trapRef} className="modal">
121
+ <h2>Modal Title</h2>
122
+ <button onClick={onClose}>Close</button>
123
+ </div>
124
+ );
125
+ }
126
+ ```
127
+
128
+ **Props:**
129
+ - `isActive: boolean` - Whether focus trap is active
130
+ - `onEscape?: () => void` - Called when Escape key is pressed
131
+ - `restoreFocus?: boolean` - Restore focus on deactivation (default: true)
132
+ - `autoFocus?: boolean` - Auto-focus first element (default: true)
133
+
134
+ ### useKeyboardNavigation
135
+
136
+ Implements roving tabindex keyboard navigation.
137
+
138
+ ```tsx
139
+ import { useKeyboardNavigation } from '@a13y/react';
140
+
141
+ function Toolbar() {
142
+ const { containerProps, getItemProps, currentIndex } =
143
+ useKeyboardNavigation({
144
+ orientation: 'horizontal', // Arrow Left/Right
145
+ loop: true, // Wrap around at edges
146
+ });
147
+
148
+ const tools = ['Cut', 'Copy', 'Paste', 'Undo', 'Redo'];
149
+
150
+ return (
151
+ <div className="toolbar" {...containerProps}>
152
+ {tools.map((tool, index) => (
153
+ <button
154
+ key={tool}
155
+ {...getItemProps(index)}
156
+ className={index === currentIndex ? 'focused' : ''}
157
+ >
158
+ {tool}
159
+ </button>
160
+ ))}
161
+ </div>
162
+ );
163
+ }
164
+ ```
165
+
166
+ **Props:**
167
+ - `orientation: 'horizontal' | 'vertical' | 'both'` - Navigation direction
168
+ - `loop?: boolean` - Whether to loop at boundaries (default: false)
169
+ - `onNavigate?: (index: number) => void` - Called when navigation occurs
170
+ - `defaultIndex?: number` - Initial focused index (default: 0)
171
+ - `currentIndex?: number` - Controlled current index
172
+
173
+ ## Type Safety
174
+
175
+ ### Forbidden Props
176
+
177
+ The hooks prevent misuse by forbidding certain props:
178
+
179
+ ```tsx
180
+ const { buttonProps } = useAccessibleButton({ onPress: () => {} });
181
+
182
+ // ❌ TypeScript Error: These props are managed by the hook
183
+ <button {...buttonProps} onClick={() => {}} />
184
+ <button {...buttonProps} onKeyDown={() => {}} />
185
+ ```
186
+
187
+ ### Required Props
188
+
189
+ Required props are enforced at compile-time:
190
+
191
+ ```tsx
192
+ // ❌ TypeScript Error: 'title' is required
193
+ useAccessibleDialog({
194
+ isOpen: true,
195
+ onClose: () => {},
196
+ // title: missing!
197
+ });
198
+ ```
199
+
200
+ ## Development Warnings
201
+
202
+ When `@a13y/devtools` is installed, you get runtime warnings in development:
203
+
204
+ ```tsx
205
+ // ⚠️ Console warning: Element is missing an accessible name
206
+ <button {...buttonProps}></button>
207
+
208
+ // ⚠️ Console warning: Focus trap has no focusable elements
209
+ <div ref={trapRef}></div>
210
+ ```
211
+
212
+ All warnings include:
213
+ - Clear description of the issue
214
+ - WCAG 2.2 criterion reference
215
+ - Multiple fix suggestions with code examples
216
+
217
+ ## Production Build
218
+
219
+ Zero overhead in production:
220
+
221
+ ```tsx
222
+ // Development build
223
+ useAccessibleButton({ onPress: () => {} });
224
+ // ✅ Includes validation code from @a13y/devtools
225
+
226
+ // Production build (NODE_ENV=production)
227
+ useAccessibleButton({ onPress: () => {} });
228
+ // ✅ All validation code removed via dead code elimination
229
+ ```
230
+
231
+ ## Examples
232
+
233
+ See [EXAMPLES.md](./EXAMPLES.md) for complete usage examples.
234
+
235
+ ## Author
236
+
237
+ Created and maintained by **Diego Aneli** ([@DiegoAneli](https://github.com/DiegoAneli))
238
+
239
+ ## License
240
+
241
+ MIT © Diego Aneli and contributors
@@ -0,0 +1,374 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { P as PressEvent } from '../use-accessible-button-B0syf-Az.js';
3
+ import { ReactNode } from 'react';
4
+
5
+ /**
6
+ * Button variants
7
+ */
8
+ type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';
9
+ /**
10
+ * Props for AccessibleButton
11
+ */
12
+ interface AccessibleButtonProps {
13
+ /**
14
+ * Button content
15
+ * If content is not text (e.g., icon only), label is REQUIRED
16
+ */
17
+ children: ReactNode;
18
+ /**
19
+ * Accessible label
20
+ * REQUIRED if children is not text (e.g., icon-only button)
21
+ */
22
+ label?: string;
23
+ /**
24
+ * Click handler
25
+ */
26
+ onPress: (event: PressEvent) => void;
27
+ /**
28
+ * Whether button is disabled
29
+ */
30
+ disabled?: boolean;
31
+ /**
32
+ * Visual variant (does not affect accessibility)
33
+ */
34
+ variant?: ButtonVariant;
35
+ /**
36
+ * Custom className
37
+ */
38
+ className?: string;
39
+ /**
40
+ * Button type
41
+ */
42
+ type?: 'button' | 'submit' | 'reset';
43
+ }
44
+ /**
45
+ * Accessible Button Component
46
+ *
47
+ * Features:
48
+ * - Automatic keyboard support (Enter, Space)
49
+ * - Required accessible name (compile-time + runtime)
50
+ * - Disabled state handling
51
+ * - Development-time validation
52
+ *
53
+ * @example
54
+ * ```tsx
55
+ * // Text button (label optional)
56
+ * <AccessibleButton onPress={() => console.log('Clicked')}>
57
+ * Save
58
+ * </AccessibleButton>
59
+ *
60
+ * // Icon button (label REQUIRED)
61
+ * <AccessibleButton
62
+ * label="Delete item"
63
+ * onPress={() => console.log('Deleted')}
64
+ * >
65
+ * 🗑️
66
+ * </AccessibleButton>
67
+ * ```
68
+ */
69
+ declare const AccessibleButton: (props: AccessibleButtonProps) => react_jsx_runtime.JSX.Element;
70
+
71
+ /**
72
+ * Props for AccessibleDialog
73
+ */
74
+ interface AccessibleDialogProps {
75
+ /**
76
+ * Whether dialog is open
77
+ */
78
+ isOpen: boolean;
79
+ /**
80
+ * Called when dialog should close
81
+ */
82
+ onClose: () => void;
83
+ /**
84
+ * Dialog title - REQUIRED for accessibility
85
+ * This will be announced to screen readers
86
+ */
87
+ title: string;
88
+ /**
89
+ * Dialog content
90
+ */
91
+ children: ReactNode;
92
+ /**
93
+ * Optional description
94
+ * Provides additional context to screen readers
95
+ */
96
+ description?: string;
97
+ /**
98
+ * ARIA role
99
+ */
100
+ role?: 'dialog' | 'alertdialog';
101
+ /**
102
+ * Whether to show close button
103
+ */
104
+ showCloseButton?: boolean;
105
+ /**
106
+ * Custom className for dialog container
107
+ */
108
+ className?: string;
109
+ /**
110
+ * Custom className for backdrop
111
+ */
112
+ backdropClassName?: string;
113
+ }
114
+ /**
115
+ * Accessible Dialog Component
116
+ *
117
+ * Features:
118
+ * - Focus trap with Tab/Shift+Tab cycling
119
+ * - Escape key to close
120
+ * - Focus restoration on close
121
+ * - Click outside to close
122
+ * - Required title for screen readers
123
+ * - Body scroll lock when open
124
+ *
125
+ * @example
126
+ * ```tsx
127
+ * <AccessibleDialog
128
+ * isOpen={isOpen}
129
+ * onClose={() => setIsOpen(false)}
130
+ * title="Confirm Action"
131
+ * description="This action cannot be undone"
132
+ * >
133
+ * <p>Are you sure you want to delete this item?</p>
134
+ * <button onClick={handleConfirm}>Confirm</button>
135
+ * <button onClick={() => setIsOpen(false)}>Cancel</button>
136
+ * </AccessibleDialog>
137
+ * ```
138
+ */
139
+ declare const AccessibleDialog: (props: AccessibleDialogProps) => react_jsx_runtime.JSX.Element | null;
140
+
141
+ /**
142
+ * Menu item definition
143
+ */
144
+ interface MenuItem {
145
+ /**
146
+ * Unique identifier
147
+ */
148
+ id: string;
149
+ /**
150
+ * Item label
151
+ */
152
+ label: string;
153
+ /**
154
+ * Click handler
155
+ */
156
+ onPress: () => void;
157
+ /**
158
+ * Whether item is disabled
159
+ */
160
+ disabled?: boolean;
161
+ /**
162
+ * Optional icon
163
+ */
164
+ icon?: ReactNode;
165
+ }
166
+ /**
167
+ * Props for AccessibleMenu
168
+ */
169
+ interface AccessibleMenuProps {
170
+ /**
171
+ * Menu trigger button label
172
+ */
173
+ label: string;
174
+ /**
175
+ * Trigger button content
176
+ */
177
+ trigger: ReactNode;
178
+ /**
179
+ * Menu items
180
+ */
181
+ items: MenuItem[];
182
+ /**
183
+ * Custom className for trigger button
184
+ */
185
+ className?: string;
186
+ /**
187
+ * Custom className for menu container
188
+ */
189
+ menuClassName?: string;
190
+ }
191
+ /**
192
+ * Accessible Menu Component
193
+ *
194
+ * Dropdown menu with full keyboard navigation:
195
+ * - Arrow Up/Down to navigate items
196
+ * - Enter/Space to select
197
+ * - Escape to close
198
+ * - Focus trap when open
199
+ *
200
+ * @example
201
+ * ```tsx
202
+ * <AccessibleMenu
203
+ * label="Open actions menu"
204
+ * trigger="Actions ▼"
205
+ * items={[
206
+ * { id: 'edit', label: 'Edit', onPress: () => console.log('Edit') },
207
+ * { id: 'delete', label: 'Delete', onPress: () => console.log('Delete'), disabled: true },
208
+ * { id: 'share', label: 'Share', onPress: () => console.log('Share') },
209
+ * ]}
210
+ * />
211
+ * ```
212
+ */
213
+ declare const AccessibleMenu: (props: AccessibleMenuProps) => react_jsx_runtime.JSX.Element;
214
+
215
+ /**
216
+ * Props for AccessibleModal
217
+ */
218
+ interface AccessibleModalProps {
219
+ /**
220
+ * Whether modal is open
221
+ */
222
+ isOpen: boolean;
223
+ /**
224
+ * Called when modal should close
225
+ */
226
+ onClose: () => void;
227
+ /**
228
+ * Modal title - REQUIRED
229
+ */
230
+ title: string;
231
+ /**
232
+ * Modal body content
233
+ */
234
+ children: ReactNode;
235
+ /**
236
+ * Footer content (typically action buttons)
237
+ */
238
+ footer?: ReactNode;
239
+ /**
240
+ * Modal size
241
+ */
242
+ size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
243
+ /**
244
+ * Whether modal can be closed by clicking outside
245
+ */
246
+ closeOnBackdropClick?: boolean;
247
+ /**
248
+ * Custom className for modal container
249
+ */
250
+ className?: string;
251
+ }
252
+ /**
253
+ * Accessible Modal Component
254
+ *
255
+ * Full-featured modal with header, body, and footer sections.
256
+ *
257
+ * @example
258
+ * ```tsx
259
+ * <AccessibleModal
260
+ * isOpen={isOpen}
261
+ * onClose={() => setIsOpen(false)}
262
+ * title="Edit Profile"
263
+ * size="md"
264
+ * footer={
265
+ * <>
266
+ * <AccessibleButton onPress={() => setIsOpen(false)}>
267
+ * Cancel
268
+ * </AccessibleButton>
269
+ * <AccessibleButton onPress={handleSave} variant="primary">
270
+ * Save Changes
271
+ * </AccessibleButton>
272
+ * </>
273
+ * }
274
+ * >
275
+ * <form>
276
+ * <input type="text" placeholder="Name" />
277
+ * <input type="email" placeholder="Email" />
278
+ * </form>
279
+ * </AccessibleModal>
280
+ * ```
281
+ */
282
+ declare const AccessibleModal: (props: AccessibleModalProps) => react_jsx_runtime.JSX.Element | null;
283
+
284
+ /**
285
+ * Tab definition
286
+ */
287
+ interface Tab {
288
+ /**
289
+ * Unique identifier
290
+ */
291
+ id: string;
292
+ /**
293
+ * Tab label - REQUIRED for accessibility
294
+ */
295
+ label: string;
296
+ /**
297
+ * Tab panel content
298
+ */
299
+ content: ReactNode;
300
+ /**
301
+ * Whether tab is disabled
302
+ */
303
+ disabled?: boolean;
304
+ /**
305
+ * Optional icon
306
+ */
307
+ icon?: ReactNode;
308
+ }
309
+ /**
310
+ * Props for AccessibleTabs
311
+ */
312
+ interface AccessibleTabsProps {
313
+ /**
314
+ * Tabs configuration - REQUIRED
315
+ * Must have at least one tab
316
+ */
317
+ tabs: [Tab, ...Tab[]];
318
+ /**
319
+ * Initially selected tab index
320
+ */
321
+ defaultTab?: number;
322
+ /**
323
+ * Controlled selected tab index
324
+ */
325
+ selectedTab?: number;
326
+ /**
327
+ * Called when tab changes
328
+ */
329
+ onTabChange?: (index: number) => void;
330
+ /**
331
+ * Custom className for tabs container
332
+ */
333
+ className?: string;
334
+ /**
335
+ * Custom className for panel
336
+ */
337
+ panelClassName?: string;
338
+ }
339
+ /**
340
+ * Accessible Tabs Component
341
+ *
342
+ * Tab interface following WAI-ARIA Tabs pattern:
343
+ * - Arrow Left/Right to navigate tabs
344
+ * - Home/End to jump to first/last
345
+ * - Automatic panel switching
346
+ * - Proper ARIA attributes
347
+ *
348
+ * @example
349
+ * ```tsx
350
+ * <AccessibleTabs
351
+ * tabs={[
352
+ * {
353
+ * id: 'account',
354
+ * label: 'Account',
355
+ * content: <div>Account settings...</div>,
356
+ * },
357
+ * {
358
+ * id: 'security',
359
+ * label: 'Security',
360
+ * content: <div>Security settings...</div>,
361
+ * },
362
+ * {
363
+ * id: 'billing',
364
+ * label: 'Billing',
365
+ * content: <div>Billing information...</div>,
366
+ * disabled: true,
367
+ * },
368
+ * ]}
369
+ * />
370
+ * ```
371
+ */
372
+ declare const AccessibleTabs: (props: AccessibleTabsProps) => react_jsx_runtime.JSX.Element;
373
+
374
+ export { AccessibleButton, AccessibleDialog, AccessibleMenu, AccessibleModal, AccessibleTabs, type AccessibleButtonProps as ButtonComponentProps, type ButtonVariant, type AccessibleDialogProps as DialogComponentProps, type AccessibleMenuProps as MenuComponentProps, type MenuItem, type AccessibleModalProps as ModalComponentProps, type Tab, type AccessibleTabsProps as TabsComponentProps };