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