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