@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.
- package/LICENSE +21 -0
- package/README.md +708 -0
- package/dist/__tests__/Anchor.test.js +145 -0
- package/dist/__tests__/ArrowRight.test.js +91 -0
- package/dist/__tests__/Avatar.test.js +123 -0
- package/dist/__tests__/Button.test.js +82 -0
- package/dist/__tests__/Card.test.js +198 -0
- package/dist/__tests__/CheckCircle.test.js +98 -0
- package/dist/__tests__/Checkbox.test.js +161 -0
- package/dist/__tests__/ChevronDown.test.js +73 -0
- package/dist/__tests__/Close.test.js +98 -0
- package/dist/__tests__/EditSquare.test.js +99 -0
- package/dist/__tests__/Error.test.js +74 -0
- package/dist/__tests__/Footer.test.js +66 -0
- package/dist/__tests__/Heading.test.js +227 -0
- package/dist/__tests__/Hero.test.js +74 -0
- package/dist/__tests__/Label.test.js +123 -0
- package/dist/__tests__/Loader.test.js +115 -0
- package/dist/__tests__/MenuHover.test.js +137 -0
- package/dist/__tests__/Paragraph.test.js +93 -0
- package/dist/__tests__/PlusCircle.test.js +99 -0
- package/dist/__tests__/Radio.test.js +153 -0
- package/dist/__tests__/Select.test.js +187 -0
- package/dist/__tests__/Tabs.test.js +162 -0
- package/dist/__tests__/TextArea.test.js +127 -0
- package/dist/__tests__/TextInput.test.js +181 -0
- package/dist/__tests__/Toggle.test.js +120 -0
- package/dist/__tests__/TrashX.test.js +99 -0
- package/dist/__tests__/useHeadingAccessibility.test.js +144 -0
- package/dist/components/Anchor.js +131 -0
- package/dist/components/Animation.js +129 -0
- package/dist/components/AnimationGroup.js +207 -0
- package/dist/components/AnimationToggle.js +216 -0
- package/dist/components/Avatar.js +153 -0
- package/dist/components/Button.js +218 -0
- package/dist/components/Card.js +222 -0
- package/dist/components/Checkbox.js +305 -0
- package/dist/components/Crud.js +564 -0
- package/dist/components/DragAndDrop.js +337 -0
- package/dist/components/Error.js +206 -0
- package/dist/components/Footer.js +99 -0
- package/dist/components/Form.js +412 -0
- package/dist/components/Header.js +372 -0
- package/dist/components/Heading.js +134 -0
- package/dist/components/Hero.js +181 -0
- package/dist/components/Label.js +256 -0
- package/dist/components/Loader.js +302 -0
- package/dist/components/MenuHover.js +114 -0
- package/dist/components/Paragraph.js +128 -0
- package/dist/components/Prompt.js +61 -0
- package/dist/components/Radio.js +254 -0
- package/dist/components/Select.js +422 -0
- package/dist/components/SideMenu.js +313 -0
- package/dist/components/Tabs.js +297 -0
- package/dist/components/TextArea.js +370 -0
- package/dist/components/TextInput.js +286 -0
- package/dist/components/Toggle.js +186 -0
- package/dist/components/crudFiles/CrudEditBase.js +150 -0
- package/dist/components/crudFiles/CrudViewBase.js +39 -0
- package/dist/components/crudFiles/crudDevelopment.js +118 -0
- package/dist/components/crudFiles/crudEditHandlers.js +50 -0
- package/dist/constants/animation.js +30 -0
- package/dist/icons/ArrowIcon.js +32 -0
- package/dist/icons/ArrowRight.js +33 -0
- package/dist/icons/CheckCircle.js +33 -0
- package/dist/icons/ChevronDown.js +28 -0
- package/dist/icons/Close.js +33 -0
- package/dist/icons/EditSquare.js +33 -0
- package/dist/icons/Ellipses.js +34 -0
- package/dist/icons/Hamburger.js +39 -0
- package/dist/icons/LoadingSpinner.js +42 -0
- package/dist/icons/PlusCircle.js +33 -0
- package/dist/icons/SaveIcon.js +32 -0
- package/dist/icons/TrashX.js +33 -0
- package/dist/icons/__tests__/CheckCircle.test.js +9 -0
- package/dist/icons/__tests__/ChevronDown.test.js +9 -0
- package/dist/icons/__tests__/Close.test.js +9 -0
- package/dist/icons/__tests__/EditSquare.test.js +9 -0
- package/dist/icons/__tests__/PlusCircle.test.js +9 -0
- package/dist/icons/__tests__/TrashX.test.js +9 -0
- package/dist/icons/index.js +89 -0
- package/dist/index.js +332 -0
- package/dist/setupTests.js +3 -0
- package/dist/styles/_variables.scss +286 -0
- package/dist/styles/anchor.scss +40 -0
- package/dist/styles/animation-accessibility.scss +96 -0
- package/dist/styles/animation-toggle.scss +233 -0
- package/dist/styles/animation.scss +3781 -0
- package/dist/styles/avatar.scss +285 -0
- package/dist/styles/button.scss +430 -0
- package/dist/styles/card.scss +210 -0
- package/dist/styles/checkbox.scss +160 -0
- package/dist/styles/crud.scss +474 -0
- package/dist/styles/dragAndDrop.scss +312 -0
- package/dist/styles/error.scss +232 -0
- package/dist/styles/footer.scss +58 -0
- package/dist/styles/form.scss +420 -0
- package/dist/styles/grid.scss +29 -0
- package/dist/styles/header.scss +276 -0
- package/dist/styles/heading.scss +118 -0
- package/dist/styles/hero.scss +185 -0
- package/dist/styles/htmlElements.scss +20 -0
- package/dist/styles/image.scss +9 -0
- package/dist/styles/label.scss +340 -0
- package/dist/styles/list-item.scss +5 -0
- package/dist/styles/loader.scss +354 -0
- package/dist/styles/logo.scss +19 -0
- package/dist/styles/main.css +9056 -0
- package/dist/styles/main.css.map +1 -0
- package/dist/styles/main.scss +0 -0
- package/dist/styles/menu-hover.scss +30 -0
- package/dist/styles/paragraph.scss +88 -0
- package/dist/styles/prompt.scss +51 -0
- package/dist/styles/radio.scss +202 -0
- package/dist/styles/select.scss +363 -0
- package/dist/styles/side-menu.scss +334 -0
- package/dist/styles/tabs.scss +540 -0
- package/dist/styles/text-area.scss +388 -0
- package/dist/styles/text-input.scss +171 -0
- package/dist/styles/toggle.scss +0 -0
- package/dist/styles/unordered-list.scss +8 -0
- package/dist/utils/ScrollHandler.js +30 -0
- package/dist/utils/accessibility.js +128 -0
- package/dist/utils/heroUtils.js +316 -0
- package/dist/utils/index.js +104 -0
- package/dist/utils/inputValidation.js +29 -0
- package/dist/utils/keyboardNavigation.js +536 -0
- package/dist/utils/labelUtils.js +708 -0
- package/dist/utils/loaderUtils.js +387 -0
- package/dist/utils/menuUtils.js +575 -0
- package/dist/utils/useHeadingAccessibility.js +298 -0
- package/dist/utils/useRadioGroup.js +260 -0
- package/dist/utils/useSelectAccessibility.js +426 -0
- package/dist/utils/useTabsAccessibility.js +278 -0
- package/dist/utils/useTextAreaAccessibility.js +255 -0
- package/dist/utils/useTextInputAccessibility.js +295 -0
- package/dist/utils/useTypographyAccessibility.js +168 -0
- package/dist/utils/useWindowSize.js +32 -0
- package/dist/utils/utils/ScrollHandler.js +26 -0
- package/dist/utils/utils/accessibility.js +133 -0
- package/dist/utils/utils/heroUtils.js +348 -0
- package/dist/utils/utils/index.js +9 -0
- package/dist/utils/utils/inputValidation.js +22 -0
- package/dist/utils/utils/keyboardNavigation.js +664 -0
- package/dist/utils/utils/labelUtils.js +772 -0
- package/dist/utils/utils/loaderUtils.js +436 -0
- package/dist/utils/utils/menuUtils.js +651 -0
- package/dist/utils/utils/useHeadingAccessibility.js +334 -0
- package/dist/utils/utils/useRadioGroup.js +311 -0
- package/dist/utils/utils/useSelectAccessibility.js +498 -0
- package/dist/utils/utils/useTabsAccessibility.js +316 -0
- package/dist/utils/utils/useTextAreaAccessibility.js +303 -0
- package/dist/utils/utils/useTextInputAccessibility.js +338 -0
- package/dist/utils/utils/useTypographyAccessibility.js +180 -0
- package/dist/utils/utils/useWindowSize.js +26 -0
- package/dist/utils/utils/validation.js +131 -0
- package/dist/utils/validation.js +139 -0
- 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
|
+
};
|