@a11ypros/a11y-ui-components 1.0.1 → 1.0.3
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 +49 -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,160 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import React from 'react'
|
|
4
|
-
import { combineAriaDescribedBy } from '../../utils/aria'
|
|
5
|
-
import { createArrowKeyHandler, isActivationKey } from '../../utils/keyboard'
|
|
6
|
-
import './Select.css'
|
|
7
|
-
|
|
8
|
-
export interface SelectOption {
|
|
9
|
-
value: string
|
|
10
|
-
label: string
|
|
11
|
-
disabled?: boolean
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'children'> {
|
|
15
|
-
/**
|
|
16
|
-
* Options for the select
|
|
17
|
-
*/
|
|
18
|
-
options: SelectOption[]
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Error message to display
|
|
22
|
-
*/
|
|
23
|
-
error?: string
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Helper text to display below the select
|
|
27
|
-
*/
|
|
28
|
-
helperText?: string
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Label for the select
|
|
32
|
-
*/
|
|
33
|
-
label?: string
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Placeholder option text
|
|
37
|
-
*/
|
|
38
|
-
placeholder?: string
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Accessible Select component
|
|
43
|
-
*
|
|
44
|
-
* WCAG Compliance:
|
|
45
|
-
* - 1.3.1 Info and Relationships: Proper label-select association
|
|
46
|
-
* - 2.1.1 Keyboard: Full keyboard navigation
|
|
47
|
-
* - 4.1.2 Name, Role, Value: Proper ARIA attributes
|
|
48
|
-
*
|
|
49
|
-
* @example
|
|
50
|
-
* ```tsx
|
|
51
|
-
* <Select
|
|
52
|
-
* id="country"
|
|
53
|
-
* label="Country"
|
|
54
|
-
* options={[
|
|
55
|
-
* { value: 'us', label: 'United States' },
|
|
56
|
-
* { value: 'ca', label: 'Canada' },
|
|
57
|
-
* ]}
|
|
58
|
-
* />
|
|
59
|
-
* ```
|
|
60
|
-
*/
|
|
61
|
-
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
|
|
62
|
-
(
|
|
63
|
-
{
|
|
64
|
-
id,
|
|
65
|
-
options,
|
|
66
|
-
error,
|
|
67
|
-
helperText,
|
|
68
|
-
label,
|
|
69
|
-
placeholder,
|
|
70
|
-
className = '',
|
|
71
|
-
'aria-describedby': ariaDescribedBy,
|
|
72
|
-
...props
|
|
73
|
-
},
|
|
74
|
-
ref
|
|
75
|
-
) => {
|
|
76
|
-
const selectId = React.useId()
|
|
77
|
-
const finalId = id || `select-${selectId}`
|
|
78
|
-
const errorId = error ? `${finalId}-error` : undefined
|
|
79
|
-
const helperId = helperText ? `${finalId}-helper` : undefined
|
|
80
|
-
|
|
81
|
-
const describedBy = combineAriaDescribedBy(
|
|
82
|
-
ariaDescribedBy,
|
|
83
|
-
errorId,
|
|
84
|
-
helperId
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
const handleKeyDown = React.useCallback(
|
|
88
|
-
(event: React.KeyboardEvent<HTMLSelectElement>) => {
|
|
89
|
-
// Arrow keys are handled natively by select
|
|
90
|
-
// But we can add custom handling if needed
|
|
91
|
-
if (props.onKeyDown) {
|
|
92
|
-
props.onKeyDown(event)
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
[props.onKeyDown]
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
const classes = [
|
|
99
|
-
'form-select',
|
|
100
|
-
error && 'form-select--error',
|
|
101
|
-
props.disabled && 'form-select--disabled',
|
|
102
|
-
className,
|
|
103
|
-
]
|
|
104
|
-
.filter(Boolean)
|
|
105
|
-
.join(' ')
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
<div className="form-select-wrapper">
|
|
109
|
-
{label && (
|
|
110
|
-
<label htmlFor={finalId} className="form-label">
|
|
111
|
-
{label}
|
|
112
|
-
{props.required && (
|
|
113
|
-
<span className="form-label__required" aria-hidden="true">
|
|
114
|
-
{' '}*
|
|
115
|
-
</span>
|
|
116
|
-
)}
|
|
117
|
-
</label>
|
|
118
|
-
)}
|
|
119
|
-
<select
|
|
120
|
-
ref={ref}
|
|
121
|
-
id={finalId}
|
|
122
|
-
className={classes}
|
|
123
|
-
aria-invalid={error ? true : undefined}
|
|
124
|
-
aria-describedby={describedBy}
|
|
125
|
-
onKeyDown={handleKeyDown}
|
|
126
|
-
required={props.required ? true : undefined}
|
|
127
|
-
{...props}
|
|
128
|
-
>
|
|
129
|
-
{placeholder && (
|
|
130
|
-
<option value="" disabled>
|
|
131
|
-
{placeholder}
|
|
132
|
-
</option>
|
|
133
|
-
)}
|
|
134
|
-
{options.map((option) => (
|
|
135
|
-
<option
|
|
136
|
-
key={option.value}
|
|
137
|
-
value={option.value}
|
|
138
|
-
disabled={option.disabled}
|
|
139
|
-
>
|
|
140
|
-
{option.label}
|
|
141
|
-
</option>
|
|
142
|
-
))}
|
|
143
|
-
</select>
|
|
144
|
-
{helperText && !error && (
|
|
145
|
-
<span id={helperId} className="form-helper-text">
|
|
146
|
-
{helperText}
|
|
147
|
-
</span>
|
|
148
|
-
)}
|
|
149
|
-
{error && (
|
|
150
|
-
<span id={errorId} className="form-error-text" role="alert">
|
|
151
|
-
{error}
|
|
152
|
-
</span>
|
|
153
|
-
)}
|
|
154
|
-
</div>
|
|
155
|
-
)
|
|
156
|
-
}
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
Select.displayName = 'Select'
|
|
160
|
-
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
-
import { useState } from 'react'
|
|
3
|
-
import { Textarea } from './Textarea'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* # Textarea Component
|
|
7
|
-
*
|
|
8
|
-
* An accessible textarea component with proper label association, error handling,
|
|
9
|
-
* character counting, and ARIA attributes. Ideal for multi-line text input.
|
|
10
|
-
*
|
|
11
|
-
* ## Usage
|
|
12
|
-
*
|
|
13
|
-
* ```tsx
|
|
14
|
-
* import { Textarea } from '@a11ypros/a11y-ui-components'
|
|
15
|
-
*
|
|
16
|
-
* function MyComponent() {
|
|
17
|
-
* return (
|
|
18
|
-
* <Textarea
|
|
19
|
-
* label="Message"
|
|
20
|
-
* placeholder="Enter your message"
|
|
21
|
-
* maxLength={500}
|
|
22
|
-
* showCount
|
|
23
|
-
* />
|
|
24
|
-
* )
|
|
25
|
-
* }
|
|
26
|
-
* ```
|
|
27
|
-
*
|
|
28
|
-
* ## Features
|
|
29
|
-
*
|
|
30
|
-
* - **Label association**: Labels are properly associated with textareas using \`htmlFor\` and \`id\`
|
|
31
|
-
* - **Error handling**: Error messages are announced to screen readers via ARIA
|
|
32
|
-
* - **Helper text**: Optional helper text for additional context
|
|
33
|
-
* - **Character counting**: Optional character count display with \`maxLength\` support
|
|
34
|
-
* - **Auto-resize**: Can be configured to grow with content
|
|
35
|
-
*
|
|
36
|
-
* ## Accessibility
|
|
37
|
-
*
|
|
38
|
-
* ### WCAG 2.1/2.2 Compliance
|
|
39
|
-
*
|
|
40
|
-
* - **1.3.1 Info and Relationships**: Proper label-textarea association via \`htmlFor\` and \`id\`
|
|
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 and character count announced to screen readers
|
|
43
|
-
*
|
|
44
|
-
* ### Keyboard Interactions
|
|
45
|
-
*
|
|
46
|
-
* | Key | Action |
|
|
47
|
-
* |-----|--------|
|
|
48
|
-
* | **Tab** | Moves focus to the textarea |
|
|
49
|
-
* | **Shift+Tab** | Moves focus away from the textarea |
|
|
50
|
-
* | **Enter** | Creates new line (does not submit form) |
|
|
51
|
-
* | **Arrow keys** | Navigate within text content |
|
|
52
|
-
*
|
|
53
|
-
* ### Screen Reader Support
|
|
54
|
-
*
|
|
55
|
-
* - Textarea role and label are announced when focused
|
|
56
|
-
* - Character count is announced when \`showCount\` is enabled
|
|
57
|
-
* - Required state is announced ("required")
|
|
58
|
-
* - Error messages are announced when present
|
|
59
|
-
* - Helper text is announced via \`aria-describedby\`
|
|
60
|
-
* - Max length is announced when \`maxLength\` is set
|
|
61
|
-
*
|
|
62
|
-
* ### Focus Management
|
|
63
|
-
*
|
|
64
|
-
* - Focus indicators use 2px solid outline with 2px offset
|
|
65
|
-
* - Focus styles respect \`prefers-reduced-motion\` media query
|
|
66
|
-
* - Error state has distinct focus styling
|
|
67
|
-
* - Focus visible only on keyboard navigation using \`:focus-visible\`
|
|
68
|
-
*
|
|
69
|
-
* ## Best Practices
|
|
70
|
-
*
|
|
71
|
-
* 1. **Always provide a label**: Required for accessibility and usability
|
|
72
|
-
* 2. **Use for multi-line input**: Textarea is for longer, multi-line text (use Input for single-line)
|
|
73
|
-
* 3. **Set appropriate maxLength**: Helps users understand constraints
|
|
74
|
-
* 4. **Provide helpful error messages**: Be specific about validation failures
|
|
75
|
-
* 5. **Use helper text for guidance**: Help users understand what to enter
|
|
76
|
-
*
|
|
77
|
-
* ## Common Pitfalls
|
|
78
|
-
*
|
|
79
|
-
* - Missing label (screen readers can't identify the textarea)
|
|
80
|
-
* - Using textarea for single-line input (use Input component instead)
|
|
81
|
-
* - Not setting maxLength when needed (users don't know limits)
|
|
82
|
-
* - Vague error messages (be specific about validation failures)
|
|
83
|
-
* - Too small initial size (hard to see what you're typing)
|
|
84
|
-
*
|
|
85
|
-
* @component
|
|
86
|
-
* @example
|
|
87
|
-
* ```tsx
|
|
88
|
-
* <Textarea
|
|
89
|
-
* label="Feedback"
|
|
90
|
-
* maxLength={500}
|
|
91
|
-
* showCount
|
|
92
|
-
* required
|
|
93
|
-
* error={errors.feedback}
|
|
94
|
-
* helperText="Please provide detailed feedback"
|
|
95
|
-
* />
|
|
96
|
-
* ```
|
|
97
|
-
*/
|
|
98
|
-
const meta: Meta<typeof Textarea> = {
|
|
99
|
-
title: 'Components/Form/Textarea',
|
|
100
|
-
component: Textarea,
|
|
101
|
-
tags: ['autodocs'],
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export default meta
|
|
105
|
-
type Story = StoryObj<typeof Textarea>
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Standard textarea with label and placeholder. The label is properly associated
|
|
109
|
-
* with the textarea for screen readers and clicking the label focuses the textarea.
|
|
110
|
-
*/
|
|
111
|
-
export const Default: Story = {
|
|
112
|
-
render: () => {
|
|
113
|
-
const [value, setValue] = useState('')
|
|
114
|
-
return (
|
|
115
|
-
<Textarea
|
|
116
|
-
id="textarea-default"
|
|
117
|
-
label="Message"
|
|
118
|
-
value={value}
|
|
119
|
-
onChange={(e) => setValue(e.target.value)}
|
|
120
|
-
placeholder="Enter your message here..."
|
|
121
|
-
/>
|
|
122
|
-
)
|
|
123
|
-
},
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Textarea with character count. Shows remaining characters when \`maxLength\`
|
|
128
|
-
* and \`showCount\` are set. The count is announced to screen readers.
|
|
129
|
-
*/
|
|
130
|
-
export const WithCharacterCount: Story = {
|
|
131
|
-
render: () => {
|
|
132
|
-
const [value, setValue] = useState('')
|
|
133
|
-
return (
|
|
134
|
-
<Textarea
|
|
135
|
-
id="textarea-count"
|
|
136
|
-
label="Description"
|
|
137
|
-
value={value}
|
|
138
|
-
onChange={(e) => setValue(e.target.value)}
|
|
139
|
-
maxLength={200}
|
|
140
|
-
showCount
|
|
141
|
-
placeholder="Enter a description (max 200 characters)"
|
|
142
|
-
/>
|
|
143
|
-
)
|
|
144
|
-
},
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Textarea with error message. Error messages are announced to screen readers
|
|
149
|
-
* and displayed visually below the textarea. The textarea is marked as invalid
|
|
150
|
-
* with \`aria-invalid="true"\`.
|
|
151
|
-
*/
|
|
152
|
-
export const WithError: Story = {
|
|
153
|
-
render: () => {
|
|
154
|
-
const [value, setValue] = useState('')
|
|
155
|
-
return (
|
|
156
|
-
<Textarea
|
|
157
|
-
id="textarea-error"
|
|
158
|
-
label="Comments"
|
|
159
|
-
value={value}
|
|
160
|
-
onChange={(e) => setValue(e.target.value)}
|
|
161
|
-
error="Please enter at least 10 characters"
|
|
162
|
-
maxLength={500}
|
|
163
|
-
showCount
|
|
164
|
-
/>
|
|
165
|
-
)
|
|
166
|
-
},
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Textarea with helper text. Helper text provides additional context or guidance
|
|
171
|
-
* and is associated with the textarea via \`aria-describedby\`.
|
|
172
|
-
*/
|
|
173
|
-
export const WithHelperText: Story = {
|
|
174
|
-
render: () => {
|
|
175
|
-
const [value, setValue] = useState('')
|
|
176
|
-
return (
|
|
177
|
-
<Textarea
|
|
178
|
-
id="textarea-helper"
|
|
179
|
-
label="Additional notes"
|
|
180
|
-
value={value}
|
|
181
|
-
onChange={(e) => setValue(e.target.value)}
|
|
182
|
-
helperText="Include any additional information that might be helpful"
|
|
183
|
-
maxLength={1000}
|
|
184
|
-
showCount
|
|
185
|
-
/>
|
|
186
|
-
)
|
|
187
|
-
},
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Required textarea field. Required textareas are marked with \`aria-required="true"\`
|
|
192
|
-
* and display a visual indicator (asterisk) next to the label.
|
|
193
|
-
*/
|
|
194
|
-
export const Required: Story = {
|
|
195
|
-
render: () => {
|
|
196
|
-
const [value, setValue] = useState('')
|
|
197
|
-
return (
|
|
198
|
-
<Textarea
|
|
199
|
-
id="textarea-required"
|
|
200
|
-
label="Bio"
|
|
201
|
-
value={value}
|
|
202
|
-
onChange={(e) => setValue(e.target.value)}
|
|
203
|
-
required
|
|
204
|
-
maxLength={500}
|
|
205
|
-
showCount
|
|
206
|
-
placeholder="Tell us about yourself"
|
|
207
|
-
/>
|
|
208
|
-
)
|
|
209
|
-
},
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Disabled textarea that cannot be edited. Disabled textareas are announced as
|
|
214
|
-
* "disabled" by screen readers and appear visually dimmed.
|
|
215
|
-
*/
|
|
216
|
-
export const Disabled: Story = {
|
|
217
|
-
args: {
|
|
218
|
-
id: 'textarea-disabled',
|
|
219
|
-
label: 'Disabled textarea',
|
|
220
|
-
disabled: true,
|
|
221
|
-
defaultValue: 'This textarea cannot be edited',
|
|
222
|
-
},
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Large textarea for longer content. Use appropriate rows and cols attributes
|
|
227
|
-
* to provide a good initial size for the content type.
|
|
228
|
-
*/
|
|
229
|
-
export const Large: Story = {
|
|
230
|
-
render: () => {
|
|
231
|
-
const [value, setValue] = useState('')
|
|
232
|
-
return (
|
|
233
|
-
<Textarea
|
|
234
|
-
id="textarea-large"
|
|
235
|
-
label="Full article"
|
|
236
|
-
value={value}
|
|
237
|
-
onChange={(e) => setValue(e.target.value)}
|
|
238
|
-
rows={10}
|
|
239
|
-
maxLength={5000}
|
|
240
|
-
showCount
|
|
241
|
-
placeholder="Write your article here..."
|
|
242
|
-
/>
|
|
243
|
-
)
|
|
244
|
-
},
|
|
245
|
-
parameters: {
|
|
246
|
-
docs: {
|
|
247
|
-
description: {
|
|
248
|
-
story: 'Large textarea with more rows for longer content. Adjust rows and cols to fit your use case.',
|
|
249
|
-
},
|
|
250
|
-
},
|
|
251
|
-
},
|
|
252
|
-
}
|
|
253
|
-
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import React from 'react'
|
|
4
|
-
import { combineAriaDescribedBy } from '../../utils/aria'
|
|
5
|
-
import './Textarea.css'
|
|
6
|
-
|
|
7
|
-
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
8
|
-
/**
|
|
9
|
-
* Error message to display
|
|
10
|
-
*/
|
|
11
|
-
error?: string
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Helper text to display below the textarea
|
|
15
|
-
*/
|
|
16
|
-
helperText?: string
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Label for the textarea
|
|
20
|
-
*/
|
|
21
|
-
label?: string
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Maximum character count (shows counter)
|
|
25
|
-
*/
|
|
26
|
-
maxLength?: number
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Whether to show character count
|
|
30
|
-
*/
|
|
31
|
-
showCount?: boolean
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Accessible Textarea component
|
|
36
|
-
*
|
|
37
|
-
* WCAG Compliance:
|
|
38
|
-
* - 1.3.1 Info and Relationships: Proper label-textarea association
|
|
39
|
-
* - 4.1.2 Name, Role, Value: Proper ARIA attributes
|
|
40
|
-
* - 4.1.3 Status Messages: Error messages announced
|
|
41
|
-
*
|
|
42
|
-
* @example
|
|
43
|
-
* ```tsx
|
|
44
|
-
* <Textarea
|
|
45
|
-
* id="message"
|
|
46
|
-
* label="Message"
|
|
47
|
-
* maxLength={500}
|
|
48
|
-
* showCount
|
|
49
|
-
* />
|
|
50
|
-
* ```
|
|
51
|
-
*/
|
|
52
|
-
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
53
|
-
(
|
|
54
|
-
{
|
|
55
|
-
id,
|
|
56
|
-
error,
|
|
57
|
-
helperText,
|
|
58
|
-
label,
|
|
59
|
-
maxLength,
|
|
60
|
-
showCount = false,
|
|
61
|
-
className = '',
|
|
62
|
-
value,
|
|
63
|
-
'aria-describedby': ariaDescribedBy,
|
|
64
|
-
...props
|
|
65
|
-
},
|
|
66
|
-
ref
|
|
67
|
-
) => {
|
|
68
|
-
const textareaId = React.useId()
|
|
69
|
-
const finalId = id || `textarea-${textareaId}`
|
|
70
|
-
const errorId = error ? `${finalId}-error` : undefined
|
|
71
|
-
const helperId = helperText ? `${finalId}-helper` : undefined
|
|
72
|
-
const countId = showCount ? `${finalId}-count` : undefined
|
|
73
|
-
|
|
74
|
-
const describedBy = combineAriaDescribedBy(
|
|
75
|
-
ariaDescribedBy,
|
|
76
|
-
errorId,
|
|
77
|
-
helperId,
|
|
78
|
-
countId
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
const currentLength = typeof value === 'string' ? value.length : 0
|
|
82
|
-
const remainingChars = maxLength ? maxLength - currentLength : undefined
|
|
83
|
-
|
|
84
|
-
const classes = [
|
|
85
|
-
'form-textarea',
|
|
86
|
-
error && 'form-textarea--error',
|
|
87
|
-
props.disabled && 'form-textarea--disabled',
|
|
88
|
-
className,
|
|
89
|
-
]
|
|
90
|
-
.filter(Boolean)
|
|
91
|
-
.join(' ')
|
|
92
|
-
|
|
93
|
-
return (
|
|
94
|
-
<div className="form-textarea-wrapper">
|
|
95
|
-
{label && (
|
|
96
|
-
<label htmlFor={finalId} className="form-label">
|
|
97
|
-
{label}
|
|
98
|
-
{props.required && (
|
|
99
|
-
<span className="form-label__required" aria-hidden="true">
|
|
100
|
-
{' '}*
|
|
101
|
-
</span>
|
|
102
|
-
)}
|
|
103
|
-
</label>
|
|
104
|
-
)}
|
|
105
|
-
<textarea
|
|
106
|
-
ref={ref}
|
|
107
|
-
id={finalId}
|
|
108
|
-
className={classes}
|
|
109
|
-
maxLength={maxLength}
|
|
110
|
-
value={value}
|
|
111
|
-
aria-invalid={error ? true : undefined}
|
|
112
|
-
aria-describedby={describedBy}
|
|
113
|
-
required={props.required ? true : undefined}
|
|
114
|
-
{...props}
|
|
115
|
-
/>
|
|
116
|
-
{(showCount || helperText) && (
|
|
117
|
-
<div className="form-textarea-footer">
|
|
118
|
-
{helperText && !error && (
|
|
119
|
-
<span id={helperId} className="form-helper-text">
|
|
120
|
-
{helperText}
|
|
121
|
-
</span>
|
|
122
|
-
)}
|
|
123
|
-
{error && (
|
|
124
|
-
<span id={errorId} className="form-error-text" role="alert">
|
|
125
|
-
{error}
|
|
126
|
-
</span>
|
|
127
|
-
)}
|
|
128
|
-
{showCount && maxLength && (
|
|
129
|
-
<span
|
|
130
|
-
id={countId}
|
|
131
|
-
className="form-character-count"
|
|
132
|
-
aria-live="polite"
|
|
133
|
-
>
|
|
134
|
-
{currentLength} / {maxLength}
|
|
135
|
-
</span>
|
|
136
|
-
)}
|
|
137
|
-
</div>
|
|
138
|
-
)}
|
|
139
|
-
</div>
|
|
140
|
-
)
|
|
141
|
-
}
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
Textarea.displayName = 'Textarea'
|
|
145
|
-
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
-
import { Link } from './Link'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* # Link Component
|
|
6
|
-
*
|
|
7
|
-
* An accessible link component with automatic external link detection, skip link support,
|
|
8
|
-
* and proper focus management. Uses semantic `<a>` element with enhanced accessibility features.
|
|
9
|
-
*
|
|
10
|
-
* ## Usage
|
|
11
|
-
*
|
|
12
|
-
* ```tsx
|
|
13
|
-
* import { Link } from '@a11ypros/a11y-ui-components'
|
|
14
|
-
*
|
|
15
|
-
* function MyComponent() {
|
|
16
|
-
* return (
|
|
17
|
-
* <Link href="/about">
|
|
18
|
-
* Learn more about us
|
|
19
|
-
* </Link>
|
|
20
|
-
* )
|
|
21
|
-
* }
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* ## Features
|
|
25
|
-
*
|
|
26
|
-
* - **External link detection**: Automatically adds \`rel="noopener noreferrer"\` for security
|
|
27
|
-
* - **Skip links**: Special styling and behavior for keyboard navigation skip links
|
|
28
|
-
* - **Focus management**: Clear focus indicators that meet WCAG contrast requirements
|
|
29
|
-
* - **ARIA support**: Proper aria-label support for icon-only or context-dependent links
|
|
30
|
-
*
|
|
31
|
-
* ## Accessibility
|
|
32
|
-
*
|
|
33
|
-
* ### WCAG 2.1/2.2 Compliance
|
|
34
|
-
*
|
|
35
|
-
* - **2.4.4 Link Purpose (In Context)**: Clear link text or aria-label that describes the link purpose
|
|
36
|
-
* - **2.4.7 Focus Visible**: Clear 2px outline on focus (meets 3:1 contrast ratio)
|
|
37
|
-
* - **4.1.2 Name, Role, Value**: Proper semantic HTML with accessible name
|
|
38
|
-
*
|
|
39
|
-
* ### Keyboard Interactions
|
|
40
|
-
*
|
|
41
|
-
* | Key | Action |
|
|
42
|
-
* |-----|--------|
|
|
43
|
-
* | **Enter** | Activates the link |
|
|
44
|
-
* | **Tab** | Moves focus to the link |
|
|
45
|
-
* | **Shift+Tab** | Moves focus away from the link |
|
|
46
|
-
*
|
|
47
|
-
* ### Screen Reader Support
|
|
48
|
-
*
|
|
49
|
-
* - Link role and name are announced automatically
|
|
50
|
-
* - External links: "Link, external" announced (when \`external\` prop is true)
|
|
51
|
-
* - Skip links: Properly announced for keyboard navigation
|
|
52
|
-
* - Custom labels via \`aria-label\` prop for context-dependent links
|
|
53
|
-
*
|
|
54
|
-
* ### Focus Management
|
|
55
|
-
*
|
|
56
|
-
* - Focus indicators use 2px solid outline with 2px offset
|
|
57
|
-
* - Focus styles respect \`prefers-reduced-motion\` media query
|
|
58
|
-
* - High contrast mode supported via \`prefers-contrast\` media query
|
|
59
|
-
* - Focus visible only on keyboard navigation (not mouse clicks) using \`:focus-visible\`
|
|
60
|
-
*
|
|
61
|
-
* ## Best Practices
|
|
62
|
-
*
|
|
63
|
-
* 1. **Use descriptive link text**: Avoid "click here" or "read more" - describe what the link does
|
|
64
|
-
* 2. **Use external prop for external links**: Automatically adds security attributes
|
|
65
|
-
* 3. **Provide aria-label for icon-only links**: Screen readers need context
|
|
66
|
-
* 4. **Use skip links for main content**: Helps keyboard users navigate large pages
|
|
67
|
-
* 5. **Don't use links for buttons**: Use Button component for actions, Link for navigation
|
|
68
|
-
*
|
|
69
|
-
* ## Common Pitfalls
|
|
70
|
-
*
|
|
71
|
-
* - Using generic link text like "click here" (screen readers lose context)
|
|
72
|
-
* - Missing \`external\` prop on external links (security risk)
|
|
73
|
-
* - Using links for actions instead of navigation (use Button component)
|
|
74
|
-
* - Missing \`aria-label\` on icon-only links (screen readers can't understand purpose)
|
|
75
|
-
* - Removing focus styles (breaks keyboard navigation visibility)
|
|
76
|
-
*
|
|
77
|
-
* @component
|
|
78
|
-
* @example
|
|
79
|
-
* ```tsx
|
|
80
|
-
* <Link href="/about" external>
|
|
81
|
-
* Learn more
|
|
82
|
-
* </Link>
|
|
83
|
-
* ```
|
|
84
|
-
*/
|
|
85
|
-
const meta: Meta<typeof Link> = {
|
|
86
|
-
title: 'Components/Link',
|
|
87
|
-
component: Link,
|
|
88
|
-
tags: ['autodocs'],
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export default meta
|
|
92
|
-
type Story = StoryObj<typeof Link>
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Standard internal link for navigation within the application.
|
|
96
|
-
*/
|
|
97
|
-
export const Default: Story = {
|
|
98
|
-
args: {
|
|
99
|
-
href: '#',
|
|
100
|
-
children: 'Link text',
|
|
101
|
-
},
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* External link that automatically adds security attributes (\`rel="noopener noreferrer"\`)
|
|
106
|
-
* to prevent security vulnerabilities when opening links in new tabs.
|
|
107
|
-
*/
|
|
108
|
-
export const External: Story = {
|
|
109
|
-
args: {
|
|
110
|
-
href: 'https://example.com',
|
|
111
|
-
external: true,
|
|
112
|
-
children: 'External link',
|
|
113
|
-
},
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Skip link for keyboard navigation. These links allow keyboard users to skip
|
|
118
|
-
* repetitive navigation and jump directly to main content. Typically placed
|
|
119
|
-
* at the top of the page and styled to be visible on focus.
|
|
120
|
-
*/
|
|
121
|
-
export const SkipLink: Story = {
|
|
122
|
-
args: {
|
|
123
|
-
href: '#main-content',
|
|
124
|
-
skip: true,
|
|
125
|
-
children: 'Skip to main content',
|
|
126
|
-
},
|
|
127
|
-
}
|
|
128
|
-
|