@a11ypros/a11y-ui-components 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/.storybook/custom.css +69 -0
  2. package/.storybook/main.ts +46 -0
  3. package/.storybook/manager.ts +26 -0
  4. package/.storybook/package.json +6 -0
  5. package/.storybook/preview.tsx +31 -0
  6. package/.storybook/public/logo.png +0 -0
  7. package/.storybook/vite.config.ts +24 -0
  8. package/.storybook/welcome.mdx +97 -0
  9. package/DEPLOYMENT.md +154 -0
  10. package/README.md +227 -0
  11. package/apps/web/app/(docs)/audit/audit.css +269 -0
  12. package/apps/web/app/(docs)/audit/page.tsx +271 -0
  13. package/apps/web/app/(docs)/components/button/page.tsx +49 -0
  14. package/apps/web/app/(docs)/components/form/page.tsx +92 -0
  15. package/apps/web/app/(docs)/components/link/page.tsx +31 -0
  16. package/apps/web/app/(docs)/components/modal/page.tsx +41 -0
  17. package/apps/web/app/(docs)/components/page.tsx +37 -0
  18. package/apps/web/app/(docs)/components/table/page.tsx +54 -0
  19. package/apps/web/app/(docs)/components/tabs/page.tsx +61 -0
  20. package/apps/web/app/(docs)/components/toast/page.tsx +51 -0
  21. package/apps/web/app/api/audit/route.ts +128 -0
  22. package/apps/web/app/favicon.ico +0 -0
  23. package/apps/web/app/layout.tsx +20 -0
  24. package/apps/web/app/page.tsx +17 -0
  25. package/apps/web/app/styles/globals.css +5 -0
  26. package/apps/web/next-env.d.ts +5 -0
  27. package/apps/web/next.config.js +21 -0
  28. package/apps/web/package.json +28 -0
  29. package/apps/web/public/_headers +17 -0
  30. package/apps/web/public/_redirects +31 -0
  31. package/apps/web/public/logo.png +0 -0
  32. package/apps/web/tsconfig.json +29 -0
  33. package/netlify/functions/audit.ts +163 -0
  34. package/netlify.toml +37 -0
  35. package/package.json +30 -58
  36. package/packages/design-system/README.md +252 -0
  37. package/packages/design-system/package.json +68 -0
  38. package/packages/design-system/scripts/copy-css.js +63 -0
  39. package/packages/design-system/src/components/Button/Button.stories.tsx +228 -0
  40. package/packages/design-system/src/components/Button/Button.tsx +137 -0
  41. package/packages/design-system/src/components/Button/index.ts +3 -0
  42. package/packages/design-system/src/components/DataTable/DataTable.stories.tsx +211 -0
  43. package/packages/design-system/src/components/DataTable/DataTable.tsx +293 -0
  44. package/packages/design-system/src/components/DataTable/index.ts +3 -0
  45. package/packages/design-system/src/components/Form/Checkbox.stories.tsx +252 -0
  46. package/packages/design-system/src/components/Form/Checkbox.tsx +114 -0
  47. package/packages/design-system/src/components/Form/Fieldset.stories.tsx +210 -0
  48. package/packages/design-system/src/components/Form/Fieldset.tsx +71 -0
  49. package/packages/design-system/src/components/Form/Input.stories.tsx +164 -0
  50. package/packages/design-system/src/components/Form/Input.tsx +113 -0
  51. package/packages/design-system/src/components/Form/Label.tsx +56 -0
  52. package/packages/design-system/src/components/Form/Radio.stories.tsx +265 -0
  53. package/packages/design-system/src/components/Form/Radio.tsx +147 -0
  54. package/packages/design-system/src/components/Form/Select.stories.tsx +295 -0
  55. package/packages/design-system/src/components/Form/Select.tsx +160 -0
  56. package/packages/design-system/src/components/Form/Textarea.stories.tsx +253 -0
  57. package/packages/design-system/src/components/Form/Textarea.tsx +145 -0
  58. package/packages/design-system/src/components/Form/index.ts +8 -0
  59. package/packages/design-system/src/components/Link/Link.stories.tsx +128 -0
  60. package/packages/design-system/src/components/Link/Link.tsx +117 -0
  61. package/packages/design-system/src/components/Link/index.ts +3 -0
  62. package/packages/design-system/src/components/Modal/Modal.stories.tsx +165 -0
  63. package/packages/design-system/src/components/Modal/Modal.tsx +202 -0
  64. package/packages/design-system/src/components/Modal/index.ts +3 -0
  65. package/packages/design-system/src/components/Tabs/Tabs.stories.tsx +213 -0
  66. package/packages/design-system/src/components/Tabs/Tabs.tsx +248 -0
  67. package/packages/design-system/src/components/Tabs/index.ts +3 -0
  68. package/packages/design-system/src/components/Toast/Toast.stories.tsx +153 -0
  69. package/packages/design-system/src/components/Toast/Toast.tsx +175 -0
  70. package/packages/design-system/src/components/Toast/ToastProvider.tsx +73 -0
  71. package/packages/design-system/src/components/Toast/index.ts +5 -0
  72. package/packages/design-system/src/hooks/useAriaLive.ts +51 -0
  73. package/packages/design-system/src/hooks/useFocusReturn.ts +40 -0
  74. package/packages/design-system/src/hooks/useFocusTrap.ts +82 -0
  75. package/{dist/index.js → packages/design-system/src/index.ts} +4 -0
  76. package/packages/design-system/src/styles/index.ts +3 -0
  77. package/packages/design-system/src/tokens/breakpoints.ts +28 -0
  78. package/packages/design-system/src/tokens/colors.ts +98 -0
  79. package/packages/design-system/src/tokens/index.ts +6 -0
  80. package/packages/design-system/src/tokens/motion.ts +41 -0
  81. package/packages/design-system/src/tokens/spacing.ts +24 -0
  82. package/packages/design-system/src/tokens/theme.ts +19 -0
  83. package/packages/design-system/src/tokens/typography.ts +64 -0
  84. package/packages/design-system/src/utils/aria.ts +108 -0
  85. package/packages/design-system/src/utils/focus.ts +87 -0
  86. package/packages/design-system/src/utils/index.ts +4 -0
  87. package/packages/design-system/src/utils/keyboard.ts +77 -0
  88. package/packages/design-system/tsconfig.json +17 -0
  89. package/public/logo.png +0 -0
  90. package/scripts/fix-storybook-paths.js +53 -0
  91. package/tsconfig.json +20 -0
  92. package/dist/components/Button/Button.d.ts +0 -37
  93. package/dist/components/Button/Button.d.ts.map +0 -1
  94. package/dist/components/Button/Button.js +0 -52
  95. package/dist/components/Button/index.d.ts +0 -3
  96. package/dist/components/Button/index.d.ts.map +0 -1
  97. package/dist/components/Button/index.js +0 -1
  98. package/dist/components/DataTable/DataTable.d.ts +0 -71
  99. package/dist/components/DataTable/DataTable.d.ts.map +0 -1
  100. package/dist/components/DataTable/DataTable.js +0 -122
  101. package/dist/components/DataTable/index.d.ts +0 -3
  102. package/dist/components/DataTable/index.d.ts.map +0 -1
  103. package/dist/components/DataTable/index.js +0 -1
  104. package/dist/components/Form/Checkbox.d.ts +0 -36
  105. package/dist/components/Form/Checkbox.d.ts.map +0 -1
  106. package/dist/components/Form/Checkbox.js +0 -39
  107. package/dist/components/Form/Fieldset.d.ts +0 -33
  108. package/dist/components/Form/Fieldset.d.ts.map +0 -1
  109. package/dist/components/Form/Fieldset.js +0 -34
  110. package/dist/components/Form/Input.d.ts +0 -37
  111. package/dist/components/Form/Input.d.ts.map +0 -1
  112. package/dist/components/Form/Input.js +0 -41
  113. package/dist/components/Form/Label.d.ts +0 -30
  114. package/dist/components/Form/Label.d.ts.map +0 -1
  115. package/dist/components/Form/Label.js +0 -30
  116. package/dist/components/Form/Radio.d.ts +0 -53
  117. package/dist/components/Form/Radio.d.ts.map +0 -1
  118. package/dist/components/Form/Radio.js +0 -39
  119. package/dist/components/Form/Select.d.ts +0 -51
  120. package/dist/components/Form/Select.d.ts.map +0 -1
  121. package/dist/components/Form/Select.js +0 -49
  122. package/dist/components/Form/Textarea.d.ts +0 -44
  123. package/dist/components/Form/Textarea.d.ts.map +0 -1
  124. package/dist/components/Form/Textarea.js +0 -43
  125. package/dist/components/Form/index.d.ts +0 -8
  126. package/dist/components/Form/index.d.ts.map +0 -1
  127. package/dist/components/Form/index.js +0 -7
  128. package/dist/components/Link/Link.d.ts +0 -34
  129. package/dist/components/Link/Link.d.ts.map +0 -1
  130. package/dist/components/Link/Link.js +0 -48
  131. package/dist/components/Link/index.d.ts +0 -3
  132. package/dist/components/Link/index.d.ts.map +0 -1
  133. package/dist/components/Link/index.js +0 -1
  134. package/dist/components/Modal/Modal.d.ts +0 -64
  135. package/dist/components/Modal/Modal.d.ts.map +0 -1
  136. package/dist/components/Modal/Modal.js +0 -108
  137. package/dist/components/Modal/index.d.ts +0 -3
  138. package/dist/components/Modal/index.d.ts.map +0 -1
  139. package/dist/components/Modal/index.js +0 -1
  140. package/dist/components/Tabs/Tabs.d.ts +0 -63
  141. package/dist/components/Tabs/Tabs.d.ts.map +0 -1
  142. package/dist/components/Tabs/Tabs.js +0 -134
  143. package/dist/components/Tabs/index.d.ts +0 -3
  144. package/dist/components/Tabs/index.d.ts.map +0 -1
  145. package/dist/components/Tabs/index.js +0 -1
  146. package/dist/components/Toast/Toast.d.ts +0 -59
  147. package/dist/components/Toast/Toast.d.ts.map +0 -1
  148. package/dist/components/Toast/Toast.js +0 -91
  149. package/dist/components/Toast/ToastProvider.d.ts +0 -22
  150. package/dist/components/Toast/ToastProvider.d.ts.map +0 -1
  151. package/dist/components/Toast/ToastProvider.js +0 -33
  152. package/dist/components/Toast/index.d.ts +0 -5
  153. package/dist/components/Toast/index.d.ts.map +0 -1
  154. package/dist/components/Toast/index.js +0 -2
  155. package/dist/hooks/useAriaLive.d.ts +0 -9
  156. package/dist/hooks/useAriaLive.d.ts.map +0 -1
  157. package/dist/hooks/useAriaLive.js +0 -39
  158. package/dist/hooks/useFocusReturn.d.ts +0 -9
  159. package/dist/hooks/useFocusReturn.d.ts.map +0 -1
  160. package/dist/hooks/useFocusReturn.js +0 -33
  161. package/dist/hooks/useFocusTrap.d.ts +0 -9
  162. package/dist/hooks/useFocusTrap.d.ts.map +0 -1
  163. package/dist/hooks/useFocusTrap.js +0 -68
  164. package/dist/index.d.ts +0 -22
  165. package/dist/index.d.ts.map +0 -1
  166. package/dist/styles/index.d.ts +0 -3
  167. package/dist/styles/index.d.ts.map +0 -1
  168. package/dist/styles/index.js +0 -1
  169. package/dist/tokens/breakpoints.d.ts +0 -25
  170. package/dist/tokens/breakpoints.d.ts.map +0 -1
  171. package/dist/tokens/breakpoints.js +0 -23
  172. package/dist/tokens/colors.d.ts +0 -81
  173. package/dist/tokens/colors.d.ts.map +0 -1
  174. package/dist/tokens/colors.js +0 -86
  175. package/dist/tokens/index.d.ts +0 -6
  176. package/dist/tokens/index.d.ts.map +0 -1
  177. package/dist/tokens/index.js +0 -5
  178. package/dist/tokens/motion.d.ts +0 -30
  179. package/dist/tokens/motion.d.ts.map +0 -1
  180. package/dist/tokens/motion.js +0 -34
  181. package/dist/tokens/spacing.d.ts +0 -22
  182. package/dist/tokens/spacing.d.ts.map +0 -1
  183. package/dist/tokens/spacing.js +0 -20
  184. package/dist/tokens/theme.d.ts +0 -159
  185. package/dist/tokens/theme.d.ts.map +0 -1
  186. package/dist/tokens/theme.js +0 -15
  187. package/dist/tokens/typography.d.ts +0 -45
  188. package/dist/tokens/typography.d.ts.map +0 -1
  189. package/dist/tokens/typography.js +0 -56
  190. package/dist/utils/aria.d.ts +0 -60
  191. package/dist/utils/aria.d.ts.map +0 -1
  192. package/dist/utils/aria.js +0 -86
  193. package/dist/utils/focus.d.ts +0 -30
  194. package/dist/utils/focus.d.ts.map +0 -1
  195. package/dist/utils/focus.js +0 -80
  196. package/dist/utils/index.d.ts +0 -4
  197. package/dist/utils/index.d.ts.map +0 -1
  198. package/dist/utils/index.js +0 -3
  199. package/dist/utils/keyboard.d.ts +0 -38
  200. package/dist/utils/keyboard.d.ts.map +0 -1
  201. package/dist/utils/keyboard.js +0 -59
  202. /package/{dist → packages/design-system/src}/components/Button/Button.css +0 -0
  203. /package/{dist → packages/design-system/src}/components/DataTable/DataTable.css +0 -0
  204. /package/{dist → packages/design-system/src}/components/Form/Checkbox.css +0 -0
  205. /package/{dist → packages/design-system/src}/components/Form/Fieldset.css +0 -0
  206. /package/{dist → packages/design-system/src}/components/Form/Input.css +0 -0
  207. /package/{dist → packages/design-system/src}/components/Form/Label.css +0 -0
  208. /package/{dist → packages/design-system/src}/components/Form/Radio.css +0 -0
  209. /package/{dist → packages/design-system/src}/components/Form/Select.css +0 -0
  210. /package/{dist → packages/design-system/src}/components/Form/Textarea.css +0 -0
  211. /package/{dist → packages/design-system/src}/components/Link/Link.css +0 -0
  212. /package/{dist → packages/design-system/src}/components/Modal/Modal.css +0 -0
  213. /package/{dist → packages/design-system/src}/components/Tabs/Tabs.css +0 -0
  214. /package/{dist → packages/design-system/src}/components/Toast/Toast.css +0 -0
  215. /package/{dist → packages/design-system/src}/components/Toast/ToastProvider.css +0 -0
  216. /package/{dist → packages/design-system/src}/styles/components.css +0 -0
  217. /package/{dist → packages/design-system/src}/styles/global.css +0 -0
