@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
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useRef, useCallback } from 'react';
|
|
4
|
+
import { useAriaLive } from '../../hooks/useAriaLive';
|
|
5
|
+
import { isNavigationKey } from '../../utils/keyboard';
|
|
6
|
+
import { Checkbox } from '../Form/Checkbox';
|
|
7
|
+
import './DataTable.css';
|
|
8
|
+
/**
|
|
9
|
+
* Accessible DataTable component
|
|
10
|
+
*
|
|
11
|
+
* WCAG Compliance:
|
|
12
|
+
* - 1.3.1 Info and Relationships: Semantic table structure
|
|
13
|
+
* - 2.1.1 Keyboard: Arrow keys, Home/End navigation
|
|
14
|
+
* - 4.1.2 Name, Role, Value: Proper ARIA attributes
|
|
15
|
+
* - 4.1.3 Status Messages: Sort announcements
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <DataTable
|
|
20
|
+
* data={users}
|
|
21
|
+
* columns={columns}
|
|
22
|
+
* getRowId={(user) => user.id}
|
|
23
|
+
* caption="User list"
|
|
24
|
+
* />
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function DataTable({ data, columns, getRowId, selectable = false, selectedRows = [], onSelectionChange, sortConfig, onSortChange, caption, }) {
|
|
28
|
+
const [focusedRow, setFocusedRow] = useState(null);
|
|
29
|
+
const tableRef = useRef(null);
|
|
30
|
+
const rowRefs = useRef(new Map());
|
|
31
|
+
// Announce sort changes
|
|
32
|
+
const sortAnnouncement = sortConfig
|
|
33
|
+
? `Sorted by ${columns.find((c) => c.key === sortConfig.column)?.header || sortConfig.column}, ${sortConfig.direction === 'asc' ? 'ascending' : 'descending'}`
|
|
34
|
+
: undefined;
|
|
35
|
+
useAriaLive(sortAnnouncement, 'polite');
|
|
36
|
+
const handleSelectAll = useCallback(() => {
|
|
37
|
+
if (!onSelectionChange)
|
|
38
|
+
return;
|
|
39
|
+
const allSelected = selectedRows.length === data.length;
|
|
40
|
+
if (allSelected) {
|
|
41
|
+
onSelectionChange([]);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
onSelectionChange(data.map(getRowId));
|
|
45
|
+
}
|
|
46
|
+
}, [data, selectedRows, onSelectionChange, getRowId]);
|
|
47
|
+
const handleSelectRow = useCallback((rowId) => {
|
|
48
|
+
if (!onSelectionChange)
|
|
49
|
+
return;
|
|
50
|
+
const isSelected = selectedRows.includes(rowId);
|
|
51
|
+
if (isSelected) {
|
|
52
|
+
onSelectionChange(selectedRows.filter((id) => id !== rowId));
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
onSelectionChange([...selectedRows, rowId]);
|
|
56
|
+
}
|
|
57
|
+
}, [selectedRows, onSelectionChange]);
|
|
58
|
+
const handleSort = useCallback((columnKey) => {
|
|
59
|
+
if (!onSortChange || !columns.find((c) => c.key === columnKey)?.sortable) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const newDirection = sortConfig?.column === columnKey && sortConfig.direction === 'asc'
|
|
63
|
+
? 'desc'
|
|
64
|
+
: 'asc';
|
|
65
|
+
onSortChange(columnKey, newDirection);
|
|
66
|
+
}, [onSortChange, sortConfig, columns]);
|
|
67
|
+
const handleKeyDown = useCallback((event, rowId, index) => {
|
|
68
|
+
const row = rowRefs.current.get(rowId);
|
|
69
|
+
if (!row)
|
|
70
|
+
return;
|
|
71
|
+
if (isNavigationKey(event.key)) {
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
let newIndex = index;
|
|
74
|
+
switch (event.key) {
|
|
75
|
+
case 'ArrowDown':
|
|
76
|
+
newIndex = Math.min(index + 1, data.length - 1);
|
|
77
|
+
break;
|
|
78
|
+
case 'ArrowUp':
|
|
79
|
+
newIndex = Math.max(index - 1, 0);
|
|
80
|
+
break;
|
|
81
|
+
case 'Home':
|
|
82
|
+
newIndex = 0;
|
|
83
|
+
break;
|
|
84
|
+
case 'End':
|
|
85
|
+
newIndex = data.length - 1;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
const newRowId = getRowId(data[newIndex]);
|
|
89
|
+
setFocusedRow(newRowId);
|
|
90
|
+
rowRefs.current.get(newRowId)?.focus();
|
|
91
|
+
}
|
|
92
|
+
else if (event.key === ' ' && selectable) {
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
handleSelectRow(rowId);
|
|
95
|
+
}
|
|
96
|
+
}, [data, selectable, handleSelectRow, getRowId]);
|
|
97
|
+
const allSelected = data.length > 0 && selectedRows.length === data.length;
|
|
98
|
+
const someSelected = selectedRows.length > 0 && selectedRows.length < data.length;
|
|
99
|
+
return (_jsx("div", { className: "data-table-wrapper", children: _jsxs("table", { ref: tableRef, className: "data-table", "aria-label": caption, children: [caption && _jsx("caption", { className: "data-table-caption", children: caption }), _jsx("thead", { children: _jsxs("tr", { children: [selectable && (_jsx("th", { scope: "col", className: "data-table-header data-table-header--checkbox", children: _jsx(Checkbox, { checked: allSelected, "aria-label": "Select all rows", onChange: handleSelectAll, "aria-checked": allSelected ? 'true' : someSelected ? 'mixed' : 'false' }) })), columns.map((column) => (_jsx("th", { scope: "col", className: `data-table-header ${column.sortable ? 'data-table-header--sortable' : ''}`, style: column.width ? { width: column.width } : undefined, "aria-sort": sortConfig?.column === column.key
|
|
100
|
+
? sortConfig.direction === 'asc'
|
|
101
|
+
? 'ascending'
|
|
102
|
+
: 'descending'
|
|
103
|
+
: 'none', children: column.sortable ? (_jsxs("button", { type: "button", className: "data-table-sort-button", "aria-label": sortConfig?.column === column.key
|
|
104
|
+
? `${column.header}, sorted ${sortConfig.direction === 'asc' ? 'ascending' : 'descending'}, activate to sort ${sortConfig.direction === 'asc' ? 'descending' : 'ascending'}`
|
|
105
|
+
: `Sort by ${column.header}`, onClick: () => handleSort(column.key), children: [column.header, sortConfig?.column === column.key && (_jsx("span", { className: "data-table-sort-indicator", "aria-hidden": "true", children: sortConfig.direction === 'asc' ? ' ↑' : ' ↓' }))] })) : (column.header) }, column.key)))] }) }), _jsx("tbody", { children: data.map((row, index) => {
|
|
106
|
+
const rowId = getRowId(row);
|
|
107
|
+
const isSelected = selectedRows.includes(rowId);
|
|
108
|
+
const isFocused = focusedRow === rowId;
|
|
109
|
+
return (_jsxs("tr", { ref: (el) => {
|
|
110
|
+
if (el) {
|
|
111
|
+
rowRefs.current.set(rowId, el);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
rowRefs.current.delete(rowId);
|
|
115
|
+
}
|
|
116
|
+
}, className: `data-table-row ${isSelected ? 'data-table-row--selected' : ''} ${isFocused ? 'data-table-row--focused' : ''}`, tabIndex: 0, "aria-selected": selectable ? isSelected : undefined, onKeyDown: (e) => handleKeyDown(e, rowId, index), onClick: () => {
|
|
117
|
+
if (selectable) {
|
|
118
|
+
handleSelectRow(rowId);
|
|
119
|
+
}
|
|
120
|
+
}, children: [selectable && (_jsx("td", { className: "data-table-cell data-table-cell--checkbox", children: _jsx(Checkbox, { checked: isSelected, "aria-label": `Select row ${index + 1}`, onChange: () => handleSelectRow(rowId), onClick: (e) => e.stopPropagation() }) })), columns.map((column) => (_jsx("td", { className: "data-table-cell", children: column.render ? column.render(row, index) : row[column.key] }, column.key)))] }, rowId));
|
|
121
|
+
}) })] }) }));
|
|
122
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/DataTable/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DataTable } from './DataTable';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import './Checkbox.css';
|
|
3
|
+
export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
4
|
+
/**
|
|
5
|
+
* Label for the checkbox
|
|
6
|
+
*/
|
|
7
|
+
label?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Error message to display
|
|
10
|
+
*/
|
|
11
|
+
error?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Helper text to display
|
|
14
|
+
*/
|
|
15
|
+
helperText?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Accessible Checkbox component
|
|
19
|
+
*
|
|
20
|
+
* WCAG Compliance:
|
|
21
|
+
* - 1.3.1 Info and Relationships: Proper label-input association
|
|
22
|
+
* - 2.5.3 Label in Name: Label text matches accessible name
|
|
23
|
+
* - 4.1.2 Name, Role, Value: Proper ARIA attributes
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* <Checkbox
|
|
28
|
+
* id="agree"
|
|
29
|
+
* label="I agree to the terms"
|
|
30
|
+
* checked={checked}
|
|
31
|
+
* onChange={handleChange}
|
|
32
|
+
* />
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare const Checkbox: React.ForwardRefExoticComponent<CheckboxProps & React.RefAttributes<HTMLInputElement>>;
|
|
36
|
+
//# sourceMappingURL=Checkbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Checkbox.d.ts","sourceRoot":"","sources":["../../../src/components/Form/Checkbox.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,gBAAgB,CAAA;AAEvB,MAAM,WAAW,aAAc,SAAQ,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC9F;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,QAAQ,wFAqEpB,CAAA"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { combineAriaDescribedBy } from '../../utils/aria';
|
|
5
|
+
import './Checkbox.css';
|
|
6
|
+
/**
|
|
7
|
+
* Accessible Checkbox component
|
|
8
|
+
*
|
|
9
|
+
* WCAG Compliance:
|
|
10
|
+
* - 1.3.1 Info and Relationships: Proper label-input association
|
|
11
|
+
* - 2.5.3 Label in Name: Label text matches accessible name
|
|
12
|
+
* - 4.1.2 Name, Role, Value: Proper ARIA attributes
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <Checkbox
|
|
17
|
+
* id="agree"
|
|
18
|
+
* label="I agree to the terms"
|
|
19
|
+
* checked={checked}
|
|
20
|
+
* onChange={handleChange}
|
|
21
|
+
* />
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export const Checkbox = React.forwardRef(({ id, label, error, helperText, className = '', 'aria-describedby': ariaDescribedBy, ...props }, ref) => {
|
|
25
|
+
const checkboxId = React.useId();
|
|
26
|
+
const finalId = id || `checkbox-${checkboxId}`;
|
|
27
|
+
const errorId = error ? `${finalId}-error` : undefined;
|
|
28
|
+
const helperId = helperText ? `${finalId}-helper` : undefined;
|
|
29
|
+
const describedBy = combineAriaDescribedBy(ariaDescribedBy, errorId, helperId);
|
|
30
|
+
const classes = [
|
|
31
|
+
'form-checkbox',
|
|
32
|
+
error && 'form-checkbox--error',
|
|
33
|
+
className,
|
|
34
|
+
]
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.join(' ');
|
|
37
|
+
return (_jsxs("div", { className: "form-checkbox-wrapper", children: [_jsxs("div", { className: "form-checkbox-input-wrapper", children: [_jsx("input", { ref: ref, id: finalId, type: "checkbox", className: classes, "aria-invalid": error ? true : undefined, "aria-describedby": describedBy, required: props.required ? true : undefined, ...props }), label && (_jsxs("label", { htmlFor: finalId, className: "form-checkbox-label", children: [label, props.required && (_jsxs("span", { className: "form-label__required", "aria-hidden": "true", children: [' ', "*"] }))] }))] }), helperText && !error && (_jsx("span", { id: helperId, className: "form-helper-text", children: helperText })), error && (_jsx("span", { id: errorId, className: "form-error-text", role: "alert", children: error }))] }));
|
|
38
|
+
});
|
|
39
|
+
Checkbox.displayName = 'Checkbox';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import './Fieldset.css';
|
|
3
|
+
export interface FieldsetProps extends React.FieldsetHTMLAttributes<HTMLFieldSetElement> {
|
|
4
|
+
/**
|
|
5
|
+
* Legend text for the fieldset
|
|
6
|
+
*/
|
|
7
|
+
legend?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the legend is visually hidden (but still accessible)
|
|
10
|
+
*/
|
|
11
|
+
legendHidden?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Whether to show a required field explanation. Use this if the fieldset contains required fields.
|
|
14
|
+
*/
|
|
15
|
+
required?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Accessible Fieldset component
|
|
19
|
+
*
|
|
20
|
+
* WCAG Compliance:
|
|
21
|
+
* - 1.3.1 Info and Relationships: Proper fieldset/legend structure
|
|
22
|
+
* - 4.1.2 Name, Role, Value: Proper semantic HTML
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <Fieldset legend="Shipping Address">
|
|
27
|
+
* <Input label="Street" />
|
|
28
|
+
* <Input label="City" />
|
|
29
|
+
* </Fieldset>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare const Fieldset: React.ForwardRefExoticComponent<FieldsetProps & React.RefAttributes<HTMLFieldSetElement>>;
|
|
33
|
+
//# sourceMappingURL=Fieldset.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Fieldset.d.ts","sourceRoot":"","sources":["../../../src/components/Form/Fieldset.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,gBAAgB,CAAA;AAEvB,MAAM,WAAW,aAAc,SAAQ,KAAK,CAAC,sBAAsB,CAAC,mBAAmB,CAAC;IACtF;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IAEtB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,QAAQ,2FAgCpB,CAAA"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import './Fieldset.css';
|
|
4
|
+
/**
|
|
5
|
+
* Accessible Fieldset component
|
|
6
|
+
*
|
|
7
|
+
* WCAG Compliance:
|
|
8
|
+
* - 1.3.1 Info and Relationships: Proper fieldset/legend structure
|
|
9
|
+
* - 4.1.2 Name, Role, Value: Proper semantic HTML
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <Fieldset legend="Shipping Address">
|
|
14
|
+
* <Input label="Street" />
|
|
15
|
+
* <Input label="City" />
|
|
16
|
+
* </Fieldset>
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export const Fieldset = React.forwardRef(({ legend, legendHidden = false, required = false, className = '', children, ...props }, ref) => {
|
|
20
|
+
const classes = [
|
|
21
|
+
'form-fieldset',
|
|
22
|
+
className,
|
|
23
|
+
]
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.join(' ');
|
|
26
|
+
const legendClasses = [
|
|
27
|
+
'form-legend',
|
|
28
|
+
legendHidden && 'form-legend--hidden',
|
|
29
|
+
]
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join(' ');
|
|
32
|
+
return (_jsxs("fieldset", { ref: ref, className: classes, ...props, children: [legend && (_jsx("legend", { className: legendClasses, children: legend })), required && (_jsxs("p", { className: "fieldset__required", children: [' ', _jsx("span", { className: 'fieldset__required-indicator', children: "*" }), " indicates a required field."] })), children] }));
|
|
33
|
+
});
|
|
34
|
+
Fieldset.displayName = 'Fieldset';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import './Input.css';
|
|
3
|
+
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
4
|
+
/**
|
|
5
|
+
* Error message to display
|
|
6
|
+
*/
|
|
7
|
+
error?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Helper text to display below the input
|
|
10
|
+
*/
|
|
11
|
+
helperText?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Label for the input (creates associated label)
|
|
14
|
+
*/
|
|
15
|
+
label?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Accessible Input component
|
|
19
|
+
*
|
|
20
|
+
* WCAG Compliance:
|
|
21
|
+
* - 1.3.1 Info and Relationships: Proper label-input association
|
|
22
|
+
* - 2.5.3 Label in Name: Label text matches accessible name
|
|
23
|
+
* - 4.1.2 Name, Role, Value: Proper ARIA attributes
|
|
24
|
+
* - 4.1.3 Status Messages: Error messages announced
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <Input
|
|
29
|
+
* id="email"
|
|
30
|
+
* type="email"
|
|
31
|
+
* label="Email address"
|
|
32
|
+
* error="Please enter a valid email"
|
|
33
|
+
* />
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare const Input: React.ForwardRefExoticComponent<InputProps & React.RefAttributes<HTMLInputElement>>;
|
|
37
|
+
//# sourceMappingURL=Input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Input.d.ts","sourceRoot":"","sources":["../../../src/components/Form/Input.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,aAAa,CAAA;AAEpB,MAAM,WAAW,UAAW,SAAQ,KAAK,CAAC,mBAAmB,CAAC,gBAAgB,CAAC;IAC7E;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,KAAK,qFAmEjB,CAAA"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { combineAriaDescribedBy } from '../../utils/aria';
|
|
5
|
+
import './Input.css';
|
|
6
|
+
/**
|
|
7
|
+
* Accessible Input component
|
|
8
|
+
*
|
|
9
|
+
* WCAG Compliance:
|
|
10
|
+
* - 1.3.1 Info and Relationships: Proper label-input association
|
|
11
|
+
* - 2.5.3 Label in Name: Label text matches accessible name
|
|
12
|
+
* - 4.1.2 Name, Role, Value: Proper ARIA attributes
|
|
13
|
+
* - 4.1.3 Status Messages: Error messages announced
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* <Input
|
|
18
|
+
* id="email"
|
|
19
|
+
* type="email"
|
|
20
|
+
* label="Email address"
|
|
21
|
+
* error="Please enter a valid email"
|
|
22
|
+
* />
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export const Input = React.forwardRef(({ id, error, helperText, label, className = '', 'aria-describedby': ariaDescribedBy, ...props }, ref) => {
|
|
26
|
+
const inputId = React.useId();
|
|
27
|
+
const finalId = id || `input-${inputId}`;
|
|
28
|
+
const errorId = error ? `${finalId}-error` : undefined;
|
|
29
|
+
const helperId = helperText ? `${finalId}-helper` : undefined;
|
|
30
|
+
const describedBy = combineAriaDescribedBy(ariaDescribedBy, errorId, helperId);
|
|
31
|
+
const classes = [
|
|
32
|
+
'form-input',
|
|
33
|
+
error && 'form-input--error',
|
|
34
|
+
props.disabled && 'form-input--disabled',
|
|
35
|
+
className,
|
|
36
|
+
]
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.join(' ');
|
|
39
|
+
return (_jsxs("div", { className: "form-input-wrapper", children: [label && (_jsxs("label", { htmlFor: finalId, className: "form-label", children: [label, props.required && (_jsxs("span", { className: "form-label__required", "aria-hidden": "true", children: [' ', "*"] }))] })), _jsx("input", { ref: ref, id: finalId, className: classes, "aria-invalid": error ? true : undefined, "aria-describedby": describedBy, required: props.required ? true : undefined, ...props }), helperText && !error && (_jsx("span", { id: helperId, className: "form-helper-text", children: helperText })), error && (_jsx("span", { id: errorId, className: "form-error-text", role: "alert", children: error }))] }));
|
|
40
|
+
});
|
|
41
|
+
Input.displayName = 'Input';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import './Label.css';
|
|
3
|
+
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
|
4
|
+
/**
|
|
5
|
+
* Whether this label is required (shows asterisk)
|
|
6
|
+
*/
|
|
7
|
+
required?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* ID of the input this label is associated with
|
|
10
|
+
*/
|
|
11
|
+
htmlFor?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Accessible Label component
|
|
15
|
+
*
|
|
16
|
+
* WCAG Compliance:
|
|
17
|
+
* - 1.3.1 Info and Relationships: Proper label-input association
|
|
18
|
+
* - 2.5.3 Label in Name: Label text matches accessible name
|
|
19
|
+
* - 4.1.2 Name, Role, Value: Proper semantic HTML
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <Label htmlFor="email" required>
|
|
24
|
+
* Email address
|
|
25
|
+
* </Label>
|
|
26
|
+
* <Input id="email" type="email" />
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare const Label: React.ForwardRefExoticComponent<LabelProps & React.RefAttributes<HTMLLabelElement>>;
|
|
30
|
+
//# sourceMappingURL=Label.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Label.d.ts","sourceRoot":"","sources":["../../../src/components/Form/Label.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,aAAa,CAAA;AAEpB,MAAM,WAAW,UAAW,SAAQ,KAAK,CAAC,mBAAmB,CAAC,gBAAgB,CAAC;IAC7E;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAElB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,KAAK,qFAqBjB,CAAA"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import './Label.css';
|
|
4
|
+
/**
|
|
5
|
+
* Accessible Label component
|
|
6
|
+
*
|
|
7
|
+
* WCAG Compliance:
|
|
8
|
+
* - 1.3.1 Info and Relationships: Proper label-input association
|
|
9
|
+
* - 2.5.3 Label in Name: Label text matches accessible name
|
|
10
|
+
* - 4.1.2 Name, Role, Value: Proper semantic HTML
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* <Label htmlFor="email" required>
|
|
15
|
+
* Email address
|
|
16
|
+
* </Label>
|
|
17
|
+
* <Input id="email" type="email" />
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export const Label = React.forwardRef(({ required = false, className = '', children, ...props }, ref) => {
|
|
21
|
+
const classes = [
|
|
22
|
+
'form-label',
|
|
23
|
+
required && 'form-label--required',
|
|
24
|
+
className,
|
|
25
|
+
]
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.join(' ');
|
|
28
|
+
return (_jsxs("label", { ref: ref, className: classes, ...props, children: [children, required && (_jsxs("span", { className: "form-label__required", "aria-hidden": "true", children: [' ', "*"] }))] }));
|
|
29
|
+
});
|
|
30
|
+
Label.displayName = 'Label';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import './Radio.css';
|
|
3
|
+
export interface RadioOption {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface RadioProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
9
|
+
/**
|
|
10
|
+
* Options for the radio group
|
|
11
|
+
*/
|
|
12
|
+
options: RadioOption[];
|
|
13
|
+
/**
|
|
14
|
+
* Name attribute for the radio group (required)
|
|
15
|
+
*/
|
|
16
|
+
name: string;
|
|
17
|
+
/**
|
|
18
|
+
* Label for the radio group
|
|
19
|
+
*/
|
|
20
|
+
label?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Error message to display
|
|
23
|
+
*/
|
|
24
|
+
error?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Helper text to display
|
|
27
|
+
*/
|
|
28
|
+
helperText?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Accessible Radio component (radio group)
|
|
32
|
+
*
|
|
33
|
+
* WCAG Compliance:
|
|
34
|
+
* - 1.3.1 Info and Relationships: Proper fieldset/legend structure
|
|
35
|
+
* - 2.1.1 Keyboard: Full keyboard navigation
|
|
36
|
+
* - 4.1.2 Name, Role, Value: Proper ARIA attributes
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* <Radio
|
|
41
|
+
* name="size"
|
|
42
|
+
* label="Size"
|
|
43
|
+
* options={[
|
|
44
|
+
* { value: 's', label: 'Small' },
|
|
45
|
+
* { value: 'm', label: 'Medium' },
|
|
46
|
+
* ]}
|
|
47
|
+
* value={selected}
|
|
48
|
+
* onChange={handleChange}
|
|
49
|
+
* />
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare const Radio: React.ForwardRefExoticComponent<RadioProps & React.RefAttributes<HTMLInputElement>>;
|
|
53
|
+
//# sourceMappingURL=Radio.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Radio.d.ts","sourceRoot":"","sources":["../../../src/components/Form/Radio.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,aAAa,CAAA;AAEpB,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,UAAW,SAAQ,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC3F;;OAEG;IACH,OAAO,EAAE,WAAW,EAAE,CAAA;IAEtB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAA;IAEZ;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,KAAK,qFAkFjB,CAAA"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { combineAriaDescribedBy } from '../../utils/aria';
|
|
5
|
+
import './Radio.css';
|
|
6
|
+
/**
|
|
7
|
+
* Accessible Radio component (radio group)
|
|
8
|
+
*
|
|
9
|
+
* WCAG Compliance:
|
|
10
|
+
* - 1.3.1 Info and Relationships: Proper fieldset/legend structure
|
|
11
|
+
* - 2.1.1 Keyboard: Full keyboard navigation
|
|
12
|
+
* - 4.1.2 Name, Role, Value: Proper ARIA attributes
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <Radio
|
|
17
|
+
* name="size"
|
|
18
|
+
* label="Size"
|
|
19
|
+
* options={[
|
|
20
|
+
* { value: 's', label: 'Small' },
|
|
21
|
+
* { value: 'm', label: 'Medium' },
|
|
22
|
+
* ]}
|
|
23
|
+
* value={selected}
|
|
24
|
+
* onChange={handleChange}
|
|
25
|
+
* />
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export const Radio = React.forwardRef(({ name, options, label, error, helperText, className = '', value, onChange, disabled, required, 'aria-describedby': ariaDescribedBy, ...props }, ref) => {
|
|
29
|
+
const groupId = React.useId();
|
|
30
|
+
const errorId = error ? `radio-${groupId}-error` : undefined;
|
|
31
|
+
const helperId = helperText ? `radio-${groupId}-helper` : undefined;
|
|
32
|
+
const describedBy = combineAriaDescribedBy(ariaDescribedBy, errorId, helperId);
|
|
33
|
+
return (_jsxs("div", { className: "form-radio-wrapper", children: [label && (_jsx("div", { className: "form-radio-label", role: "group", "aria-labelledby": label ? `radio-label-${groupId}` : undefined, children: _jsxs("span", { id: `radio-label-${groupId}`, className: "form-label", children: [label, required && (_jsxs("span", { className: "form-label__required", "aria-hidden": "true", children: [' ', "*"] }))] }) })), _jsx("div", { className: "form-radio-group", role: "radiogroup", "aria-describedby": describedBy, "aria-invalid": error ? true : undefined, children: options.map((option, index) => {
|
|
34
|
+
const optionId = `radio-${groupId}-${index}`;
|
|
35
|
+
const isChecked = value === option.value;
|
|
36
|
+
return (_jsxs("div", { className: "form-radio-option", children: [_jsx("input", { ref: index === 0 ? ref : undefined, id: optionId, type: "radio", name: name, value: option.value, checked: isChecked, onChange: onChange, disabled: option.disabled || disabled, required: required, className: "form-radio", ...props }), _jsx("label", { htmlFor: optionId, className: "form-radio-label", children: option.label })] }, option.value));
|
|
37
|
+
}) }), helperText && !error && (_jsx("span", { id: helperId, className: "form-helper-text", children: helperText })), error && (_jsx("span", { id: errorId, className: "form-error-text", role: "alert", children: error }))] }));
|
|
38
|
+
});
|
|
39
|
+
Radio.displayName = 'Radio';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import './Select.css';
|
|
3
|
+
export interface SelectOption {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'children'> {
|
|
9
|
+
/**
|
|
10
|
+
* Options for the select
|
|
11
|
+
*/
|
|
12
|
+
options: SelectOption[];
|
|
13
|
+
/**
|
|
14
|
+
* Error message to display
|
|
15
|
+
*/
|
|
16
|
+
error?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Helper text to display below the select
|
|
19
|
+
*/
|
|
20
|
+
helperText?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Label for the select
|
|
23
|
+
*/
|
|
24
|
+
label?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Placeholder option text
|
|
27
|
+
*/
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Accessible Select component
|
|
32
|
+
*
|
|
33
|
+
* WCAG Compliance:
|
|
34
|
+
* - 1.3.1 Info and Relationships: Proper label-select association
|
|
35
|
+
* - 2.1.1 Keyboard: Full keyboard navigation
|
|
36
|
+
* - 4.1.2 Name, Role, Value: Proper ARIA attributes
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* <Select
|
|
41
|
+
* id="country"
|
|
42
|
+
* label="Country"
|
|
43
|
+
* options={[
|
|
44
|
+
* { value: 'us', label: 'United States' },
|
|
45
|
+
* { value: 'ca', label: 'Canada' },
|
|
46
|
+
* ]}
|
|
47
|
+
* />
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare const Select: React.ForwardRefExoticComponent<SelectProps & React.RefAttributes<HTMLSelectElement>>;
|
|
51
|
+
//# sourceMappingURL=Select.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Select.d.ts","sourceRoot":"","sources":["../../../src/components/Form/Select.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,cAAc,CAAA;AAErB,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,UAAU,CAAC;IAClG;;OAEG;IACH,OAAO,EAAE,YAAY,EAAE,CAAA;IAEvB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,MAAM,uFAgGlB,CAAA"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { combineAriaDescribedBy } from '../../utils/aria';
|
|
5
|
+
import './Select.css';
|
|
6
|
+
/**
|
|
7
|
+
* Accessible Select component
|
|
8
|
+
*
|
|
9
|
+
* WCAG Compliance:
|
|
10
|
+
* - 1.3.1 Info and Relationships: Proper label-select association
|
|
11
|
+
* - 2.1.1 Keyboard: Full keyboard navigation
|
|
12
|
+
* - 4.1.2 Name, Role, Value: Proper ARIA attributes
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <Select
|
|
17
|
+
* id="country"
|
|
18
|
+
* label="Country"
|
|
19
|
+
* options={[
|
|
20
|
+
* { value: 'us', label: 'United States' },
|
|
21
|
+
* { value: 'ca', label: 'Canada' },
|
|
22
|
+
* ]}
|
|
23
|
+
* />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const Select = React.forwardRef(({ id, options, error, helperText, label, placeholder, className = '', 'aria-describedby': ariaDescribedBy, ...props }, ref) => {
|
|
27
|
+
const selectId = React.useId();
|
|
28
|
+
const finalId = id || `select-${selectId}`;
|
|
29
|
+
const errorId = error ? `${finalId}-error` : undefined;
|
|
30
|
+
const helperId = helperText ? `${finalId}-helper` : undefined;
|
|
31
|
+
const describedBy = combineAriaDescribedBy(ariaDescribedBy, errorId, helperId);
|
|
32
|
+
const handleKeyDown = React.useCallback((event) => {
|
|
33
|
+
// Arrow keys are handled natively by select
|
|
34
|
+
// But we can add custom handling if needed
|
|
35
|
+
if (props.onKeyDown) {
|
|
36
|
+
props.onKeyDown(event);
|
|
37
|
+
}
|
|
38
|
+
}, [props.onKeyDown]);
|
|
39
|
+
const classes = [
|
|
40
|
+
'form-select',
|
|
41
|
+
error && 'form-select--error',
|
|
42
|
+
props.disabled && 'form-select--disabled',
|
|
43
|
+
className,
|
|
44
|
+
]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(' ');
|
|
47
|
+
return (_jsxs("div", { className: "form-select-wrapper", children: [label && (_jsxs("label", { htmlFor: finalId, className: "form-label", children: [label, props.required && (_jsxs("span", { className: "form-label__required", "aria-hidden": "true", children: [' ', "*"] }))] })), _jsxs("select", { ref: ref, id: finalId, className: classes, "aria-invalid": error ? true : undefined, "aria-describedby": describedBy, onKeyDown: handleKeyDown, required: props.required ? true : undefined, ...props, children: [placeholder && (_jsx("option", { value: "", disabled: true, children: placeholder })), options.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value)))] }), helperText && !error && (_jsx("span", { id: helperId, className: "form-helper-text", children: helperText })), error && (_jsx("span", { id: errorId, className: "form-error-text", role: "alert", children: error }))] }));
|
|
48
|
+
});
|
|
49
|
+
Select.displayName = 'Select';
|