@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,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;
|