@a11ypros/a11y-ui-components 1.0.1 → 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.
Files changed (217) hide show
  1. package/README.md +182 -157
  2. package/dist/components/Button/Button.d.ts +37 -0
  3. package/dist/components/Button/Button.d.ts.map +1 -0
  4. package/dist/components/Button/Button.js +52 -0
  5. package/dist/components/Button/index.d.ts +3 -0
  6. package/dist/components/Button/index.d.ts.map +1 -0
  7. package/dist/components/Button/index.js +1 -0
  8. package/dist/components/DataTable/DataTable.d.ts +71 -0
  9. package/dist/components/DataTable/DataTable.d.ts.map +1 -0
  10. package/dist/components/DataTable/DataTable.js +122 -0
  11. package/dist/components/DataTable/index.d.ts +3 -0
  12. package/dist/components/DataTable/index.d.ts.map +1 -0
  13. package/dist/components/DataTable/index.js +1 -0
  14. package/dist/components/Form/Checkbox.d.ts +36 -0
  15. package/dist/components/Form/Checkbox.d.ts.map +1 -0
  16. package/dist/components/Form/Checkbox.js +39 -0
  17. package/dist/components/Form/Fieldset.d.ts +33 -0
  18. package/dist/components/Form/Fieldset.d.ts.map +1 -0
  19. package/dist/components/Form/Fieldset.js +34 -0
  20. package/dist/components/Form/Input.d.ts +37 -0
  21. package/dist/components/Form/Input.d.ts.map +1 -0
  22. package/dist/components/Form/Input.js +41 -0
  23. package/dist/components/Form/Label.d.ts +30 -0
  24. package/dist/components/Form/Label.d.ts.map +1 -0
  25. package/dist/components/Form/Label.js +30 -0
  26. package/dist/components/Form/Radio.d.ts +53 -0
  27. package/dist/components/Form/Radio.d.ts.map +1 -0
  28. package/dist/components/Form/Radio.js +39 -0
  29. package/dist/components/Form/Select.d.ts +51 -0
  30. package/dist/components/Form/Select.d.ts.map +1 -0
  31. package/dist/components/Form/Select.js +49 -0
  32. package/dist/components/Form/Textarea.d.ts +44 -0
  33. package/dist/components/Form/Textarea.d.ts.map +1 -0
  34. package/dist/components/Form/Textarea.js +43 -0
  35. package/dist/components/Form/index.d.ts +8 -0
  36. package/dist/components/Form/index.d.ts.map +1 -0
  37. package/dist/components/Form/index.js +7 -0
  38. package/dist/components/Link/Link.d.ts +34 -0
  39. package/dist/components/Link/Link.d.ts.map +1 -0
  40. package/dist/components/Link/Link.js +48 -0
  41. package/dist/components/Link/index.d.ts +3 -0
  42. package/dist/components/Link/index.d.ts.map +1 -0
  43. package/dist/components/Link/index.js +1 -0
  44. package/dist/components/Modal/Modal.d.ts +64 -0
  45. package/dist/components/Modal/Modal.d.ts.map +1 -0
  46. package/dist/components/Modal/Modal.js +108 -0
  47. package/dist/components/Modal/index.d.ts +3 -0
  48. package/dist/components/Modal/index.d.ts.map +1 -0
  49. package/dist/components/Modal/index.js +1 -0
  50. package/dist/components/Tabs/Tabs.d.ts +63 -0
  51. package/dist/components/Tabs/Tabs.d.ts.map +1 -0
  52. package/dist/components/Tabs/Tabs.js +134 -0
  53. package/dist/components/Tabs/index.d.ts +3 -0
  54. package/dist/components/Tabs/index.d.ts.map +1 -0
  55. package/dist/components/Tabs/index.js +1 -0
  56. package/dist/components/Toast/Toast.d.ts +59 -0
  57. package/dist/components/Toast/Toast.d.ts.map +1 -0
  58. package/dist/components/Toast/Toast.js +91 -0
  59. package/dist/components/Toast/ToastProvider.d.ts +22 -0
  60. package/dist/components/Toast/ToastProvider.d.ts.map +1 -0
  61. package/dist/components/Toast/ToastProvider.js +33 -0
  62. package/dist/components/Toast/index.d.ts +5 -0
  63. package/dist/components/Toast/index.d.ts.map +1 -0
  64. package/dist/components/Toast/index.js +2 -0
  65. package/dist/hooks/useAriaLive.d.ts +9 -0
  66. package/dist/hooks/useAriaLive.d.ts.map +1 -0
  67. package/dist/hooks/useAriaLive.js +39 -0
  68. package/dist/hooks/useFocusReturn.d.ts +9 -0
  69. package/dist/hooks/useFocusReturn.d.ts.map +1 -0
  70. package/dist/hooks/useFocusReturn.js +33 -0
  71. package/dist/hooks/useFocusTrap.d.ts +9 -0
  72. package/dist/hooks/useFocusTrap.d.ts.map +1 -0
  73. package/dist/hooks/useFocusTrap.js +68 -0
  74. package/dist/index.d.ts +22 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/{packages/design-system/src/index.ts → dist/index.js} +0 -4
  77. package/dist/styles/index.d.ts +3 -0
  78. package/dist/styles/index.d.ts.map +1 -0
  79. package/dist/styles/index.js +1 -0
  80. package/dist/tokens/breakpoints.d.ts +25 -0
  81. package/dist/tokens/breakpoints.d.ts.map +1 -0
  82. package/dist/tokens/breakpoints.js +23 -0
  83. package/dist/tokens/colors.d.ts +81 -0
  84. package/dist/tokens/colors.d.ts.map +1 -0
  85. package/dist/tokens/colors.js +86 -0
  86. package/dist/tokens/index.d.ts +6 -0
  87. package/dist/tokens/index.d.ts.map +1 -0
  88. package/dist/tokens/index.js +5 -0
  89. package/dist/tokens/motion.d.ts +30 -0
  90. package/dist/tokens/motion.d.ts.map +1 -0
  91. package/dist/tokens/motion.js +34 -0
  92. package/dist/tokens/spacing.d.ts +22 -0
  93. package/dist/tokens/spacing.d.ts.map +1 -0
  94. package/dist/tokens/spacing.js +20 -0
  95. package/dist/tokens/theme.d.ts +159 -0
  96. package/dist/tokens/theme.d.ts.map +1 -0
  97. package/dist/tokens/theme.js +15 -0
  98. package/dist/tokens/typography.d.ts +45 -0
  99. package/dist/tokens/typography.d.ts.map +1 -0
  100. package/dist/tokens/typography.js +56 -0
  101. package/dist/utils/aria.d.ts +60 -0
  102. package/dist/utils/aria.d.ts.map +1 -0
  103. package/dist/utils/aria.js +86 -0
  104. package/dist/utils/focus.d.ts +30 -0
  105. package/dist/utils/focus.d.ts.map +1 -0
  106. package/dist/utils/focus.js +80 -0
  107. package/dist/utils/index.d.ts +4 -0
  108. package/dist/utils/index.d.ts.map +1 -0
  109. package/dist/utils/index.js +3 -0
  110. package/dist/utils/keyboard.d.ts +38 -0
  111. package/dist/utils/keyboard.d.ts.map +1 -0
  112. package/dist/utils/keyboard.js +59 -0
  113. package/package.json +58 -31
  114. package/.storybook/custom.css +0 -69
  115. package/.storybook/main.ts +0 -46
  116. package/.storybook/manager.ts +0 -26
  117. package/.storybook/package.json +0 -6
  118. package/.storybook/preview.tsx +0 -31
  119. package/.storybook/public/logo.png +0 -0
  120. package/.storybook/vite.config.ts +0 -24
  121. package/.storybook/welcome.mdx +0 -97
  122. package/DEPLOYMENT.md +0 -154
  123. package/apps/web/app/(docs)/audit/audit.css +0 -269
  124. package/apps/web/app/(docs)/audit/page.tsx +0 -271
  125. package/apps/web/app/(docs)/components/button/page.tsx +0 -49
  126. package/apps/web/app/(docs)/components/form/page.tsx +0 -92
  127. package/apps/web/app/(docs)/components/link/page.tsx +0 -31
  128. package/apps/web/app/(docs)/components/modal/page.tsx +0 -41
  129. package/apps/web/app/(docs)/components/page.tsx +0 -37
  130. package/apps/web/app/(docs)/components/table/page.tsx +0 -54
  131. package/apps/web/app/(docs)/components/tabs/page.tsx +0 -61
  132. package/apps/web/app/(docs)/components/toast/page.tsx +0 -51
  133. package/apps/web/app/api/audit/route.ts +0 -128
  134. package/apps/web/app/favicon.ico +0 -0
  135. package/apps/web/app/layout.tsx +0 -20
  136. package/apps/web/app/page.tsx +0 -17
  137. package/apps/web/app/styles/globals.css +0 -5
  138. package/apps/web/next-env.d.ts +0 -5
  139. package/apps/web/next.config.js +0 -21
  140. package/apps/web/package.json +0 -28
  141. package/apps/web/public/_headers +0 -17
  142. package/apps/web/public/_redirects +0 -31
  143. package/apps/web/public/logo.png +0 -0
  144. package/apps/web/tsconfig.json +0 -29
  145. package/netlify/functions/audit.ts +0 -163
  146. package/netlify.toml +0 -37
  147. package/packages/design-system/README.md +0 -252
  148. package/packages/design-system/package.json +0 -68
  149. package/packages/design-system/scripts/copy-css.js +0 -63
  150. package/packages/design-system/src/components/Button/Button.stories.tsx +0 -228
  151. package/packages/design-system/src/components/Button/Button.tsx +0 -137
  152. package/packages/design-system/src/components/Button/index.ts +0 -3
  153. package/packages/design-system/src/components/DataTable/DataTable.stories.tsx +0 -211
  154. package/packages/design-system/src/components/DataTable/DataTable.tsx +0 -293
  155. package/packages/design-system/src/components/DataTable/index.ts +0 -3
  156. package/packages/design-system/src/components/Form/Checkbox.stories.tsx +0 -252
  157. package/packages/design-system/src/components/Form/Checkbox.tsx +0 -114
  158. package/packages/design-system/src/components/Form/Fieldset.stories.tsx +0 -210
  159. package/packages/design-system/src/components/Form/Fieldset.tsx +0 -71
  160. package/packages/design-system/src/components/Form/Input.stories.tsx +0 -164
  161. package/packages/design-system/src/components/Form/Input.tsx +0 -113
  162. package/packages/design-system/src/components/Form/Label.tsx +0 -56
  163. package/packages/design-system/src/components/Form/Radio.stories.tsx +0 -265
  164. package/packages/design-system/src/components/Form/Radio.tsx +0 -147
  165. package/packages/design-system/src/components/Form/Select.stories.tsx +0 -295
  166. package/packages/design-system/src/components/Form/Select.tsx +0 -160
  167. package/packages/design-system/src/components/Form/Textarea.stories.tsx +0 -253
  168. package/packages/design-system/src/components/Form/Textarea.tsx +0 -145
  169. package/packages/design-system/src/components/Form/index.ts +0 -8
  170. package/packages/design-system/src/components/Link/Link.stories.tsx +0 -128
  171. package/packages/design-system/src/components/Link/Link.tsx +0 -117
  172. package/packages/design-system/src/components/Link/index.ts +0 -3
  173. package/packages/design-system/src/components/Modal/Modal.stories.tsx +0 -165
  174. package/packages/design-system/src/components/Modal/Modal.tsx +0 -202
  175. package/packages/design-system/src/components/Modal/index.ts +0 -3
  176. package/packages/design-system/src/components/Tabs/Tabs.stories.tsx +0 -213
  177. package/packages/design-system/src/components/Tabs/Tabs.tsx +0 -248
  178. package/packages/design-system/src/components/Tabs/index.ts +0 -3
  179. package/packages/design-system/src/components/Toast/Toast.stories.tsx +0 -153
  180. package/packages/design-system/src/components/Toast/Toast.tsx +0 -175
  181. package/packages/design-system/src/components/Toast/ToastProvider.tsx +0 -73
  182. package/packages/design-system/src/components/Toast/index.ts +0 -5
  183. package/packages/design-system/src/hooks/useAriaLive.ts +0 -51
  184. package/packages/design-system/src/hooks/useFocusReturn.ts +0 -40
  185. package/packages/design-system/src/hooks/useFocusTrap.ts +0 -82
  186. package/packages/design-system/src/styles/index.ts +0 -3
  187. package/packages/design-system/src/tokens/breakpoints.ts +0 -28
  188. package/packages/design-system/src/tokens/colors.ts +0 -98
  189. package/packages/design-system/src/tokens/index.ts +0 -6
  190. package/packages/design-system/src/tokens/motion.ts +0 -41
  191. package/packages/design-system/src/tokens/spacing.ts +0 -24
  192. package/packages/design-system/src/tokens/theme.ts +0 -19
  193. package/packages/design-system/src/tokens/typography.ts +0 -64
  194. package/packages/design-system/src/utils/aria.ts +0 -108
  195. package/packages/design-system/src/utils/focus.ts +0 -87
  196. package/packages/design-system/src/utils/index.ts +0 -4
  197. package/packages/design-system/src/utils/keyboard.ts +0 -77
  198. package/packages/design-system/tsconfig.json +0 -17
  199. package/public/logo.png +0 -0
  200. package/scripts/fix-storybook-paths.js +0 -53
  201. package/tsconfig.json +0 -20
  202. /package/{packages/design-system/src → dist}/components/Button/Button.css +0 -0
  203. /package/{packages/design-system/src → dist}/components/DataTable/DataTable.css +0 -0
  204. /package/{packages/design-system/src → dist}/components/Form/Checkbox.css +0 -0
  205. /package/{packages/design-system/src → dist}/components/Form/Fieldset.css +0 -0
  206. /package/{packages/design-system/src → dist}/components/Form/Input.css +0 -0
  207. /package/{packages/design-system/src → dist}/components/Form/Label.css +0 -0
  208. /package/{packages/design-system/src → dist}/components/Form/Radio.css +0 -0
  209. /package/{packages/design-system/src → dist}/components/Form/Select.css +0 -0
  210. /package/{packages/design-system/src → dist}/components/Form/Textarea.css +0 -0
  211. /package/{packages/design-system/src → dist}/components/Link/Link.css +0 -0
  212. /package/{packages/design-system/src → dist}/components/Modal/Modal.css +0 -0
  213. /package/{packages/design-system/src → dist}/components/Tabs/Tabs.css +0 -0
  214. /package/{packages/design-system/src → dist}/components/Toast/Toast.css +0 -0
  215. /package/{packages/design-system/src → dist}/components/Toast/ToastProvider.css +0 -0
  216. /package/{packages/design-system/src → dist}/styles/components.css +0 -0
  217. /package/{packages/design-system/src → dist}/styles/global.css +0 -0
