@a11ypros/a11y-ui-components 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +182 -157
- package/dist/components/Button/Button.d.ts +37 -0
- package/dist/components/Button/Button.d.ts.map +1 -0
- package/dist/components/Button/Button.js +52 -0
- package/dist/components/Button/index.d.ts +3 -0
- package/dist/components/Button/index.d.ts.map +1 -0
- package/dist/components/Button/index.js +1 -0
- package/dist/components/DataTable/DataTable.d.ts +71 -0
- package/dist/components/DataTable/DataTable.d.ts.map +1 -0
- package/dist/components/DataTable/DataTable.js +122 -0
- package/dist/components/DataTable/index.d.ts +3 -0
- package/dist/components/DataTable/index.d.ts.map +1 -0
- package/dist/components/DataTable/index.js +1 -0
- package/dist/components/Form/Checkbox.d.ts +36 -0
- package/dist/components/Form/Checkbox.d.ts.map +1 -0
- package/dist/components/Form/Checkbox.js +39 -0
- package/dist/components/Form/Fieldset.d.ts +33 -0
- package/dist/components/Form/Fieldset.d.ts.map +1 -0
- package/dist/components/Form/Fieldset.js +34 -0
- package/dist/components/Form/Input.d.ts +37 -0
- package/dist/components/Form/Input.d.ts.map +1 -0
- package/dist/components/Form/Input.js +41 -0
- package/dist/components/Form/Label.d.ts +30 -0
- package/dist/components/Form/Label.d.ts.map +1 -0
- package/dist/components/Form/Label.js +30 -0
- package/dist/components/Form/Radio.d.ts +53 -0
- package/dist/components/Form/Radio.d.ts.map +1 -0
- package/dist/components/Form/Radio.js +39 -0
- package/dist/components/Form/Select.d.ts +51 -0
- package/dist/components/Form/Select.d.ts.map +1 -0
- package/dist/components/Form/Select.js +49 -0
- package/dist/components/Form/Textarea.d.ts +44 -0
- package/dist/components/Form/Textarea.d.ts.map +1 -0
- package/dist/components/Form/Textarea.js +43 -0
- package/dist/components/Form/index.d.ts +8 -0
- package/dist/components/Form/index.d.ts.map +1 -0
- package/dist/components/Form/index.js +7 -0
- package/dist/components/Link/Link.d.ts +34 -0
- package/dist/components/Link/Link.d.ts.map +1 -0
- package/dist/components/Link/Link.js +48 -0
- package/dist/components/Link/index.d.ts +3 -0
- package/dist/components/Link/index.d.ts.map +1 -0
- package/dist/components/Link/index.js +1 -0
- package/dist/components/Modal/Modal.d.ts +64 -0
- package/dist/components/Modal/Modal.d.ts.map +1 -0
- package/dist/components/Modal/Modal.js +108 -0
- package/dist/components/Modal/index.d.ts +3 -0
- package/dist/components/Modal/index.d.ts.map +1 -0
- package/dist/components/Modal/index.js +1 -0
- package/dist/components/Tabs/Tabs.d.ts +63 -0
- package/dist/components/Tabs/Tabs.d.ts.map +1 -0
- package/dist/components/Tabs/Tabs.js +134 -0
- package/dist/components/Tabs/index.d.ts +3 -0
- package/dist/components/Tabs/index.d.ts.map +1 -0
- package/dist/components/Tabs/index.js +1 -0
- package/dist/components/Toast/Toast.d.ts +59 -0
- package/dist/components/Toast/Toast.d.ts.map +1 -0
- package/dist/components/Toast/Toast.js +91 -0
- package/dist/components/Toast/ToastProvider.d.ts +22 -0
- package/dist/components/Toast/ToastProvider.d.ts.map +1 -0
- package/dist/components/Toast/ToastProvider.js +33 -0
- package/dist/components/Toast/index.d.ts +5 -0
- package/dist/components/Toast/index.d.ts.map +1 -0
- package/dist/components/Toast/index.js +2 -0
- package/dist/hooks/useAriaLive.d.ts +9 -0
- package/dist/hooks/useAriaLive.d.ts.map +1 -0
- package/dist/hooks/useAriaLive.js +39 -0
- package/dist/hooks/useFocusReturn.d.ts +9 -0
- package/dist/hooks/useFocusReturn.d.ts.map +1 -0
- package/dist/hooks/useFocusReturn.js +33 -0
- package/dist/hooks/useFocusTrap.d.ts +9 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -0
- package/dist/hooks/useFocusTrap.js +68 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/{packages/design-system/src/index.ts → dist/index.js} +0 -4
- package/dist/styles/index.d.ts +3 -0
- package/dist/styles/index.d.ts.map +1 -0
- package/dist/styles/index.js +1 -0
- package/dist/tokens/breakpoints.d.ts +25 -0
- package/dist/tokens/breakpoints.d.ts.map +1 -0
- package/dist/tokens/breakpoints.js +23 -0
- package/dist/tokens/colors.d.ts +81 -0
- package/dist/tokens/colors.d.ts.map +1 -0
- package/dist/tokens/colors.js +86 -0
- package/dist/tokens/index.d.ts +6 -0
- package/dist/tokens/index.d.ts.map +1 -0
- package/dist/tokens/index.js +5 -0
- package/dist/tokens/motion.d.ts +30 -0
- package/dist/tokens/motion.d.ts.map +1 -0
- package/dist/tokens/motion.js +34 -0
- package/dist/tokens/spacing.d.ts +22 -0
- package/dist/tokens/spacing.d.ts.map +1 -0
- package/dist/tokens/spacing.js +20 -0
- package/dist/tokens/theme.d.ts +159 -0
- package/dist/tokens/theme.d.ts.map +1 -0
- package/dist/tokens/theme.js +15 -0
- package/dist/tokens/typography.d.ts +45 -0
- package/dist/tokens/typography.d.ts.map +1 -0
- package/dist/tokens/typography.js +56 -0
- package/dist/utils/aria.d.ts +60 -0
- package/dist/utils/aria.d.ts.map +1 -0
- package/dist/utils/aria.js +86 -0
- package/dist/utils/focus.d.ts +30 -0
- package/dist/utils/focus.d.ts.map +1 -0
- package/dist/utils/focus.js +80 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/keyboard.d.ts +38 -0
- package/dist/utils/keyboard.d.ts.map +1 -0
- package/dist/utils/keyboard.js +59 -0
- package/package.json +49 -31
- package/.storybook/custom.css +0 -69
- package/.storybook/main.ts +0 -46
- package/.storybook/manager.ts +0 -26
- package/.storybook/package.json +0 -6
- package/.storybook/preview.tsx +0 -31
- package/.storybook/public/logo.png +0 -0
- package/.storybook/vite.config.ts +0 -24
- package/.storybook/welcome.mdx +0 -97
- package/DEPLOYMENT.md +0 -154
- package/apps/web/app/(docs)/audit/audit.css +0 -269
- package/apps/web/app/(docs)/audit/page.tsx +0 -271
- package/apps/web/app/(docs)/components/button/page.tsx +0 -49
- package/apps/web/app/(docs)/components/form/page.tsx +0 -92
- package/apps/web/app/(docs)/components/link/page.tsx +0 -31
- package/apps/web/app/(docs)/components/modal/page.tsx +0 -41
- package/apps/web/app/(docs)/components/page.tsx +0 -37
- package/apps/web/app/(docs)/components/table/page.tsx +0 -54
- package/apps/web/app/(docs)/components/tabs/page.tsx +0 -61
- package/apps/web/app/(docs)/components/toast/page.tsx +0 -51
- package/apps/web/app/api/audit/route.ts +0 -128
- package/apps/web/app/favicon.ico +0 -0
- package/apps/web/app/layout.tsx +0 -20
- package/apps/web/app/page.tsx +0 -17
- package/apps/web/app/styles/globals.css +0 -5
- package/apps/web/next-env.d.ts +0 -5
- package/apps/web/next.config.js +0 -21
- package/apps/web/package.json +0 -28
- package/apps/web/public/_headers +0 -17
- package/apps/web/public/_redirects +0 -31
- package/apps/web/public/logo.png +0 -0
- package/apps/web/tsconfig.json +0 -29
- package/netlify/functions/audit.ts +0 -163
- package/netlify.toml +0 -37
- package/packages/design-system/README.md +0 -252
- package/packages/design-system/package.json +0 -68
- package/packages/design-system/scripts/copy-css.js +0 -63
- package/packages/design-system/src/components/Button/Button.stories.tsx +0 -228
- package/packages/design-system/src/components/Button/Button.tsx +0 -137
- package/packages/design-system/src/components/Button/index.ts +0 -3
- package/packages/design-system/src/components/DataTable/DataTable.stories.tsx +0 -211
- package/packages/design-system/src/components/DataTable/DataTable.tsx +0 -293
- package/packages/design-system/src/components/DataTable/index.ts +0 -3
- package/packages/design-system/src/components/Form/Checkbox.stories.tsx +0 -252
- package/packages/design-system/src/components/Form/Checkbox.tsx +0 -114
- package/packages/design-system/src/components/Form/Fieldset.stories.tsx +0 -210
- package/packages/design-system/src/components/Form/Fieldset.tsx +0 -71
- package/packages/design-system/src/components/Form/Input.stories.tsx +0 -164
- package/packages/design-system/src/components/Form/Input.tsx +0 -113
- package/packages/design-system/src/components/Form/Label.tsx +0 -56
- package/packages/design-system/src/components/Form/Radio.stories.tsx +0 -265
- package/packages/design-system/src/components/Form/Radio.tsx +0 -147
- package/packages/design-system/src/components/Form/Select.stories.tsx +0 -295
- package/packages/design-system/src/components/Form/Select.tsx +0 -160
- package/packages/design-system/src/components/Form/Textarea.stories.tsx +0 -253
- package/packages/design-system/src/components/Form/Textarea.tsx +0 -145
- package/packages/design-system/src/components/Form/index.ts +0 -8
- package/packages/design-system/src/components/Link/Link.stories.tsx +0 -128
- package/packages/design-system/src/components/Link/Link.tsx +0 -117
- package/packages/design-system/src/components/Link/index.ts +0 -3
- package/packages/design-system/src/components/Modal/Modal.stories.tsx +0 -165
- package/packages/design-system/src/components/Modal/Modal.tsx +0 -202
- package/packages/design-system/src/components/Modal/index.ts +0 -3
- package/packages/design-system/src/components/Tabs/Tabs.stories.tsx +0 -213
- package/packages/design-system/src/components/Tabs/Tabs.tsx +0 -248
- package/packages/design-system/src/components/Tabs/index.ts +0 -3
- package/packages/design-system/src/components/Toast/Toast.stories.tsx +0 -153
- package/packages/design-system/src/components/Toast/Toast.tsx +0 -175
- package/packages/design-system/src/components/Toast/ToastProvider.tsx +0 -73
- package/packages/design-system/src/components/Toast/index.ts +0 -5
- package/packages/design-system/src/hooks/useAriaLive.ts +0 -51
- package/packages/design-system/src/hooks/useFocusReturn.ts +0 -40
- package/packages/design-system/src/hooks/useFocusTrap.ts +0 -82
- package/packages/design-system/src/styles/index.ts +0 -3
- package/packages/design-system/src/tokens/breakpoints.ts +0 -28
- package/packages/design-system/src/tokens/colors.ts +0 -98
- package/packages/design-system/src/tokens/index.ts +0 -6
- package/packages/design-system/src/tokens/motion.ts +0 -41
- package/packages/design-system/src/tokens/spacing.ts +0 -24
- package/packages/design-system/src/tokens/theme.ts +0 -19
- package/packages/design-system/src/tokens/typography.ts +0 -64
- package/packages/design-system/src/utils/aria.ts +0 -108
- package/packages/design-system/src/utils/focus.ts +0 -87
- package/packages/design-system/src/utils/index.ts +0 -4
- package/packages/design-system/src/utils/keyboard.ts +0 -77
- package/packages/design-system/tsconfig.json +0 -17
- package/public/logo.png +0 -0
- package/scripts/fix-storybook-paths.js +0 -53
- package/tsconfig.json +0 -20
- /package/{packages/design-system/src → dist}/components/Button/Button.css +0 -0
- /package/{packages/design-system/src → dist}/components/DataTable/DataTable.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Checkbox.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Fieldset.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Input.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Label.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Radio.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Select.css +0 -0
- /package/{packages/design-system/src → dist}/components/Form/Textarea.css +0 -0
- /package/{packages/design-system/src → dist}/components/Link/Link.css +0 -0
- /package/{packages/design-system/src → dist}/components/Modal/Modal.css +0 -0
- /package/{packages/design-system/src → dist}/components/Tabs/Tabs.css +0 -0
- /package/{packages/design-system/src → dist}/components/Toast/Toast.css +0 -0
- /package/{packages/design-system/src → dist}/components/Toast/ToastProvider.css +0 -0
- /package/{packages/design-system/src → dist}/styles/components.css +0 -0
- /package/{packages/design-system/src → dist}/styles/global.css +0 -0
|
@@ -1,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,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,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
|
-
|