@a11ypros/a11y-ui-components 1.0.0
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/dist/components/Button/Button.css +259 -0
- 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.css +132 -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.css +46 -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.css +43 -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.css +76 -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.css +13 -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.css +81 -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.css +69 -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.css +79 -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.css +70 -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.css +118 -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.css +132 -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.css +100 -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.css +48 -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/dist/index.js +25 -0
- package/dist/styles/components.css +33 -0
- package/dist/styles/global.css +289 -0
- 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 +68 -0
|
@@ -0,0 +1,59 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
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';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
.toast-container {
|
|
2
|
+
position: fixed;
|
|
3
|
+
z-index: 1000;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: var(--spacing-2, 0.5rem);
|
|
7
|
+
padding: var(--spacing-4, 1rem);
|
|
8
|
+
pointer-events: none;
|
|
9
|
+
/* Container is not focusable - only children (toasts) are focusable */
|
|
10
|
+
/* This ensures consistent focus order without interfering with tab navigation */
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.toast-container > * {
|
|
14
|
+
pointer-events: auto;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.toast-container--top-right {
|
|
18
|
+
top: 0;
|
|
19
|
+
right: 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.toast-container--top-left {
|
|
23
|
+
top: 0;
|
|
24
|
+
left: 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.toast-container--top-center {
|
|
28
|
+
top: 0;
|
|
29
|
+
left: 50%;
|
|
30
|
+
transform: translateX(-50%);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.toast-container--bottom-right {
|
|
34
|
+
bottom: 0;
|
|
35
|
+
right: 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.toast-container--bottom-left {
|
|
39
|
+
bottom: 0;
|
|
40
|
+
left: 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.toast-container--bottom-center {
|
|
44
|
+
bottom: 0;
|
|
45
|
+
left: 50%;
|
|
46
|
+
transform: translateX(-50%);
|
|
47
|
+
}
|
|
48
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Hook to return focus to a previously focused element when component unmounts
|
|
5
|
+
* Useful for modals, dropdowns, and other temporary UI elements
|
|
6
|
+
*
|
|
7
|
+
* @param returnOnUnmount - Whether to return focus on unmount
|
|
8
|
+
* @param returnElement - Optional specific element to return focus to
|
|
9
|
+
*/
|
|
10
|
+
export function useFocusReturn(returnOnUnmount = true, returnElement) {
|
|
11
|
+
const savedElementRef = useRef(null);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!returnOnUnmount)
|
|
14
|
+
return;
|
|
15
|
+
// Save the currently focused element
|
|
16
|
+
if (document.activeElement instanceof HTMLElement) {
|
|
17
|
+
savedElementRef.current = document.activeElement;
|
|
18
|
+
}
|
|
19
|
+
return () => {
|
|
20
|
+
// Restore focus on unmount
|
|
21
|
+
const elementToFocus = returnElement || savedElementRef.current;
|
|
22
|
+
if (elementToFocus && typeof elementToFocus.focus === 'function') {
|
|
23
|
+
try {
|
|
24
|
+
elementToFocus.focus();
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
// Silently fail if focus fails
|
|
28
|
+
console.warn('Failed to return focus:', error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}, [returnOnUnmount, returnElement]);
|
|
33
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook to trap focus within a container element
|
|
3
|
+
* Implements WCAG 2.1.2 Keyboard (No Keyboard Trap)
|
|
4
|
+
*
|
|
5
|
+
* @param enabled - Whether the focus trap is active
|
|
6
|
+
* @param containerRef - Ref to the container element
|
|
7
|
+
*/
|
|
8
|
+
export declare function useFocusTrap(enabled: boolean, containerRef: React.RefObject<HTMLElement>): void;
|
|
9
|
+
//# sourceMappingURL=useFocusTrap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFocusTrap.d.ts","sourceRoot":"","sources":["../../src/hooks/useFocusTrap.ts"],"names":[],"mappings":"AAKA;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,OAAO,EAChB,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,GACzC,IAAI,CAiEN"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { getFirstFocusable, getLastFocusable } from '../utils/focus';
|
|
4
|
+
/**
|
|
5
|
+
* Hook to trap focus within a container element
|
|
6
|
+
* Implements WCAG 2.1.2 Keyboard (No Keyboard Trap)
|
|
7
|
+
*
|
|
8
|
+
* @param enabled - Whether the focus trap is active
|
|
9
|
+
* @param containerRef - Ref to the container element
|
|
10
|
+
*/
|
|
11
|
+
export function useFocusTrap(enabled, containerRef) {
|
|
12
|
+
const previousActiveElement = useRef(null);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!enabled || !containerRef.current)
|
|
15
|
+
return;
|
|
16
|
+
const container = containerRef.current;
|
|
17
|
+
// Save the previously focused element
|
|
18
|
+
if (document.activeElement instanceof HTMLElement) {
|
|
19
|
+
previousActiveElement.current = document.activeElement;
|
|
20
|
+
}
|
|
21
|
+
// Focus the first focusable element in the container
|
|
22
|
+
const firstFocusable = getFirstFocusable(container);
|
|
23
|
+
if (firstFocusable) {
|
|
24
|
+
firstFocusable.focus();
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// If no focusable elements, focus the container itself
|
|
28
|
+
container.focus();
|
|
29
|
+
}
|
|
30
|
+
// Handle Tab key to trap focus
|
|
31
|
+
const handleKeyDown = (event) => {
|
|
32
|
+
if (event.key !== 'Tab')
|
|
33
|
+
return;
|
|
34
|
+
const focusableElements = [
|
|
35
|
+
getFirstFocusable(container),
|
|
36
|
+
getLastFocusable(container),
|
|
37
|
+
].filter(Boolean);
|
|
38
|
+
if (focusableElements.length === 0) {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const firstElement = focusableElements[0];
|
|
43
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
44
|
+
if (event.shiftKey) {
|
|
45
|
+
// Shift + Tab
|
|
46
|
+
if (document.activeElement === firstElement) {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
lastElement.focus();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Tab
|
|
53
|
+
if (document.activeElement === lastElement) {
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
firstElement.focus();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
container.addEventListener('keydown', handleKeyDown);
|
|
60
|
+
return () => {
|
|
61
|
+
container.removeEventListener('keydown', handleKeyDown);
|
|
62
|
+
// Restore focus to the previously focused element
|
|
63
|
+
if (previousActiveElement.current) {
|
|
64
|
+
previousActiveElement.current.focus();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}, [enabled, containerRef]);
|
|
68
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export * from './tokens';
|
|
2
|
+
export * from './components/Button/Button';
|
|
3
|
+
export * from './components/Link/Link';
|
|
4
|
+
export * from './components/Modal/Modal';
|
|
5
|
+
export * from './components/DataTable/DataTable';
|
|
6
|
+
export * from './components/Toast/Toast';
|
|
7
|
+
export * from './components/Toast/ToastProvider';
|
|
8
|
+
export * from './components/Tabs/Tabs';
|
|
9
|
+
export * from './components/Form/Input';
|
|
10
|
+
export * from './components/Form/Textarea';
|
|
11
|
+
export * from './components/Form/Select';
|
|
12
|
+
export * from './components/Form/Checkbox';
|
|
13
|
+
export * from './components/Form/Radio';
|
|
14
|
+
export * from './components/Form/Fieldset';
|
|
15
|
+
export * from './components/Form/Label';
|
|
16
|
+
export * from './hooks/useFocusTrap';
|
|
17
|
+
export * from './hooks/useFocusReturn';
|
|
18
|
+
export * from './hooks/useAriaLive';
|
|
19
|
+
export * from './utils/aria';
|
|
20
|
+
export * from './utils/keyboard';
|
|
21
|
+
export * from './utils/focus';
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,UAAU,CAAC;AAGzB,cAAc,4BAA4B,CAAC;AAC3C,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,kCAAkC,CAAC;AACjD,cAAc,0BAA0B,CAAC;AACzC,cAAc,kCAAkC,CAAC;AACjD,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AAGxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AAGpC,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Design tokens
|
|
2
|
+
export * from './tokens';
|
|
3
|
+
// Components
|
|
4
|
+
export * from './components/Button/Button';
|
|
5
|
+
export * from './components/Link/Link';
|
|
6
|
+
export * from './components/Modal/Modal';
|
|
7
|
+
export * from './components/DataTable/DataTable';
|
|
8
|
+
export * from './components/Toast/Toast';
|
|
9
|
+
export * from './components/Toast/ToastProvider';
|
|
10
|
+
export * from './components/Tabs/Tabs';
|
|
11
|
+
export * from './components/Form/Input';
|
|
12
|
+
export * from './components/Form/Textarea';
|
|
13
|
+
export * from './components/Form/Select';
|
|
14
|
+
export * from './components/Form/Checkbox';
|
|
15
|
+
export * from './components/Form/Radio';
|
|
16
|
+
export * from './components/Form/Fieldset';
|
|
17
|
+
export * from './components/Form/Label';
|
|
18
|
+
// Hooks
|
|
19
|
+
export * from './hooks/useFocusTrap';
|
|
20
|
+
export * from './hooks/useFocusReturn';
|
|
21
|
+
export * from './hooks/useAriaLive';
|
|
22
|
+
// Utils
|
|
23
|
+
export * from './utils/aria';
|
|
24
|
+
export * from './utils/keyboard';
|
|
25
|
+
export * from './utils/focus';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import all component styles
|
|
3
|
+
* Import this file to get all component styles
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* Button */
|
|
7
|
+
@import '../components/Button/Button.css';
|
|
8
|
+
|
|
9
|
+
/* Link */
|
|
10
|
+
@import '../components/Link/Link.css';
|
|
11
|
+
|
|
12
|
+
/* Modal */
|
|
13
|
+
@import '../components/Modal/Modal.css';
|
|
14
|
+
|
|
15
|
+
/* DataTable */
|
|
16
|
+
@import '../components/DataTable/DataTable.css';
|
|
17
|
+
|
|
18
|
+
/* Toast */
|
|
19
|
+
@import '../components/Toast/Toast.css';
|
|
20
|
+
@import '../components/Toast/ToastProvider.css';
|
|
21
|
+
|
|
22
|
+
/* Tabs */
|
|
23
|
+
@import '../components/Tabs/Tabs.css';
|
|
24
|
+
|
|
25
|
+
/* Form components */
|
|
26
|
+
@import '../components/Form/Label.css';
|
|
27
|
+
@import '../components/Form/Input.css';
|
|
28
|
+
@import '../components/Form/Textarea.css';
|
|
29
|
+
@import '../components/Form/Select.css';
|
|
30
|
+
@import '../components/Form/Checkbox.css';
|
|
31
|
+
@import '../components/Form/Radio.css';
|
|
32
|
+
@import '../components/Form/Fieldset.css';
|
|
33
|
+
|