@abstraks-dev/ui-library 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/LICENSE +21 -0
- package/README.md +708 -0
- package/dist/__tests__/Anchor.test.js +145 -0
- package/dist/__tests__/ArrowRight.test.js +91 -0
- package/dist/__tests__/Avatar.test.js +123 -0
- package/dist/__tests__/Button.test.js +82 -0
- package/dist/__tests__/Card.test.js +198 -0
- package/dist/__tests__/CheckCircle.test.js +98 -0
- package/dist/__tests__/Checkbox.test.js +161 -0
- package/dist/__tests__/ChevronDown.test.js +73 -0
- package/dist/__tests__/Close.test.js +98 -0
- package/dist/__tests__/EditSquare.test.js +99 -0
- package/dist/__tests__/Error.test.js +74 -0
- package/dist/__tests__/Footer.test.js +66 -0
- package/dist/__tests__/Heading.test.js +227 -0
- package/dist/__tests__/Hero.test.js +74 -0
- package/dist/__tests__/Label.test.js +123 -0
- package/dist/__tests__/Loader.test.js +115 -0
- package/dist/__tests__/MenuHover.test.js +137 -0
- package/dist/__tests__/Paragraph.test.js +93 -0
- package/dist/__tests__/PlusCircle.test.js +99 -0
- package/dist/__tests__/Radio.test.js +153 -0
- package/dist/__tests__/Select.test.js +187 -0
- package/dist/__tests__/Tabs.test.js +162 -0
- package/dist/__tests__/TextArea.test.js +127 -0
- package/dist/__tests__/TextInput.test.js +181 -0
- package/dist/__tests__/Toggle.test.js +120 -0
- package/dist/__tests__/TrashX.test.js +99 -0
- package/dist/__tests__/useHeadingAccessibility.test.js +144 -0
- package/dist/components/Anchor.js +131 -0
- package/dist/components/Animation.js +129 -0
- package/dist/components/AnimationGroup.js +207 -0
- package/dist/components/AnimationToggle.js +216 -0
- package/dist/components/Avatar.js +153 -0
- package/dist/components/Button.js +218 -0
- package/dist/components/Card.js +222 -0
- package/dist/components/Checkbox.js +305 -0
- package/dist/components/Crud.js +564 -0
- package/dist/components/DragAndDrop.js +337 -0
- package/dist/components/Error.js +206 -0
- package/dist/components/Footer.js +99 -0
- package/dist/components/Form.js +412 -0
- package/dist/components/Header.js +372 -0
- package/dist/components/Heading.js +134 -0
- package/dist/components/Hero.js +181 -0
- package/dist/components/Label.js +256 -0
- package/dist/components/Loader.js +302 -0
- package/dist/components/MenuHover.js +114 -0
- package/dist/components/Paragraph.js +128 -0
- package/dist/components/Prompt.js +61 -0
- package/dist/components/Radio.js +254 -0
- package/dist/components/Select.js +422 -0
- package/dist/components/SideMenu.js +313 -0
- package/dist/components/Tabs.js +297 -0
- package/dist/components/TextArea.js +370 -0
- package/dist/components/TextInput.js +286 -0
- package/dist/components/Toggle.js +186 -0
- package/dist/components/crudFiles/CrudEditBase.js +150 -0
- package/dist/components/crudFiles/CrudViewBase.js +39 -0
- package/dist/components/crudFiles/crudDevelopment.js +118 -0
- package/dist/components/crudFiles/crudEditHandlers.js +50 -0
- package/dist/constants/animation.js +30 -0
- package/dist/icons/ArrowIcon.js +32 -0
- package/dist/icons/ArrowRight.js +33 -0
- package/dist/icons/CheckCircle.js +33 -0
- package/dist/icons/ChevronDown.js +28 -0
- package/dist/icons/Close.js +33 -0
- package/dist/icons/EditSquare.js +33 -0
- package/dist/icons/Ellipses.js +34 -0
- package/dist/icons/Hamburger.js +39 -0
- package/dist/icons/LoadingSpinner.js +42 -0
- package/dist/icons/PlusCircle.js +33 -0
- package/dist/icons/SaveIcon.js +32 -0
- package/dist/icons/TrashX.js +33 -0
- package/dist/icons/__tests__/CheckCircle.test.js +9 -0
- package/dist/icons/__tests__/ChevronDown.test.js +9 -0
- package/dist/icons/__tests__/Close.test.js +9 -0
- package/dist/icons/__tests__/EditSquare.test.js +9 -0
- package/dist/icons/__tests__/PlusCircle.test.js +9 -0
- package/dist/icons/__tests__/TrashX.test.js +9 -0
- package/dist/icons/index.js +89 -0
- package/dist/index.js +332 -0
- package/dist/setupTests.js +3 -0
- package/dist/styles/_variables.scss +286 -0
- package/dist/styles/anchor.scss +40 -0
- package/dist/styles/animation-accessibility.scss +96 -0
- package/dist/styles/animation-toggle.scss +233 -0
- package/dist/styles/animation.scss +3781 -0
- package/dist/styles/avatar.scss +285 -0
- package/dist/styles/button.scss +430 -0
- package/dist/styles/card.scss +210 -0
- package/dist/styles/checkbox.scss +160 -0
- package/dist/styles/crud.scss +474 -0
- package/dist/styles/dragAndDrop.scss +312 -0
- package/dist/styles/error.scss +232 -0
- package/dist/styles/footer.scss +58 -0
- package/dist/styles/form.scss +420 -0
- package/dist/styles/grid.scss +29 -0
- package/dist/styles/header.scss +276 -0
- package/dist/styles/heading.scss +118 -0
- package/dist/styles/hero.scss +185 -0
- package/dist/styles/htmlElements.scss +20 -0
- package/dist/styles/image.scss +9 -0
- package/dist/styles/label.scss +340 -0
- package/dist/styles/list-item.scss +5 -0
- package/dist/styles/loader.scss +354 -0
- package/dist/styles/logo.scss +19 -0
- package/dist/styles/main.css +9056 -0
- package/dist/styles/main.css.map +1 -0
- package/dist/styles/main.scss +0 -0
- package/dist/styles/menu-hover.scss +30 -0
- package/dist/styles/paragraph.scss +88 -0
- package/dist/styles/prompt.scss +51 -0
- package/dist/styles/radio.scss +202 -0
- package/dist/styles/select.scss +363 -0
- package/dist/styles/side-menu.scss +334 -0
- package/dist/styles/tabs.scss +540 -0
- package/dist/styles/text-area.scss +388 -0
- package/dist/styles/text-input.scss +171 -0
- package/dist/styles/toggle.scss +0 -0
- package/dist/styles/unordered-list.scss +8 -0
- package/dist/utils/ScrollHandler.js +30 -0
- package/dist/utils/accessibility.js +128 -0
- package/dist/utils/heroUtils.js +316 -0
- package/dist/utils/index.js +104 -0
- package/dist/utils/inputValidation.js +29 -0
- package/dist/utils/keyboardNavigation.js +536 -0
- package/dist/utils/labelUtils.js +708 -0
- package/dist/utils/loaderUtils.js +387 -0
- package/dist/utils/menuUtils.js +575 -0
- package/dist/utils/useHeadingAccessibility.js +298 -0
- package/dist/utils/useRadioGroup.js +260 -0
- package/dist/utils/useSelectAccessibility.js +426 -0
- package/dist/utils/useTabsAccessibility.js +278 -0
- package/dist/utils/useTextAreaAccessibility.js +255 -0
- package/dist/utils/useTextInputAccessibility.js +295 -0
- package/dist/utils/useTypographyAccessibility.js +168 -0
- package/dist/utils/useWindowSize.js +32 -0
- package/dist/utils/utils/ScrollHandler.js +26 -0
- package/dist/utils/utils/accessibility.js +133 -0
- package/dist/utils/utils/heroUtils.js +348 -0
- package/dist/utils/utils/index.js +9 -0
- package/dist/utils/utils/inputValidation.js +22 -0
- package/dist/utils/utils/keyboardNavigation.js +664 -0
- package/dist/utils/utils/labelUtils.js +772 -0
- package/dist/utils/utils/loaderUtils.js +436 -0
- package/dist/utils/utils/menuUtils.js +651 -0
- package/dist/utils/utils/useHeadingAccessibility.js +334 -0
- package/dist/utils/utils/useRadioGroup.js +311 -0
- package/dist/utils/utils/useSelectAccessibility.js +498 -0
- package/dist/utils/utils/useTabsAccessibility.js +316 -0
- package/dist/utils/utils/useTextAreaAccessibility.js +303 -0
- package/dist/utils/utils/useTextInputAccessibility.js +338 -0
- package/dist/utils/utils/useTypographyAccessibility.js +180 -0
- package/dist/utils/utils/useWindowSize.js +26 -0
- package/dist/utils/utils/validation.js +131 -0
- package/dist/utils/validation.js +139 -0
- package/package.json +90 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.useSideMenuState = exports.useMenuAnnouncements = exports.scrollLockUtils = exports.menuTestingUtils = exports.menuSelectors = exports.menuPerformanceUtils = exports.menuConfigurations = exports.default = void 0;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
/**
|
|
9
|
+
* Side Menu Component Utilities
|
|
10
|
+
* Provides helper functions and hooks for SideMenu component functionality
|
|
11
|
+
*
|
|
12
|
+
* ## Scroll Lock Implementation
|
|
13
|
+
* This file includes a robust body scroll lock system that:
|
|
14
|
+
* - Uses CSS classes instead of direct style manipulation
|
|
15
|
+
* - Handles multiple overlays with reference counting
|
|
16
|
+
* - Prevents layout shift by compensating for scrollbar width
|
|
17
|
+
* - Supports iOS devices with proper position handling
|
|
18
|
+
* - Provides cleanup mechanisms to prevent memory leaks
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Constants for menu utilities
|
|
23
|
+
* Centralized selectors and attributes to improve maintainability and prevent duplication
|
|
24
|
+
*/
|
|
25
|
+
// Selector for all focusable elements within menus
|
|
26
|
+
// Used for focus trapping and keyboard navigation
|
|
27
|
+
const FOCUSABLE_ELEMENTS_SELECTOR = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
28
|
+
|
|
29
|
+
// Selectors for menu elements and triggers
|
|
30
|
+
// Used by accessibility testing and validation functions
|
|
31
|
+
const MENU_DIALOG_SELECTOR = '[role="dialog"]';
|
|
32
|
+
const MENU_TRIGGER_SELECTOR = '[aria-controls]';
|
|
33
|
+
const SIDE_MENU_CLASS_SELECTOR = '.side-menu';
|
|
34
|
+
|
|
35
|
+
// ARIA attributes
|
|
36
|
+
// Used for consistent attribute handling
|
|
37
|
+
const ARIA_CONTROLS_ATTRIBUTE = 'aria-controls';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Body Scroll Lock Manager
|
|
41
|
+
* Manages body scroll locking with reference counting to handle multiple overlays
|
|
42
|
+
*/
|
|
43
|
+
class ScrollLockManager {
|
|
44
|
+
constructor() {
|
|
45
|
+
this.lockCount = 0;
|
|
46
|
+
this.originalOverflow = null;
|
|
47
|
+
this.originalPaddingRight = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Lock body scroll and prevent layout shift from scrollbar removal
|
|
52
|
+
*/
|
|
53
|
+
lock() {
|
|
54
|
+
if (this.lockCount === 0) {
|
|
55
|
+
// Store original values
|
|
56
|
+
this.originalOverflow = document.body.style.overflow || '';
|
|
57
|
+
this.originalPaddingRight = document.body.style.paddingRight || '';
|
|
58
|
+
|
|
59
|
+
// Calculate scrollbar width to prevent layout shift
|
|
60
|
+
const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
61
|
+
|
|
62
|
+
// Apply scroll lock with proper padding compensation
|
|
63
|
+
document.body.style.overflow = 'hidden';
|
|
64
|
+
if (scrollBarWidth > 0) {
|
|
65
|
+
document.body.style.paddingRight = `${scrollBarWidth}px`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Add CSS class for additional styling
|
|
69
|
+
document.body.classList.add('scroll-locked');
|
|
70
|
+
}
|
|
71
|
+
this.lockCount++;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Unlock body scroll when no more overlays need it locked
|
|
76
|
+
*/
|
|
77
|
+
unlock() {
|
|
78
|
+
this.lockCount = Math.max(0, this.lockCount - 1);
|
|
79
|
+
if (this.lockCount === 0) {
|
|
80
|
+
// Restore original values
|
|
81
|
+
document.body.style.overflow = this.originalOverflow;
|
|
82
|
+
document.body.style.paddingRight = this.originalPaddingRight;
|
|
83
|
+
document.body.classList.remove('scroll-locked');
|
|
84
|
+
|
|
85
|
+
// Clear stored values
|
|
86
|
+
this.originalOverflow = null;
|
|
87
|
+
this.originalPaddingRight = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Force unlock (useful for cleanup)
|
|
93
|
+
*/
|
|
94
|
+
forceUnlock() {
|
|
95
|
+
this.lockCount = 0;
|
|
96
|
+
this.unlock();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get current lock count
|
|
101
|
+
*/
|
|
102
|
+
getLockCount() {
|
|
103
|
+
return this.lockCount;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Global scroll lock manager instance
|
|
108
|
+
const scrollLockManager = new ScrollLockManager();
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Hook for managing side menu state with accessibility features
|
|
112
|
+
* @param {Object} options - Configuration options
|
|
113
|
+
* @param {boolean} options.initialOpen - Initial menu state
|
|
114
|
+
* @param {boolean} options.closeOnEscape - Close menu on Escape key
|
|
115
|
+
* @param {boolean} options.closeOnBackdrop - Close menu on backdrop click
|
|
116
|
+
* @param {boolean} options.trapFocus - Trap focus within menu
|
|
117
|
+
* @param {Function} options.onOpen - Callback when menu opens
|
|
118
|
+
* @param {Function} options.onClose - Callback when menu closes
|
|
119
|
+
* @returns {Object} Menu state and handlers
|
|
120
|
+
*/
|
|
121
|
+
const useSideMenuState = ({
|
|
122
|
+
initialOpen = false,
|
|
123
|
+
closeOnEscape = true,
|
|
124
|
+
closeOnBackdrop = true,
|
|
125
|
+
trapFocus = true,
|
|
126
|
+
onOpen = null,
|
|
127
|
+
onClose = null
|
|
128
|
+
} = {}) => {
|
|
129
|
+
const [isOpen, setIsOpen] = (0, _react.useState)(initialOpen);
|
|
130
|
+
const [previousFocus, setPreviousFocus] = (0, _react.useState)(null);
|
|
131
|
+
const menuRef = (0, _react.useRef)(null);
|
|
132
|
+
const triggerRef = (0, _react.useRef)(null);
|
|
133
|
+
const openMenu = (0, _react.useCallback)(() => {
|
|
134
|
+
// Store currently focused element
|
|
135
|
+
setPreviousFocus(document.activeElement);
|
|
136
|
+
setIsOpen(true);
|
|
137
|
+
if (onOpen) onOpen();
|
|
138
|
+
|
|
139
|
+
// Focus menu after a short delay to ensure it's rendered
|
|
140
|
+
setTimeout(() => {
|
|
141
|
+
if (menuRef.current) {
|
|
142
|
+
menuRef.current.focus();
|
|
143
|
+
}
|
|
144
|
+
}, 100);
|
|
145
|
+
}, [onOpen]);
|
|
146
|
+
const closeMenu = (0, _react.useCallback)(() => {
|
|
147
|
+
setIsOpen(false);
|
|
148
|
+
if (onClose) onClose();
|
|
149
|
+
|
|
150
|
+
// Restore focus to trigger element
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
if (previousFocus && previousFocus.focus) {
|
|
153
|
+
previousFocus.focus();
|
|
154
|
+
} else if (triggerRef.current) {
|
|
155
|
+
triggerRef.current.focus();
|
|
156
|
+
}
|
|
157
|
+
}, 100);
|
|
158
|
+
}, [onClose, previousFocus]);
|
|
159
|
+
const toggleMenu = (0, _react.useCallback)(() => {
|
|
160
|
+
if (isOpen) {
|
|
161
|
+
closeMenu();
|
|
162
|
+
} else {
|
|
163
|
+
openMenu();
|
|
164
|
+
}
|
|
165
|
+
}, [isOpen, openMenu, closeMenu]);
|
|
166
|
+
|
|
167
|
+
// Handle escape key
|
|
168
|
+
const handleEscapeKey = (0, _react.useCallback)(event => {
|
|
169
|
+
if (closeOnEscape && event.key === 'Escape' && isOpen) {
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
event.stopPropagation();
|
|
172
|
+
closeMenu();
|
|
173
|
+
}
|
|
174
|
+
}, [closeOnEscape, isOpen, closeMenu]);
|
|
175
|
+
|
|
176
|
+
// Handle backdrop click
|
|
177
|
+
const handleBackdropClick = (0, _react.useCallback)(event => {
|
|
178
|
+
if (closeOnBackdrop && isOpen) {
|
|
179
|
+
event.preventDefault();
|
|
180
|
+
closeMenu();
|
|
181
|
+
}
|
|
182
|
+
}, [closeOnBackdrop, isOpen, closeMenu]);
|
|
183
|
+
|
|
184
|
+
// Focus trap functionality
|
|
185
|
+
const handleKeyDown = (0, _react.useCallback)(event => {
|
|
186
|
+
if (!trapFocus || !isOpen || !menuRef.current) return;
|
|
187
|
+
const focusableElements = menuRef.current.querySelectorAll(FOCUSABLE_ELEMENTS_SELECTOR);
|
|
188
|
+
const firstFocusable = focusableElements[0];
|
|
189
|
+
const lastFocusable = focusableElements[focusableElements.length - 1];
|
|
190
|
+
if (event.key === 'Tab') {
|
|
191
|
+
if (event.shiftKey) {
|
|
192
|
+
// Shift + Tab
|
|
193
|
+
if (document.activeElement === firstFocusable) {
|
|
194
|
+
event.preventDefault();
|
|
195
|
+
lastFocusable.focus();
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
// Tab
|
|
199
|
+
if (document.activeElement === lastFocusable) {
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
firstFocusable.focus();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}, [trapFocus, isOpen]);
|
|
206
|
+
|
|
207
|
+
// Attach global event listeners
|
|
208
|
+
(0, _react.useEffect)(() => {
|
|
209
|
+
if (isOpen) {
|
|
210
|
+
document.addEventListener('keydown', handleEscapeKey);
|
|
211
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
212
|
+
|
|
213
|
+
// Lock body scroll when menu is open
|
|
214
|
+
scrollLockManager.lock();
|
|
215
|
+
} else {
|
|
216
|
+
document.removeEventListener('keydown', handleEscapeKey);
|
|
217
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
218
|
+
|
|
219
|
+
// Unlock body scroll
|
|
220
|
+
scrollLockManager.unlock();
|
|
221
|
+
}
|
|
222
|
+
return () => {
|
|
223
|
+
document.removeEventListener('keydown', handleEscapeKey);
|
|
224
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
225
|
+
scrollLockManager.unlock();
|
|
226
|
+
};
|
|
227
|
+
}, [isOpen, handleEscapeKey, handleKeyDown]);
|
|
228
|
+
return {
|
|
229
|
+
isOpen,
|
|
230
|
+
openMenu,
|
|
231
|
+
closeMenu,
|
|
232
|
+
toggleMenu,
|
|
233
|
+
handleBackdropClick,
|
|
234
|
+
menuRef,
|
|
235
|
+
triggerRef
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Hook for managing menu accessibility announcements
|
|
241
|
+
* @param {Object} options - Configuration options
|
|
242
|
+
* @returns {Object} Accessibility utilities
|
|
243
|
+
*/
|
|
244
|
+
exports.useSideMenuState = useSideMenuState;
|
|
245
|
+
const useMenuAnnouncements = ({
|
|
246
|
+
announceStateChanges = true
|
|
247
|
+
} = {}) => {
|
|
248
|
+
const [announcement, setAnnouncement] = (0, _react.useState)('');
|
|
249
|
+
const liveRegionRef = (0, _react.useRef)(null);
|
|
250
|
+
const announce = (0, _react.useCallback)(message => {
|
|
251
|
+
if (!announceStateChanges) return;
|
|
252
|
+
setAnnouncement(message);
|
|
253
|
+
|
|
254
|
+
// Create live region if needed
|
|
255
|
+
if (!liveRegionRef.current) {
|
|
256
|
+
const region = document.createElement('div');
|
|
257
|
+
region.setAttribute('aria-live', 'polite');
|
|
258
|
+
region.setAttribute('aria-atomic', 'true');
|
|
259
|
+
region.className = 'sr-only';
|
|
260
|
+
region.style.cssText = `
|
|
261
|
+
position: absolute !important;
|
|
262
|
+
width: 1px !important;
|
|
263
|
+
height: 1px !important;
|
|
264
|
+
padding: 0 !important;
|
|
265
|
+
margin: -1px !important;
|
|
266
|
+
overflow: hidden !important;
|
|
267
|
+
clip: rect(0, 0, 0, 0) !important;
|
|
268
|
+
white-space: nowrap !important;
|
|
269
|
+
border: 0 !important;
|
|
270
|
+
`;
|
|
271
|
+
document.body.appendChild(region);
|
|
272
|
+
liveRegionRef.current = region;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Announce message
|
|
276
|
+
liveRegionRef.current.textContent = message;
|
|
277
|
+
|
|
278
|
+
// Clear after announcement
|
|
279
|
+
setTimeout(() => {
|
|
280
|
+
if (liveRegionRef.current) {
|
|
281
|
+
liveRegionRef.current.textContent = '';
|
|
282
|
+
}
|
|
283
|
+
}, 1000);
|
|
284
|
+
}, [announceStateChanges]);
|
|
285
|
+
const announceMenuOpened = (0, _react.useCallback)((menuTitle = 'Menu') => {
|
|
286
|
+
announce(`${menuTitle} opened`);
|
|
287
|
+
}, [announce]);
|
|
288
|
+
const announceMenuClosed = (0, _react.useCallback)((menuTitle = 'Menu') => {
|
|
289
|
+
announce(`${menuTitle} closed`);
|
|
290
|
+
}, [announce]);
|
|
291
|
+
|
|
292
|
+
// Cleanup
|
|
293
|
+
(0, _react.useEffect)(() => {
|
|
294
|
+
return () => {
|
|
295
|
+
if (liveRegionRef.current && document.body.contains(liveRegionRef.current)) {
|
|
296
|
+
document.body.removeChild(liveRegionRef.current);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}, []);
|
|
300
|
+
return {
|
|
301
|
+
announcement,
|
|
302
|
+
announce,
|
|
303
|
+
announceMenuOpened,
|
|
304
|
+
announceMenuClosed
|
|
305
|
+
};
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Accessibility testing utilities for side menus
|
|
310
|
+
*/
|
|
311
|
+
exports.useMenuAnnouncements = useMenuAnnouncements;
|
|
312
|
+
const menuTestingUtils = exports.menuTestingUtils = {
|
|
313
|
+
/**
|
|
314
|
+
* Validates menu accessibility attributes
|
|
315
|
+
*/
|
|
316
|
+
validateMenuAccessibility() {
|
|
317
|
+
const menus = document.querySelectorAll(MENU_DIALOG_SELECTOR);
|
|
318
|
+
const results = [];
|
|
319
|
+
menus.forEach(menu => {
|
|
320
|
+
const hasAriaModal = menu.getAttribute('aria-modal') === 'true';
|
|
321
|
+
const hasAriaLabel = !!menu.getAttribute('aria-label');
|
|
322
|
+
const hasAriaLabelledBy = !!menu.getAttribute('aria-labelledby');
|
|
323
|
+
const hasTrigger = !!document.querySelector(`[${ARIA_CONTROLS_ATTRIBUTE}="${menu.id}"]`);
|
|
324
|
+
results.push({
|
|
325
|
+
menuId: menu.id,
|
|
326
|
+
className: menu.className,
|
|
327
|
+
hasAriaModal,
|
|
328
|
+
hasAriaLabel,
|
|
329
|
+
hasAriaLabelledBy,
|
|
330
|
+
hasTrigger,
|
|
331
|
+
isAccessible: hasAriaModal && (hasAriaLabel || hasAriaLabelledBy) && hasTrigger
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
return results;
|
|
335
|
+
},
|
|
336
|
+
/**
|
|
337
|
+
* Tests trigger button accessibility
|
|
338
|
+
*/
|
|
339
|
+
validateTriggerAccessibility() {
|
|
340
|
+
const triggers = document.querySelectorAll(MENU_TRIGGER_SELECTOR);
|
|
341
|
+
const results = [];
|
|
342
|
+
triggers.forEach(trigger => {
|
|
343
|
+
const controlsId = trigger.getAttribute(ARIA_CONTROLS_ATTRIBUTE);
|
|
344
|
+
const hasExpandedState = trigger.hasAttribute('aria-expanded');
|
|
345
|
+
const hasAriaLabel = !!trigger.getAttribute('aria-label');
|
|
346
|
+
const controlsElement = document.getElementById(controlsId);
|
|
347
|
+
const isButton = trigger.tagName === 'BUTTON' || trigger.getAttribute('role') === 'button';
|
|
348
|
+
results.push({
|
|
349
|
+
element: trigger.tagName,
|
|
350
|
+
controlsId,
|
|
351
|
+
hasExpandedState,
|
|
352
|
+
hasAriaLabel,
|
|
353
|
+
controlsElement: !!controlsElement,
|
|
354
|
+
isButton,
|
|
355
|
+
tabIndex: trigger.tabIndex,
|
|
356
|
+
isAccessible: hasExpandedState && (hasAriaLabel || trigger.textContent.trim()) && isButton
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
return results;
|
|
360
|
+
},
|
|
361
|
+
/**
|
|
362
|
+
* Tests focus management
|
|
363
|
+
*/
|
|
364
|
+
validateFocusManagement() {
|
|
365
|
+
const menus = document.querySelectorAll(MENU_DIALOG_SELECTOR);
|
|
366
|
+
const results = [];
|
|
367
|
+
menus.forEach(menu => {
|
|
368
|
+
const isVisible = menu.offsetParent !== null;
|
|
369
|
+
const isFocusable = menu.tabIndex >= 0;
|
|
370
|
+
const focusableChildren = menu.querySelectorAll(FOCUSABLE_ELEMENTS_SELECTOR);
|
|
371
|
+
results.push({
|
|
372
|
+
menuId: menu.id,
|
|
373
|
+
isVisible,
|
|
374
|
+
isFocusable,
|
|
375
|
+
focusableChildrenCount: focusableChildren.length,
|
|
376
|
+
hasFocusableChildren: focusableChildren.length > 0,
|
|
377
|
+
isAccessible: !isVisible || isFocusable && focusableChildren.length > 0
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
return results;
|
|
381
|
+
},
|
|
382
|
+
/**
|
|
383
|
+
* Simulates keyboard navigation
|
|
384
|
+
*/
|
|
385
|
+
async simulateKeyboardNavigation() {
|
|
386
|
+
const triggers = document.querySelectorAll(MENU_TRIGGER_SELECTOR);
|
|
387
|
+
const results = [];
|
|
388
|
+
for (let trigger of triggers) {
|
|
389
|
+
// Test Enter key
|
|
390
|
+
const enterEvent = new KeyboardEvent('keydown', {
|
|
391
|
+
key: 'Enter'
|
|
392
|
+
});
|
|
393
|
+
trigger.dispatchEvent(enterEvent);
|
|
394
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
395
|
+
const controlsId = trigger.getAttribute(ARIA_CONTROLS_ATTRIBUTE);
|
|
396
|
+
const menu = document.getElementById(controlsId);
|
|
397
|
+
const menuVisible = menu && menu.offsetParent !== null;
|
|
398
|
+
|
|
399
|
+
// Test Escape key if menu is visible
|
|
400
|
+
let escapeWorks = false;
|
|
401
|
+
if (menuVisible) {
|
|
402
|
+
const escapeEvent = new KeyboardEvent('keydown', {
|
|
403
|
+
key: 'Escape'
|
|
404
|
+
});
|
|
405
|
+
document.dispatchEvent(escapeEvent);
|
|
406
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
407
|
+
escapeWorks = menu.offsetParent === null;
|
|
408
|
+
}
|
|
409
|
+
results.push({
|
|
410
|
+
triggerText: trigger.textContent?.trim(),
|
|
411
|
+
enterOpensMenu: menuVisible,
|
|
412
|
+
escapeClosesMenu: escapeWorks,
|
|
413
|
+
isAccessible: menuVisible && (escapeWorks || !menuVisible)
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Small delay for real-world simulation
|
|
417
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
418
|
+
}
|
|
419
|
+
return results;
|
|
420
|
+
},
|
|
421
|
+
/**
|
|
422
|
+
* Tests high contrast compatibility
|
|
423
|
+
*/
|
|
424
|
+
validateHighContrast() {
|
|
425
|
+
const menus = document.querySelectorAll(SIDE_MENU_CLASS_SELECTOR);
|
|
426
|
+
const results = [];
|
|
427
|
+
menus.forEach(menu => {
|
|
428
|
+
const overlay = menu.querySelector('.overlay');
|
|
429
|
+
const menuPanel = menu.querySelector('.menu');
|
|
430
|
+
const overlayStyle = overlay ? window.getComputedStyle(overlay) : null;
|
|
431
|
+
const panelStyle = menuPanel ? window.getComputedStyle(menuPanel) : null;
|
|
432
|
+
results.push({
|
|
433
|
+
menuClass: menu.className,
|
|
434
|
+
overlayBackgroundColor: overlayStyle?.backgroundColor,
|
|
435
|
+
panelBackgroundColor: panelStyle?.backgroundColor,
|
|
436
|
+
panelBorder: panelStyle?.border,
|
|
437
|
+
hasContrast: overlayStyle?.backgroundColor !== 'rgba(0, 0, 0, 0)' && panelStyle?.backgroundColor !== 'transparent'
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
return results;
|
|
441
|
+
},
|
|
442
|
+
/**
|
|
443
|
+
* Comprehensive menu audit
|
|
444
|
+
*/
|
|
445
|
+
auditMenu() {
|
|
446
|
+
return {
|
|
447
|
+
accessibility: this.validateMenuAccessibility(),
|
|
448
|
+
triggers: this.validateTriggerAccessibility(),
|
|
449
|
+
focusManagement: this.validateFocusManagement(),
|
|
450
|
+
highContrast: this.validateHighContrast(),
|
|
451
|
+
timestamp: new Date().toISOString()
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Common menu configurations for different use cases
|
|
458
|
+
*/
|
|
459
|
+
const menuConfigurations = exports.menuConfigurations = {
|
|
460
|
+
navigation: {
|
|
461
|
+
ariaLabel: 'Main navigation menu',
|
|
462
|
+
closeOnBackdrop: true,
|
|
463
|
+
closeOnEscape: true,
|
|
464
|
+
trapFocus: true
|
|
465
|
+
},
|
|
466
|
+
settings: {
|
|
467
|
+
ariaLabel: 'Settings menu',
|
|
468
|
+
closeOnBackdrop: true,
|
|
469
|
+
closeOnEscape: true,
|
|
470
|
+
trapFocus: true
|
|
471
|
+
},
|
|
472
|
+
notifications: {
|
|
473
|
+
ariaLabel: 'Notifications panel',
|
|
474
|
+
closeOnBackdrop: true,
|
|
475
|
+
closeOnEscape: true,
|
|
476
|
+
trapFocus: false // Allow background interaction
|
|
477
|
+
},
|
|
478
|
+
quickActions: {
|
|
479
|
+
ariaLabel: 'Quick actions menu',
|
|
480
|
+
closeOnBackdrop: true,
|
|
481
|
+
closeOnEscape: true,
|
|
482
|
+
trapFocus: true
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Performance utilities for menus
|
|
488
|
+
*/
|
|
489
|
+
const menuPerformanceUtils = exports.menuPerformanceUtils = {
|
|
490
|
+
/**
|
|
491
|
+
* Measures menu animation performance
|
|
492
|
+
*/
|
|
493
|
+
measureAnimationPerformance: menuElement => {
|
|
494
|
+
let startTime = performance.now();
|
|
495
|
+
let frameCount = 0;
|
|
496
|
+
const measureFrame = () => {
|
|
497
|
+
frameCount++;
|
|
498
|
+
const currentTime = performance.now();
|
|
499
|
+
const elapsedTime = currentTime - startTime;
|
|
500
|
+
if (elapsedTime >= 1000) {
|
|
501
|
+
const fps = Math.round(frameCount * 1000 / elapsedTime);
|
|
502
|
+
console.log(`Menu animation FPS: ${fps}`);
|
|
503
|
+
frameCount = 0;
|
|
504
|
+
startTime = currentTime;
|
|
505
|
+
}
|
|
506
|
+
if (menuElement && menuElement.offsetParent) {
|
|
507
|
+
requestAnimationFrame(measureFrame);
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
requestAnimationFrame(measureFrame);
|
|
511
|
+
},
|
|
512
|
+
/**
|
|
513
|
+
* Optimizes menu rendering
|
|
514
|
+
*/
|
|
515
|
+
optimizeMenuRendering: menuElement => {
|
|
516
|
+
if (!menuElement) return;
|
|
517
|
+
|
|
518
|
+
// Use GPU acceleration for smooth animations
|
|
519
|
+
menuElement.style.transform = menuElement.style.transform || 'translateZ(0)';
|
|
520
|
+
menuElement.style.backfaceVisibility = 'hidden';
|
|
521
|
+
menuElement.style.perspective = '1000px';
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Scroll Lock Utilities
|
|
527
|
+
* Public API for managing body scroll locking
|
|
528
|
+
*/
|
|
529
|
+
const scrollLockUtils = exports.scrollLockUtils = {
|
|
530
|
+
/**
|
|
531
|
+
* Lock body scroll
|
|
532
|
+
* Use this to prevent body scrolling when overlays are open
|
|
533
|
+
*/
|
|
534
|
+
lock: () => scrollLockManager.lock(),
|
|
535
|
+
/**
|
|
536
|
+
* Unlock body scroll
|
|
537
|
+
* Call this when closing overlays
|
|
538
|
+
*/
|
|
539
|
+
unlock: () => scrollLockManager.unlock(),
|
|
540
|
+
/**
|
|
541
|
+
* Force unlock all scroll locks
|
|
542
|
+
* Useful for cleanup or error recovery
|
|
543
|
+
*/
|
|
544
|
+
forceUnlock: () => scrollLockManager.forceUnlock(),
|
|
545
|
+
/**
|
|
546
|
+
* Get current lock count
|
|
547
|
+
* Useful for debugging multiple overlay scenarios
|
|
548
|
+
*/
|
|
549
|
+
getLockCount: () => scrollLockManager.getLockCount(),
|
|
550
|
+
/**
|
|
551
|
+
* Check if body is currently scroll locked
|
|
552
|
+
*/
|
|
553
|
+
isLocked: () => scrollLockManager.getLockCount() > 0
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Menu Selector Constants
|
|
558
|
+
* Exported for use by other components that need consistent menu selectors
|
|
559
|
+
*/
|
|
560
|
+
const menuSelectors = exports.menuSelectors = {
|
|
561
|
+
FOCUSABLE_ELEMENTS: FOCUSABLE_ELEMENTS_SELECTOR,
|
|
562
|
+
MENU_DIALOG: MENU_DIALOG_SELECTOR,
|
|
563
|
+
MENU_TRIGGER: MENU_TRIGGER_SELECTOR,
|
|
564
|
+
SIDE_MENU_CLASS: SIDE_MENU_CLASS_SELECTOR,
|
|
565
|
+
ARIA_CONTROLS_ATTR: ARIA_CONTROLS_ATTRIBUTE
|
|
566
|
+
};
|
|
567
|
+
var _default = exports.default = {
|
|
568
|
+
useSideMenuState,
|
|
569
|
+
useMenuAnnouncements,
|
|
570
|
+
menuTestingUtils,
|
|
571
|
+
menuConfigurations,
|
|
572
|
+
menuPerformanceUtils,
|
|
573
|
+
scrollLockUtils,
|
|
574
|
+
menuSelectors
|
|
575
|
+
};
|