@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,426 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.validateSelectAccessibility = exports.useSelectLiveRegion = exports.useSelectKeyboardNavigation = exports.useSelectAccessibility = void 0;
7
+ var _react = require("react");
8
+ /**
9
+ * Custom hook for managing select state with accessibility features
10
+ * Provides controlled state management for select inputs with validation
11
+ *
12
+ * @param {Object} options - Configuration options
13
+ * @param {string} options.name - Name for the select input (required)
14
+ * @param {string|Array} options.defaultValue - Default selected value(s)
15
+ * @param {boolean} options.multiple - Whether multiple selection is allowed
16
+ * @param {boolean} options.required - Whether selection is required
17
+ * @param {Function} options.onChange - Change callback function
18
+ * @param {Function} options.onValidate - Custom validation function
19
+ * @returns {Object} Select state and handlers
20
+ */
21
+ const useSelectAccessibility = (options = {}) => {
22
+ const {
23
+ name,
24
+ defaultValue = '',
25
+ multiple = false,
26
+ required = false,
27
+ onChange,
28
+ onValidate
29
+ } = options;
30
+ const [selectedValue, setSelectedValue] = (0, _react.useState)(defaultValue);
31
+ const [hasError, setHasError] = (0, _react.useState)(false);
32
+ const [errorMessage, setErrorMessage] = (0, _react.useState)('');
33
+ const [touched, setTouched] = (0, _react.useState)(false);
34
+ const [isFocused, setIsFocused] = (0, _react.useState)(false);
35
+
36
+ // Handle select value change
37
+ const handleChange = (0, _react.useCallback)(event => {
38
+ const value = multiple ? Array.from(event.target.selectedOptions, option => option.value) : event.target.value;
39
+ setSelectedValue(value);
40
+ setTouched(true);
41
+
42
+ // Clear error when user makes a selection
43
+ if (hasError && value && (multiple ? value.length > 0 : value !== '')) {
44
+ setHasError(false);
45
+ setErrorMessage('');
46
+ }
47
+
48
+ // Call external onChange if provided
49
+ onChange?.(event, value);
50
+ }, [hasError, multiple, onChange]);
51
+
52
+ // Handle focus events
53
+ const handleFocus = (0, _react.useCallback)(event => {
54
+ setIsFocused(true);
55
+ }, []);
56
+
57
+ // Handle blur events
58
+ const handleBlur = (0, _react.useCallback)(event => {
59
+ setIsFocused(false);
60
+ setTouched(true);
61
+ }, []);
62
+
63
+ // Validate the current selection
64
+ const validate = (0, _react.useCallback)(() => {
65
+ let isValid = true;
66
+ let message = '';
67
+
68
+ // Required field validation
69
+ if (required) {
70
+ const isEmpty = multiple ? !selectedValue || selectedValue.length === 0 : !selectedValue || selectedValue === '';
71
+ if (isEmpty) {
72
+ isValid = false;
73
+ message = 'Please select an option.';
74
+ }
75
+ }
76
+
77
+ // Custom validation
78
+ if (onValidate && selectedValue) {
79
+ const customValidation = onValidate(selectedValue);
80
+ if (customValidation !== true) {
81
+ isValid = false;
82
+ message = customValidation || 'Invalid selection.';
83
+ }
84
+ }
85
+ setHasError(!isValid);
86
+ setErrorMessage(message);
87
+ return isValid;
88
+ }, [required, selectedValue, onValidate, multiple]);
89
+
90
+ // Reset the select state
91
+ const reset = (0, _react.useCallback)(() => {
92
+ setSelectedValue(defaultValue);
93
+ setHasError(false);
94
+ setErrorMessage('');
95
+ setTouched(false);
96
+ setIsFocused(false);
97
+ }, [defaultValue]);
98
+
99
+ // Set value programmatically
100
+ const setValue = (0, _react.useCallback)(value => {
101
+ setSelectedValue(value);
102
+ setTouched(true);
103
+ if (hasError) {
104
+ setHasError(false);
105
+ setErrorMessage('');
106
+ }
107
+ }, [hasError]);
108
+
109
+ // Get props for the select element
110
+ const getSelectProps = (0, _react.useCallback)(() => ({
111
+ name,
112
+ value: selectedValue,
113
+ onChange: handleChange,
114
+ onFocus: handleFocus,
115
+ onBlur: handleBlur,
116
+ error: hasError,
117
+ errorText: hasError ? errorMessage : '',
118
+ 'data-select-name': name
119
+ }), [name, selectedValue, handleChange, handleFocus, handleBlur, hasError, errorMessage]);
120
+ return {
121
+ // State
122
+ selectedValue,
123
+ hasError,
124
+ errorMessage,
125
+ touched,
126
+ isFocused,
127
+ isValid: !hasError,
128
+ // Handlers
129
+ handleChange,
130
+ handleFocus,
131
+ handleBlur,
132
+ validate,
133
+ reset,
134
+ setValue,
135
+ getSelectProps
136
+ };
137
+ };
138
+
139
+ /**
140
+ * Utility for managing keyboard navigation within select dropdowns
141
+ * Implements proper arrow key navigation and accessibility features
142
+ *
143
+ * @param {HTMLElement} selectElement - The select element
144
+ * @param {Object} options - Navigation options
145
+ * @param {Function} options.onNavigate - Optional callback when navigation occurs
146
+ * @returns {Object} Navigation utilities
147
+ */
148
+ exports.useSelectAccessibility = useSelectAccessibility;
149
+ const useSelectKeyboardNavigation = (selectElement, options = {}) => {
150
+ const {
151
+ onNavigate
152
+ } = options;
153
+ const getOptions = (0, _react.useCallback)(() => {
154
+ if (!selectElement) return [];
155
+ return Array.from(selectElement.options).filter(option => !option.disabled);
156
+ }, [selectElement]);
157
+ const handleKeyDown = (0, _react.useCallback)(event => {
158
+ if (!selectElement) return;
159
+ const options = getOptions();
160
+ if (options.length === 0) return;
161
+ const currentIndex = selectElement.selectedIndex;
162
+ let nextIndex = currentIndex;
163
+ switch (event.key) {
164
+ case 'ArrowUp':
165
+ event.preventDefault();
166
+ nextIndex = currentIndex > 0 ? currentIndex - 1 : options.length - 1;
167
+ break;
168
+ case 'ArrowDown':
169
+ event.preventDefault();
170
+ nextIndex = currentIndex < options.length - 1 ? currentIndex + 1 : 0;
171
+ break;
172
+ case 'Home':
173
+ event.preventDefault();
174
+ nextIndex = 0;
175
+ break;
176
+ case 'End':
177
+ event.preventDefault();
178
+ nextIndex = options.length - 1;
179
+ break;
180
+ default:
181
+ return;
182
+ }
183
+ if (nextIndex !== currentIndex) {
184
+ selectElement.selectedIndex = nextIndex;
185
+ selectElement.dispatchEvent(new Event('change', {
186
+ bubbles: true
187
+ }));
188
+ onNavigate?.(options[nextIndex].value, nextIndex);
189
+ }
190
+ }, [selectElement, getOptions, onNavigate]);
191
+ const focusFirst = (0, _react.useCallback)(() => {
192
+ if (!selectElement) return;
193
+ const options = getOptions();
194
+ if (options.length > 0) {
195
+ selectElement.selectedIndex = 0;
196
+ selectElement.focus();
197
+ }
198
+ }, [selectElement, getOptions]);
199
+ const focusLast = (0, _react.useCallback)(() => {
200
+ if (!selectElement) return;
201
+ const options = getOptions();
202
+ if (options.length > 0) {
203
+ selectElement.selectedIndex = options.length - 1;
204
+ selectElement.focus();
205
+ }
206
+ }, [selectElement, getOptions]);
207
+ return {
208
+ handleKeyDown,
209
+ focusFirst,
210
+ focusLast,
211
+ getOptions
212
+ };
213
+ };
214
+
215
+ /**
216
+ * Validates proper labeling for select elements
217
+ * @param {HTMLElement} selectElement - The select element to validate
218
+ * @returns {Object} Validation results for labeling
219
+ */
220
+ exports.useSelectKeyboardNavigation = useSelectKeyboardNavigation;
221
+ const validateSelectLabeling = selectElement => {
222
+ const results = {
223
+ errors: [],
224
+ warnings: []
225
+ };
226
+
227
+ // Check for proper labeling
228
+ const label = selectElement.closest('label') || document.querySelector(`label[for="${selectElement.id}"]`);
229
+ const ariaLabel = selectElement.getAttribute('aria-label');
230
+ const ariaLabelledBy = selectElement.getAttribute('aria-labelledby');
231
+ if (!label && !ariaLabel && !ariaLabelledBy) {
232
+ results.errors.push('Select element must have an accessible label');
233
+ }
234
+
235
+ // Check for name attribute
236
+ if (!selectElement.name) {
237
+ results.warnings.push('Select element should have a name attribute for form submission');
238
+ }
239
+ return results;
240
+ };
241
+
242
+ /**
243
+ * Validates select options for accessibility
244
+ * @param {HTMLElement} selectElement - The select element to validate
245
+ * @returns {Object} Validation results for options
246
+ */
247
+ const validateSelectOptions = selectElement => {
248
+ const results = {
249
+ errors: [],
250
+ warnings: [],
251
+ info: []
252
+ };
253
+
254
+ // Check for options
255
+ const options = Array.from(selectElement.options);
256
+ if (options.length === 0) {
257
+ results.errors.push('Select element must have at least one option');
258
+ return results;
259
+ }
260
+
261
+ // Check for unique values
262
+ const values = options.map(option => option.value);
263
+ const uniqueValues = [...new Set(values)];
264
+ if (values.length !== uniqueValues.length) {
265
+ results.warnings.push('Select options should have unique values');
266
+ }
267
+
268
+ // Check for accessible option text
269
+ options.forEach((option, index) => {
270
+ if (!option.textContent.trim()) {
271
+ results.warnings.push(`Option ${index + 1} should have descriptive text content`);
272
+ }
273
+ });
274
+ results.info.push(`Found ${options.length} options (${options.filter(o => !o.disabled).length} enabled)`);
275
+ return results;
276
+ };
277
+
278
+ /**
279
+ * Validates ARIA attributes for select elements
280
+ * @param {HTMLElement} selectElement - The select element to validate
281
+ * @returns {Object} Validation results for ARIA attributes
282
+ */
283
+ const validateSelectARIA = selectElement => {
284
+ const results = {
285
+ warnings: []
286
+ };
287
+
288
+ // Check ARIA attributes
289
+ if (selectElement.hasAttribute('aria-invalid')) {
290
+ const ariaInvalid = selectElement.getAttribute('aria-invalid');
291
+ if (ariaInvalid === 'true') {
292
+ const describedBy = selectElement.getAttribute('aria-describedby');
293
+ if (!describedBy) {
294
+ results.warnings.push('Select with aria-invalid="true" should have aria-describedby pointing to error message');
295
+ }
296
+ }
297
+ }
298
+ return results;
299
+ };
300
+
301
+ /**
302
+ * Validates select element states and special attributes
303
+ * @param {HTMLElement} selectElement - The select element to validate
304
+ * @returns {Object} Validation results for element states
305
+ */
306
+ const validateSelectStates = selectElement => {
307
+ const results = {
308
+ warnings: [],
309
+ info: []
310
+ };
311
+
312
+ // Check for multiple select considerations
313
+ if (selectElement.multiple) {
314
+ results.info.push('Multiple select detected - ensure users understand multiple selection is possible');
315
+ if (!selectElement.hasAttribute('size') || selectElement.size < 4) {
316
+ results.warnings.push('Multiple selects should show multiple options (size >= 4) for better usability');
317
+ }
318
+ }
319
+
320
+ // Check required attribute
321
+ if (selectElement.required) {
322
+ results.info.push('Required select detected - ensure form validation provides clear feedback');
323
+ }
324
+
325
+ // Check disabled state
326
+ if (selectElement.disabled) {
327
+ results.info.push("Disabled select detected - ensure users understand why it's disabled");
328
+ }
329
+ return results;
330
+ };
331
+
332
+ /**
333
+ * Accessibility testing utility for select elements
334
+ * Validates proper ARIA attributes and keyboard navigation
335
+ *
336
+ * @param {HTMLElement} selectElement - The select element to test
337
+ * @returns {Object} Test results
338
+ */
339
+ const validateSelectAccessibility = selectElement => {
340
+ const results = {
341
+ passed: true,
342
+ errors: [],
343
+ warnings: [],
344
+ info: []
345
+ };
346
+ if (!selectElement) {
347
+ results.passed = false;
348
+ results.errors.push('No select element provided for testing');
349
+ return results;
350
+ }
351
+
352
+ // Validate different aspects of accessibility
353
+ const labelingResults = validateSelectLabeling(selectElement);
354
+ const optionsResults = validateSelectOptions(selectElement);
355
+ const ariaResults = validateSelectARIA(selectElement);
356
+ const statesResults = validateSelectStates(selectElement);
357
+
358
+ // Combine all results
359
+ results.errors = [...labelingResults.errors, ...optionsResults.errors];
360
+ results.warnings = [...labelingResults.warnings, ...optionsResults.warnings, ...ariaResults.warnings, ...statesResults.warnings];
361
+ results.info = [...optionsResults.info, ...statesResults.info];
362
+
363
+ // Set overall pass/fail status
364
+ results.passed = results.errors.length === 0;
365
+ return results;
366
+ };
367
+
368
+ /**
369
+ * Utility for enhancing select accessibility with live regions
370
+ * Provides screen reader announcements for select changes
371
+ *
372
+ * @param {HTMLElement} selectElement - The select element
373
+ * @param {Object} options - Configuration options
374
+ * @returns {Object} Live region utilities
375
+ */
376
+ exports.validateSelectAccessibility = validateSelectAccessibility;
377
+ const useSelectLiveRegion = (selectElement, options = {}) => {
378
+ const {
379
+ enableSelectionAnnouncement = true,
380
+ enableOptionsAnnouncement = false,
381
+ customMessages = {}
382
+ } = options;
383
+ const [liveRegion, setLiveRegion] = (0, _react.useState)(null);
384
+
385
+ // Create live region for announcements
386
+ (0, _react.useEffect)(() => {
387
+ const region = document.createElement('div');
388
+ region.setAttribute('aria-live', 'polite');
389
+ region.setAttribute('aria-atomic', 'true');
390
+ region.style.position = 'absolute';
391
+ region.style.left = '-10000px';
392
+ region.style.top = 'auto';
393
+ region.style.width = '1px';
394
+ region.style.height = '1px';
395
+ region.style.overflow = 'hidden';
396
+ document.body.appendChild(region);
397
+ setLiveRegion(region);
398
+ return () => {
399
+ if (document.body.contains(region)) {
400
+ document.body.removeChild(region);
401
+ }
402
+ };
403
+ }, []);
404
+ const announce = (0, _react.useCallback)(message => {
405
+ if (liveRegion) {
406
+ liveRegion.textContent = message;
407
+ }
408
+ }, [liveRegion]);
409
+ const announceSelection = (0, _react.useCallback)(selectedOption => {
410
+ if (!enableSelectionAnnouncement || !selectedOption) return;
411
+ const message = customMessages.selection || `Selected: ${selectedOption.textContent}`;
412
+ announce(message);
413
+ }, [announce, enableSelectionAnnouncement, customMessages.selection]);
414
+ const announceOptionsCount = (0, _react.useCallback)(count => {
415
+ if (!enableOptionsAnnouncement) return;
416
+ const message = customMessages.optionsCount || `${count} options available`;
417
+ announce(message);
418
+ }, [announce, enableOptionsAnnouncement, customMessages.optionsCount]);
419
+ return {
420
+ announce,
421
+ announceSelection,
422
+ announceOptionsCount,
423
+ liveRegion
424
+ };
425
+ };
426
+ exports.useSelectLiveRegion = useSelectLiveRegion;
@@ -0,0 +1,278 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.validateTabsAccessibility = exports.useTabsLiveRegion = exports.useTabsAccessibility = void 0;
7
+ var _react = require("react");
8
+ /**
9
+ * Custom hook for managing tabs state with accessibility features
10
+ * Provides controlled state management for tab navigation with ARIA support
11
+ *
12
+ * @param {Object} options - Configuration options
13
+ * @param {Array} options.tabs - Array of tab objects with id, label, and content
14
+ * @param {string} options.defaultActiveTab - Default active tab ID
15
+ * @param {Function} options.onChange - Change callback function
16
+ * @returns {Object} Tabs state and handlers
17
+ */
18
+ const useTabsAccessibility = (options = {}) => {
19
+ const {
20
+ tabs = [],
21
+ defaultActiveTab = '',
22
+ onChange
23
+ } = options;
24
+ const [activeTabId, setActiveTabId] = (0, _react.useState)(defaultActiveTab || tabs[0]?.id || '');
25
+ const [focusedTabIndex, setFocusedTabIndex] = (0, _react.useState)(0);
26
+ const tabListRef = (0, _react.useRef)(null);
27
+ const tabRefs = (0, _react.useRef)({});
28
+
29
+ // Handle tab selection
30
+ const handleTabSelect = (0, _react.useCallback)((tabId, index) => {
31
+ setActiveTabId(tabId);
32
+ setFocusedTabIndex(index);
33
+ onChange?.(tabId, index);
34
+ }, [onChange]);
35
+
36
+ // Handle keyboard navigation
37
+ const handleKeyDown = (0, _react.useCallback)((event, tabId, currentIndex) => {
38
+ let newIndex = currentIndex;
39
+ switch (event.key) {
40
+ case 'ArrowLeft':
41
+ event.preventDefault();
42
+ newIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1;
43
+ break;
44
+ case 'ArrowRight':
45
+ event.preventDefault();
46
+ newIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0;
47
+ break;
48
+ case 'Home':
49
+ event.preventDefault();
50
+ newIndex = 0;
51
+ break;
52
+ case 'End':
53
+ event.preventDefault();
54
+ newIndex = tabs.length - 1;
55
+ break;
56
+ case 'Enter':
57
+ case ' ':
58
+ event.preventDefault();
59
+ handleTabSelect(tabId, currentIndex);
60
+ return;
61
+ default:
62
+ return;
63
+ }
64
+
65
+ // Focus the new tab
66
+ const newTabId = tabs[newIndex]?.id;
67
+ if (newTabId && tabRefs.current[newTabId]) {
68
+ tabRefs.current[newTabId].focus();
69
+ setFocusedTabIndex(newIndex);
70
+ }
71
+ }, [tabs, handleTabSelect]);
72
+
73
+ // Get props for tab list
74
+ const getTabListProps = (0, _react.useCallback)(() => ({
75
+ ref: tabListRef,
76
+ role: 'tablist',
77
+ 'aria-orientation': 'horizontal'
78
+ }), []);
79
+
80
+ // Get props for individual tabs
81
+ const getTabProps = (0, _react.useCallback)((tab, index) => ({
82
+ ref: el => {
83
+ if (el) {
84
+ tabRefs.current[tab.id] = el;
85
+ }
86
+ },
87
+ id: `tab-${tab.id}`,
88
+ role: 'tab',
89
+ 'aria-selected': activeTabId === tab.id,
90
+ 'aria-controls': `panel-${tab.id}`,
91
+ tabIndex: activeTabId === tab.id ? 0 : -1,
92
+ onClick: () => handleTabSelect(tab.id, index),
93
+ onKeyDown: event => handleKeyDown(event, tab.id, index)
94
+ }), [activeTabId, handleTabSelect, handleKeyDown]);
95
+
96
+ // Get props for tab panels
97
+ const getTabPanelProps = (0, _react.useCallback)(tab => ({
98
+ id: `panel-${tab.id}`,
99
+ role: 'tabpanel',
100
+ 'aria-labelledby': `tab-${tab.id}`,
101
+ hidden: activeTabId !== tab.id,
102
+ tabIndex: activeTabId === tab.id ? 0 : -1
103
+ }), [activeTabId]);
104
+
105
+ // Focus management - focus active tab on mount
106
+ (0, _react.useEffect)(() => {
107
+ if (activeTabId && tabRefs.current[activeTabId]) {
108
+ const timer = setTimeout(() => {
109
+ tabRefs.current[activeTabId]?.focus();
110
+ }, 0);
111
+ return () => clearTimeout(timer);
112
+ }
113
+ }, []);
114
+ return {
115
+ // State
116
+ activeTabId,
117
+ focusedTabIndex,
118
+ tabListRef,
119
+ tabRefs,
120
+ // Handlers
121
+ handleTabSelect,
122
+ handleKeyDown,
123
+ setActiveTabId,
124
+ // Props getters
125
+ getTabListProps,
126
+ getTabProps,
127
+ getTabPanelProps
128
+ };
129
+ };
130
+
131
+ /**
132
+ * Accessibility validation utility for tabs
133
+ * Validates proper ARIA attributes and keyboard navigation
134
+ *
135
+ * @param {HTMLElement} tabsElement - The tabs container element to test
136
+ * @returns {Object} Test results
137
+ */
138
+ exports.useTabsAccessibility = useTabsAccessibility;
139
+ const validateTabsAccessibility = tabsElement => {
140
+ const results = {
141
+ passed: true,
142
+ errors: [],
143
+ warnings: [],
144
+ info: []
145
+ };
146
+ if (!tabsElement) {
147
+ results.passed = false;
148
+ results.errors.push('No tabs element provided for testing');
149
+ return results;
150
+ }
151
+
152
+ // Check for tablist
153
+ const tabList = tabsElement.querySelector('[role="tablist"]');
154
+ if (!tabList) {
155
+ results.passed = false;
156
+ results.errors.push('Tabs must have a container with role="tablist"');
157
+ } else {
158
+ // Check for aria-orientation
159
+ if (!tabList.hasAttribute('aria-orientation')) {
160
+ results.warnings.push('Tablist should have aria-orientation attribute');
161
+ }
162
+
163
+ // Check tabs
164
+ const tabs = tabList.querySelectorAll('[role="tab"]');
165
+ if (tabs.length === 0) {
166
+ results.passed = false;
167
+ results.errors.push('Tablist must contain elements with role="tab"');
168
+ } else {
169
+ let hasSelectedTab = false;
170
+ tabs.forEach((tab, index) => {
171
+ // Check for aria-selected
172
+ if (!tab.hasAttribute('aria-selected')) {
173
+ results.warnings.push(`Tab ${index + 1} should have aria-selected attribute`);
174
+ } else if (tab.getAttribute('aria-selected') === 'true') {
175
+ hasSelectedTab = true;
176
+ }
177
+
178
+ // Check for aria-controls
179
+ if (!tab.hasAttribute('aria-controls')) {
180
+ results.warnings.push(`Tab ${index + 1} should have aria-controls attribute`);
181
+ }
182
+
183
+ // Check for proper tabindex
184
+ const tabIndex = tab.getAttribute('tabindex');
185
+ const isSelected = tab.getAttribute('aria-selected') === 'true';
186
+ if (isSelected && tabIndex !== '0') {
187
+ results.warnings.push(`Selected tab should have tabindex="0"`);
188
+ } else if (!isSelected && tabIndex !== '-1') {
189
+ results.warnings.push(`Unselected tabs should have tabindex="-1"`);
190
+ }
191
+ });
192
+ if (!hasSelectedTab) {
193
+ results.warnings.push('One tab should have aria-selected="true"');
194
+ }
195
+ results.info.push(`Found ${tabs.length} tabs`);
196
+ }
197
+
198
+ // Check panels
199
+ const panels = tabsElement.querySelectorAll('[role="tabpanel"]');
200
+ tabs.forEach((tab, index) => {
201
+ const controls = tab.getAttribute('aria-controls');
202
+ if (controls) {
203
+ const panel = tabsElement.querySelector(`#${controls}`);
204
+ if (!panel) {
205
+ results.warnings.push(`Tab ${index + 1} aria-controls points to non-existent panel`);
206
+ } else if (panel.getAttribute('role') !== 'tabpanel') {
207
+ results.warnings.push(`Element referenced by tab ${index + 1} should have role="tabpanel"`);
208
+ }
209
+ }
210
+ });
211
+ results.info.push(`Found ${panels.length} tab panels`);
212
+ }
213
+ return results;
214
+ };
215
+
216
+ /**
217
+ * Utility for enhancing tabs accessibility with live regions
218
+ * Provides screen reader announcements for tab changes
219
+ *
220
+ * @param {Array} tabs - Array of tab objects
221
+ * @param {string} activeTabId - Currently active tab ID
222
+ * @param {Object} options - Configuration options
223
+ * @returns {Object} Live region utilities
224
+ */
225
+ exports.validateTabsAccessibility = validateTabsAccessibility;
226
+ const useTabsLiveRegion = (tabs, activeTabId, options = {}) => {
227
+ const {
228
+ enableTabAnnouncement = true,
229
+ customMessages = {}
230
+ } = options;
231
+ const [liveRegion, setLiveRegion] = (0, _react.useState)(null);
232
+
233
+ // Create live region for announcements
234
+ (0, _react.useEffect)(() => {
235
+ const region = document.createElement('div');
236
+ region.setAttribute('aria-live', 'polite');
237
+ region.setAttribute('aria-atomic', 'true');
238
+ region.style.position = 'absolute';
239
+ region.style.left = '-10000px';
240
+ region.style.top = 'auto';
241
+ region.style.width = '1px';
242
+ region.style.height = '1px';
243
+ region.style.overflow = 'hidden';
244
+ document.body.appendChild(region);
245
+ setLiveRegion(region);
246
+ return () => {
247
+ if (document.body.contains(region)) {
248
+ document.body.removeChild(region);
249
+ }
250
+ };
251
+ }, []);
252
+ const announce = (0, _react.useCallback)(message => {
253
+ if (liveRegion) {
254
+ liveRegion.textContent = message;
255
+ }
256
+ }, [liveRegion]);
257
+ const announceTabChange = (0, _react.useCallback)(tabId => {
258
+ if (!enableTabAnnouncement || !tabId) return;
259
+ const activeTab = tabs.find(tab => tab.id === tabId);
260
+ if (activeTab) {
261
+ const message = customMessages.tabChange || `${activeTab.label} tab selected. ${tabs.indexOf(activeTab) + 1} of ${tabs.length}`;
262
+ announce(message);
263
+ }
264
+ }, [announce, enableTabAnnouncement, customMessages.tabChange, tabs]);
265
+
266
+ // Announce tab changes
267
+ (0, _react.useEffect)(() => {
268
+ if (activeTabId) {
269
+ announceTabChange(activeTabId);
270
+ }
271
+ }, [activeTabId, announceTabChange]);
272
+ return {
273
+ announce,
274
+ announceTabChange,
275
+ liveRegion
276
+ };
277
+ };
278
+ exports.useTabsLiveRegion = useTabsLiveRegion;