@a11ypros/a11y-ui-components 1.0.0 → 1.0.1

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/.storybook/custom.css +69 -0
  2. package/.storybook/main.ts +46 -0
  3. package/.storybook/manager.ts +26 -0
  4. package/.storybook/package.json +6 -0
  5. package/.storybook/preview.tsx +31 -0
  6. package/.storybook/public/logo.png +0 -0
  7. package/.storybook/vite.config.ts +24 -0
  8. package/.storybook/welcome.mdx +97 -0
  9. package/DEPLOYMENT.md +154 -0
  10. package/README.md +227 -0
  11. package/apps/web/app/(docs)/audit/audit.css +269 -0
  12. package/apps/web/app/(docs)/audit/page.tsx +271 -0
  13. package/apps/web/app/(docs)/components/button/page.tsx +49 -0
  14. package/apps/web/app/(docs)/components/form/page.tsx +92 -0
  15. package/apps/web/app/(docs)/components/link/page.tsx +31 -0
  16. package/apps/web/app/(docs)/components/modal/page.tsx +41 -0
  17. package/apps/web/app/(docs)/components/page.tsx +37 -0
  18. package/apps/web/app/(docs)/components/table/page.tsx +54 -0
  19. package/apps/web/app/(docs)/components/tabs/page.tsx +61 -0
  20. package/apps/web/app/(docs)/components/toast/page.tsx +51 -0
  21. package/apps/web/app/api/audit/route.ts +128 -0
  22. package/apps/web/app/favicon.ico +0 -0
  23. package/apps/web/app/layout.tsx +20 -0
  24. package/apps/web/app/page.tsx +17 -0
  25. package/apps/web/app/styles/globals.css +5 -0
  26. package/apps/web/next-env.d.ts +5 -0
  27. package/apps/web/next.config.js +21 -0
  28. package/apps/web/package.json +28 -0
  29. package/apps/web/public/_headers +17 -0
  30. package/apps/web/public/_redirects +31 -0
  31. package/apps/web/public/logo.png +0 -0
  32. package/apps/web/tsconfig.json +29 -0
  33. package/netlify/functions/audit.ts +163 -0
  34. package/netlify.toml +37 -0
  35. package/package.json +30 -58
  36. package/packages/design-system/README.md +252 -0
  37. package/packages/design-system/package.json +68 -0
  38. package/packages/design-system/scripts/copy-css.js +63 -0
  39. package/packages/design-system/src/components/Button/Button.stories.tsx +228 -0
  40. package/packages/design-system/src/components/Button/Button.tsx +137 -0
  41. package/packages/design-system/src/components/Button/index.ts +3 -0
  42. package/packages/design-system/src/components/DataTable/DataTable.stories.tsx +211 -0
  43. package/packages/design-system/src/components/DataTable/DataTable.tsx +293 -0
  44. package/packages/design-system/src/components/DataTable/index.ts +3 -0
  45. package/packages/design-system/src/components/Form/Checkbox.stories.tsx +252 -0
  46. package/packages/design-system/src/components/Form/Checkbox.tsx +114 -0
  47. package/packages/design-system/src/components/Form/Fieldset.stories.tsx +210 -0
  48. package/packages/design-system/src/components/Form/Fieldset.tsx +71 -0
  49. package/packages/design-system/src/components/Form/Input.stories.tsx +164 -0
  50. package/packages/design-system/src/components/Form/Input.tsx +113 -0
  51. package/packages/design-system/src/components/Form/Label.tsx +56 -0
  52. package/packages/design-system/src/components/Form/Radio.stories.tsx +265 -0
  53. package/packages/design-system/src/components/Form/Radio.tsx +147 -0
  54. package/packages/design-system/src/components/Form/Select.stories.tsx +295 -0
  55. package/packages/design-system/src/components/Form/Select.tsx +160 -0
  56. package/packages/design-system/src/components/Form/Textarea.stories.tsx +253 -0
  57. package/packages/design-system/src/components/Form/Textarea.tsx +145 -0
  58. package/packages/design-system/src/components/Form/index.ts +8 -0
  59. package/packages/design-system/src/components/Link/Link.stories.tsx +128 -0
  60. package/packages/design-system/src/components/Link/Link.tsx +117 -0
  61. package/packages/design-system/src/components/Link/index.ts +3 -0
  62. package/packages/design-system/src/components/Modal/Modal.stories.tsx +165 -0
  63. package/packages/design-system/src/components/Modal/Modal.tsx +202 -0
  64. package/packages/design-system/src/components/Modal/index.ts +3 -0
  65. package/packages/design-system/src/components/Tabs/Tabs.stories.tsx +213 -0
  66. package/packages/design-system/src/components/Tabs/Tabs.tsx +248 -0
  67. package/packages/design-system/src/components/Tabs/index.ts +3 -0
  68. package/packages/design-system/src/components/Toast/Toast.stories.tsx +153 -0
  69. package/packages/design-system/src/components/Toast/Toast.tsx +175 -0
  70. package/packages/design-system/src/components/Toast/ToastProvider.tsx +73 -0
  71. package/packages/design-system/src/components/Toast/index.ts +5 -0
  72. package/packages/design-system/src/hooks/useAriaLive.ts +51 -0
  73. package/packages/design-system/src/hooks/useFocusReturn.ts +40 -0
  74. package/packages/design-system/src/hooks/useFocusTrap.ts +82 -0
  75. package/{dist/index.js → packages/design-system/src/index.ts} +4 -0
  76. package/packages/design-system/src/styles/index.ts +3 -0
  77. package/packages/design-system/src/tokens/breakpoints.ts +28 -0
  78. package/packages/design-system/src/tokens/colors.ts +98 -0
  79. package/packages/design-system/src/tokens/index.ts +6 -0
  80. package/packages/design-system/src/tokens/motion.ts +41 -0
  81. package/packages/design-system/src/tokens/spacing.ts +24 -0
  82. package/packages/design-system/src/tokens/theme.ts +19 -0
  83. package/packages/design-system/src/tokens/typography.ts +64 -0
  84. package/packages/design-system/src/utils/aria.ts +108 -0
  85. package/packages/design-system/src/utils/focus.ts +87 -0
  86. package/packages/design-system/src/utils/index.ts +4 -0
  87. package/packages/design-system/src/utils/keyboard.ts +77 -0
  88. package/packages/design-system/tsconfig.json +17 -0
  89. package/public/logo.png +0 -0
  90. package/scripts/fix-storybook-paths.js +53 -0
  91. package/tsconfig.json +20 -0
  92. package/dist/components/Button/Button.d.ts +0 -37
  93. package/dist/components/Button/Button.d.ts.map +0 -1
  94. package/dist/components/Button/Button.js +0 -52
  95. package/dist/components/Button/index.d.ts +0 -3
  96. package/dist/components/Button/index.d.ts.map +0 -1
  97. package/dist/components/Button/index.js +0 -1
  98. package/dist/components/DataTable/DataTable.d.ts +0 -71
  99. package/dist/components/DataTable/DataTable.d.ts.map +0 -1
  100. package/dist/components/DataTable/DataTable.js +0 -122
  101. package/dist/components/DataTable/index.d.ts +0 -3
  102. package/dist/components/DataTable/index.d.ts.map +0 -1
  103. package/dist/components/DataTable/index.js +0 -1
  104. package/dist/components/Form/Checkbox.d.ts +0 -36
  105. package/dist/components/Form/Checkbox.d.ts.map +0 -1
  106. package/dist/components/Form/Checkbox.js +0 -39
  107. package/dist/components/Form/Fieldset.d.ts +0 -33
  108. package/dist/components/Form/Fieldset.d.ts.map +0 -1
  109. package/dist/components/Form/Fieldset.js +0 -34
  110. package/dist/components/Form/Input.d.ts +0 -37
  111. package/dist/components/Form/Input.d.ts.map +0 -1
  112. package/dist/components/Form/Input.js +0 -41
  113. package/dist/components/Form/Label.d.ts +0 -30
  114. package/dist/components/Form/Label.d.ts.map +0 -1
  115. package/dist/components/Form/Label.js +0 -30
  116. package/dist/components/Form/Radio.d.ts +0 -53
  117. package/dist/components/Form/Radio.d.ts.map +0 -1
  118. package/dist/components/Form/Radio.js +0 -39
  119. package/dist/components/Form/Select.d.ts +0 -51
  120. package/dist/components/Form/Select.d.ts.map +0 -1
  121. package/dist/components/Form/Select.js +0 -49
  122. package/dist/components/Form/Textarea.d.ts +0 -44
  123. package/dist/components/Form/Textarea.d.ts.map +0 -1
  124. package/dist/components/Form/Textarea.js +0 -43
  125. package/dist/components/Form/index.d.ts +0 -8
  126. package/dist/components/Form/index.d.ts.map +0 -1
  127. package/dist/components/Form/index.js +0 -7
  128. package/dist/components/Link/Link.d.ts +0 -34
  129. package/dist/components/Link/Link.d.ts.map +0 -1
  130. package/dist/components/Link/Link.js +0 -48
  131. package/dist/components/Link/index.d.ts +0 -3
  132. package/dist/components/Link/index.d.ts.map +0 -1
  133. package/dist/components/Link/index.js +0 -1
  134. package/dist/components/Modal/Modal.d.ts +0 -64
  135. package/dist/components/Modal/Modal.d.ts.map +0 -1
  136. package/dist/components/Modal/Modal.js +0 -108
  137. package/dist/components/Modal/index.d.ts +0 -3
  138. package/dist/components/Modal/index.d.ts.map +0 -1
  139. package/dist/components/Modal/index.js +0 -1
  140. package/dist/components/Tabs/Tabs.d.ts +0 -63
  141. package/dist/components/Tabs/Tabs.d.ts.map +0 -1
  142. package/dist/components/Tabs/Tabs.js +0 -134
  143. package/dist/components/Tabs/index.d.ts +0 -3
  144. package/dist/components/Tabs/index.d.ts.map +0 -1
  145. package/dist/components/Tabs/index.js +0 -1
  146. package/dist/components/Toast/Toast.d.ts +0 -59
  147. package/dist/components/Toast/Toast.d.ts.map +0 -1
  148. package/dist/components/Toast/Toast.js +0 -91
  149. package/dist/components/Toast/ToastProvider.d.ts +0 -22
  150. package/dist/components/Toast/ToastProvider.d.ts.map +0 -1
  151. package/dist/components/Toast/ToastProvider.js +0 -33
  152. package/dist/components/Toast/index.d.ts +0 -5
  153. package/dist/components/Toast/index.d.ts.map +0 -1
  154. package/dist/components/Toast/index.js +0 -2
  155. package/dist/hooks/useAriaLive.d.ts +0 -9
  156. package/dist/hooks/useAriaLive.d.ts.map +0 -1
  157. package/dist/hooks/useAriaLive.js +0 -39
  158. package/dist/hooks/useFocusReturn.d.ts +0 -9
  159. package/dist/hooks/useFocusReturn.d.ts.map +0 -1
  160. package/dist/hooks/useFocusReturn.js +0 -33
  161. package/dist/hooks/useFocusTrap.d.ts +0 -9
  162. package/dist/hooks/useFocusTrap.d.ts.map +0 -1
  163. package/dist/hooks/useFocusTrap.js +0 -68
  164. package/dist/index.d.ts +0 -22
  165. package/dist/index.d.ts.map +0 -1
  166. package/dist/styles/index.d.ts +0 -3
  167. package/dist/styles/index.d.ts.map +0 -1
  168. package/dist/styles/index.js +0 -1
  169. package/dist/tokens/breakpoints.d.ts +0 -25
  170. package/dist/tokens/breakpoints.d.ts.map +0 -1
  171. package/dist/tokens/breakpoints.js +0 -23
  172. package/dist/tokens/colors.d.ts +0 -81
  173. package/dist/tokens/colors.d.ts.map +0 -1
  174. package/dist/tokens/colors.js +0 -86
  175. package/dist/tokens/index.d.ts +0 -6
  176. package/dist/tokens/index.d.ts.map +0 -1
  177. package/dist/tokens/index.js +0 -5
  178. package/dist/tokens/motion.d.ts +0 -30
  179. package/dist/tokens/motion.d.ts.map +0 -1
  180. package/dist/tokens/motion.js +0 -34
  181. package/dist/tokens/spacing.d.ts +0 -22
  182. package/dist/tokens/spacing.d.ts.map +0 -1
  183. package/dist/tokens/spacing.js +0 -20
  184. package/dist/tokens/theme.d.ts +0 -159
  185. package/dist/tokens/theme.d.ts.map +0 -1
  186. package/dist/tokens/theme.js +0 -15
  187. package/dist/tokens/typography.d.ts +0 -45
  188. package/dist/tokens/typography.d.ts.map +0 -1
  189. package/dist/tokens/typography.js +0 -56
  190. package/dist/utils/aria.d.ts +0 -60
  191. package/dist/utils/aria.d.ts.map +0 -1
  192. package/dist/utils/aria.js +0 -86
  193. package/dist/utils/focus.d.ts +0 -30
  194. package/dist/utils/focus.d.ts.map +0 -1
  195. package/dist/utils/focus.js +0 -80
  196. package/dist/utils/index.d.ts +0 -4
  197. package/dist/utils/index.d.ts.map +0 -1
  198. package/dist/utils/index.js +0 -3
  199. package/dist/utils/keyboard.d.ts +0 -38
  200. package/dist/utils/keyboard.d.ts.map +0 -1
  201. package/dist/utils/keyboard.js +0 -59
  202. /package/{dist → packages/design-system/src}/components/Button/Button.css +0 -0
  203. /package/{dist → packages/design-system/src}/components/DataTable/DataTable.css +0 -0
  204. /package/{dist → packages/design-system/src}/components/Form/Checkbox.css +0 -0
  205. /package/{dist → packages/design-system/src}/components/Form/Fieldset.css +0 -0
  206. /package/{dist → packages/design-system/src}/components/Form/Input.css +0 -0
  207. /package/{dist → packages/design-system/src}/components/Form/Label.css +0 -0
  208. /package/{dist → packages/design-system/src}/components/Form/Radio.css +0 -0
  209. /package/{dist → packages/design-system/src}/components/Form/Select.css +0 -0
  210. /package/{dist → packages/design-system/src}/components/Form/Textarea.css +0 -0
  211. /package/{dist → packages/design-system/src}/components/Link/Link.css +0 -0
  212. /package/{dist → packages/design-system/src}/components/Modal/Modal.css +0 -0
  213. /package/{dist → packages/design-system/src}/components/Tabs/Tabs.css +0 -0
  214. /package/{dist → packages/design-system/src}/components/Toast/Toast.css +0 -0
  215. /package/{dist → packages/design-system/src}/components/Toast/ToastProvider.css +0 -0
  216. /package/{dist → packages/design-system/src}/styles/components.css +0 -0
  217. /package/{dist → packages/design-system/src}/styles/global.css +0 -0
