@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.
Files changed (217) hide show
  1. package/README.md +182 -157
  2. package/dist/components/Button/Button.d.ts +37 -0
  3. package/dist/components/Button/Button.d.ts.map +1 -0
  4. package/dist/components/Button/Button.js +52 -0
  5. package/dist/components/Button/index.d.ts +3 -0
  6. package/dist/components/Button/index.d.ts.map +1 -0
  7. package/dist/components/Button/index.js +1 -0
  8. package/dist/components/DataTable/DataTable.d.ts +71 -0
  9. package/dist/components/DataTable/DataTable.d.ts.map +1 -0
  10. package/dist/components/DataTable/DataTable.js +122 -0
  11. package/dist/components/DataTable/index.d.ts +3 -0
  12. package/dist/components/DataTable/index.d.ts.map +1 -0
  13. package/dist/components/DataTable/index.js +1 -0
  14. package/dist/components/Form/Checkbox.d.ts +36 -0
  15. package/dist/components/Form/Checkbox.d.ts.map +1 -0
  16. package/dist/components/Form/Checkbox.js +39 -0
  17. package/dist/components/Form/Fieldset.d.ts +33 -0
  18. package/dist/components/Form/Fieldset.d.ts.map +1 -0
  19. package/dist/components/Form/Fieldset.js +34 -0
  20. package/dist/components/Form/Input.d.ts +37 -0
  21. package/dist/components/Form/Input.d.ts.map +1 -0
  22. package/dist/components/Form/Input.js +41 -0
  23. package/dist/components/Form/Label.d.ts +30 -0
  24. package/dist/components/Form/Label.d.ts.map +1 -0
  25. package/dist/components/Form/Label.js +30 -0
  26. package/dist/components/Form/Radio.d.ts +53 -0
  27. package/dist/components/Form/Radio.d.ts.map +1 -0
  28. package/dist/components/Form/Radio.js +39 -0
  29. package/dist/components/Form/Select.d.ts +51 -0
  30. package/dist/components/Form/Select.d.ts.map +1 -0
  31. package/dist/components/Form/Select.js +49 -0
  32. package/dist/components/Form/Textarea.d.ts +44 -0
  33. package/dist/components/Form/Textarea.d.ts.map +1 -0
  34. package/dist/components/Form/Textarea.js +43 -0
  35. package/dist/components/Form/index.d.ts +8 -0
  36. package/dist/components/Form/index.d.ts.map +1 -0
  37. package/dist/components/Form/index.js +7 -0
  38. package/dist/components/Link/Link.d.ts +34 -0
  39. package/dist/components/Link/Link.d.ts.map +1 -0
  40. package/dist/components/Link/Link.js +48 -0
  41. package/dist/components/Link/index.d.ts +3 -0
  42. package/dist/components/Link/index.d.ts.map +1 -0
  43. package/dist/components/Link/index.js +1 -0
  44. package/dist/components/Modal/Modal.d.ts +64 -0
  45. package/dist/components/Modal/Modal.d.ts.map +1 -0
  46. package/dist/components/Modal/Modal.js +108 -0
  47. package/dist/components/Modal/index.d.ts +3 -0
  48. package/dist/components/Modal/index.d.ts.map +1 -0
  49. package/dist/components/Modal/index.js +1 -0
  50. package/dist/components/Tabs/Tabs.d.ts +63 -0
  51. package/dist/components/Tabs/Tabs.d.ts.map +1 -0
  52. package/dist/components/Tabs/Tabs.js +134 -0
  53. package/dist/components/Tabs/index.d.ts +3 -0
  54. package/dist/components/Tabs/index.d.ts.map +1 -0
  55. package/dist/components/Tabs/index.js +1 -0
  56. package/dist/components/Toast/Toast.d.ts +59 -0
  57. package/dist/components/Toast/Toast.d.ts.map +1 -0
  58. package/dist/components/Toast/Toast.js +91 -0
  59. package/dist/components/Toast/ToastProvider.d.ts +22 -0
  60. package/dist/components/Toast/ToastProvider.d.ts.map +1 -0
  61. package/dist/components/Toast/ToastProvider.js +33 -0
  62. package/dist/components/Toast/index.d.ts +5 -0
  63. package/dist/components/Toast/index.d.ts.map +1 -0
  64. package/dist/components/Toast/index.js +2 -0
  65. package/dist/hooks/useAriaLive.d.ts +9 -0
  66. package/dist/hooks/useAriaLive.d.ts.map +1 -0
  67. package/dist/hooks/useAriaLive.js +39 -0
  68. package/dist/hooks/useFocusReturn.d.ts +9 -0
  69. package/dist/hooks/useFocusReturn.d.ts.map +1 -0
  70. package/dist/hooks/useFocusReturn.js +33 -0
  71. package/dist/hooks/useFocusTrap.d.ts +9 -0
  72. package/dist/hooks/useFocusTrap.d.ts.map +1 -0
  73. package/dist/hooks/useFocusTrap.js +68 -0
  74. package/dist/index.d.ts +22 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/{packages/design-system/src/index.ts → dist/index.js} +0 -4
  77. package/dist/styles/index.d.ts +3 -0
  78. package/dist/styles/index.d.ts.map +1 -0
  79. package/dist/styles/index.js +1 -0
  80. package/dist/tokens/breakpoints.d.ts +25 -0
  81. package/dist/tokens/breakpoints.d.ts.map +1 -0
  82. package/dist/tokens/breakpoints.js +23 -0
  83. package/dist/tokens/colors.d.ts +81 -0
  84. package/dist/tokens/colors.d.ts.map +1 -0
  85. package/dist/tokens/colors.js +86 -0
  86. package/dist/tokens/index.d.ts +6 -0
  87. package/dist/tokens/index.d.ts.map +1 -0
  88. package/dist/tokens/index.js +5 -0
  89. package/dist/tokens/motion.d.ts +30 -0
  90. package/dist/tokens/motion.d.ts.map +1 -0
  91. package/dist/tokens/motion.js +34 -0
  92. package/dist/tokens/spacing.d.ts +22 -0
  93. package/dist/tokens/spacing.d.ts.map +1 -0
  94. package/dist/tokens/spacing.js +20 -0
  95. package/dist/tokens/theme.d.ts +159 -0
  96. package/dist/tokens/theme.d.ts.map +1 -0
  97. package/dist/tokens/theme.js +15 -0
  98. package/dist/tokens/typography.d.ts +45 -0
  99. package/dist/tokens/typography.d.ts.map +1 -0
  100. package/dist/tokens/typography.js +56 -0
  101. package/dist/utils/aria.d.ts +60 -0
  102. package/dist/utils/aria.d.ts.map +1 -0
  103. package/dist/utils/aria.js +86 -0
  104. package/dist/utils/focus.d.ts +30 -0
  105. package/dist/utils/focus.d.ts.map +1 -0
  106. package/dist/utils/focus.js +80 -0
  107. package/dist/utils/index.d.ts +4 -0
  108. package/dist/utils/index.d.ts.map +1 -0
  109. package/dist/utils/index.js +3 -0
  110. package/dist/utils/keyboard.d.ts +38 -0
  111. package/dist/utils/keyboard.d.ts.map +1 -0
  112. package/dist/utils/keyboard.js +59 -0
  113. package/package.json +49 -31
  114. package/.storybook/custom.css +0 -69
  115. package/.storybook/main.ts +0 -46
  116. package/.storybook/manager.ts +0 -26
  117. package/.storybook/package.json +0 -6
  118. package/.storybook/preview.tsx +0 -31
  119. package/.storybook/public/logo.png +0 -0
  120. package/.storybook/vite.config.ts +0 -24
  121. package/.storybook/welcome.mdx +0 -97
  122. package/DEPLOYMENT.md +0 -154
  123. package/apps/web/app/(docs)/audit/audit.css +0 -269
  124. package/apps/web/app/(docs)/audit/page.tsx +0 -271
  125. package/apps/web/app/(docs)/components/button/page.tsx +0 -49
  126. package/apps/web/app/(docs)/components/form/page.tsx +0 -92
  127. package/apps/web/app/(docs)/components/link/page.tsx +0 -31
  128. package/apps/web/app/(docs)/components/modal/page.tsx +0 -41
  129. package/apps/web/app/(docs)/components/page.tsx +0 -37
  130. package/apps/web/app/(docs)/components/table/page.tsx +0 -54
  131. package/apps/web/app/(docs)/components/tabs/page.tsx +0 -61
  132. package/apps/web/app/(docs)/components/toast/page.tsx +0 -51
  133. package/apps/web/app/api/audit/route.ts +0 -128
  134. package/apps/web/app/favicon.ico +0 -0
  135. package/apps/web/app/layout.tsx +0 -20
  136. package/apps/web/app/page.tsx +0 -17
  137. package/apps/web/app/styles/globals.css +0 -5
  138. package/apps/web/next-env.d.ts +0 -5
  139. package/apps/web/next.config.js +0 -21
  140. package/apps/web/package.json +0 -28
  141. package/apps/web/public/_headers +0 -17
  142. package/apps/web/public/_redirects +0 -31
  143. package/apps/web/public/logo.png +0 -0
  144. package/apps/web/tsconfig.json +0 -29
  145. package/netlify/functions/audit.ts +0 -163
  146. package/netlify.toml +0 -37
  147. package/packages/design-system/README.md +0 -252
  148. package/packages/design-system/package.json +0 -68
  149. package/packages/design-system/scripts/copy-css.js +0 -63
  150. package/packages/design-system/src/components/Button/Button.stories.tsx +0 -228
  151. package/packages/design-system/src/components/Button/Button.tsx +0 -137
  152. package/packages/design-system/src/components/Button/index.ts +0 -3
  153. package/packages/design-system/src/components/DataTable/DataTable.stories.tsx +0 -211
  154. package/packages/design-system/src/components/DataTable/DataTable.tsx +0 -293
  155. package/packages/design-system/src/components/DataTable/index.ts +0 -3
  156. package/packages/design-system/src/components/Form/Checkbox.stories.tsx +0 -252
  157. package/packages/design-system/src/components/Form/Checkbox.tsx +0 -114
  158. package/packages/design-system/src/components/Form/Fieldset.stories.tsx +0 -210
  159. package/packages/design-system/src/components/Form/Fieldset.tsx +0 -71
  160. package/packages/design-system/src/components/Form/Input.stories.tsx +0 -164
  161. package/packages/design-system/src/components/Form/Input.tsx +0 -113
  162. package/packages/design-system/src/components/Form/Label.tsx +0 -56
  163. package/packages/design-system/src/components/Form/Radio.stories.tsx +0 -265
  164. package/packages/design-system/src/components/Form/Radio.tsx +0 -147
  165. package/packages/design-system/src/components/Form/Select.stories.tsx +0 -295
  166. package/packages/design-system/src/components/Form/Select.tsx +0 -160
  167. package/packages/design-system/src/components/Form/Textarea.stories.tsx +0 -253
  168. package/packages/design-system/src/components/Form/Textarea.tsx +0 -145
  169. package/packages/design-system/src/components/Form/index.ts +0 -8
  170. package/packages/design-system/src/components/Link/Link.stories.tsx +0 -128
  171. package/packages/design-system/src/components/Link/Link.tsx +0 -117
  172. package/packages/design-system/src/components/Link/index.ts +0 -3
  173. package/packages/design-system/src/components/Modal/Modal.stories.tsx +0 -165
  174. package/packages/design-system/src/components/Modal/Modal.tsx +0 -202
  175. package/packages/design-system/src/components/Modal/index.ts +0 -3
  176. package/packages/design-system/src/components/Tabs/Tabs.stories.tsx +0 -213
  177. package/packages/design-system/src/components/Tabs/Tabs.tsx +0 -248
  178. package/packages/design-system/src/components/Tabs/index.ts +0 -3
  179. package/packages/design-system/src/components/Toast/Toast.stories.tsx +0 -153
  180. package/packages/design-system/src/components/Toast/Toast.tsx +0 -175
  181. package/packages/design-system/src/components/Toast/ToastProvider.tsx +0 -73
  182. package/packages/design-system/src/components/Toast/index.ts +0 -5
  183. package/packages/design-system/src/hooks/useAriaLive.ts +0 -51
  184. package/packages/design-system/src/hooks/useFocusReturn.ts +0 -40
  185. package/packages/design-system/src/hooks/useFocusTrap.ts +0 -82
  186. package/packages/design-system/src/styles/index.ts +0 -3
  187. package/packages/design-system/src/tokens/breakpoints.ts +0 -28
  188. package/packages/design-system/src/tokens/colors.ts +0 -98
  189. package/packages/design-system/src/tokens/index.ts +0 -6
  190. package/packages/design-system/src/tokens/motion.ts +0 -41
  191. package/packages/design-system/src/tokens/spacing.ts +0 -24
  192. package/packages/design-system/src/tokens/theme.ts +0 -19
  193. package/packages/design-system/src/tokens/typography.ts +0 -64
  194. package/packages/design-system/src/utils/aria.ts +0 -108
  195. package/packages/design-system/src/utils/focus.ts +0 -87
  196. package/packages/design-system/src/utils/index.ts +0 -4
  197. package/packages/design-system/src/utils/keyboard.ts +0 -77
  198. package/packages/design-system/tsconfig.json +0 -17
  199. package/public/logo.png +0 -0
  200. package/scripts/fix-storybook-paths.js +0 -53
  201. package/tsconfig.json +0 -20
  202. /package/{packages/design-system/src → dist}/components/Button/Button.css +0 -0
  203. /package/{packages/design-system/src → dist}/components/DataTable/DataTable.css +0 -0
  204. /package/{packages/design-system/src → dist}/components/Form/Checkbox.css +0 -0
  205. /package/{packages/design-system/src → dist}/components/Form/Fieldset.css +0 -0
  206. /package/{packages/design-system/src → dist}/components/Form/Input.css +0 -0
  207. /package/{packages/design-system/src → dist}/components/Form/Label.css +0 -0
  208. /package/{packages/design-system/src → dist}/components/Form/Radio.css +0 -0
  209. /package/{packages/design-system/src → dist}/components/Form/Select.css +0 -0
  210. /package/{packages/design-system/src → dist}/components/Form/Textarea.css +0 -0
  211. /package/{packages/design-system/src → dist}/components/Link/Link.css +0 -0
  212. /package/{packages/design-system/src → dist}/components/Modal/Modal.css +0 -0
  213. /package/{packages/design-system/src → dist}/components/Tabs/Tabs.css +0 -0
  214. /package/{packages/design-system/src → dist}/components/Toast/Toast.css +0 -0
  215. /package/{packages/design-system/src → dist}/components/Toast/ToastProvider.css +0 -0
  216. /package/{packages/design-system/src → dist}/styles/components.css +0 -0
  217. /package/{packages/design-system/src → dist}/styles/global.css +0 -0
