@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,210 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { useState } from 'react'
3
+ import { Fieldset } from './Fieldset'
4
+ import { Input } from './Input'
5
+ import { Checkbox } from './Checkbox'
6
+ import { Radio } from './Radio'
7
+
8
+ /**
9
+ * # Fieldset Component
10
+ *
11
+ * An accessible fieldset component for grouping related form controls. Uses semantic
12
+ * HTML fieldset/legend structure to provide context for screen readers.
13
+ *
14
+ * ## Usage
15
+ *
16
+ * ```tsx
17
+ * import { Fieldset, Input } from '@a11ypros/a11y-ui-components'
18
+ *
19
+ * function MyComponent() {
20
+ * return (
21
+ * <Fieldset legend="Shipping Address">
22
+ * <Input label="Street" />
23
+ * <Input label="City" />
24
+ * <Input label="Zip Code" />
25
+ * </Fieldset>
26
+ * )
27
+ * }
28
+ * ```
29
+ *
30
+ * ## Features
31
+ *
32
+ * - **Semantic grouping**: Uses proper `<fieldset>` and `<legend>` elements
33
+ * - **Visual legend**: Legend text provides context for the grouped fields
34
+ * - **Hidden legend**: Option to hide legend visually while keeping it accessible
35
+ * - **Required indicator**: Shows asterisk when fieldset contains required fields
36
+ *
37
+ * ## Accessibility
38
+ *
39
+ * ### WCAG 2.1/2.2 Compliance
40
+ *
41
+ * - **1.3.1 Info and Relationships**: Proper fieldset/legend structure for grouping related controls
42
+ * - **4.1.2 Name, Role, Value**: Proper semantic HTML with fieldset role
43
+ *
44
+ * ### Screen Reader Support
45
+ *
46
+ * - Fieldset legend is announced when entering the group
47
+ * - Provides context for all form controls within the fieldset
48
+ * - Required state is announced when fieldset contains required fields
49
+ * - Hidden legend is still announced to screen readers
50
+ *
51
+ * ## Best Practices
52
+ *
53
+ * 1. **Always provide a legend**: Required for accessibility - describes what the group is for
54
+ * 2. **Group related fields**: Use fieldsets to group logically related form controls
55
+ * 3. **Use for radio/checkbox groups**: Essential for grouping radio buttons and checkboxes
56
+ * 4. **Keep legends concise**: Short, descriptive legends work best
57
+ * 5. **Don't nest fieldsets**: Avoid nesting fieldsets within fieldsets
58
+ *
59
+ * ## Common Pitfalls
60
+ *
61
+ * - Missing legend (screen readers lose context for grouped fields)
62
+ * - Using div instead of fieldset (loses semantic meaning)
63
+ * - Nesting fieldsets (can confuse screen readers)
64
+ * - Too verbose legends (keep them concise)
65
+ * - Not using for radio/checkbox groups (required for proper grouping)
66
+ *
67
+ * @component
68
+ * @example
69
+ * ```tsx
70
+ * <Fieldset legend="Contact Information">
71
+ * <Input label="Email" type="email" required />
72
+ * <Input label="Phone" type="tel" />
73
+ * </Fieldset>
74
+ * ```
75
+ */
76
+ const meta: Meta<typeof Fieldset> = {
77
+ title: 'Components/Form/Fieldset',
78
+ component: Fieldset,
79
+ tags: ['autodocs'],
80
+ }
81
+
82
+ export default meta
83
+ type Story = StoryObj<typeof Fieldset>
84
+
85
+ /**
86
+ * Standard fieldset with visible legend. Groups related form controls
87
+ * and provides context through the legend text.
88
+ */
89
+ export const Default: Story = {
90
+ render: () => (
91
+ <Fieldset legend="Shipping Address">
92
+ <Input label="Street address"/>
93
+ <Input label="City" />
94
+ <Input label="Zip code"/>
95
+ </Fieldset>
96
+ ),
97
+ }
98
+
99
+ /**
100
+ * Fieldset with required fields. The required indicator (asterisk) appears
101
+ * next to the legend when the fieldset contains required fields.
102
+ */
103
+ export const WithRequiredFields: Story = {
104
+ render: () => (
105
+ <Fieldset legend="Personal Information" required>
106
+ <Input label="First name" id="first-name" required />
107
+ <Input label="Last name" id="last-name" required />
108
+ <Input label="Email" id="email" type="email" required />
109
+ </Fieldset>
110
+ ),
111
+ }
112
+
113
+ /**
114
+ * Fieldset with hidden legend. The legend is visually hidden but still
115
+ * announced to screen readers, useful when visual grouping is clear.
116
+ */
117
+ export const HiddenLegend: Story = {
118
+ render: () => (
119
+ <Fieldset legend="Billing Information" legendHidden>
120
+ <Input label="Card number" id="card" />
121
+ <Input label="Expiry date" id="expiry" />
122
+ <Input label="CVV" id="cvv" />
123
+ </Fieldset>
124
+ ),
125
+ parameters: {
126
+ docs: {
127
+ description: {
128
+ story: 'Legend is visually hidden but still accessible to screen readers. Use when visual grouping is obvious but you want to maintain semantic structure.',
129
+ },
130
+ },
131
+ },
132
+ }
133
+
134
+ /**
135
+ * Fieldset grouping radio buttons. Essential for proper semantic grouping
136
+ * of radio button groups.
137
+ */
138
+ export const WithRadioButtons: Story = {
139
+ render: () => {
140
+ const [value, setValue] = useState('')
141
+ return (
142
+ <Fieldset legend="Choose a size">
143
+ <Radio
144
+ name="size"
145
+ value={value}
146
+ onChange={(e) => setValue(e.target.value)}
147
+ options={[
148
+ { value: 'small', label: 'Small' },
149
+ { value: 'medium', label: 'Medium' },
150
+ { value: 'large', label: 'Large' },
151
+ ]}
152
+ />
153
+ </Fieldset>
154
+ )
155
+ },
156
+ parameters: {
157
+ docs: {
158
+ description: {
159
+ story: 'Fieldset is essential for grouping radio buttons. The Radio component uses fieldset internally, but you can wrap multiple radio groups in a fieldset for higher-level grouping.',
160
+ },
161
+ },
162
+ },
163
+ }
164
+
165
+ /**
166
+ * Fieldset grouping checkboxes. Groups related checkboxes together
167
+ * for better semantic structure.
168
+ */
169
+ export const WithCheckboxes: Story = {
170
+ render: () => {
171
+ const [preferences, setPreferences] = useState({
172
+ email: false,
173
+ sms: false,
174
+ push: false,
175
+ })
176
+
177
+ return (
178
+ <Fieldset legend="Notification Preferences">
179
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
180
+ <Checkbox
181
+ id="pref-email"
182
+ label="Email notifications"
183
+ checked={preferences.email}
184
+ onChange={(e) => setPreferences((prev) => ({ ...prev, email: e.target.checked }))}
185
+ />
186
+ <Checkbox
187
+ id="pref-sms"
188
+ label="SMS notifications"
189
+ checked={preferences.sms}
190
+ onChange={(e) => setPreferences((prev) => ({ ...prev, sms: e.target.checked }))}
191
+ />
192
+ <Checkbox
193
+ id="pref-push"
194
+ label="Push notifications"
195
+ checked={preferences.push}
196
+ onChange={(e) => setPreferences((prev) => ({ ...prev, push: e.target.checked }))}
197
+ />
198
+ </div>
199
+ </Fieldset>
200
+ )
201
+ },
202
+ parameters: {
203
+ docs: {
204
+ description: {
205
+ story: 'Fieldset groups related checkboxes together, providing context about what the checkboxes are for.',
206
+ },
207
+ },
208
+ },
209
+ }
210
+
@@ -0,0 +1,71 @@
1
+ import React from 'react'
2
+ import './Fieldset.css'
3
+
4
+ export interface FieldsetProps extends React.FieldsetHTMLAttributes<HTMLFieldSetElement> {
5
+ /**
6
+ * Legend text for the fieldset
7
+ */
8
+ legend?: string
9
+
10
+ /**
11
+ * Whether the legend is visually hidden (but still accessible)
12
+ */
13
+ legendHidden?: boolean
14
+
15
+ /**
16
+ * Whether to show a required field explanation. Use this if the fieldset contains required fields.
17
+ */
18
+ required?: boolean
19
+ }
20
+
21
+ /**
22
+ * Accessible Fieldset component
23
+ *
24
+ * WCAG Compliance:
25
+ * - 1.3.1 Info and Relationships: Proper fieldset/legend structure
26
+ * - 4.1.2 Name, Role, Value: Proper semantic HTML
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * <Fieldset legend="Shipping Address">
31
+ * <Input label="Street" />
32
+ * <Input label="City" />
33
+ * </Fieldset>
34
+ * ```
35
+ */
36
+ export const Fieldset = React.forwardRef<HTMLFieldSetElement, FieldsetProps>(
37
+ ({ legend, legendHidden = false, required = false, className = '', children, ...props }, ref) => {
38
+ const classes = [
39
+ 'form-fieldset',
40
+ className,
41
+ ]
42
+ .filter(Boolean)
43
+ .join(' ')
44
+
45
+ const legendClasses = [
46
+ 'form-legend',
47
+ legendHidden && 'form-legend--hidden',
48
+ ]
49
+ .filter(Boolean)
50
+ .join(' ')
51
+
52
+ return (
53
+ <fieldset ref={ref} className={classes} {...props}>
54
+ {legend && (
55
+ <legend className={legendClasses}>
56
+ {legend}
57
+ </legend>
58
+ )}
59
+ {required && (
60
+ <p className="fieldset__required">
61
+ {' '}<span className='fieldset__required-indicator'>*</span> indicates a required field.
62
+ </p>
63
+ )}
64
+ {children}
65
+ </fieldset>
66
+ )
67
+ }
68
+ )
69
+
70
+ Fieldset.displayName = 'Fieldset'
71
+
@@ -0,0 +1,164 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { Input } from './Input'
3
+
4
+ /**
5
+ * # Input Component
6
+ *
7
+ * An accessible form input component with proper label association, error handling,
8
+ * and ARIA attributes. Supports all standard HTML input types.
9
+ *
10
+ * ## Usage
11
+ *
12
+ * ```tsx
13
+ * import { Input } from '@a11ypros/a11y-ui-components'
14
+ *
15
+ * function MyComponent() {
16
+ * return (
17
+ * <Input
18
+ * label="Email address"
19
+ * type="email"
20
+ * required
21
+ * placeholder="you@example.com"
22
+ * />
23
+ * )
24
+ * }
25
+ * ```
26
+ *
27
+ * ## Features
28
+ *
29
+ * - **Label association**: Labels are properly associated with inputs using \`htmlFor\` and \`id\`
30
+ * - **Error handling**: Error messages are announced to screen readers via ARIA
31
+ * - **Helper text**: Optional helper text for additional context
32
+ * - **Required indicators**: Visual and ARIA indicators for required fields
33
+ * - **All input types**: Supports text, email, password, number, tel, url, search, etc.
34
+ *
35
+ * ## Accessibility
36
+ *
37
+ * ### WCAG 2.1/2.2 Compliance
38
+ *
39
+ * - **1.3.1 Info and Relationships**: Proper label-input association via \`htmlFor\` and \`id\`
40
+ * - **2.5.3 Label in Name**: Label text matches accessible name
41
+ * - **4.1.2 Name, Role, Value**: Proper ARIA attributes including \`aria-required\`, \`aria-invalid\`, \`aria-describedby\`
42
+ * - **4.1.3 Status Messages**: Error messages announced via ARIA live regions
43
+ *
44
+ * ### Keyboard Interactions
45
+ *
46
+ * | Key | Action |
47
+ * |-----|--------|
48
+ * | **Tab** | Moves focus to the input |
49
+ * | **Shift+Tab** | Moves focus away from the input |
50
+ * | **Enter** | Submits form (if in a form) |
51
+ * | **Arrow keys** | Navigate within input value (for text inputs) |
52
+ *
53
+ * ### Screen Reader Support
54
+ *
55
+ * - Label is announced when input receives focus
56
+ * - Required state is announced ("required")
57
+ * - Error messages are announced when present
58
+ * - Helper text is announced via \`aria-describedby\`
59
+ * - Input type is announced (e.g., "email", "password")
60
+ *
61
+ * ### Focus Management
62
+ *
63
+ * - Focus indicators use 2px solid outline with 2px offset
64
+ * - Focus styles respect \`prefers-reduced-motion\` media query
65
+ * - Error state has distinct focus styling
66
+ * - Focus visible only on keyboard navigation using \`:focus-visible\`
67
+ *
68
+ * ## Best Practices
69
+ *
70
+ * 1. **Always provide a label**: Required for accessibility and usability
71
+ * 2. **Use appropriate input types**: Helps with validation and mobile keyboards
72
+ * 3. **Provide helpful error messages**: Be specific about what went wrong
73
+ * 4. **Use helper text for guidance**: Help users understand what to enter
74
+ * 5. **Mark required fields**: Use \`required\` prop and visual indicator
75
+ *
76
+ * ## Common Pitfalls
77
+ *
78
+ * - Missing label (screen readers can't identify the input)
79
+ * - Using placeholder as label (placeholders disappear and aren't reliable)
80
+ * - Vague error messages (be specific about validation failures)
81
+ * - Not marking required fields (users don't know what's mandatory)
82
+ * - Using wrong input type (affects mobile keyboards and validation)
83
+ *
84
+ * @component
85
+ * @example
86
+ * ```tsx
87
+ * <Input
88
+ * label="Email"
89
+ * type="email"
90
+ * required
91
+ * error={errors.email}
92
+ * helperText="We'll never share your email"
93
+ * />
94
+ * ```
95
+ */
96
+ const meta: Meta<typeof Input> = {
97
+ title: 'Components/Form/Input',
98
+ component: Input,
99
+ tags: ['autodocs'],
100
+ }
101
+
102
+ export default meta
103
+ type Story = StoryObj<typeof Input>
104
+
105
+ /**
106
+ * Standard input with label and placeholder. The label is properly associated
107
+ * with the input for screen readers and clicking the label focuses the input.
108
+ */
109
+ export const Default: Story = {
110
+ args: {
111
+ label: 'Email address',
112
+ type: 'email',
113
+ placeholder: 'you@example.com',
114
+ },
115
+ }
116
+
117
+ /**
118
+ * Input with error message. Error messages are announced to screen readers
119
+ * and displayed visually below the input. The input is marked as invalid
120
+ * with \`aria-invalid="true"\`.
121
+ */
122
+ export const WithError: Story = {
123
+ args: {
124
+ label: 'Email address',
125
+ type: 'email',
126
+ error: 'Please enter a valid email address',
127
+ },
128
+ }
129
+
130
+ /**
131
+ * Input with helper text. Helper text provides additional context or guidance
132
+ * and is associated with the input via \`aria-describedby\`.
133
+ */
134
+ export const WithHelperText: Story = {
135
+ args: {
136
+ label: 'Password',
137
+ type: 'password',
138
+ helperText: 'Must be at least 8 characters',
139
+ },
140
+ }
141
+
142
+ /**
143
+ * Required input field. Required fields are marked with \`aria-required="true"\`
144
+ * and display a visual indicator (asterisk) next to the label.
145
+ */
146
+ export const Required: Story = {
147
+ args: {
148
+ label: 'Name',
149
+ required: true,
150
+ },
151
+ }
152
+
153
+ /**
154
+ * Disabled input that cannot be edited. Disabled inputs are announced as
155
+ * "disabled" by screen readers and appear visually dimmed.
156
+ */
157
+ export const Disabled: Story = {
158
+ args: {
159
+ label: 'Disabled input',
160
+ disabled: true,
161
+ defaultValue: 'Cannot edit',
162
+ },
163
+ }
164
+
@@ -0,0 +1,113 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import { generateId, combineAriaDescribedBy } from '../../utils/aria'
5
+ import './Input.css'
6
+
7
+ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
8
+ /**
9
+ * Error message to display
10
+ */
11
+ error?: string
12
+
13
+ /**
14
+ * Helper text to display below the input
15
+ */
16
+ helperText?: string
17
+
18
+ /**
19
+ * Label for the input (creates associated label)
20
+ */
21
+ label?: string
22
+ }
23
+
24
+ /**
25
+ * Accessible Input 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
+ * - 4.1.3 Status Messages: Error messages announced
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * <Input
36
+ * id="email"
37
+ * type="email"
38
+ * label="Email address"
39
+ * error="Please enter a valid email"
40
+ * />
41
+ * ```
42
+ */
43
+ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
44
+ (
45
+ {
46
+ id,
47
+ error,
48
+ helperText,
49
+ label,
50
+ className = '',
51
+ 'aria-describedby': ariaDescribedBy,
52
+ ...props
53
+ },
54
+ ref
55
+ ) => {
56
+ const inputId = React.useId()
57
+ const finalId = id || `input-${inputId}`
58
+ const errorId = error ? `${finalId}-error` : undefined
59
+ const helperId = helperText ? `${finalId}-helper` : undefined
60
+
61
+ const describedBy = combineAriaDescribedBy(
62
+ ariaDescribedBy,
63
+ errorId,
64
+ helperId
65
+ )
66
+
67
+ const classes = [
68
+ 'form-input',
69
+ error && 'form-input--error',
70
+ props.disabled && 'form-input--disabled',
71
+ className,
72
+ ]
73
+ .filter(Boolean)
74
+ .join(' ')
75
+
76
+ return (
77
+ <div className="form-input-wrapper">
78
+ {label && (
79
+ <label htmlFor={finalId} className="form-label">
80
+ {label}
81
+ {props.required && (
82
+ <span className="form-label__required" aria-hidden="true">
83
+ {' '}*
84
+ </span>
85
+ )}
86
+ </label>
87
+ )}
88
+ <input
89
+ ref={ref}
90
+ id={finalId}
91
+ className={classes}
92
+ aria-invalid={error ? true : undefined}
93
+ aria-describedby={describedBy}
94
+ required={props.required ? true : undefined}
95
+ {...props}
96
+ />
97
+ {helperText && !error && (
98
+ <span id={helperId} className="form-helper-text">
99
+ {helperText}
100
+ </span>
101
+ )}
102
+ {error && (
103
+ <span id={errorId} className="form-error-text" role="alert">
104
+ {error}
105
+ </span>
106
+ )}
107
+ </div>
108
+ )
109
+ }
110
+ )
111
+
112
+ Input.displayName = 'Input'
113
+
@@ -0,0 +1,56 @@
1
+ import React from 'react'
2
+ import './Label.css'
3
+
4
+ export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
5
+ /**
6
+ * Whether this label is required (shows asterisk)
7
+ */
8
+ required?: boolean
9
+
10
+ /**
11
+ * ID of the input this label is associated with
12
+ */
13
+ htmlFor?: string
14
+ }
15
+
16
+ /**
17
+ * Accessible Label component
18
+ *
19
+ * WCAG Compliance:
20
+ * - 1.3.1 Info and Relationships: Proper label-input association
21
+ * - 2.5.3 Label in Name: Label text matches accessible name
22
+ * - 4.1.2 Name, Role, Value: Proper semantic HTML
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * <Label htmlFor="email" required>
27
+ * Email address
28
+ * </Label>
29
+ * <Input id="email" type="email" />
30
+ * ```
31
+ */
32
+ export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
33
+ ({ required = false, className = '', children, ...props }, ref) => {
34
+ const classes = [
35
+ 'form-label',
36
+ required && 'form-label--required',
37
+ className,
38
+ ]
39
+ .filter(Boolean)
40
+ .join(' ')
41
+
42
+ return (
43
+ <label ref={ref} className={classes} {...props}>
44
+ {children}
45
+ {required && (
46
+ <span className="form-label__required" aria-hidden="true">
47
+ {' '}*
48
+ </span>
49
+ )}
50
+ </label>
51
+ )
52
+ }
53
+ )
54
+
55
+ Label.displayName = 'Label'
56
+