@@ -0,0 +1,293 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useRef, useCallback } from 'react'
4
+ import { useAriaLive } from '../../hooks/useAriaLive'
5
+ import { createArrowKeyHandler, isNavigationKey } from '../../utils/keyboard'
6
+ import { Checkbox } from '../Form/Checkbox'
7
+ import './DataTable.css'
8
+
9
+ export interface DataTableColumn<T> {
10
+ key: string
11
+ header: string
12
+ render?: (row: T, index: number) => React.ReactNode
13
+ sortable?: boolean
14
+ width?: string
15
+ }
16
+
17
+ export interface DataTableProps<T> {
18
+ /**
19
+ * Data rows
20
+ */
21
+ data: T[]
22
+
23
+ /**
24
+ * Column definitions
25
+ */
26
+ columns: DataTableColumn<T>[]
27
+
28
+ /**
29
+ * Key function to get unique ID for each row
30
+ */
31
+ getRowId: (row: T) => string
32
+
33
+ /**
34
+ * Whether rows are selectable
35
+ */
36
+ selectable?: boolean
37
+
38
+ /**
39
+ * Selected row IDs
40
+ */
41
+ selectedRows?: string[]
42
+
43
+ /**
44
+ * Callback when selection changes
45
+ */
46
+ onSelectionChange?: (selectedIds: string[]) => void
47
+
48
+ /**
49
+ * Sort configuration
50
+ */
51
+ sortConfig?: {
52
+ column: string
53
+ direction: 'asc' | 'desc'
54
+ }
55
+
56
+ /**
57
+ * Callback when sort changes
58
+ */
59
+ onSortChange?: (column: string, direction: 'asc' | 'desc') => void
60
+
61
+ /**
62
+ * Caption for the table (required for accessibility)
63
+ */
64
+ caption?: string
65
+ }
66
+
67
+ /**
68
+ * Accessible DataTable component
69
+ *
70
+ * WCAG Compliance:
71
+ * - 1.3.1 Info and Relationships: Semantic table structure
72
+ * - 2.1.1 Keyboard: Arrow keys, Home/End navigation
73
+ * - 4.1.2 Name, Role, Value: Proper ARIA attributes
74
+ * - 4.1.3 Status Messages: Sort announcements
75
+ *
76
+ * @example
77
+ * ```tsx
78
+ * <DataTable
79
+ * data={users}
80
+ * columns={columns}
81
+ * getRowId={(user) => user.id}
82
+ * caption="User list"
83
+ * />
84
+ * ```
85
+ */
86
+ export function DataTable<T extends Record<string, any>>({
87
+ data,
88
+ columns,
89
+ getRowId,
90
+ selectable = false,
91
+ selectedRows = [],
92
+ onSelectionChange,
93
+ sortConfig,
94
+ onSortChange,
95
+ caption,
96
+ }: DataTableProps<T>) {
97
+ const [focusedRow, setFocusedRow] = useState<string | null>(null)
98
+ const tableRef = useRef<HTMLTableElement>(null)
99
+ const rowRefs = useRef<Map<string, HTMLTableRowElement>>(new Map())
100
+
101
+ // Announce sort changes
102
+ const sortAnnouncement = sortConfig
103
+ ? `Sorted by ${columns.find((c) => c.key === sortConfig.column)?.header || sortConfig.column}, ${sortConfig.direction === 'asc' ? 'ascending' : 'descending'}`
104
+ : undefined
105
+ useAriaLive(sortAnnouncement, 'polite')
106
+
107
+ const handleSelectAll = useCallback(() => {
108
+ if (!onSelectionChange) return
109
+
110
+ const allSelected = selectedRows.length === data.length
111
+ if (allSelected) {
112
+ onSelectionChange([])
113
+ } else {
114
+ onSelectionChange(data.map(getRowId))
115
+ }
116
+ }, [data, selectedRows, onSelectionChange, getRowId])
117
+
118
+ const handleSelectRow = useCallback(
119
+ (rowId: string) => {
120
+ if (!onSelectionChange) return
121
+
122
+ const isSelected = selectedRows.includes(rowId)
123
+ if (isSelected) {
124
+ onSelectionChange(selectedRows.filter((id) => id !== rowId))
125
+ } else {
126
+ onSelectionChange([...selectedRows, rowId])
127
+ }
128
+ },
129
+ [selectedRows, onSelectionChange]
130
+ )
131
+
132
+ const handleSort = useCallback(
133
+ (columnKey: string) => {
134
+ if (!onSortChange || !columns.find((c) => c.key === columnKey)?.sortable) {
135
+ return
136
+ }
137
+
138
+ const newDirection =
139
+ sortConfig?.column === columnKey && sortConfig.direction === 'asc'
140
+ ? 'desc'
141
+ : 'asc'
142
+ onSortChange(columnKey, newDirection)
143
+ },
144
+ [onSortChange, sortConfig, columns]
145
+ )
146
+
147
+ const handleKeyDown = useCallback(
148
+ (event: React.KeyboardEvent<HTMLTableRowElement>, rowId: string, index: number) => {
149
+ const row = rowRefs.current.get(rowId)
150
+ if (!row) return
151
+
152
+ if (isNavigationKey(event.key)) {
153
+ event.preventDefault()
154
+
155
+ let newIndex = index
156
+ switch (event.key) {
157
+ case 'ArrowDown':
158
+ newIndex = Math.min(index + 1, data.length - 1)
159
+ break
160
+ case 'ArrowUp':
161
+ newIndex = Math.max(index - 1, 0)
162
+ break
163
+ case 'Home':
164
+ newIndex = 0
165
+ break
166
+ case 'End':
167
+ newIndex = data.length - 1
168
+ break
169
+ }
170
+
171
+ const newRowId = getRowId(data[newIndex])
172
+ setFocusedRow(newRowId)
173
+ rowRefs.current.get(newRowId)?.focus()
174
+ } else if (event.key === ' ' && selectable) {
175
+ event.preventDefault()
176
+ handleSelectRow(rowId)
177
+ }
178
+ },
179
+ [data, selectable, handleSelectRow, getRowId]
180
+ )
181
+
182
+ const allSelected = data.length > 0 && selectedRows.length === data.length
183
+ const someSelected = selectedRows.length > 0 && selectedRows.length < data.length
184
+
185
+ return (
186
+ <div className="data-table-wrapper">
187
+ <table
188
+ ref={tableRef}
189
+ className="data-table"
190
+ aria-label={caption}
191
+ >
192
+ {caption && <caption className="data-table-caption">{caption}</caption>}
193
+ <thead>
194
+ <tr>
195
+ {selectable && (
196
+ <th scope="col" className="data-table-header data-table-header--checkbox">
197
+ <Checkbox
198
+ checked={allSelected}
199
+ aria-label="Select all rows"
200
+ onChange={handleSelectAll}
201
+ aria-checked={allSelected ? 'true' : someSelected ? 'mixed' : 'false'}
202
+ />
203
+ </th>
204
+ )}
205
+ {columns.map((column) => (
206
+ <th
207
+ key={column.key}
208
+ scope="col"
209
+ className={`data-table-header ${column.sortable ? 'data-table-header--sortable' : ''}`}
210
+ style={column.width ? { width: column.width } : undefined}
211
+ aria-sort={
212
+ sortConfig?.column === column.key
213
+ ? sortConfig.direction === 'asc'
214
+ ? 'ascending'
215
+ : 'descending'
216
+ : 'none'
217
+ }
218
+ >
219
+ {column.sortable ? (
220
+ <button
221
+ type="button"
222
+ className="data-table-sort-button"
223
+ aria-label={
224
+ sortConfig?.column === column.key
225
+ ? `${column.header}, sorted ${sortConfig.direction === 'asc' ? 'ascending' : 'descending'}, activate to sort ${sortConfig.direction === 'asc' ? 'descending' : 'ascending'}`
226
+ : `Sort by ${column.header}`
227
+ }
228
+ onClick={() => handleSort(column.key)}
229
+ >
230
+ {column.header}
231
+ {sortConfig?.column === column.key && (
232
+ <span className="data-table-sort-indicator" aria-hidden="true">
233
+ {sortConfig.direction === 'asc' ? ' ↑' : ' ↓'}
234
+ </span>
235
+ )}
236
+ </button>
237
+ ) : (
238
+ column.header
239
+ )}
240
+ </th>
241
+ ))}
242
+ </tr>
243
+ </thead>
244
+ <tbody>
245
+ {data.map((row, index) => {
246
+ const rowId = getRowId(row)
247
+ const isSelected = selectedRows.includes(rowId)
248
+ const isFocused = focusedRow === rowId
249
+
250
+ return (
251
+ <tr
252
+ key={rowId}
253
+ ref={(el) => {
254
+ if (el) {
255
+ rowRefs.current.set(rowId, el)
256
+ } else {
257
+ rowRefs.current.delete(rowId)
258
+ }
259
+ }}
260
+ className={`data-table-row ${isSelected ? 'data-table-row--selected' : ''} ${isFocused ? 'data-table-row--focused' : ''}`}
261
+ tabIndex={0}
262
+ aria-selected={selectable ? isSelected : undefined}
263
+ onKeyDown={(e) => handleKeyDown(e, rowId, index)}
264
+ onClick={() => {
265
+ if (selectable) {
266
+ handleSelectRow(rowId)
267
+ }
268
+ }}
269
+ >
270
+ {selectable && (
271
+ <td className="data-table-cell data-table-cell--checkbox">
272
+ <Checkbox
273
+ checked={isSelected}
274
+ aria-label={`Select row ${index + 1}`}
275
+ onChange={() => handleSelectRow(rowId)}
276
+ onClick={(e) => e.stopPropagation()}
277
+ />
278
+ </td>
279
+ )}
280
+ {columns.map((column) => (
281
+ <td key={column.key} className="data-table-cell">
282
+ {column.render ? column.render(row, index) : row[column.key]}
283
+ </td>
284
+ ))}
285
+ </tr>
286
+ )
287
+ })}
288
+ </tbody>
289
+ </table>
290
+ </div>
291
+ )
292
+ }
293
+
@@ -0,0 +1,3 @@
1
+ export { DataTable } from './DataTable'
2
+ export type { DataTableProps, DataTableColumn } from './DataTable'
3
+
@@ -0,0 +1,252 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { useState } from 'react'
3
+ import { Checkbox } from './Checkbox'
4
+
5
+ /**
6
+ * # Checkbox Component
7
+ *
8
+ * An accessible checkbox component with proper label association, error handling,
9
+ * and ARIA attributes. Supports both controlled and uncontrolled usage.
10
+ *
11
+ * ## Usage
12
+ *
13
+ * ```tsx
14
+ * import { Checkbox } from '@a11ypros/a11y-ui-components'
15
+ *
16
+ * function MyComponent() {
17
+ * const [checked, setChecked] = useState(false)
18
+ *
19
+ * return (
20
+ * <Checkbox
21
+ * id="agree"
22
+ * label="I agree to the terms and conditions"
23
+ * checked={checked}
24
+ * onChange={(e) => setChecked(e.target.checked)}
25
+ * />
26
+ * )
27
+ * }
28
+ * ```
29
+ *
30
+ * ## Features
31
+ *
32
+ * - **Label association**: Labels are properly associated with checkboxes using \`htmlFor\` and \`id\`
33
+ * - **Error handling**: Error messages are announced to screen readers via ARIA
34
+ * - **Helper text**: Optional helper text for additional context
35
+ * - **Keyboard accessible**: Full keyboard support with Space key for toggling
36
+ *
37
+ * ## Accessibility
38
+ *
39
+ * ### WCAG 2.1/2.2 Compliance
40
+ *
41
+ * - **1.3.1 Info and Relationships**: Proper label-checkbox association via \`htmlFor\` and \`id\`
42
+ * - **2.5.3 Label in Name**: Label text matches accessible name
43
+ * - **4.1.2 Name, Role, Value**: Proper ARIA attributes including \`aria-required\`, \`aria-invalid\`, \`aria-describedby\`
44
+ *
45
+ * ### Keyboard Interactions
46
+ *
47
+ * | Key | Action |
48
+ * |-----|--------|
49
+ * | **Tab** | Moves focus to the checkbox |
50
+ * | **Shift+Tab** | Moves focus away from the checkbox |
51
+ * | **Space** | Toggles checkbox state |
52
+ * | **Enter** | Toggles checkbox state (in some contexts) |
53
+ *
54
+ * ### Screen Reader Support
55
+ *
56
+ * - Checkbox role and label are announced when focused
57
+ * - Checked state is announced ("checked" or "not checked")
58
+ * - Required state is announced ("required")
59
+ * - Error messages are announced when present
60
+ * - Helper text is announced via \`aria-describedby\`
61
+ *
62
+ * ### Focus Management
63
+ *
64
+ * - Focus indicators use 2px solid outline with 2px offset
65
+ * - Focus styles respect \`prefers-reduced-motion\` media query
66
+ * - Error state has distinct focus styling
67
+ * - Focus visible only on keyboard navigation using \`:focus-visible\`
68
+ *
69
+ * ## Best Practices
70
+ *
71
+ * 1. **Always provide a label**: Required for accessibility and usability
72
+ * 2. **Use for binary choices**: Checkboxes are for independent yes/no choices
73
+ * 3. **Group related checkboxes**: Use Fieldset component to group related options
74
+ * 4. **Provide helpful error messages**: Be specific about what needs to be checked
75
+ * 5. **Use helper text for guidance**: Help users understand the implications of checking
76
+ *
77
+ * ## Common Pitfalls
78
+ *
79
+ * - Missing label (screen readers can't identify the checkbox)
80
+ * - Using checkbox for single choice (use Radio for mutually exclusive options)
81
+ * - Vague error messages (be specific about validation failures)
82
+ * - Not grouping related checkboxes (use Fieldset for logical grouping)
83
+ * - Using wrong element (use Radio for single choice from multiple options)
84
+ *
85
+ * @component
86
+ * @example
87
+ * ```tsx
88
+ * <Checkbox
89
+ * id="terms"
90
+ * label="I agree to the terms"
91
+ * required
92
+ * error={errors.terms}
93
+ * />
94
+ * ```
95
+ */
96
+ const meta: Meta<typeof Checkbox> = {
97
+ title: 'Components/Form/Checkbox',
98
+ component: Checkbox,
99
+ tags: ['autodocs'],
100
+ }
101
+
102
+ export default meta
103
+ type Story = StoryObj<typeof Checkbox>
104
+
105
+ /**
106
+ * Standard checkbox with label. The label is properly associated
107
+ * with the checkbox for screen readers and clicking the label toggles the checkbox.
108
+ */
109
+ export const Default: Story = {
110
+ render: () => {
111
+ const [checked, setChecked] = useState(false)
112
+ return (
113
+ <Checkbox
114
+ id="checkbox-default"
115
+ label="I agree to the terms and conditions"
116
+ checked={checked}
117
+ onChange={(e) => setChecked(e.target.checked)}
118
+ />
119
+ )
120
+ },
121
+ }
122
+
123
+ /**
124
+ * Checkbox with error message. Error messages are announced to screen readers
125
+ * and displayed visually below the checkbox. The checkbox is marked as invalid
126
+ * with \`aria-invalid="true"\`.
127
+ */
128
+ export const WithError: Story = {
129
+ render: () => {
130
+ const [checked, setChecked] = useState(false)
131
+ return (
132
+ <Checkbox
133
+ id="checkbox-error"
134
+ label="I agree to the terms"
135
+ checked={checked}
136
+ onChange={(e) => setChecked(e.target.checked)}
137
+ error="You must agree to the terms to continue"
138
+ />
139
+ )
140
+ },
141
+ }
142
+
143
+ /**
144
+ * Checkbox with helper text. Helper text provides additional context or guidance
145
+ * and is associated with the checkbox via \`aria-describedby\`.
146
+ */
147
+ export const WithHelperText: Story = {
148
+ render: () => {
149
+ const [checked, setChecked] = useState(false)
150
+ return (
151
+ <Checkbox
152
+ id="checkbox-helper"
153
+ label="Subscribe to newsletter"
154
+ checked={checked}
155
+ onChange={(e) => setChecked(e.target.checked)}
156
+ helperText="Receive weekly updates about new features"
157
+ />
158
+ )
159
+ },
160
+ }
161
+
162
+ /**
163
+ * Required checkbox field. Required checkboxes are marked with \`aria-required="true"\`
164
+ * and display a visual indicator (asterisk) next to the label.
165
+ */
166
+ export const Required: Story = {
167
+ render: () => {
168
+ const [checked, setChecked] = useState(false)
169
+ return (
170
+ <Checkbox
171
+ id="checkbox-required"
172
+ label="I accept the privacy policy"
173
+ checked={checked}
174
+ onChange={(e) => setChecked(e.target.checked)}
175
+ required
176
+ />
177
+ )
178
+ },
179
+ }
180
+
181
+ /**
182
+ * Disabled checkbox that cannot be toggled. Disabled checkboxes are announced as
183
+ * "disabled" by screen readers and appear visually dimmed.
184
+ */
185
+ export const Disabled: Story = {
186
+ args: {
187
+ id: 'checkbox-disabled',
188
+ label: 'This option is disabled',
189
+ disabled: true,
190
+ checked: false,
191
+ },
192
+ }
193
+
194
+ /**
195
+ * Checked checkbox state. Shows the checkbox in its checked state.
196
+ */
197
+ export const Checked: Story = {
198
+ args: {
199
+ id: 'checkbox-checked',
200
+ label: 'This option is selected',
201
+ checked: true,
202
+ },
203
+ }
204
+
205
+ /**
206
+ * Multiple checkboxes grouped together. Use Fieldset component to group
207
+ * related checkboxes for better semantic structure and accessibility.
208
+ */
209
+ export const Grouped: Story = {
210
+ render: () => {
211
+ const [preferences, setPreferences] = useState({
212
+ email: false,
213
+ sms: false,
214
+ push: false,
215
+ })
216
+
217
+ const handleChange = (key: keyof typeof preferences) => (e: React.ChangeEvent<HTMLInputElement>) => {
218
+ setPreferences((prev) => ({ ...prev, [key]: e.target.checked }))
219
+ }
220
+
221
+ return (
222
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
223
+ <Checkbox
224
+ id="pref-email"
225
+ label="Email notifications"
226
+ checked={preferences.email}
227
+ onChange={handleChange('email')}
228
+ />
229
+ <Checkbox
230
+ id="pref-sms"
231
+ label="SMS notifications"
232
+ checked={preferences.sms}
233
+ onChange={handleChange('sms')}
234
+ />
235
+ <Checkbox
236
+ id="pref-push"
237
+ label="Push notifications"
238
+ checked={preferences.push}
239
+ onChange={handleChange('push')}
240
+ />
241
+ </div>
242
+ )
243
+ },
244
+ parameters: {
245
+ docs: {
246
+ description: {
247
+ story: 'Multiple checkboxes that can be selected independently. For better accessibility, wrap related checkboxes in a Fieldset component.',
248
+ },
249
+ },
250
+ },
251
+ }
252
+
@@ -0,0 +1,114 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import { combineAriaDescribedBy } from '../../utils/aria'
5
+ import './Checkbox.css'
6
+
7
+ export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
8
+ /**
9
+ * Label for the checkbox
10
+ */
11
+ label?: string
12
+
13
+ /**
14
+ * Error message to display
15
+ */
16
+ error?: string
17
+
18
+ /**
19
+ * Helper text to display
20
+ */
21
+ helperText?: string
22
+ }
23
+
24
+ /**
25
+ * Accessible Checkbox component
26
+ *
27
+ * WCAG Compliance:
28
+ * - 1.3.1 Info and Relationships: Proper label-input association
29
+ * - 2.5.3 Label in Name: Label text matches accessible name
30
+ * - 4.1.2 Name, Role, Value: Proper ARIA attributes
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * <Checkbox
35
+ * id="agree"
36
+ * label="I agree to the terms"
37
+ * checked={checked}
38
+ * onChange={handleChange}
39
+ * />
40
+ * ```
41
+ */
42
+ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
43
+ (
44
+ {
45
+ id,
46
+ label,
47
+ error,
48
+ helperText,
49
+ className = '',
50
+ 'aria-describedby': ariaDescribedBy,
51
+ ...props
52
+ },
53
+ ref
54
+ ) => {
55
+ const checkboxId = React.useId()
56
+ const finalId = id || `checkbox-${checkboxId}`
57
+ const errorId = error ? `${finalId}-error` : undefined
58
+ const helperId = helperText ? `${finalId}-helper` : undefined
59
+
60
+ const describedBy = combineAriaDescribedBy(
61
+ ariaDescribedBy,
62
+ errorId,
63
+ helperId
64
+ )
65
+
66
+ const classes = [
67
+ 'form-checkbox',
68
+ error && 'form-checkbox--error',
69
+ className,
70
+ ]
71
+ .filter(Boolean)
72
+ .join(' ')
73
+
74
+ return (
75
+ <div className="form-checkbox-wrapper">
76
+ <div className="form-checkbox-input-wrapper">
77
+ <input
78
+ ref={ref}
79
+ id={finalId}
80
+ type="checkbox"
81
+ className={classes}
82
+ aria-invalid={error ? true : undefined}
83
+ aria-describedby={describedBy}
84
+ required={props.required ? true : undefined}
85
+ {...props}
86
+ />
87
+ {label && (
88
+ <label htmlFor={finalId} className="form-checkbox-label">
89
+ {label}
90
+ {props.required && (
91
+ <span className="form-label__required" aria-hidden="true">
92
+ {' '}*
93
+ </span>
94
+ )}
95
+ </label>
96
+ )}
97
+ </div>
98
+ {helperText && !error && (
99
+ <span id={helperId} className="form-helper-text">
100
+ {helperText}
101
+ </span>
102
+ )}
103
+ {error && (
104
+ <span id={errorId} className="form-error-text" role="alert">
105
+ {error}
106
+ </span>
107
+ )}
108
+ </div>
109
+ )
110
+ }
111
+ )
112
+
113
+ Checkbox.displayName = 'Checkbox'
114
+