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