@@ -1,117 +0,0 @@
1
- 'use client'
2
-
3
- import React from 'react'
4
- import './Link.css'
5
-
6
- export interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
7
- /**
8
- * Whether this is an external link
9
- * Automatically adds rel="noopener noreferrer" for security
10
- */
11
- external?: boolean
12
-
13
- /**
14
- * Whether this is a skip link (for keyboard navigation)
15
- */
16
- skip?: boolean
17
-
18
- /**
19
- * ARIA label for the link (required if no visible text)
20
- */
21
- 'aria-label'?: string
22
- }
23
-
24
- /**
25
- * Accessible Link component
26
- *
27
- * WCAG Compliance:
28
- * - 2.4.4 Link Purpose: Clear link text or aria-label
29
- * - 2.4.7 Focus Visible: Clear focus indicators
30
- * - 4.1.2 Name, Role, Value: Proper semantic HTML
31
- *
32
- * @example
33
- * ```tsx
34
- * <Link href="/about" external>
35
- * Learn more
36
- * </Link>
37
- * ```
38
- */
39
- export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
40
- (
41
- {
42
- external = false,
43
- skip = false,
44
- href,
45
- rel,
46
- target,
47
- className = '',
48
- children,
49
- 'aria-label': ariaLabel,
50
- ...props
51
- },
52
- ref
53
- ) => {
54
- // Determine if link is external based on href or explicit prop
55
- const isExternal =
56
- external ||
57
- (href && (href.startsWith('http') || href.startsWith('//')))
58
-
59
- // Build rel attribute
60
- const relAttributes = React.useMemo(() => {
61
- const attrs = new Set(rel?.split(' ') || [])
62
- if (isExternal) {
63
- attrs.add('noopener')
64
- attrs.add('noreferrer')
65
- }
66
- return Array.from(attrs).join(' ')
67
- }, [isExternal, rel])
68
-
69
- // Set target for external links
70
- const linkTarget = isExternal && !target ? '_blank' : target
71
-
72
- const classes = [
73
- 'link',
74
- skip && 'link--skip',
75
- className,
76
- ]
77
- .filter(Boolean)
78
- .join(' ')
79
-
80
- // Skip links should use button semantics if no href
81
- if (skip && !href) {
82
- return (
83
- <button
84
- ref={ref as any}
85
- className={classes}
86
- aria-label={ariaLabel}
87
- {...(props as any)}
88
- >
89
- {children}
90
- </button>
91
- )
92
- }
93
-
94
- return (
95
- <a
96
- ref={ref}
97
- href={href}
98
- rel={relAttributes || undefined}
99
- target={linkTarget}
100
- className={classes}
101
- aria-label={ariaLabel}
102
- {...props}
103
- >
104
- {children}
105
- {isExternal && (
106
- <span className="link__external-icon" aria-hidden="true">
107
- {' '}
108
- <span aria-label="(opens in new tab)">↗</span>
109
- </span>
110
- )}
111
- </a>
112
- )
113
- }
114
- )
115
-
116
- Link.displayName = 'Link'
117
-
@@ -1,3 +0,0 @@
1
- export { Link } from './Link'
2
- export type { LinkProps } from './Link'
3
-
@@ -1,165 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react'
2
- import { useState } from 'react'
3
- import { Modal } from './Modal'
4
- import { Button } from '../Button/Button'
5
-
6
- /**
7
- * # Modal Component
8
- *
9
- * An accessible modal dialog component with focus trapping, keyboard support,
10
- * and proper ARIA attributes following the WAI-ARIA modal pattern.
11
- *
12
- * ## Usage
13
- *
14
- * ```tsx
15
- * import { Modal } from '@a11ypros/a11y-ui-components'
16
- * import { useState } from 'react'
17
- *
18
- * function MyComponent() {
19
- * const [isOpen, setIsOpen] = useState(false)
20
- *
21
- * return (
22
- * <>
23
- * <Button onClick={() => setIsOpen(true)}>Open Modal</Button>
24
- * <Modal
25
- * isOpen={isOpen}
26
- * onClose={() => setIsOpen(false)}
27
- * title="Modal Title"
28
- * >
29
- * <p>Modal content goes here</p>
30
- * </Modal>
31
- * </>
32
- * )
33
- * }
34
- * ```
35
- *
36
- * ## Features
37
- *
38
- * - **Focus trapping**: Focus is trapped within the modal when open
39
- * - **Focus return**: Focus returns to the trigger element when modal closes
40
- * - **Keyboard support**: ESC key closes the modal
41
- * - **Backdrop click**: Optional backdrop click to close
42
- * - **Portal rendering**: Rendered in a portal to avoid z-index issues
43
- * - **Multiple sizes**: sm, md, lg, and full screen options
44
- *
45
- * ## Accessibility
46
- *
47
- * ### WCAG 2.1/2.2 Compliance
48
- *
49
- * - **2.1.1 Keyboard**: ESC key support, full keyboard navigation
50
- * - **2.1.2 No Keyboard Trap**: Focus returns to trigger element when modal closes
51
- * - **2.4.3 Focus Order**: Focus trapped within modal, proper tab order
52
- * - **2.4.7 Focus Visible**: Clear focus indicators on all interactive elements
53
- * - **4.1.2 Name, Role, Value**: Proper ARIA modal pattern with role="dialog"
54
- * - **4.1.3 Status Messages**: Modal title announced when opened
55
- *
56
- * ### Keyboard Interactions
57
- *
58
- * | Key | Action |
59
- * |-----|--------|
60
- * | **ESC** | Closes the modal |
61
- * | **Tab** | Moves focus to next focusable element (trapped within modal) |
62
- * | **Shift+Tab** | Moves focus to previous focusable element (wraps at boundaries) |
63
- * | **Enter/Space** | Activates buttons or links within modal |
64
- *
65
- * ### Screen Reader Support
66
- *
67
- * - Modal role and title are announced when opened
68
- * - Focus moves to modal content automatically
69
- * - Backdrop is hidden from screen readers (\`aria-hidden="true"\`)
70
- * - Close button has accessible label
71
- * - Focus returns to trigger element when closed
72
- *
73
- * ### Focus Management
74
- *
75
- * - Focus is trapped within modal when open (cannot tab to background content)
76
- * - Focus moves to first focusable element when modal opens
77
- * - Focus returns to trigger element when modal closes
78
- * - Backdrop receives focus trap but is not focusable itself
79
- *
80
- * ## Best Practices
81
- *
82
- * 1. **Always provide a title**: Required for accessibility and user understanding
83
- * 2. **Provide a close mechanism**: Close button, ESC key, or backdrop click
84
- * 3. **Return focus appropriately**: Focus should return to the element that opened the modal
85
- * 4. **Keep content focused**: Don't put too much content in modals - use pages for complex flows
86
- * 5. **Handle mobile**: Ensure modal is usable on mobile devices with proper sizing
87
- *
88
- * ## Common Pitfalls
89
- *
90
- * - Missing title prop (required for accessibility)
91
- * - Not returning focus to trigger element (breaks keyboard navigation flow)
92
- * - Allowing focus to escape modal (should be trapped)
93
- * - Using modals for non-modal content (use regular pages or sections)
94
- * - Too much content in modal (makes it hard to navigate)
95
- * - Missing close button or ESC key support
96
- *
97
- * @component
98
- * @example
99
- * ```tsx
100
- * <Modal
101
- * isOpen={isOpen}
102
- * onClose={() => setIsOpen(false)}
103
- * title="Confirm Action"
104
- * >
105
- * <p>Are you sure you want to proceed?</p>
106
- * </Modal>
107
- * ```
108
- */
109
- const meta: Meta<typeof Modal> = {
110
- title: 'Components/Modal',
111
- component: Modal,
112
- tags: ['autodocs'],
113
- }
114
-
115
- export default meta
116
- type Story = StoryObj<typeof Modal>
117
-
118
- const ModalExample = (args: any) => {
119
- const [isOpen, setIsOpen] = useState(false)
120
- return (
121
- <>
122
- <Button onClick={() => setIsOpen(true)}>Open Modal</Button>
123
- <Modal {...args} isOpen={isOpen} onClose={() => setIsOpen(false)} />
124
- </>
125
- )
126
- }
127
-
128
- /**
129
- * Default modal with standard size. Focus is trapped within the modal
130
- * and returns to the trigger button when closed.
131
- */
132
- export const Default: Story = {
133
- render: ModalExample,
134
- args: {
135
- title: 'Modal Title',
136
- children: <p>This is the modal content. Press ESC to close.</p>,
137
- },
138
- }
139
-
140
- /**
141
- * Small modal for simple confirmations or brief messages.
142
- * Use when you need a compact dialog that doesn't take up much screen space.
143
- */
144
- export const Small: Story = {
145
- render: ModalExample,
146
- args: {
147
- title: 'Small Modal',
148
- size: 'sm',
149
- children: <p>This is a small modal.</p>,
150
- },
151
- }
152
-
153
- /**
154
- * Large modal for more complex content that needs more space.
155
- * Use when displaying forms, detailed information, or multiple sections.
156
- */
157
- export const Large: Story = {
158
- render: ModalExample,
159
- args: {
160
- title: 'Large Modal',
161
- size: 'lg',
162
- children: <p>This is a large modal with more content.</p>,
163
- },
164
- }
165
-
@@ -1,202 +0,0 @@
1
- 'use client'
2
-
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
- export interface ModalProps {
9
- /**
10
- * Whether the modal is open
11
- */
12
- isOpen: boolean
13
-
14
- /**
15
- * Callback when modal should close
16
- */
17
- onClose: () => void
18
-
19
- /**
20
- * Title of the modal (required for accessibility)
21
- */
22
- title: string
23
-
24
- /**
25
- * Content of the modal
26
- */
27
- children: React.ReactNode
28
-
29
- /**
30
- * Whether to close on backdrop click
31
- */
32
- closeOnBackdropClick?: boolean
33
-
34
- /**
35
- * Whether to close on ESC key press
36
- */
37
- closeOnEscape?: boolean
38
-
39
- /**
40
- * Size of the modal
41
- */
42
- size?: 'sm' | 'md' | 'lg' | 'full'
43
-
44
- /**
45
- * Element to return focus to when modal closes
46
- */
47
- returnFocusTo?: HTMLElement | null
48
- }
49
-
50
- /**
51
- * Accessible Modal component using HTML5 dialog element
52
- *
53
- * Uses the native `<dialog>` element which provides:
54
- * - Built-in focus management and focus trapping
55
- * - Automatic body scroll prevention
56
- * - Native backdrop overlay
57
- * - ESC key handling (configurable)
58
- *
59
- * WCAG Compliance:
60
- * - 2.1.1 Keyboard: ESC key support, built-in focus trap
61
- * - 2.1.2 No Keyboard Trap: Focus returns to trigger
62
- * - 2.4.3 Focus Order: Focus trapped within modal (native behavior)
63
- * - 4.1.2 Name, Role, Value: ARIA modal pattern
64
- *
65
- * @example
66
- * ```tsx
67
- * <Modal
68
- * isOpen={isOpen}
69
- * onClose={() => setIsOpen(false)}
70
- * title="Confirm Action"
71
- * >
72
- * <p>Are you sure?</p>
73
- * </Modal>
74
- * ```
75
- */
76
- export const Modal: React.FC<ModalProps> = ({
77
- isOpen,
78
- onClose,
79
- title,
80
- children,
81
- closeOnBackdropClick = true,
82
- closeOnEscape = true,
83
- size = 'md',
84
- returnFocusTo,
85
- }) => {
86
- const dialogRef = useRef<HTMLDialogElement>(null)
87
- const contentRef = useRef<HTMLDivElement>(null)
88
- const titleId = React.useId()
89
- const descriptionId = React.useId()
90
-
91
- // Return focus on close
92
- useFocusReturn(isOpen, returnFocusTo)
93
-
94
- // Handle dialog open/close
95
- useEffect(() => {
96
- const dialog = dialogRef.current
97
- if (!dialog) return
98
-
99
- if (isOpen) {
100
- // Show modal dialog
101
- dialog.showModal();
102
- } else {
103
- // Close dialog
104
- dialog.close();
105
- }
106
-
107
- return () => {
108
- // Cleanup: ensure dialog is closed when component unmounts
109
- if (dialog.open) {
110
- dialog.close();
111
- }
112
- }
113
- }, [isOpen])
114
-
115
- // Handle backdrop clicks
116
- // The ::backdrop pseudo-element doesn't bubble events to the dialog element,
117
- // so we need to listen for clicks on the document and check if they're outside
118
- // the dialog content area.
119
- useEffect(() => {
120
- if (!isOpen || !closeOnBackdropClick) return
121
-
122
- const handleDocumentClick = (event: MouseEvent) => {
123
- const dialog = dialogRef.current
124
- const content = contentRef.current
125
-
126
- if (!dialog || !content) return
127
-
128
- // Check if click target is outside the dialog content area
129
- const target = event.target as Node
130
-
131
- // If the click is not inside the content wrapper, it's a backdrop click
132
- if (!content.contains(target)) {
133
- // Verify click coordinates are outside content bounds for extra safety
134
- const rect = content.getBoundingClientRect()
135
- const clickX = event.clientX
136
- const clickY = event.clientY
137
-
138
- const isOutsideContent =
139
- clickX < rect.left ||
140
- clickX > rect.right ||
141
- clickY < rect.top ||
142
- clickY > rect.bottom
143
-
144
- if (isOutsideContent) {
145
- event.preventDefault()
146
- event.stopPropagation()
147
- onClose()
148
- }
149
- }
150
- }
151
-
152
- // Use capture phase to catch events before they bubble
153
- document.addEventListener('mousedown', handleDocumentClick, true)
154
-
155
- return () => {
156
- document.removeEventListener('mousedown', handleDocumentClick, true)
157
- }
158
- }, [isOpen, closeOnBackdropClick, onClose])
159
-
160
- // Handle cancel event (fires when ESC key is pressed)
161
- const handleCancel = (event: React.SyntheticEvent<HTMLDialogElement>) => {
162
- // Prevent default close behavior
163
- event.preventDefault()
164
- // Only close if closeOnEscape is enabled
165
- if (closeOnEscape) {
166
- onClose();
167
- }
168
- }
169
-
170
- return (
171
- <dialog
172
- ref={dialogRef}
173
- className={`modal modal--${size} ${isOpen ? 'modal--open' : ''}`}
174
- aria-labelledby={titleId}
175
- aria-describedby={descriptionId}
176
- onCancel={handleCancel}
177
- >
178
- <div ref={contentRef} className="modal-content-wrapper">
179
- <div className="modal-header">
180
- <h2 id={titleId} className="modal-title">
181
- {title}
182
- </h2>
183
- <Button
184
- variant="ghost"
185
- size="sm"
186
- onClick={onClose}
187
- aria-label="Close modal"
188
- className="modal-close"
189
- >
190
- ×
191
- </Button>
192
- </div>
193
- <div id={descriptionId} className="modal-content">
194
- {children}
195
- </div>
196
- </div>
197
- </dialog>
198
- )
199
- }
200
-
201
- Modal.displayName = 'Modal'
202
-
@@ -1,3 +0,0 @@
1
- export { Modal } from './Modal'
2
- export type { ModalProps } from './Modal'
3
-
@@ -1,213 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react'
2
- import { useState } from 'react'
3
- import { Tabs } from './Tabs'
4
-
5
- /**
6
- * # Tabs Component
7
- *
8
- * An accessible tabs component following the WAI-ARIA tabs pattern with full keyboard
9
- * navigation support and proper focus management.
10
- *
11
- * ## Usage
12
- *
13
- * ```tsx
14
- * import { Tabs } from '@a11ypros/a11y-ui-components'
15
- *
16
- * function MyComponent() {
17
- * return (
18
- * <Tabs
19
- * aria-label="Settings tabs"
20
- * items={[
21
- * { id: 'general', label: 'General', content: <div>General content</div> },
22
- * { id: 'account', label: 'Account', content: <div>Account content</div> },
23
- * ]}
24
- * />
25
- * )
26
- * }
27
- * ```
28
- *
29
- * ## Features
30
- *
31
- * - **Arrow key navigation**: Navigate between tabs using arrow keys
32
- * - **Activation modes**: Automatic (arrow keys activate immediately) or manual (arrow keys move focus, Enter/Space activates)
33
- * - **Home/End support**: Jump to first or last tab
34
- * - **Orientation support**: Horizontal (default) or vertical layouts
35
- * - **ARIA tabs pattern**: Proper semantic structure and ARIA attributes
36
- * - **Focus management**: Focus moves to active tab panel content
37
- *
38
- * ## Accessibility
39
- *
40
- * ### WCAG 2.1/2.2 Compliance
41
- *
42
- * - **2.1.1 Keyboard**: Full keyboard navigation with arrow keys, Home, and End
43
- * - **4.1.2 Name, Role, Value**: Proper ARIA tabs pattern with role="tablist", role="tab", role="tabpanel"
44
- * - **2.4.3 Focus Order**: Proper focus management between tabs and panels
45
- * - **2.4.7 Focus Visible**: Clear focus indicators on tab buttons
46
- *
47
- * ### Keyboard Interactions
48
- *
49
- * | Key | Action (Automatic) | Action (Manual) |
50
- * |-----|---------------------|-----------------|
51
- * | **Arrow Right/Down** | Move to next tab and activate | Move focus to next tab |
52
- * | **Arrow Left/Up** | Move to previous tab and activate | Move focus to previous tab |
53
- * | **Home** | Move to first tab and activate | Move focus to first tab |
54
- * | **End** | Move to last tab and activate | Move focus to last tab |
55
- * | **Enter/Space** | N/A | Activate focused tab |
56
- * | **Tab** | Move focus to tab panel content | Move focus to tab panel content |
57
- * | **Shift+Tab** | Move focus away from tabs | Move focus away from tabs |
58
- *
59
- * ### Screen Reader Support
60
- *
61
- * - Tab list role and label are announced
62
- * - Active tab is announced with "selected" state
63
- * - Tab panel content is associated with its tab
64
- * - Tab count and position are announced (e.g., "Tab 2 of 3")
65
- *
66
- * ### Focus Management
67
- *
68
- * - Focus moves to active tab when component mounts
69
- * - Arrow keys move focus between tabs
70
- * - Tab key moves focus into tab panel content
71
- * - Focus returns to tab when panel content loses focus
72
- *
73
- * ## Best Practices
74
- *
75
- * 1. **Provide aria-label**: Required for screen readers to understand tab purpose
76
- * 2. **Keep tab labels concise**: Short, descriptive labels work best
77
- * 3. **Use appropriate orientation**: Horizontal for most cases, vertical for sidebars
78
- * 4. **Limit tab count**: Too many tabs can be overwhelming (aim for 5-7 max)
79
- * 5. **Make content accessible**: Ensure tab panel content is keyboard navigable
80
- *
81
- * ## Common Pitfalls
82
- *
83
- * - Missing aria-label (screen readers can't understand tab purpose)
84
- * - Too many tabs (becomes hard to navigate)
85
- * - Non-keyboard accessible content in panels (breaks keyboard navigation)
86
- * - Not following ARIA tabs pattern (breaks screen reader support)
87
- * - Changing tab order dynamically (confuses keyboard users)
88
- *
89
- * @component
90
- * @example
91
- * ```tsx
92
- * <Tabs
93
- * aria-label="Settings"
94
- * items={[
95
- * { id: 'general', label: 'General', content: <SettingsForm /> },
96
- * { id: 'advanced', label: 'Advanced', content: <AdvancedForm /> },
97
- * ]}
98
- * />
99
- * ```
100
- */
101
- const meta: Meta<typeof Tabs> = {
102
- title: 'Components/Tabs',
103
- component: Tabs,
104
- tags: ['autodocs'],
105
- }
106
-
107
- export default meta
108
- type Story = StoryObj<typeof Tabs>
109
-
110
- /**
111
- * Horizontal tabs layout (default). Tabs are arranged in a row at the top,
112
- * with tab panels displayed below. Use for most common tab scenarios.
113
- */
114
- export const Default: Story = {
115
- render: () => {
116
- const [selectedId, setSelectedId] = useState('general')
117
-
118
- return (
119
- <Tabs
120
- aria-label="Settings tabs"
121
- selectedId={selectedId}
122
- onSelectionChange={setSelectedId}
123
- items={[
124
- {
125
- id: 'general',
126
- label: 'General',
127
- content: <div>General settings content</div>,
128
- },
129
- {
130
- id: 'account',
131
- label: 'Account',
132
- content: <div>Account settings content</div>,
133
- },
134
- {
135
- id: 'privacy',
136
- label: 'Privacy',
137
- content: <div>Privacy settings content</div>,
138
- },
139
- ]}
140
- />
141
- )
142
- },
143
- }
144
-
145
- /**
146
- * Vertical tabs layout. Tabs are arranged in a column on the left side,
147
- * with tab panels displayed to the right. Use for sidebar navigation or
148
- * when you have many tabs that would overflow horizontally.
149
- */
150
- export const Vertical: Story = {
151
- render: () => {
152
- const [selectedId, setSelectedId] = useState('tab1')
153
-
154
- return (
155
- <Tabs
156
- aria-label="Vertical tabs"
157
- orientation="vertical"
158
- selectedId={selectedId}
159
- onSelectionChange={setSelectedId}
160
- items={[
161
- {
162
- id: 'tab1',
163
- label: 'Tab 1',
164
- content: <div>Content 1</div>,
165
- },
166
- {
167
- id: 'tab2',
168
- label: 'Tab 2',
169
- content: <div>Content 2</div>,
170
- },
171
- ]}
172
- />
173
- )
174
- },
175
- }
176
-
177
- /**
178
- * Manual activation mode. Arrow keys move focus between tabs without activating them.
179
- * Press Enter or Space to activate the focused tab. This is useful when tab content
180
- * changes are expensive or when you want users to preview tabs before activating.
181
- */
182
- export const ManualActivation: Story = {
183
- render: () => {
184
- const [selectedId, setSelectedId] = useState('general')
185
-
186
- return (
187
- <Tabs
188
- aria-label="Settings tabs with manual activation"
189
- activationMode="manual"
190
- selectedId={selectedId}
191
- onSelectionChange={setSelectedId}
192
- items={[
193
- {
194
- id: 'general',
195
- label: 'General',
196
- content: <div>General settings content</div>,
197
- },
198
- {
199
- id: 'account',
200
- label: 'Account',
201
- content: <div>Account settings content</div>,
202
- },
203
- {
204
- id: 'privacy',
205
- label: 'Privacy',
206
- content: <div>Privacy settings content</div>,
207
- },
208
- ]}
209
- />
210
- )
211
- },
212
- }
213
-