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