@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,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
+ };