@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,536 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.validateMenuARIA = exports.testRovingTabindex = exports.testKeyboardNavigation = exports.testFocusTrapping = exports.testArrowKeyNavigation = exports.simulateKeyPress = exports.isHighContrastMode = exports.generateTestReport = exports.auditComponentAccessibility = exports.KEYBOARD_KEYS = exports.A11Y_PATTERNS = void 0;
7
+ /**
8
+ * Keyboard Navigation Testing Utilities
9
+ *
10
+ * Provides helper functions for testing keyboard accessibility in components.
11
+ * These utilities help ensure WCAG 2.1 AA compliance for keyboard navigation.
12
+ */
13
+
14
+ /**
15
+ * Essential keyboard event codes for navigation testing
16
+ * Simplified to most commonly used keys
17
+ */
18
+ const KEYBOARD_KEYS = exports.KEYBOARD_KEYS = {
19
+ TAB: 'Tab',
20
+ ENTER: 'Enter',
21
+ SPACE: ' ',
22
+ ESCAPE: 'Escape',
23
+ ARROW_UP: 'ArrowUp',
24
+ ARROW_DOWN: 'ArrowDown',
25
+ ARROW_LEFT: 'ArrowLeft',
26
+ ARROW_RIGHT: 'ArrowRight',
27
+ HOME: 'Home',
28
+ END: 'End'
29
+ };
30
+
31
+ /**
32
+ * Common accessibility test patterns
33
+ */
34
+ const A11Y_PATTERNS = exports.A11Y_PATTERNS = {
35
+ // ARIA roles that require specific keyboard behaviors
36
+ ROVING_TABINDEX: ['tablist', 'menubar', 'listbox', 'tree', 'grid'],
37
+ SINGLE_TAB_STOP: ['radiogroup', 'tablist', 'toolbar'],
38
+ ARROW_NAVIGATION: ['menu', 'menubar', 'listbox', 'tree', 'tablist'],
39
+ // Required ARIA attributes for interactive elements
40
+ REQUIRED_ARIA: {
41
+ button: [],
42
+ menuitem: ['role'],
43
+ tab: ['role', 'aria-selected'],
44
+ tabpanel: ['role', 'aria-labelledby'],
45
+ listbox: ['role'],
46
+ option: ['role', 'aria-selected']
47
+ }
48
+ };
49
+
50
+ /**
51
+ * Tests keyboard navigation for a component
52
+ * @param {HTMLElement} container - The container element to test
53
+ * @param {Object} options - Testing options
54
+ * @returns {Object} Test results
55
+ */
56
+ const testKeyboardNavigation = (container, options = {}) => {
57
+ const {
58
+ expectedFocusableCount = null,
59
+ shouldTrapFocus = false,
60
+ customKeys = [],
61
+ skipNativeElements = false
62
+ } = options;
63
+ const results = {
64
+ passed: true,
65
+ errors: [],
66
+ focusableElements: [],
67
+ tabOrder: []
68
+ };
69
+ try {
70
+ // Get all focusable elements
71
+ const focusableSelectors = ['a[href]', 'button:not([disabled])', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', '[tabindex]:not([tabindex="-1"])', '[role="button"]:not([disabled])', '[role="link"]:not([disabled])', '[role="menuitem"]:not([disabled])', '[role="tab"]:not([disabled])'];
72
+ const focusableElements = container.querySelectorAll(focusableSelectors.join(', '));
73
+ results.focusableElements = Array.from(focusableElements);
74
+
75
+ // Check expected count
76
+ if (expectedFocusableCount !== null && focusableElements.length !== expectedFocusableCount) {
77
+ results.passed = false;
78
+ results.errors.push(`Expected ${expectedFocusableCount} focusable elements, found ${focusableElements.length}`);
79
+ }
80
+
81
+ // Test tab order
82
+ focusableElements.forEach((element, index) => {
83
+ const tabIndex = element.getAttribute('tabindex');
84
+ results.tabOrder.push({
85
+ index,
86
+ element: element.tagName.toLowerCase(),
87
+ tabIndex: tabIndex ? parseInt(tabIndex) : 0,
88
+ ariaLabel: element.getAttribute('aria-label'),
89
+ text: element.textContent?.trim().substring(0, 50)
90
+ });
91
+ });
92
+
93
+ // Check for focus indicators
94
+ focusableElements.forEach((element, index) => {
95
+ const computedStyle = window.getComputedStyle(element, ':focus');
96
+ const hasOutline = computedStyle.outline !== 'none' && computedStyle.outline !== '';
97
+ const hasBoxShadow = computedStyle.boxShadow !== 'none';
98
+ const hasCustomFocus = element.classList.contains('focus-visible') || element.hasAttribute('data-focus-visible');
99
+ if (!hasOutline && !hasBoxShadow && !hasCustomFocus) {
100
+ results.passed = false;
101
+ results.errors.push(`Element ${index + 1} (${element.tagName}) lacks visible focus indicator`);
102
+ }
103
+ });
104
+
105
+ // Test ARIA attributes
106
+ focusableElements.forEach((element, index) => {
107
+ const role = element.getAttribute('role');
108
+ const ariaLabel = element.getAttribute('aria-label');
109
+ const ariaLabelledBy = element.getAttribute('aria-labelledby');
110
+ const ariaDescribedBy = element.getAttribute('aria-describedby');
111
+
112
+ // Interactive elements should have accessible names
113
+ if (!ariaLabel && !ariaLabelledBy && !element.textContent?.trim()) {
114
+ const tagName = element.tagName.toLowerCase();
115
+ if (['button', 'a'].includes(tagName) || role) {
116
+ results.passed = false;
117
+ results.errors.push(`Element ${index + 1} (${tagName}) lacks accessible name`);
118
+ }
119
+ }
120
+ });
121
+ } catch (error) {
122
+ results.passed = false;
123
+ results.errors.push(`Test error: ${error.message}`);
124
+ }
125
+ return results;
126
+ };
127
+
128
+ /**
129
+ * Simulates keyboard events for testing
130
+ * @param {HTMLElement} element - Element to receive the event
131
+ * @param {string} key - Key code to simulate
132
+ * @param {Object} options - Event options
133
+ */
134
+ exports.testKeyboardNavigation = testKeyboardNavigation;
135
+ const simulateKeyPress = (element, key, options = {}) => {
136
+ const {
137
+ shiftKey = false,
138
+ ctrlKey = false,
139
+ altKey = false,
140
+ metaKey = false,
141
+ preventDefault = true
142
+ } = options;
143
+ const event = new KeyboardEvent('keydown', {
144
+ key,
145
+ code: key,
146
+ shiftKey,
147
+ ctrlKey,
148
+ altKey,
149
+ metaKey,
150
+ bubbles: true,
151
+ cancelable: true
152
+ });
153
+ if (preventDefault) {
154
+ event.preventDefault = () => {};
155
+ }
156
+ element.dispatchEvent(event);
157
+ return event;
158
+ };
159
+
160
+ /**
161
+ * Tests focus trapping in modal/dropdown components
162
+ * @param {HTMLElement} container - The container that should trap focus
163
+ * @returns {Object} Test results
164
+ */
165
+ exports.simulateKeyPress = simulateKeyPress;
166
+ const testFocusTrapping = container => {
167
+ const results = {
168
+ passed: true,
169
+ errors: [],
170
+ firstFocusable: null,
171
+ lastFocusable: null
172
+ };
173
+ try {
174
+ const focusableElements = container.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])');
175
+ if (focusableElements.length === 0) {
176
+ results.passed = false;
177
+ results.errors.push('No focusable elements found for focus trapping');
178
+ return results;
179
+ }
180
+ results.firstFocusable = focusableElements[0];
181
+ results.lastFocusable = focusableElements[focusableElements.length - 1];
182
+
183
+ // Test that first element receives focus on container open
184
+ // Test that focus cycles from last to first element
185
+ // Test that focus cycles from first to last element (shift+tab)
186
+ // These would need to be implemented in actual test scenarios
187
+ } catch (error) {
188
+ results.passed = false;
189
+ results.errors.push(`Focus trapping test error: ${error.message}`);
190
+ }
191
+ return results;
192
+ };
193
+
194
+ /**
195
+ * Validates ARIA attributes for dropdown/menu components
196
+ * @param {HTMLElement} trigger - The trigger element
197
+ * @param {HTMLElement} menu - The menu/dropdown element
198
+ * @returns {Object} Validation results
199
+ */
200
+ exports.testFocusTrapping = testFocusTrapping;
201
+ const validateMenuARIA = (trigger, menu) => {
202
+ const results = {
203
+ passed: true,
204
+ errors: [],
205
+ warnings: []
206
+ };
207
+ try {
208
+ // Check trigger attributes
209
+ const ariaExpanded = trigger.getAttribute('aria-expanded');
210
+ const ariaHaspopup = trigger.getAttribute('aria-haspopup');
211
+ const ariaControls = trigger.getAttribute('aria-controls');
212
+ if (!ariaExpanded) {
213
+ results.passed = false;
214
+ results.errors.push('Trigger missing aria-expanded attribute');
215
+ }
216
+ if (!ariaHaspopup) {
217
+ results.warnings.push('Trigger missing aria-haspopup attribute (recommended)');
218
+ }
219
+ if (!ariaControls) {
220
+ results.warnings.push('Trigger missing aria-controls attribute (recommended)');
221
+ }
222
+
223
+ // Check menu attributes
224
+ const menuRole = menu.getAttribute('role');
225
+ const menuId = menu.getAttribute('id');
226
+ const ariaLabelledBy = menu.getAttribute('aria-labelledby');
227
+ if (!menuRole || !['menu', 'listbox', 'tree'].includes(menuRole)) {
228
+ results.passed = false;
229
+ results.errors.push('Menu missing appropriate role attribute');
230
+ }
231
+ if (ariaControls && !menuId) {
232
+ results.passed = false;
233
+ results.errors.push('Menu missing id attribute (referenced by aria-controls)');
234
+ }
235
+ if (ariaControls && menuId && ariaControls !== menuId) {
236
+ results.passed = false;
237
+ results.errors.push('aria-controls value does not match menu id');
238
+ }
239
+
240
+ // Check menu items
241
+ const menuItems = menu.querySelectorAll('[role="menuitem"], [role="option"]');
242
+ menuItems.forEach((item, index) => {
243
+ if (!item.getAttribute('role')) {
244
+ results.warnings.push(`Menu item ${index + 1} missing role attribute`);
245
+ }
246
+ if (!item.getAttribute('aria-label') && !item.textContent?.trim()) {
247
+ results.passed = false;
248
+ results.errors.push(`Menu item ${index + 1} lacks accessible name`);
249
+ }
250
+ });
251
+ } catch (error) {
252
+ results.passed = false;
253
+ results.errors.push(`ARIA validation error: ${error.message}`);
254
+ }
255
+ return results;
256
+ };
257
+
258
+ /**
259
+ * High contrast mode detection for testing
260
+ * @returns {boolean} Whether high contrast mode is active
261
+ */
262
+ exports.validateMenuARIA = validateMenuARIA;
263
+ const isHighContrastMode = () => {
264
+ // Create a test element to detect high contrast mode
265
+ const testElement = document.createElement('div');
266
+ testElement.style.borderWidth = '1px';
267
+ testElement.style.borderStyle = 'solid';
268
+ testElement.style.borderColor = 'red green blue';
269
+ testElement.style.position = 'absolute';
270
+ testElement.style.left = '-9999px';
271
+ document.body.appendChild(testElement);
272
+ const computedStyle = window.getComputedStyle(testElement);
273
+ const borderColors = [computedStyle.borderTopColor, computedStyle.borderRightColor, computedStyle.borderBottomColor, computedStyle.borderLeftColor];
274
+ document.body.removeChild(testElement);
275
+
276
+ // In high contrast mode, all borders will be the same color
277
+ const allSameColor = borderColors.every(color => color === borderColors[0]);
278
+ return allSameColor && borderColors[0] !== 'rgba(0, 0, 0, 0)';
279
+ };
280
+
281
+ /**
282
+ * Generates a keyboard navigation test report
283
+ * @param {Object} testResults - Results from testKeyboardNavigation
284
+ * @returns {string} Formatted report
285
+ */
286
+ exports.isHighContrastMode = isHighContrastMode;
287
+ const generateTestReport = testResults => {
288
+ const {
289
+ passed,
290
+ errors,
291
+ focusableElements,
292
+ tabOrder
293
+ } = testResults;
294
+ let report = `🔍 Keyboard Navigation Test Report\n`;
295
+ report += `Status: ${passed ? '✅ PASSED' : '❌ FAILED'}\n`;
296
+ report += `Focusable Elements: ${focusableElements.length}\n\n`;
297
+ if (errors.length > 0) {
298
+ report += `❌ Errors:\n`;
299
+ errors.forEach((error, index) => {
300
+ report += `${index + 1}. ${error}\n`;
301
+ });
302
+ report += '\n';
303
+ }
304
+ report += `📋 Tab Order:\n`;
305
+ tabOrder.forEach((item, index) => {
306
+ report += `${index + 1}. ${item.element}`;
307
+ if (item.ariaLabel) report += ` (${item.ariaLabel})`;
308
+ if (item.text) report += ` - "${item.text}"`;
309
+ report += '\n';
310
+ });
311
+ return report;
312
+ };
313
+
314
+ /**
315
+ * Tests roving tabindex behavior (common in toolbars, tab lists, etc.)
316
+ * @param {HTMLElement} container - Container with roving tabindex pattern
317
+ * @param {Object} options - Testing options
318
+ * @returns {Object} Test results
319
+ */
320
+ exports.generateTestReport = generateTestReport;
321
+ const testRovingTabindex = (container, options = {}) => {
322
+ const {
323
+ expectedActiveIndex = 0,
324
+ orientation = 'horizontal',
325
+ // 'horizontal', 'vertical', 'both'
326
+ wraparound = true
327
+ } = options;
328
+ const results = {
329
+ passed: true,
330
+ errors: [],
331
+ warnings: [],
332
+ activeElement: null,
333
+ tabbableElements: [],
334
+ focusableElements: []
335
+ };
336
+ try {
337
+ // Find all elements that should participate in roving tabindex
338
+ const allElements = container.querySelectorAll('[role="tab"], [role="menuitem"], [role="option"], .roving-tabindex');
339
+ results.focusableElements = Array.from(allElements);
340
+ if (allElements.length === 0) {
341
+ results.passed = false;
342
+ results.errors.push('No elements found for roving tabindex pattern');
343
+ return results;
344
+ }
345
+
346
+ // Check that only one element has tabindex="0" (is tabbable)
347
+ const tabbableElements = Array.from(allElements).filter(el => el.getAttribute('tabindex') === '0' || !el.hasAttribute('tabindex') && el.tabIndex === 0);
348
+ const nonTabbableElements = Array.from(allElements).filter(el => el.getAttribute('tabindex') === '-1');
349
+ results.tabbableElements = tabbableElements;
350
+ if (tabbableElements.length !== 1) {
351
+ results.passed = false;
352
+ results.errors.push(`Expected exactly 1 tabbable element, found ${tabbableElements.length}`);
353
+ }
354
+ if (tabbableElements.length + nonTabbableElements.length !== allElements.length) {
355
+ results.passed = false;
356
+ results.errors.push('Some elements missing explicit tabindex values (-1 or 0)');
357
+ }
358
+
359
+ // Verify active element matches expected
360
+ if (tabbableElements.length === 1) {
361
+ results.activeElement = tabbableElements[0];
362
+ const activeIndex = Array.from(allElements).indexOf(results.activeElement);
363
+ if (expectedActiveIndex !== null && activeIndex !== expectedActiveIndex) {
364
+ results.warnings.push(`Expected element ${expectedActiveIndex} to be active, but element ${activeIndex} is active`);
365
+ }
366
+ }
367
+
368
+ // Check for proper ARIA attributes
369
+ allElements.forEach((element, index) => {
370
+ const role = element.getAttribute('role');
371
+ if (!role) {
372
+ results.warnings.push(`Element ${index} missing role attribute for roving tabindex`);
373
+ }
374
+
375
+ // Check for aria-selected on tabs/options
376
+ if (['tab', 'option'].includes(role) && !element.hasAttribute('aria-selected')) {
377
+ results.passed = false;
378
+ results.errors.push(`Element ${index} with role="${role}" missing aria-selected attribute`);
379
+ }
380
+ });
381
+ } catch (error) {
382
+ results.passed = false;
383
+ results.errors.push(`Roving tabindex test error: ${error.message}`);
384
+ }
385
+ return results;
386
+ };
387
+
388
+ /**
389
+ * Tests arrow key navigation patterns
390
+ * @param {HTMLElement} container - Container with arrow navigation
391
+ * @param {string} expectedPattern - Expected navigation pattern ('horizontal', 'vertical', 'both', 'grid')
392
+ * @returns {Object} Test results
393
+ */
394
+ exports.testRovingTabindex = testRovingTabindex;
395
+ const testArrowKeyNavigation = (container, expectedPattern = 'horizontal') => {
396
+ const results = {
397
+ passed: true,
398
+ errors: [],
399
+ warnings: [],
400
+ pattern: expectedPattern,
401
+ navigableElements: []
402
+ };
403
+ try {
404
+ // Find navigable elements
405
+ const elements = container.querySelectorAll('[role="tab"], [role="menuitem"], [role="option"], .arrow-navigable');
406
+ results.navigableElements = Array.from(elements);
407
+ if (elements.length < 2) {
408
+ results.warnings.push('Need at least 2 elements to test arrow navigation');
409
+ return results;
410
+ }
411
+
412
+ // Check if elements have proper event handlers (this is tricky to test statically)
413
+ // In real implementation, we'd simulate arrow key events and check focus changes
414
+
415
+ // Check for proper ARIA orientation attribute
416
+ const orientationAttr = container.getAttribute('aria-orientation');
417
+ if (expectedPattern === 'vertical' && orientationAttr !== 'vertical') {
418
+ results.warnings.push('Container should have aria-orientation="vertical" for vertical navigation');
419
+ }
420
+ if (expectedPattern === 'horizontal' && orientationAttr === 'vertical') {
421
+ results.warnings.push('Container has aria-orientation="vertical" but horizontal navigation expected');
422
+ }
423
+
424
+ // Check grid pattern requirements
425
+ if (expectedPattern === 'grid') {
426
+ const gridRole = container.getAttribute('role');
427
+ if (gridRole !== 'grid' && gridRole !== 'treegrid') {
428
+ results.passed = false;
429
+ results.errors.push('Grid navigation pattern requires role="grid" or role="treegrid"');
430
+ }
431
+ }
432
+ } catch (error) {
433
+ results.passed = false;
434
+ results.errors.push(`Arrow navigation test error: ${error.message}`);
435
+ }
436
+ return results;
437
+ };
438
+
439
+ /**
440
+ * Comprehensive accessibility audit for interactive components
441
+ * @param {HTMLElement} component - Component to audit
442
+ * @param {Object} options - Audit options
443
+ * @returns {Object} Comprehensive audit results
444
+ */
445
+ exports.testArrowKeyNavigation = testArrowKeyNavigation;
446
+ const auditComponentAccessibility = (component, options = {}) => {
447
+ const {
448
+ expectedRole = null,
449
+ checkColorContrast = false,
450
+ checkFocusManagement = true,
451
+ checkARIACompliance = true
452
+ } = options;
453
+ const audit = {
454
+ passed: true,
455
+ score: 100,
456
+ errors: [],
457
+ warnings: [],
458
+ recommendations: [],
459
+ tests: {}
460
+ };
461
+ try {
462
+ // Basic keyboard navigation test
463
+ audit.tests.keyboardNavigation = testKeyboardNavigation(component, {
464
+ expectedFocusableCount: null
465
+ });
466
+ if (!audit.tests.keyboardNavigation.passed) {
467
+ audit.passed = false;
468
+ audit.score -= 30;
469
+ }
470
+
471
+ // Check for roving tabindex patterns
472
+ const rovingElements = component.querySelectorAll('[role="tablist"], [role="menubar"], [role="toolbar"]');
473
+ if (rovingElements.length > 0) {
474
+ audit.tests.rovingTabindex = testRovingTabindex(rovingElements[0]);
475
+ if (!audit.tests.rovingTabindex.passed) {
476
+ audit.score -= 20;
477
+ }
478
+ }
479
+
480
+ // ARIA compliance check
481
+ if (checkARIACompliance) {
482
+ const interactiveElements = component.querySelectorAll('button, [role="button"], [role="menuitem"], [role="tab"]');
483
+ interactiveElements.forEach((element, index) => {
484
+ const role = element.getAttribute('role') || element.tagName.toLowerCase();
485
+ const requiredAttrs = A11Y_PATTERNS.REQUIRED_ARIA[role] || [];
486
+ requiredAttrs.forEach(attr => {
487
+ if (!element.hasAttribute(attr)) {
488
+ audit.errors.push(`Element ${index} missing required ${attr} attribute`);
489
+ audit.passed = false;
490
+ audit.score -= 10;
491
+ }
492
+ });
493
+ });
494
+ }
495
+
496
+ // Focus management check
497
+ if (checkFocusManagement) {
498
+ const focusableElements = component.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])');
499
+ if (focusableElements.length === 0) {
500
+ audit.warnings.push('Component has no focusable elements');
501
+ audit.score -= 5;
502
+ }
503
+ }
504
+
505
+ // Expected role check
506
+ if (expectedRole) {
507
+ const actualRole = component.getAttribute('role');
508
+ if (actualRole !== expectedRole) {
509
+ audit.warnings.push(`Expected role="${expectedRole}" but found "${actualRole || 'none'}"`);
510
+ audit.score -= 10;
511
+ }
512
+ }
513
+
514
+ // Generate recommendations based on findings
515
+ if (audit.score < 100) {
516
+ if (audit.errors.length > 0) {
517
+ audit.recommendations.push('Fix critical accessibility errors before release');
518
+ }
519
+ if (audit.warnings.length > 0) {
520
+ audit.recommendations.push('Address accessibility warnings to improve user experience');
521
+ }
522
+ if (audit.score < 70) {
523
+ audit.recommendations.push('Consider comprehensive accessibility review with disabled users');
524
+ }
525
+ }
526
+
527
+ // Ensure score doesn't go below 0
528
+ audit.score = Math.max(0, audit.score);
529
+ } catch (error) {
530
+ audit.passed = false;
531
+ audit.score = 0;
532
+ audit.errors.push(`Audit error: ${error.message}`);
533
+ }
534
+ return audit;
535
+ };
536
+ exports.auditComponentAccessibility = auditComponentAccessibility;