@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.
- package/.storybook/custom.css +69 -0
- package/.storybook/main.ts +46 -0
- package/.storybook/manager.ts +26 -0
- package/.storybook/package.json +6 -0
- package/.storybook/preview.tsx +31 -0
- package/.storybook/public/logo.png +0 -0
- package/.storybook/vite.config.ts +24 -0
- package/.storybook/welcome.mdx +97 -0
- package/DEPLOYMENT.md +154 -0
- package/README.md +227 -0
- package/apps/web/app/(docs)/audit/audit.css +269 -0
- package/apps/web/app/(docs)/audit/page.tsx +271 -0
- package/apps/web/app/(docs)/components/button/page.tsx +49 -0
- package/apps/web/app/(docs)/components/form/page.tsx +92 -0
- package/apps/web/app/(docs)/components/link/page.tsx +31 -0
- package/apps/web/app/(docs)/components/modal/page.tsx +41 -0
- package/apps/web/app/(docs)/components/page.tsx +37 -0
- package/apps/web/app/(docs)/components/table/page.tsx +54 -0
- package/apps/web/app/(docs)/components/tabs/page.tsx +61 -0
- package/apps/web/app/(docs)/components/toast/page.tsx +51 -0
- package/apps/web/app/api/audit/route.ts +128 -0
- package/apps/web/app/favicon.ico +0 -0
- package/apps/web/app/layout.tsx +20 -0
- package/apps/web/app/page.tsx +17 -0
- package/apps/web/app/styles/globals.css +5 -0
- package/apps/web/next-env.d.ts +5 -0
- package/apps/web/next.config.js +21 -0
- package/apps/web/package.json +28 -0
- package/apps/web/public/_headers +17 -0
- package/apps/web/public/_redirects +31 -0
- package/apps/web/public/logo.png +0 -0
- package/apps/web/tsconfig.json +29 -0
- package/netlify/functions/audit.ts +163 -0
- package/netlify.toml +37 -0
- package/package.json +30 -58
- package/packages/design-system/README.md +252 -0
- package/packages/design-system/package.json +68 -0
- package/packages/design-system/scripts/copy-css.js +63 -0
- package/packages/design-system/src/components/Button/Button.stories.tsx +228 -0
- package/packages/design-system/src/components/Button/Button.tsx +137 -0
- package/packages/design-system/src/components/Button/index.ts +3 -0
- package/packages/design-system/src/components/DataTable/DataTable.stories.tsx +211 -0
- package/packages/design-system/src/components/DataTable/DataTable.tsx +293 -0
- package/packages/design-system/src/components/DataTable/index.ts +3 -0
- package/packages/design-system/src/components/Form/Checkbox.stories.tsx +252 -0
- package/packages/design-system/src/components/Form/Checkbox.tsx +114 -0
- package/packages/design-system/src/components/Form/Fieldset.stories.tsx +210 -0
- package/packages/design-system/src/components/Form/Fieldset.tsx +71 -0
- package/packages/design-system/src/components/Form/Input.stories.tsx +164 -0
- package/packages/design-system/src/components/Form/Input.tsx +113 -0
- package/packages/design-system/src/components/Form/Label.tsx +56 -0
- package/packages/design-system/src/components/Form/Radio.stories.tsx +265 -0
- package/packages/design-system/src/components/Form/Radio.tsx +147 -0
- package/packages/design-system/src/components/Form/Select.stories.tsx +295 -0
- package/packages/design-system/src/components/Form/Select.tsx +160 -0
- package/packages/design-system/src/components/Form/Textarea.stories.tsx +253 -0
- package/packages/design-system/src/components/Form/Textarea.tsx +145 -0
- package/packages/design-system/src/components/Form/index.ts +8 -0
- package/packages/design-system/src/components/Link/Link.stories.tsx +128 -0
- package/packages/design-system/src/components/Link/Link.tsx +117 -0
- package/packages/design-system/src/components/Link/index.ts +3 -0
- package/packages/design-system/src/components/Modal/Modal.stories.tsx +165 -0
- package/packages/design-system/src/components/Modal/Modal.tsx +202 -0
- package/packages/design-system/src/components/Modal/index.ts +3 -0
- package/packages/design-system/src/components/Tabs/Tabs.stories.tsx +213 -0
- package/packages/design-system/src/components/Tabs/Tabs.tsx +248 -0
- package/packages/design-system/src/components/Tabs/index.ts +3 -0
- package/packages/design-system/src/components/Toast/Toast.stories.tsx +153 -0
- package/packages/design-system/src/components/Toast/Toast.tsx +175 -0
- package/packages/design-system/src/components/Toast/ToastProvider.tsx +73 -0
- package/packages/design-system/src/components/Toast/index.ts +5 -0
- package/packages/design-system/src/hooks/useAriaLive.ts +51 -0
- package/packages/design-system/src/hooks/useFocusReturn.ts +40 -0
- package/packages/design-system/src/hooks/useFocusTrap.ts +82 -0
- package/{dist/index.js → packages/design-system/src/index.ts} +4 -0
- package/packages/design-system/src/styles/index.ts +3 -0
- package/packages/design-system/src/tokens/breakpoints.ts +28 -0
- package/packages/design-system/src/tokens/colors.ts +98 -0
- package/packages/design-system/src/tokens/index.ts +6 -0
- package/packages/design-system/src/tokens/motion.ts +41 -0
- package/packages/design-system/src/tokens/spacing.ts +24 -0
- package/packages/design-system/src/tokens/theme.ts +19 -0
- package/packages/design-system/src/tokens/typography.ts +64 -0
- package/packages/design-system/src/utils/aria.ts +108 -0
- package/packages/design-system/src/utils/focus.ts +87 -0
- package/packages/design-system/src/utils/index.ts +4 -0
- package/packages/design-system/src/utils/keyboard.ts +77 -0
- package/packages/design-system/tsconfig.json +17 -0
- package/public/logo.png +0 -0
- package/scripts/fix-storybook-paths.js +53 -0
- package/tsconfig.json +20 -0
- package/dist/components/Button/Button.d.ts +0 -37
- package/dist/components/Button/Button.d.ts.map +0 -1
- package/dist/components/Button/Button.js +0 -52
- package/dist/components/Button/index.d.ts +0 -3
- package/dist/components/Button/index.d.ts.map +0 -1
- package/dist/components/Button/index.js +0 -1
- package/dist/components/DataTable/DataTable.d.ts +0 -71
- package/dist/components/DataTable/DataTable.d.ts.map +0 -1
- package/dist/components/DataTable/DataTable.js +0 -122
- package/dist/components/DataTable/index.d.ts +0 -3
- package/dist/components/DataTable/index.d.ts.map +0 -1
- package/dist/components/DataTable/index.js +0 -1
- package/dist/components/Form/Checkbox.d.ts +0 -36
- package/dist/components/Form/Checkbox.d.ts.map +0 -1
- package/dist/components/Form/Checkbox.js +0 -39
- package/dist/components/Form/Fieldset.d.ts +0 -33
- package/dist/components/Form/Fieldset.d.ts.map +0 -1
- package/dist/components/Form/Fieldset.js +0 -34
- package/dist/components/Form/Input.d.ts +0 -37
- package/dist/components/Form/Input.d.ts.map +0 -1
- package/dist/components/Form/Input.js +0 -41
- package/dist/components/Form/Label.d.ts +0 -30
- package/dist/components/Form/Label.d.ts.map +0 -1
- package/dist/components/Form/Label.js +0 -30
- package/dist/components/Form/Radio.d.ts +0 -53
- package/dist/components/Form/Radio.d.ts.map +0 -1
- package/dist/components/Form/Radio.js +0 -39
- package/dist/components/Form/Select.d.ts +0 -51
- package/dist/components/Form/Select.d.ts.map +0 -1
- package/dist/components/Form/Select.js +0 -49
- package/dist/components/Form/Textarea.d.ts +0 -44
- package/dist/components/Form/Textarea.d.ts.map +0 -1
- package/dist/components/Form/Textarea.js +0 -43
- package/dist/components/Form/index.d.ts +0 -8
- package/dist/components/Form/index.d.ts.map +0 -1
- package/dist/components/Form/index.js +0 -7
- package/dist/components/Link/Link.d.ts +0 -34
- package/dist/components/Link/Link.d.ts.map +0 -1
- package/dist/components/Link/Link.js +0 -48
- package/dist/components/Link/index.d.ts +0 -3
- package/dist/components/Link/index.d.ts.map +0 -1
- package/dist/components/Link/index.js +0 -1
- package/dist/components/Modal/Modal.d.ts +0 -64
- package/dist/components/Modal/Modal.d.ts.map +0 -1
- package/dist/components/Modal/Modal.js +0 -108
- package/dist/components/Modal/index.d.ts +0 -3
- package/dist/components/Modal/index.d.ts.map +0 -1
- package/dist/components/Modal/index.js +0 -1
- package/dist/components/Tabs/Tabs.d.ts +0 -63
- package/dist/components/Tabs/Tabs.d.ts.map +0 -1
- package/dist/components/Tabs/Tabs.js +0 -134
- package/dist/components/Tabs/index.d.ts +0 -3
- package/dist/components/Tabs/index.d.ts.map +0 -1
- package/dist/components/Tabs/index.js +0 -1
- package/dist/components/Toast/Toast.d.ts +0 -59
- package/dist/components/Toast/Toast.d.ts.map +0 -1
- package/dist/components/Toast/Toast.js +0 -91
- package/dist/components/Toast/ToastProvider.d.ts +0 -22
- package/dist/components/Toast/ToastProvider.d.ts.map +0 -1
- package/dist/components/Toast/ToastProvider.js +0 -33
- package/dist/components/Toast/index.d.ts +0 -5
- package/dist/components/Toast/index.d.ts.map +0 -1
- package/dist/components/Toast/index.js +0 -2
- package/dist/hooks/useAriaLive.d.ts +0 -9
- package/dist/hooks/useAriaLive.d.ts.map +0 -1
- package/dist/hooks/useAriaLive.js +0 -39
- package/dist/hooks/useFocusReturn.d.ts +0 -9
- package/dist/hooks/useFocusReturn.d.ts.map +0 -1
- package/dist/hooks/useFocusReturn.js +0 -33
- package/dist/hooks/useFocusTrap.d.ts +0 -9
- package/dist/hooks/useFocusTrap.d.ts.map +0 -1
- package/dist/hooks/useFocusTrap.js +0 -68
- package/dist/index.d.ts +0 -22
- package/dist/index.d.ts.map +0 -1
- package/dist/styles/index.d.ts +0 -3
- package/dist/styles/index.d.ts.map +0 -1
- package/dist/styles/index.js +0 -1
- package/dist/tokens/breakpoints.d.ts +0 -25
- package/dist/tokens/breakpoints.d.ts.map +0 -1
- package/dist/tokens/breakpoints.js +0 -23
- package/dist/tokens/colors.d.ts +0 -81
- package/dist/tokens/colors.d.ts.map +0 -1
- package/dist/tokens/colors.js +0 -86
- package/dist/tokens/index.d.ts +0 -6
- package/dist/tokens/index.d.ts.map +0 -1
- package/dist/tokens/index.js +0 -5
- package/dist/tokens/motion.d.ts +0 -30
- package/dist/tokens/motion.d.ts.map +0 -1
- package/dist/tokens/motion.js +0 -34
- package/dist/tokens/spacing.d.ts +0 -22
- package/dist/tokens/spacing.d.ts.map +0 -1
- package/dist/tokens/spacing.js +0 -20
- package/dist/tokens/theme.d.ts +0 -159
- package/dist/tokens/theme.d.ts.map +0 -1
- package/dist/tokens/theme.js +0 -15
- package/dist/tokens/typography.d.ts +0 -45
- package/dist/tokens/typography.d.ts.map +0 -1
- package/dist/tokens/typography.js +0 -56
- package/dist/utils/aria.d.ts +0 -60
- package/dist/utils/aria.d.ts.map +0 -1
- package/dist/utils/aria.js +0 -86
- package/dist/utils/focus.d.ts +0 -30
- package/dist/utils/focus.d.ts.map +0 -1
- package/dist/utils/focus.js +0 -80
- package/dist/utils/index.d.ts +0 -4
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -3
- package/dist/utils/keyboard.d.ts +0 -38
- package/dist/utils/keyboard.d.ts.map +0 -1
- package/dist/utils/keyboard.js +0 -59
- /package/{dist → packages/design-system/src}/components/Button/Button.css +0 -0
- /package/{dist → packages/design-system/src}/components/DataTable/DataTable.css +0 -0
- /package/{dist → packages/design-system/src}/components/Form/Checkbox.css +0 -0
- /package/{dist → packages/design-system/src}/components/Form/Fieldset.css +0 -0
- /package/{dist → packages/design-system/src}/components/Form/Input.css +0 -0
- /package/{dist → packages/design-system/src}/components/Form/Label.css +0 -0
- /package/{dist → packages/design-system/src}/components/Form/Radio.css +0 -0
- /package/{dist → packages/design-system/src}/components/Form/Select.css +0 -0
- /package/{dist → packages/design-system/src}/components/Form/Textarea.css +0 -0
- /package/{dist → packages/design-system/src}/components/Link/Link.css +0 -0
- /package/{dist → packages/design-system/src}/components/Modal/Modal.css +0 -0
- /package/{dist → packages/design-system/src}/components/Tabs/Tabs.css +0 -0
- /package/{dist → packages/design-system/src}/components/Toast/Toast.css +0 -0
- /package/{dist → packages/design-system/src}/components/Toast/ToastProvider.css +0 -0
- /package/{dist → packages/design-system/src}/styles/components.css +0 -0
- /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
|
+
|