@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.
Files changed (128) hide show
  1. package/dist/components/Button/Button.css +259 -0
  2. package/dist/components/Button/Button.d.ts +37 -0
  3. package/dist/components/Button/Button.d.ts.map +1 -0
  4. package/dist/components/Button/Button.js +52 -0
  5. package/dist/components/Button/index.d.ts +3 -0
  6. package/dist/components/Button/index.d.ts.map +1 -0
  7. package/dist/components/Button/index.js +1 -0
  8. package/dist/components/DataTable/DataTable.css +132 -0
  9. package/dist/components/DataTable/DataTable.d.ts +71 -0
  10. package/dist/components/DataTable/DataTable.d.ts.map +1 -0
  11. package/dist/components/DataTable/DataTable.js +122 -0
  12. package/dist/components/DataTable/index.d.ts +3 -0
  13. package/dist/components/DataTable/index.d.ts.map +1 -0
  14. package/dist/components/DataTable/index.js +1 -0
  15. package/dist/components/Form/Checkbox.css +46 -0
  16. package/dist/components/Form/Checkbox.d.ts +36 -0
  17. package/dist/components/Form/Checkbox.d.ts.map +1 -0
  18. package/dist/components/Form/Checkbox.js +39 -0
  19. package/dist/components/Form/Fieldset.css +43 -0
  20. package/dist/components/Form/Fieldset.d.ts +33 -0
  21. package/dist/components/Form/Fieldset.d.ts.map +1 -0
  22. package/dist/components/Form/Fieldset.js +34 -0
  23. package/dist/components/Form/Input.css +76 -0
  24. package/dist/components/Form/Input.d.ts +37 -0
  25. package/dist/components/Form/Input.d.ts.map +1 -0
  26. package/dist/components/Form/Input.js +41 -0
  27. package/dist/components/Form/Label.css +13 -0
  28. package/dist/components/Form/Label.d.ts +30 -0
  29. package/dist/components/Form/Label.d.ts.map +1 -0
  30. package/dist/components/Form/Label.js +30 -0
  31. package/dist/components/Form/Radio.css +81 -0
  32. package/dist/components/Form/Radio.d.ts +53 -0
  33. package/dist/components/Form/Radio.d.ts.map +1 -0
  34. package/dist/components/Form/Radio.js +39 -0
  35. package/dist/components/Form/Select.css +69 -0
  36. package/dist/components/Form/Select.d.ts +51 -0
  37. package/dist/components/Form/Select.d.ts.map +1 -0
  38. package/dist/components/Form/Select.js +49 -0
  39. package/dist/components/Form/Textarea.css +79 -0
  40. package/dist/components/Form/Textarea.d.ts +44 -0
  41. package/dist/components/Form/Textarea.d.ts.map +1 -0
  42. package/dist/components/Form/Textarea.js +43 -0
  43. package/dist/components/Form/index.d.ts +8 -0
  44. package/dist/components/Form/index.d.ts.map +1 -0
  45. package/dist/components/Form/index.js +7 -0
  46. package/dist/components/Link/Link.css +70 -0
  47. package/dist/components/Link/Link.d.ts +34 -0
  48. package/dist/components/Link/Link.d.ts.map +1 -0
  49. package/dist/components/Link/Link.js +48 -0
  50. package/dist/components/Link/index.d.ts +3 -0
  51. package/dist/components/Link/index.d.ts.map +1 -0
  52. package/dist/components/Link/index.js +1 -0
  53. package/dist/components/Modal/Modal.css +118 -0
  54. package/dist/components/Modal/Modal.d.ts +64 -0
  55. package/dist/components/Modal/Modal.d.ts.map +1 -0
  56. package/dist/components/Modal/Modal.js +108 -0
  57. package/dist/components/Modal/index.d.ts +3 -0
  58. package/dist/components/Modal/index.d.ts.map +1 -0
  59. package/dist/components/Modal/index.js +1 -0
  60. package/dist/components/Tabs/Tabs.css +132 -0
  61. package/dist/components/Tabs/Tabs.d.ts +63 -0
  62. package/dist/components/Tabs/Tabs.d.ts.map +1 -0
  63. package/dist/components/Tabs/Tabs.js +134 -0
  64. package/dist/components/Tabs/index.d.ts +3 -0
  65. package/dist/components/Tabs/index.d.ts.map +1 -0
  66. package/dist/components/Tabs/index.js +1 -0
  67. package/dist/components/Toast/Toast.css +100 -0
  68. package/dist/components/Toast/Toast.d.ts +59 -0
  69. package/dist/components/Toast/Toast.d.ts.map +1 -0
  70. package/dist/components/Toast/Toast.js +91 -0
  71. package/dist/components/Toast/ToastProvider.css +48 -0
  72. package/dist/components/Toast/ToastProvider.d.ts +22 -0
  73. package/dist/components/Toast/ToastProvider.d.ts.map +1 -0
  74. package/dist/components/Toast/ToastProvider.js +33 -0
  75. package/dist/components/Toast/index.d.ts +5 -0
  76. package/dist/components/Toast/index.d.ts.map +1 -0
  77. package/dist/components/Toast/index.js +2 -0
  78. package/dist/hooks/useAriaLive.d.ts +9 -0
  79. package/dist/hooks/useAriaLive.d.ts.map +1 -0
  80. package/dist/hooks/useAriaLive.js +39 -0
  81. package/dist/hooks/useFocusReturn.d.ts +9 -0
  82. package/dist/hooks/useFocusReturn.d.ts.map +1 -0
  83. package/dist/hooks/useFocusReturn.js +33 -0
  84. package/dist/hooks/useFocusTrap.d.ts +9 -0
  85. package/dist/hooks/useFocusTrap.d.ts.map +1 -0
  86. package/dist/hooks/useFocusTrap.js +68 -0
  87. package/dist/index.d.ts +22 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +25 -0
  90. package/dist/styles/components.css +33 -0
  91. package/dist/styles/global.css +289 -0
  92. package/dist/styles/index.d.ts +3 -0
  93. package/dist/styles/index.d.ts.map +1 -0
  94. package/dist/styles/index.js +1 -0
  95. package/dist/tokens/breakpoints.d.ts +25 -0
  96. package/dist/tokens/breakpoints.d.ts.map +1 -0
  97. package/dist/tokens/breakpoints.js +23 -0
  98. package/dist/tokens/colors.d.ts +81 -0
  99. package/dist/tokens/colors.d.ts.map +1 -0
  100. package/dist/tokens/colors.js +86 -0
  101. package/dist/tokens/index.d.ts +6 -0
  102. package/dist/tokens/index.d.ts.map +1 -0
  103. package/dist/tokens/index.js +5 -0
  104. package/dist/tokens/motion.d.ts +30 -0
  105. package/dist/tokens/motion.d.ts.map +1 -0
  106. package/dist/tokens/motion.js +34 -0
  107. package/dist/tokens/spacing.d.ts +22 -0
  108. package/dist/tokens/spacing.d.ts.map +1 -0
  109. package/dist/tokens/spacing.js +20 -0
  110. package/dist/tokens/theme.d.ts +159 -0
  111. package/dist/tokens/theme.d.ts.map +1 -0
  112. package/dist/tokens/theme.js +15 -0
  113. package/dist/tokens/typography.d.ts +45 -0
  114. package/dist/tokens/typography.d.ts.map +1 -0
  115. package/dist/tokens/typography.js +56 -0
  116. package/dist/utils/aria.d.ts +60 -0
  117. package/dist/utils/aria.d.ts.map +1 -0
  118. package/dist/utils/aria.js +86 -0
  119. package/dist/utils/focus.d.ts +30 -0
  120. package/dist/utils/focus.d.ts.map +1 -0
  121. package/dist/utils/focus.js +80 -0
  122. package/dist/utils/index.d.ts +4 -0
  123. package/dist/utils/index.d.ts.map +1 -0
  124. package/dist/utils/index.js +3 -0
  125. package/dist/utils/keyboard.d.ts +38 -0
  126. package/dist/utils/keyboard.d.ts.map +1 -0
  127. package/dist/utils/keyboard.js +59 -0
  128. 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,5 @@
1
+ export { Toast } from './Toast';
2
+ export { ToastProvider, useToast } from './ToastProvider';
3
+ export type { ToastProps } from './Toast';
4
+ export type { ToastItem, ToastProviderProps } from './ToastProvider';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export { Toast } from './Toast';
2
+ export { ToastProvider, useToast } from './ToastProvider';
@@ -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
+ }
@@ -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
+