@@ -1,228 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react'
2
- import { Button } from './Button'
3
-
4
- /**
5
- * # Button Component
6
- *
7
- * An accessible, WCAG 2.1/2.2 compliant button component with full keyboard support,
8
- * focus management, and ARIA attributes.
9
- *
10
- * ## Usage
11
- *
12
- * ```tsx
13
- * import { Button } from '@a11ypros/a11y-ui-components'
14
- *
15
- * function MyComponent() {
16
- * return (
17
- * <Button variant="primary" onClick={handleClick}>
18
- * Submit Form
19
- * </Button>
20
- * )
21
- * }
22
- * ```
23
- *
24
- * ## Variants
25
- *
26
- * - **primary**: Main call-to-action buttons (default)
27
- * - **secondary**: Secondary actions that are important but not the primary action
28
- * - **ghost**: Subtle actions with no background, minimal styling
29
- * - **danger**: Destructive actions like delete, remove, or cancel operations
30
- *
31
- * ## Sizes
32
- *
33
- * - **sm**: Small buttons for compact UIs (height: 32px)
34
- * - **md**: Medium buttons for standard use (height: 40px) - default
35
- * - **lg**: Large buttons for prominent actions (height: 48px)
36
- *
37
- * ## States
38
- *
39
- * - **loading**: Shows a loading indicator and disables interaction. Automatically sets `aria-busy="true"`
40
- * - **disabled**: Prevents all interaction. Button appears visually dimmed and is announced as "disabled button" by screen readers
41
- *
42
- * ## Accessibility
43
- *
44
- * ### WCAG 2.1/2.2 Compliance
45
- *
46
- * - **1.3.1 Info and Relationships**: Uses semantic `<button>` element
47
- * - **2.1.1 Keyboard**: Full keyboard support (Enter/Space activation)
48
- * - **2.4.7 Focus Visible**: Clear 2px outline on focus (meets 3:1 contrast ratio)
49
- * - **4.1.2 Name, Role, Value**: Proper ARIA attributes and semantic HTML
50
- * - **4.1.3 Status Messages**: Loading state announced via `aria-busy="true"`
51
- *
52
- * ### Keyboard Interactions
53
- *
54
- * | Key | Action |
55
- * |-----|--------|
56
- * | Enter | Activates the button |
57
- * | Space | Activates the button |
58
- * | Tab | Moves focus to the button |
59
- * | Shift+Tab | Moves focus away from the button |
60
- *
61
- * ### Screen Reader Support
62
- *
63
- * - Button role and name are announced automatically
64
- * - Loading state: "Busy" announced via `aria-busy="true"`
65
- * - Disabled state: "Disabled button" announced via `aria-disabled="true"`
66
- * - Custom labels via `aria-label` prop for icon-only buttons
67
- * - Loading spinner hidden from screen readers (`aria-hidden="true"`)
68
- *
69
- * ### Focus Management
70
- *
71
- * - Focus indicators use 2px solid outline with 2px offset
72
- * - Focus styles respect `prefers-reduced-motion` media query
73
- * - High contrast mode supported via `prefers-contrast` media query
74
- * - Focus visible only on keyboard navigation (not mouse clicks) using `:focus-visible`
75
- *
76
- * ## Best Practices
77
- *
78
- * 1. Always provide accessible text: Use `children` for visible text or `aria-label` for icon-only buttons
79
- * 2. Use appropriate variants: Primary for main actions (one per page/section), danger for destructive actions
80
- * 3. Handle loading states: Use `loading` prop instead of manually disabling during async operations
81
- * 4. Don't use buttons for navigation: Use the Link component for navigation instead
82
- * 5. Provide feedback: Don't disable buttons without explaining why or providing alternative actions
83
- *
84
- * ## Common Pitfalls
85
- *
86
- * - Using `<div onClick={...}>` instead of Button (loses keyboard support and semantic meaning)
87
- * - Missing `aria-label` on icon-only buttons (screen readers can't understand the action)
88
- * - Using `disabled` without providing feedback or alternative actions
89
- * - Not handling loading states during async operations (users don't know if action is processing)
90
- * - Using buttons for navigation (use Link component instead for proper semantics)
91
- * - Removing focus styles (breaks keyboard navigation visibility)
92
- *
93
- * @component
94
- * @example
95
- * ```tsx
96
- * <Button variant="primary" onClick={handleClick}>
97
- * Click me
98
- * </Button>
99
- * ```
100
- */
101
- const meta: Meta<typeof Button> = {
102
- title: 'Components/Button',
103
- component: Button,
104
- tags: ['autodocs'],
105
- argTypes: {
106
- variant: {
107
- control: 'select',
108
- options: ['primary', 'secondary', 'ghost', 'danger'],
109
- },
110
- size: {
111
- control: 'select',
112
- options: ['sm', 'md', 'lg'],
113
- },
114
- loading: {
115
- control: 'boolean',
116
- },
117
- disabled: {
118
- control: 'boolean',
119
- },
120
- },
121
- }
122
-
123
- export default meta
124
- type Story = StoryObj<typeof Button>
125
-
126
- /**
127
- * Primary buttons are used for the main call-to-action on a page or in a section.
128
- * They should be used sparingly - typically one per page or section.
129
- */
130
- export const Primary: Story = {
131
- args: {
132
- variant: 'primary',
133
- children: 'Primary Button',
134
- },
135
- }
136
-
137
- /**
138
- * Secondary buttons are used for secondary actions that are important but not
139
- * the primary action. They provide visual hierarchy.
140
- */
141
- export const Secondary: Story = {
142
- args: {
143
- variant: 'secondary',
144
- children: 'Secondary Button',
145
- },
146
- }
147
-
148
- /**
149
- * Ghost buttons have no background and minimal styling. Use for tertiary actions
150
- * or when you want a more subtle appearance.
151
- */
152
- export const Ghost: Story = {
153
- args: {
154
- variant: 'ghost',
155
- children: 'Ghost Button',
156
- },
157
- }
158
-
159
- /**
160
- * Danger buttons are used for destructive actions like delete, remove, or cancel
161
- * operations. They use red coloring to indicate the potentially harmful nature
162
- * of the action.
163
- */
164
- export const Danger: Story = {
165
- args: {
166
- variant: 'danger',
167
- children: 'Delete',
168
- },
169
- }
170
-
171
- /**
172
- * Buttons come in three sizes. Choose the size that fits your layout and
173
- * maintains visual hierarchy. Medium is the default size.
174
- */
175
- export const Sizes: Story = {
176
- render: () => (
177
- <div style={{ display: 'flex', gap: '1rem', alignItems: 'center', flexWrap: 'wrap' }}>
178
- <Button size="sm">Small</Button>
179
- <Button size="md">Medium</Button>
180
- <Button size="lg">Large</Button>
181
- </div>
182
- ),
183
- }
184
-
185
- /**
186
- * Loading state shows a spinner and disables the button. Use this during
187
- * async operations to provide feedback and prevent duplicate submissions.
188
- * The button automatically sets `aria-busy="true"` to announce the loading state.
189
- */
190
- export const Loading: Story = {
191
- args: {
192
- loading: true,
193
- children: 'Save',
194
- },
195
- }
196
-
197
- /**
198
- * Disabled buttons cannot be interacted with. They appear visually dimmed
199
- * and are announced as "disabled button" by screen readers.
200
- */
201
- export const Disabled: Story = {
202
- args: {
203
- disabled: true,
204
- children: 'Disabled Button',
205
- },
206
- }
207
-
208
- /**
209
- * Icon-only buttons require an aria-label for accessibility. Without visible
210
- * text, screen readers need the aria-label to understand what the button does.
211
- */
212
- export const WithAriaLabel: Story = {
213
- args: {
214
- 'aria-label': 'Close dialog',
215
- children: '×',
216
- },
217
- }
218
-
219
- export const AllVariants: Story = {
220
- render: () => (
221
- <div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
222
- <Button variant="primary">Primary</Button>
223
- <Button variant="secondary">Secondary</Button>
224
- <Button variant="ghost">Ghost</Button>
225
- <Button variant="danger">Danger</Button>
226
- </div>
227
- ),
228
- }
@@ -1,137 +0,0 @@
1
- import React from 'react'
2
- import { createActivationHandler } from '../../utils/keyboard'
3
- import { getAriaLabel, getBusyAttributes } from '../../utils/aria'
4
- import './Button.css'
5
-
6
- export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
7
- /**
8
- * Visual variant of the button
9
- */
10
- variant?: 'primary' | 'secondary' | 'ghost' | 'danger'
11
-
12
- /**
13
- * Size of the button
14
- */
15
- size?: 'sm' | 'md' | 'lg'
16
-
17
- /**
18
- * Whether the button is in a loading state
19
- */
20
- loading?: boolean
21
-
22
- /**
23
- * ARIA label for the button (required if no visible text)
24
- */
25
- 'aria-label'?: string
26
- }
27
-
28
- /**
29
- * Accessible Button component
30
- *
31
- * WCAG Compliance:
32
- * - 2.1.1 Keyboard: Full keyboard support (Enter/Space)
33
- * - 2.4.7 Focus Visible: Clear focus indicators
34
- * - 4.1.2 Name, Role, Value: Proper ARIA attributes
35
- *
36
- * @example
37
- * ```tsx
38
- * <Button variant="primary" onClick={handleClick}>
39
- * Click me
40
- * </Button>
41
- * ```
42
- */
43
- export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44
- (
45
- {
46
- variant = 'primary',
47
- size = 'md',
48
- loading = false,
49
- disabled,
50
- children,
51
- className = '',
52
- onClick,
53
- onKeyDown,
54
- 'aria-label': ariaLabel,
55
- ...props
56
- },
57
- ref
58
- ) => {
59
- const isDisabled = disabled || loading
60
-
61
- const handleKeyDown = React.useCallback(
62
- (event: React.KeyboardEvent<HTMLButtonElement>) => {
63
- // Handle activation keys
64
- const activationHandler = createActivationHandler((e) => {
65
- if (!isDisabled && onClick) {
66
- onClick(e as any)
67
- }
68
- })
69
- activationHandler(event)
70
-
71
- // Call user's onKeyDown if provided
72
- if (onKeyDown) {
73
- onKeyDown(event)
74
- }
75
- },
76
- [isDisabled, onClick, onKeyDown]
77
- )
78
-
79
- const ariaProps = {
80
- ...getAriaLabel(ariaLabel),
81
- ...getBusyAttributes(loading),
82
- ...props,
83
- }
84
-
85
- const classes = [
86
- 'btn',
87
- `btn--${variant}`,
88
- `btn--${size}`,
89
- loading && 'btn--loading',
90
- className,
91
- ]
92
- .filter(Boolean)
93
- .join(' ')
94
-
95
- return (
96
- <button
97
- ref={ref}
98
- type="button"
99
- className={classes}
100
- disabled={isDisabled}
101
- onClick={onClick}
102
- onKeyDown={handleKeyDown}
103
- aria-disabled={isDisabled}
104
- {...ariaProps}
105
- >
106
- {loading && (
107
- <span className="btn__spinner" aria-hidden="true">
108
- <svg
109
- className="btn__spinner-icon"
110
- width="16"
111
- height="16"
112
- viewBox="0 0 16 16"
113
- fill="none"
114
- xmlns="http://www.w3.org/2000/svg"
115
- >
116
- <circle
117
- className="btn__spinner-circle"
118
- cx="8"
119
- cy="8"
120
- r="6"
121
- stroke="currentColor"
122
- strokeWidth="2"
123
- strokeLinecap="round"
124
- strokeDasharray="31.416"
125
- strokeDashoffset="31.416"
126
- />
127
- </svg>
128
- </span>
129
- )}
130
- <span className="btn__content">{children}</span>
131
- </button>
132
- )
133
- }
134
- )
135
-
136
- Button.displayName = 'Button'
137
-
@@ -1,3 +0,0 @@
1
- export { Button } from './Button'
2
- export type { ButtonProps } from './Button'
3
-
@@ -1,211 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react'
2
- import { useState, useMemo } from 'react'
3
- import { DataTable } from './DataTable'
4
-
5
- /**
6
- * # DataTable Component
7
- *
8
- * An accessible data table component with keyboard navigation, sorting, and selection
9
- * capabilities. Uses semantic HTML table structure with enhanced accessibility features.
10
- *
11
- * ## Usage
12
- *
13
- * ```tsx
14
- * import { DataTable } from '@a11ypros/a11y-ui-components'
15
- *
16
- * const data = [
17
- * { id: '1', name: 'John Doe', email: 'john@example.com' },
18
- * { id: '2', name: 'Jane Smith', email: 'jane@example.com' },
19
- * ]
20
- *
21
- * const columns = [
22
- * { key: 'name', header: 'Name', sortable: true },
23
- * { key: 'email', header: 'Email', sortable: true },
24
- * ]
25
- *
26
- * function MyComponent() {
27
- * return (
28
- * <DataTable
29
- * data={data}
30
- * columns={columns}
31
- * getRowId={(row) => row.id}
32
- * caption="User list"
33
- * />
34
- * )
35
- * }
36
- * ```
37
- *
38
- * ## Features
39
- *
40
- * - **Keyboard navigation**: Arrow keys to navigate rows, Home/End for first/last
41
- * - **Sorting**: Click column headers to sort (optional)
42
- * - **Selection**: Select rows with Space key or checkbox (optional)
43
- * - **Semantic HTML**: Uses proper `<table>`, `<thead>`, `<tbody>` structure
44
- * - **ARIA support**: Proper ARIA attributes for sortable columns and selected rows
45
- *
46
- * ## Accessibility
47
- *
48
- * ### WCAG 2.1/2.2 Compliance
49
- *
50
- * - **1.3.1 Info and Relationships**: Semantic table structure with proper headers
51
- * - **2.1.1 Keyboard**: Full keyboard navigation with arrow keys, Home, End, Space
52
- * - **4.1.2 Name, Role, Value**: Proper ARIA attributes for table, rows, and cells
53
- * - **4.1.3 Status Messages**: Sort state and selection state announced to screen readers
54
- *
55
- * ### Keyboard Interactions
56
- *
57
- * | Key | Action |
58
- * |-----|--------|
59
- * | **Arrow Up** | Move to previous row |
60
- * | **Arrow Down** | Move to next row |
61
- * | **Home** | Move to first row |
62
- * | **End** | Move to last row |
63
- * | **Space** | Select/deselect row (if selectable) |
64
- * | **Tab** | Move focus to sortable column headers or other controls |
65
- * | **Enter** | Activate sort on column header |
66
- *
67
- * ### Screen Reader Support
68
- *
69
- * - Table caption is announced
70
- * - Column headers are associated with cells
71
- * - Sort state is announced (e.g., "Name column, sorted ascending")
72
- * - Row selection state is announced (e.g., "Row 2, selected")
73
- * - Cell content is read with column header context
74
- *
75
- * ### Focus Management
76
- *
77
- * - Focus moves between rows using arrow keys
78
- * - Focus moves to sortable column headers with Tab
79
- * - Selected rows maintain focus state
80
- * - Focus indicators visible on all interactive elements
81
- *
82
- * ## Best Practices
83
- *
84
- * 1. **Always provide a caption**: Required for screen readers to understand table purpose
85
- * 2. **Use proper column headers**: Headers should clearly describe column content
86
- * 3. **Keep tables focused**: Don't nest tables or use tables for layout
87
- * 4. **Limit row count**: Very large tables should use pagination
88
- * 5. **Make sortable columns clear**: Use visual indicators for sortable columns
89
- *
90
- * ## Common Pitfalls
91
- *
92
- * - Missing caption (screen readers can't understand table purpose)
93
- * - Using tables for layout (use CSS Grid or Flexbox instead)
94
- * - Missing column headers (breaks screen reader navigation)
95
- * - Too many columns (becomes hard to navigate)
96
- * - Not announcing sort/selection state (users don't know current state)
97
- *
98
- * @component
99
- * @example
100
- * ```tsx
101
- * <DataTable
102
- * data={users}
103
- * columns={columns}
104
- * getRowId={(row) => row.id}
105
- * caption="User directory"
106
- * sortable
107
- * />
108
- * ```
109
- */
110
- const meta: Meta<typeof DataTable> = {
111
- title: 'Components/DataTable',
112
- component: DataTable,
113
- tags: ['autodocs'],
114
- }
115
-
116
- export default meta
117
- type Story = StoryObj<typeof DataTable>
118
-
119
- const sampleData = [
120
- { id: '1', name: 'John Doe', email: 'john@example.com', role: 'Admin' },
121
- { id: '2', name: 'Jane Smith', email: 'jane@example.com', role: 'User' },
122
- { id: '3', name: 'Bob Johnson', email: 'bob@example.com', role: 'User' },
123
- ]
124
-
125
- const columns = [
126
- { key: 'name', header: 'Name', sortable: true },
127
- { key: 'email', header: 'Email', sortable: true },
128
- { key: 'role', header: 'Role', sortable: true },
129
- ]
130
-
131
- // Helper function to sort data
132
- function sortData<T extends Record<string, any>>(
133
- data: T[],
134
- sortConfig: { column: string; direction: 'asc' | 'desc' } | undefined
135
- ): T[] {
136
- if (!sortConfig) return data
137
-
138
- return [...data].sort((a, b) => {
139
- const aValue = a[sortConfig.column]
140
- const bValue = b[sortConfig.column]
141
-
142
- if (aValue === bValue) return 0
143
-
144
- const comparison = aValue < bValue ? -1 : 1
145
- return sortConfig.direction === 'asc' ? comparison : -comparison
146
- })
147
- }
148
-
149
- /**
150
- * Basic data table without sorting. Simple table display with keyboard navigation.
151
- */
152
- export const Default: Story = {
153
- args: {
154
- data: sampleData,
155
- columns: [
156
- { key: 'name', header: 'Name' },
157
- { key: 'email', header: 'Email' },
158
- { key: 'role', header: 'Role' },
159
- ],
160
- getRowId: (row) => row.id,
161
- caption: 'User list',
162
- },
163
- }
164
-
165
- /**
166
- * Selectable table allows users to select rows using Space key or checkboxes.
167
- * Selection state is announced to screen readers and can be used for bulk actions.
168
- */
169
- export const Selectable: Story = {
170
- render: () => {
171
- const [selectedRows, setSelectedRows] = useState<string[]>([])
172
- return (
173
- <DataTable
174
- data={sampleData}
175
- columns={[
176
- { key: 'name', header: 'Name' },
177
- { key: 'email', header: 'Email' },
178
- { key: 'role', header: 'Role' },
179
- ]}
180
- getRowId={(row) => row.id}
181
- selectable
182
- selectedRows={selectedRows}
183
- onSelectionChange={setSelectedRows}
184
- caption="Selectable user list"
185
- />
186
- )
187
- },
188
- }
189
-
190
- /**
191
- * Sortable table with controlled sorting. Sort configuration is managed externally,
192
- * allowing for server-side sorting or custom sort logic.
193
- */
194
- export const Sortable: Story = {
195
- render: () => {
196
- const [sortConfig, setSortConfig] = useState<{ column: string; direction: 'asc' | 'desc' } | undefined>()
197
- const sortedData = useMemo(() => sortData(sampleData, sortConfig), [sortConfig])
198
-
199
- return (
200
- <DataTable
201
- data={sortedData}
202
- columns={columns}
203
- getRowId={(row) => row.id}
204
- sortConfig={sortConfig}
205
- onSortChange={(column, direction) => setSortConfig({ column, direction })}
206
- caption="Sortable user list"
207
- />
208
- )
209
- },
210
- }
211
-