@@ -1,108 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import React, { useEffect, useRef } from 'react';
4
- import { useFocusReturn } from '../../hooks/useFocusReturn';
5
- import { Button } from '../Button/Button';
6
- import './Modal.css';
7
- /**
8
- * Accessible Modal component using HTML5 dialog element
9
- *
10
- * Uses the native `<dialog>` element which provides:
11
- * - Built-in focus management and focus trapping
12
- * - Automatic body scroll prevention
13
- * - Native backdrop overlay
14
- * - ESC key handling (configurable)
15
- *
16
- * WCAG Compliance:
17
- * - 2.1.1 Keyboard: ESC key support, built-in focus trap
18
- * - 2.1.2 No Keyboard Trap: Focus returns to trigger
19
- * - 2.4.3 Focus Order: Focus trapped within modal (native behavior)
20
- * - 4.1.2 Name, Role, Value: ARIA modal pattern
21
- *
22
- * @example
23
- * ```tsx
24
- * <Modal
25
- * isOpen={isOpen}
26
- * onClose={() => setIsOpen(false)}
27
- * title="Confirm Action"
28
- * >
29
- * <p>Are you sure?</p>
30
- * </Modal>
31
- * ```
32
- */
33
- export const Modal = ({ isOpen, onClose, title, children, closeOnBackdropClick = true, closeOnEscape = true, size = 'md', returnFocusTo, }) => {
34
- const dialogRef = useRef(null);
35
- const contentRef = useRef(null);
36
- const titleId = React.useId();
37
- const descriptionId = React.useId();
38
- // Return focus on close
39
- useFocusReturn(isOpen, returnFocusTo);
40
- // Handle dialog open/close
41
- useEffect(() => {
42
- const dialog = dialogRef.current;
43
- if (!dialog)
44
- return;
45
- if (isOpen) {
46
- // Show modal dialog
47
- dialog.showModal();
48
- }
49
- else {
50
- // Close dialog
51
- dialog.close();
52
- }
53
- return () => {
54
- // Cleanup: ensure dialog is closed when component unmounts
55
- if (dialog.open) {
56
- dialog.close();
57
- }
58
- };
59
- }, [isOpen]);
60
- // Handle backdrop clicks
61
- // The ::backdrop pseudo-element doesn't bubble events to the dialog element,
62
- // so we need to listen for clicks on the document and check if they're outside
63
- // the dialog content area.
64
- useEffect(() => {
65
- if (!isOpen || !closeOnBackdropClick)
66
- return;
67
- const handleDocumentClick = (event) => {
68
- const dialog = dialogRef.current;
69
- const content = contentRef.current;
70
- if (!dialog || !content)
71
- return;
72
- // Check if click target is outside the dialog content area
73
- const target = event.target;
74
- // If the click is not inside the content wrapper, it's a backdrop click
75
- if (!content.contains(target)) {
76
- // Verify click coordinates are outside content bounds for extra safety
77
- const rect = content.getBoundingClientRect();
78
- const clickX = event.clientX;
79
- const clickY = event.clientY;
80
- const isOutsideContent = clickX < rect.left ||
81
- clickX > rect.right ||
82
- clickY < rect.top ||
83
- clickY > rect.bottom;
84
- if (isOutsideContent) {
85
- event.preventDefault();
86
- event.stopPropagation();
87
- onClose();
88
- }
89
- }
90
- };
91
- // Use capture phase to catch events before they bubble
92
- document.addEventListener('mousedown', handleDocumentClick, true);
93
- return () => {
94
- document.removeEventListener('mousedown', handleDocumentClick, true);
95
- };
96
- }, [isOpen, closeOnBackdropClick, onClose]);
97
- // Handle cancel event (fires when ESC key is pressed)
98
- const handleCancel = (event) => {
99
- // Prevent default close behavior
100
- event.preventDefault();
101
- // Only close if closeOnEscape is enabled
102
- if (closeOnEscape) {
103
- onClose();
104
- }
105
- };
106
- return (_jsx("dialog", { ref: dialogRef, className: `modal modal--${size} ${isOpen ? 'modal--open' : ''}`, "aria-labelledby": titleId, "aria-describedby": descriptionId, onCancel: handleCancel, children: _jsxs("div", { ref: contentRef, className: "modal-content-wrapper", children: [_jsxs("div", { className: "modal-header", children: [_jsx("h2", { id: titleId, className: "modal-title", children: title }), _jsx(Button, { variant: "ghost", size: "sm", onClick: onClose, "aria-label": "Close modal", className: "modal-close", children: "\u00D7" })] }), _jsx("div", { id: descriptionId, className: "modal-content", children: children })] }) }));
107
- };
108
- Modal.displayName = 'Modal';
@@ -1,3 +0,0 @@
1
- export { Modal } from './Modal';
2
- export type { ModalProps } from './Modal';
3
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Modal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA"}
@@ -1 +0,0 @@
1
- export { Modal } from './Modal';
@@ -1,63 +0,0 @@
1
- import React from 'react';
2
- import './Tabs.css';
3
- export interface TabItem {
4
- id: string;
5
- label: string;
6
- content: React.ReactNode;
7
- disabled?: boolean;
8
- }
9
- export interface TabsProps {
10
- /**
11
- * Tab items
12
- */
13
- items: TabItem[];
14
- /**
15
- * Default selected tab ID
16
- */
17
- defaultSelectedId?: string;
18
- /**
19
- * Controlled selected tab ID
20
- */
21
- selectedId?: string;
22
- /**
23
- * Callback when tab selection changes
24
- */
25
- onSelectionChange?: (id: string) => void;
26
- /**
27
- * Orientation of tabs
28
- */
29
- orientation?: 'horizontal' | 'vertical';
30
- /**
31
- * Activation mode for tabs
32
- * - 'automatic': Arrow keys both move focus and activate tabs immediately
33
- * - 'manual': Arrow keys move focus only, Enter/Space activates the focused tab
34
- * @default 'automatic'
35
- */
36
- activationMode?: 'automatic' | 'manual';
37
- /**
38
- * Label for the tab list (required for accessibility)
39
- */
40
- 'aria-label'?: string;
41
- 'aria-labelledby'?: string;
42
- }
43
- /**
44
- * Accessible Tabs component
45
- *
46
- * WCAG Compliance:
47
- * - 2.1.1 Keyboard: Arrow key navigation, Home/End support
48
- * - 4.1.2 Name, Role, Value: ARIA tabs pattern
49
- * - 2.4.3 Focus Order: Proper focus management
50
- *
51
- * @example
52
- * ```tsx
53
- * <Tabs
54
- * items={[
55
- * { id: 'tab1', label: 'Tab 1', content: <div>Content 1</div> },
56
- * { id: 'tab2', label: 'Tab 2', content: <div>Content 2</div> },
57
- * ]}
58
- * aria-label="Settings tabs"
59
- * />
60
- * ```
61
- */
62
- export declare const Tabs: React.FC<TabsProps>;
63
- //# sourceMappingURL=Tabs.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../../src/components/Tabs/Tabs.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAwC,MAAM,OAAO,CAAA;AAG5D,OAAO,YAAY,CAAA;AAEnB,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,KAAK,CAAC,SAAS,CAAA;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,SAAS;IACxB;;OAEG;IACH,KAAK,EAAE,OAAO,EAAE,CAAA;IAEhB;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAE1B;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;OAEG;IACH,iBAAiB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAExC;;OAEG;IACH,WAAW,CAAC,EAAE,YAAY,GAAG,UAAU,CAAA;IAEvC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,WAAW,GAAG,QAAQ,CAAA;IAEvC;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,SAAS,CA0KpC,CAAA"}
@@ -1,134 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState, useCallback, useRef } from 'react';
4
- import { isNavigationKey, isArrowKey } from '../../utils/keyboard';
5
- import { getCurrentAttributes } from '../../utils/aria';
6
- import './Tabs.css';
7
- /**
8
- * Accessible Tabs component
9
- *
10
- * WCAG Compliance:
11
- * - 2.1.1 Keyboard: Arrow key navigation, Home/End support
12
- * - 4.1.2 Name, Role, Value: ARIA tabs pattern
13
- * - 2.4.3 Focus Order: Proper focus management
14
- *
15
- * @example
16
- * ```tsx
17
- * <Tabs
18
- * items={[
19
- * { id: 'tab1', label: 'Tab 1', content: <div>Content 1</div> },
20
- * { id: 'tab2', label: 'Tab 2', content: <div>Content 2</div> },
21
- * ]}
22
- * aria-label="Settings tabs"
23
- * />
24
- * ```
25
- */
26
- export const Tabs = ({ items, defaultSelectedId, selectedId: controlledSelectedId, onSelectionChange, orientation = 'horizontal', activationMode = 'automatic', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, }) => {
27
- const initialSelectedId = defaultSelectedId || items[0]?.id;
28
- const [internalSelectedId, setInternalSelectedId] = useState(initialSelectedId);
29
- const [focusedId, setFocusedId] = useState(initialSelectedId);
30
- const tabRefs = useRef(new Map());
31
- const selectedId = controlledSelectedId ?? internalSelectedId;
32
- const selectedIndex = items.findIndex((item) => item.id === selectedId);
33
- // In automatic mode, focused tab is always the selected tab
34
- // In manual mode, focused tab can be different from selected tab
35
- const effectiveFocusedId = activationMode === 'automatic' ? selectedId : (focusedId || selectedId);
36
- const handleSelect = useCallback((id) => {
37
- if (onSelectionChange) {
38
- onSelectionChange(id);
39
- }
40
- else {
41
- setInternalSelectedId(id);
42
- }
43
- // In manual mode, update focused tab when selecting
44
- if (activationMode === 'manual') {
45
- setFocusedId(id);
46
- }
47
- }, [onSelectionChange, activationMode]);
48
- const handleKeyDown = useCallback((event, currentIndex) => {
49
- const isHorizontal = orientation === 'horizontal';
50
- let newIndex = currentIndex;
51
- // Handle Enter/Space for manual activation
52
- if (activationMode === 'manual' && (event.key === 'Enter' || event.key === ' ')) {
53
- event.preventDefault();
54
- const currentTab = items[currentIndex];
55
- if (currentTab && !currentTab.disabled) {
56
- handleSelect(currentTab.id);
57
- }
58
- return;
59
- }
60
- // Handle arrow keys and Home/End
61
- if (isNavigationKey(event.key) || isArrowKey(event.key)) {
62
- event.preventDefault();
63
- switch (event.key) {
64
- case 'Home':
65
- newIndex = 0;
66
- break;
67
- case 'End':
68
- newIndex = items.length - 1;
69
- break;
70
- case 'ArrowRight':
71
- if (isHorizontal) {
72
- newIndex = (currentIndex + 1) % items.length;
73
- }
74
- break;
75
- case 'ArrowLeft':
76
- if (isHorizontal) {
77
- newIndex = (currentIndex - 1 + items.length) % items.length;
78
- }
79
- break;
80
- case 'ArrowDown':
81
- if (!isHorizontal) {
82
- newIndex = (currentIndex + 1) % items.length;
83
- }
84
- break;
85
- case 'ArrowUp':
86
- if (!isHorizontal) {
87
- newIndex = (currentIndex - 1 + items.length) % items.length;
88
- }
89
- break;
90
- }
91
- // Skip disabled tabs
92
- while (items[newIndex]?.disabled && newIndex !== currentIndex) {
93
- if (event.key === 'Home' || event.key === 'ArrowRight' || event.key === 'ArrowDown') {
94
- newIndex = (newIndex + 1) % items.length;
95
- }
96
- else {
97
- newIndex = (newIndex - 1 + items.length) % items.length;
98
- }
99
- }
100
- const newTab = items[newIndex];
101
- if (newTab && !newTab.disabled) {
102
- if (activationMode === 'automatic') {
103
- // Automatic: move focus and activate
104
- handleSelect(newTab.id);
105
- tabRefs.current.get(newTab.id)?.focus();
106
- }
107
- else {
108
- // Manual: move focus only
109
- setFocusedId(newTab.id);
110
- tabRefs.current.get(newTab.id)?.focus();
111
- }
112
- }
113
- }
114
- }, [items, orientation, activationMode, handleSelect]);
115
- const selectedTab = items.find((item) => item.id === selectedId);
116
- return (_jsxs("div", { className: `tabs tabs--${orientation}`, children: [_jsx("div", { className: "tabs-list", role: "tablist", "aria-orientation": orientation, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children: items.map((item, index) => {
117
- const isSelected = item.id === selectedId;
118
- const isFocused = item.id === effectiveFocusedId;
119
- // In manual mode, focused tab should be focusable even if not selected
120
- // In automatic mode, only selected tab is focusable
121
- const tabIndex = activationMode === 'manual'
122
- ? (isFocused ? 0 : -1)
123
- : (isSelected ? 0 : -1);
124
- return (_jsx("button", { ref: (el) => {
125
- if (el) {
126
- tabRefs.current.set(item.id, el);
127
- }
128
- else {
129
- tabRefs.current.delete(item.id);
130
- }
131
- }, id: `tab-${item.id}`, role: "tab", "aria-controls": `tabpanel-${item.id}`, "aria-selected": isSelected, tabIndex: tabIndex, disabled: item.disabled, className: `tabs-tab ${isSelected ? 'tabs-tab--selected' : ''} ${item.disabled ? 'tabs-tab--disabled' : ''}`, onClick: () => !item.disabled && handleSelect(item.id), onKeyDown: (e) => handleKeyDown(e, index), onFocus: () => setFocusedId(item.id), ...getCurrentAttributes(isSelected ? 'page' : undefined), children: item.label }, item.id));
132
- }) }), selectedTab && (_jsx("div", { id: `tabpanel-${selectedTab.id}`, role: "tabpanel", "aria-labelledby": `tab-${selectedTab.id}`, className: "tabs-panel", children: selectedTab.content }))] }));
133
- };
134
- Tabs.displayName = 'Tabs';
@@ -1,3 +0,0 @@
1
- export { Tabs } from './Tabs';
2
- export type { TabsProps, TabItem } from './Tabs';
3
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Tabs/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAC7B,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA"}
@@ -1 +0,0 @@
1
- export { Tabs } from './Tabs';
@@ -1,59 +0,0 @@
1
- import React from 'react';
2
- import './Toast.css';
3
- export interface ToastProps {
4
- /**
5
- * Unique ID for the toast
6
- */
7
- id: string;
8
- /**
9
- * Toast message
10
- */
11
- message: string;
12
- /**
13
- * Toast type
14
- */
15
- type?: 'info' | 'success' | 'warning' | 'error';
16
- /**
17
- * Whether toast can be dismissed
18
- */
19
- dismissible?: boolean;
20
- /**
21
- * Auto-dismiss duration in milliseconds (0 = no auto-dismiss). For WCAG compliance, this should be 6 seconds.
22
- */
23
- duration?: number;
24
- /**
25
- * Callback when toast is dismissed
26
- */
27
- onDismiss: (id: string) => void;
28
- /**
29
- * Pause auto-dismiss on hover
30
- */
31
- pauseOnHover?: boolean;
32
- }
33
- /**
34
- * Accessible Toast component
35
- *
36
- * WCAG Compliance:
37
- * - 4.1.3 Status Messages: ARIA live region announcements
38
- * - 2.1.1 Keyboard: ESC key support, Tab navigation support
39
- * - 2.4.3 Focus Order: Consistent focus order - toasts always appear in same position
40
- * - 4.1.2 Name, Role, Value: Proper ARIA attributes
41
- *
42
- * Focus Order:
43
- * - Toasts are focusable with tabIndex={0} (not positive tabindex)
44
- * - Toast container is always rendered in the same DOM position (via portal to body)
45
- * - Toasts appear in consistent order (order added) for predictable tab navigation
46
- * - Container itself is not focusable, only individual toasts are focusable
47
- *
48
- * @example
49
- * ```tsx
50
- * <Toast
51
- * id="toast-1"
52
- * message="Successfully saved!"
53
- * type="success"
54
- * onDismiss={handleDismiss}
55
- * />
56
- * ```
57
- */
58
- export declare const Toast: React.FC<ToastProps>;
59
- //# sourceMappingURL=Toast.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Toast.d.ts","sourceRoot":"","sources":["../../../src/components/Toast/Toast.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAA;AAIlD,OAAO,aAAa,CAAA;AAEpB,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,EAAE,EAAE,MAAM,CAAA;IAEV;;OAEG;IACH,OAAO,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;IAE/C;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IAErB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB;;OAEG;IACH,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAE/B;;OAEG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,UAAU,CAqGtC,CAAA"}
@@ -1,91 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import React, { useEffect, useState } from 'react';
4
- import { useAriaLive } from '../../hooks/useAriaLive';
5
- import { isEscapeKey } from '../../utils/keyboard';
6
- import { Button } from '../Button/Button';
7
- import './Toast.css';
8
- /**
9
- * Accessible Toast component
10
- *
11
- * WCAG Compliance:
12
- * - 4.1.3 Status Messages: ARIA live region announcements
13
- * - 2.1.1 Keyboard: ESC key support, Tab navigation support
14
- * - 2.4.3 Focus Order: Consistent focus order - toasts always appear in same position
15
- * - 4.1.2 Name, Role, Value: Proper ARIA attributes
16
- *
17
- * Focus Order:
18
- * - Toasts are focusable with tabIndex={0} (not positive tabindex)
19
- * - Toast container is always rendered in the same DOM position (via portal to body)
20
- * - Toasts appear in consistent order (order added) for predictable tab navigation
21
- * - Container itself is not focusable, only individual toasts are focusable
22
- *
23
- * @example
24
- * ```tsx
25
- * <Toast
26
- * id="toast-1"
27
- * message="Successfully saved!"
28
- * type="success"
29
- * onDismiss={handleDismiss}
30
- * />
31
- * ```
32
- */
33
- export const Toast = ({ id, message, type = 'info', dismissible = true, duration = 6000, onDismiss, pauseOnHover = true, }) => {
34
- const [isPaused, setIsPaused] = useState(false);
35
- const timeoutRef = React.useRef(null);
36
- // Announce toast via ARIA live region
37
- useAriaLive(message, type === 'error' ? 'assertive' : 'polite');
38
- // Auto-dismiss
39
- useEffect(() => {
40
- if (duration === 0 || isPaused) {
41
- if (timeoutRef.current) {
42
- clearTimeout(timeoutRef.current);
43
- timeoutRef.current = null;
44
- }
45
- return;
46
- }
47
- timeoutRef.current = setTimeout(() => {
48
- onDismiss(id);
49
- }, duration);
50
- return () => {
51
- if (timeoutRef.current) {
52
- clearTimeout(timeoutRef.current);
53
- }
54
- };
55
- }, [duration, id, onDismiss, isPaused]);
56
- // Handle ESC key
57
- useEffect(() => {
58
- if (!dismissible)
59
- return;
60
- const handleKeyDown = (event) => {
61
- if (isEscapeKey(event.key)) {
62
- onDismiss(id);
63
- }
64
- };
65
- document.addEventListener('keydown', handleKeyDown);
66
- return () => {
67
- document.removeEventListener('keydown', handleKeyDown);
68
- };
69
- }, [dismissible, id, onDismiss]);
70
- const handleMouseEnter = () => {
71
- if (pauseOnHover) {
72
- setIsPaused(true);
73
- }
74
- };
75
- const handleMouseLeave = () => {
76
- if (pauseOnHover) {
77
- setIsPaused(false);
78
- }
79
- };
80
- const handleDismiss = () => {
81
- onDismiss(id);
82
- };
83
- const classes = [
84
- 'toast',
85
- `toast--${type}`,
86
- ]
87
- .filter(Boolean)
88
- .join(' ');
89
- return (_jsxs("div", { className: classes, role: "alert", "aria-live": type === 'error' ? 'assertive' : 'polite', "aria-atomic": "true", tabIndex: 0, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: [_jsx("div", { className: "toast-content", children: _jsx("span", { className: "toast-message", children: message }) }), dismissible && (_jsx(Button, { variant: "ghost", size: "sm", onClick: handleDismiss, "aria-label": "Dismiss notification", className: "toast-dismiss", children: "\u00D7" }))] }));
90
- };
91
- Toast.displayName = 'Toast';
@@ -1,22 +0,0 @@
1
- import React from 'react';
2
- import { ToastProps } from './Toast';
3
- import './ToastProvider.css';
4
- export interface ToastItem extends Omit<ToastProps, 'onDismiss'> {
5
- id: string;
6
- }
7
- interface ToastContextValue {
8
- addToast: (toast: Omit<ToastItem, 'id'>) => void;
9
- removeToast: (id: string) => void;
10
- }
11
- export declare const useToast: () => ToastContextValue;
12
- export interface ToastProviderProps {
13
- children: React.ReactNode;
14
- position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center';
15
- }
16
- /**
17
- * Toast Provider component
18
- * Manages toast stack and positioning
19
- */
20
- export declare const ToastProvider: React.FC<ToastProviderProps>;
21
- export {};
22
- //# sourceMappingURL=ToastProvider.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ToastProvider.d.ts","sourceRoot":"","sources":["../../../src/components/Toast/ToastProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA2D,MAAM,OAAO,CAAA;AAE/E,OAAO,EAAS,UAAU,EAAE,MAAM,SAAS,CAAA;AAC3C,OAAO,qBAAqB,CAAA;AAE5B,MAAM,WAAW,SAAU,SAAQ,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC;IAC9D,EAAE,EAAE,MAAM,CAAA;CACX;AAED,UAAU,iBAAiB;IACzB,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,KAAK,IAAI,CAAA;IAChD,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CAClC;AAID,eAAO,MAAM,QAAQ,yBAMpB,CAAA;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,QAAQ,CAAC,EAAE,WAAW,GAAG,UAAU,GAAG,cAAc,GAAG,aAAa,GAAG,YAAY,GAAG,eAAe,CAAA;CACtG;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAoCtD,CAAA"}
@@ -1,33 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { createContext, useContext, useState, useCallback } from 'react';
4
- import { createPortal } from 'react-dom';
5
- import { Toast } from './Toast';
6
- import './ToastProvider.css';
7
- const ToastContext = createContext(undefined);
8
- export const useToast = () => {
9
- const context = useContext(ToastContext);
10
- if (!context) {
11
- throw new Error('useToast must be used within ToastProvider');
12
- }
13
- return context;
14
- };
15
- /**
16
- * Toast Provider component
17
- * Manages toast stack and positioning
18
- */
19
- export const ToastProvider = ({ children, position = 'top-right', }) => {
20
- const [toasts, setToasts] = useState([]);
21
- const addToast = useCallback((toast) => {
22
- const id = `toast-${Date.now()}-${Math.random()}`;
23
- setToasts((prev) => [...prev, { ...toast, id }]);
24
- }, []);
25
- const removeToast = useCallback((id) => {
26
- setToasts((prev) => prev.filter((toast) => toast.id !== id));
27
- }, []);
28
- // Always render container to maintain consistent DOM position for focus order
29
- // Toasts appear in order they were added (newest last), maintaining consistent tab order
30
- const toastContainer = (_jsx("div", { className: `toast-container toast-container--${position}`, role: "region", "aria-label": "Notifications", children: toasts.map((toast) => (_jsx(Toast, { ...toast, onDismiss: removeToast }, toast.id))) }));
31
- return (_jsxs(ToastContext.Provider, { value: { addToast, removeToast }, children: [children, typeof document !== 'undefined' &&
32
- createPortal(toastContainer, document.body)] }));
33
- };
@@ -1,5 +0,0 @@
1
- export { Toast } from './Toast';
2
- export { ToastProvider, useToast } from './ToastProvider';
3
- export type { ToastProps } from './Toast';
4
- export type { ToastItem, ToastProviderProps } from './ToastProvider';
5
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Toast/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACzC,YAAY,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA"}
@@ -1,2 +0,0 @@
1
- export { Toast } from './Toast';
2
- export { ToastProvider, useToast } from './ToastProvider';
@@ -1,9 +0,0 @@
1
- /**
2
- * Hook to manage ARIA live regions for screen reader announcements
3
- *
4
- * @param message - Message to announce
5
- * @param priority - 'polite' (default) or 'assertive'
6
- * @param clearOnUnmount - Whether to clear the message on unmount
7
- */
8
- export declare function useAriaLive(message: string | undefined, priority?: 'polite' | 'assertive', clearOnUnmount?: boolean): void;
9
- //# sourceMappingURL=useAriaLive.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useAriaLive.d.ts","sourceRoot":"","sources":["../../src/hooks/useAriaLive.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,QAAQ,GAAE,QAAQ,GAAG,WAAsB,EAC3C,cAAc,GAAE,OAAc,GAC7B,IAAI,CAkCN"}
@@ -1,39 +0,0 @@
1
- 'use client';
2
- import { useEffect, useRef } from 'react';
3
- /**
4
- * Hook to manage ARIA live regions for screen reader announcements
5
- *
6
- * @param message - Message to announce
7
- * @param priority - 'polite' (default) or 'assertive'
8
- * @param clearOnUnmount - Whether to clear the message on unmount
9
- */
10
- export function useAriaLive(message, priority = 'polite', clearOnUnmount = true) {
11
- const liveRegionRef = useRef(null);
12
- useEffect(() => {
13
- // Create or get the live region element
14
- let liveRegion = document.getElementById(`aria-live-${priority}`);
15
- if (!liveRegion) {
16
- liveRegion = document.createElement('div');
17
- liveRegion.id = `aria-live-${priority}`;
18
- liveRegion.setAttribute('role', 'status');
19
- liveRegion.setAttribute('aria-live', priority);
20
- liveRegion.setAttribute('aria-atomic', 'true');
21
- liveRegion.style.position = 'absolute';
22
- liveRegion.style.left = '-10000px';
23
- liveRegion.style.width = '1px';
24
- liveRegion.style.height = '1px';
25
- liveRegion.style.overflow = 'hidden';
26
- document.body.appendChild(liveRegion);
27
- }
28
- liveRegionRef.current = liveRegion;
29
- // Update the message
30
- if (message) {
31
- liveRegion.textContent = message;
32
- }
33
- return () => {
34
- if (clearOnUnmount && liveRegionRef.current) {
35
- liveRegionRef.current.textContent = '';
36
- }
37
- };
38
- }, [message, priority, clearOnUnmount]);
39
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * Hook to return focus to a previously focused element when component unmounts
3
- * Useful for modals, dropdowns, and other temporary UI elements
4
- *
5
- * @param returnOnUnmount - Whether to return focus on unmount
6
- * @param returnElement - Optional specific element to return focus to
7
- */
8
- export declare function useFocusReturn(returnOnUnmount?: boolean, returnElement?: HTMLElement | null): void;
9
- //# sourceMappingURL=useFocusReturn.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useFocusReturn.d.ts","sourceRoot":"","sources":["../../src/hooks/useFocusReturn.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,eAAe,GAAE,OAAc,EAC/B,aAAa,CAAC,EAAE,WAAW,GAAG,IAAI,GACjC,IAAI,CAwBN"}