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