@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,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.validateTextAreaAccessibility = exports.useTextAreaAccessibility = exports.getCharacterCountInfo = void 0;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
/**
|
|
9
|
+
* Custom hook for managing textarea state with accessibility features
|
|
10
|
+
* Provides controlled state management for textarea with ARIA support
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} options - Configuration options
|
|
13
|
+
* @param {string} options.defaultValue - Default textarea value
|
|
14
|
+
* @param {Function} options.onChange - Change callback function
|
|
15
|
+
* @param {boolean} options.required - Whether the field is required
|
|
16
|
+
* @param {number} options.maxLength - Maximum character count
|
|
17
|
+
* @param {number} options.minLength - Minimum character count
|
|
18
|
+
* @returns {Object} TextArea state and handlers
|
|
19
|
+
*/
|
|
20
|
+
const useTextAreaAccessibility = (options = {}) => {
|
|
21
|
+
const {
|
|
22
|
+
defaultValue = '',
|
|
23
|
+
onChange,
|
|
24
|
+
required = false,
|
|
25
|
+
maxLength,
|
|
26
|
+
minLength
|
|
27
|
+
} = options;
|
|
28
|
+
const [value, setValue] = (0, _react.useState)(defaultValue);
|
|
29
|
+
const [charCount, setCharCount] = (0, _react.useState)(defaultValue.length);
|
|
30
|
+
const [isValid, setIsValid] = (0, _react.useState)(!required || defaultValue.length > 0);
|
|
31
|
+
const [hasBeenTouched, setHasBeenTouched] = (0, _react.useState)(false);
|
|
32
|
+
const textareaRef = (0, _react.useRef)(null);
|
|
33
|
+
const handleChange = (0, _react.useCallback)(event => {
|
|
34
|
+
const newValue = event.target.value;
|
|
35
|
+
setValue(newValue);
|
|
36
|
+
setCharCount(newValue.length);
|
|
37
|
+
setHasBeenTouched(true);
|
|
38
|
+
|
|
39
|
+
// Validate the field
|
|
40
|
+
let valid = true;
|
|
41
|
+
if (required && newValue.trim().length === 0) {
|
|
42
|
+
valid = false;
|
|
43
|
+
}
|
|
44
|
+
if (minLength && newValue.length < minLength) {
|
|
45
|
+
valid = false;
|
|
46
|
+
}
|
|
47
|
+
if (maxLength && newValue.length > maxLength) {
|
|
48
|
+
valid = false;
|
|
49
|
+
}
|
|
50
|
+
setIsValid(valid);
|
|
51
|
+
|
|
52
|
+
// Call external onChange handler
|
|
53
|
+
onChange?.(event, {
|
|
54
|
+
value: newValue,
|
|
55
|
+
isValid: valid,
|
|
56
|
+
charCount: newValue.length
|
|
57
|
+
});
|
|
58
|
+
}, [onChange, required, minLength, maxLength]);
|
|
59
|
+
const handleFocus = (0, _react.useCallback)(event => {
|
|
60
|
+
// Focus handling for accessibility
|
|
61
|
+
setHasBeenTouched(true);
|
|
62
|
+
}, []);
|
|
63
|
+
const handleBlur = (0, _react.useCallback)(event => {
|
|
64
|
+
// Blur handling for validation
|
|
65
|
+
const newValue = event.target.value;
|
|
66
|
+
let valid = true;
|
|
67
|
+
if (required && newValue.trim().length === 0) {
|
|
68
|
+
valid = false;
|
|
69
|
+
}
|
|
70
|
+
if (minLength && newValue.length < minLength) {
|
|
71
|
+
valid = false;
|
|
72
|
+
}
|
|
73
|
+
if (maxLength && newValue.length > maxLength) {
|
|
74
|
+
valid = false;
|
|
75
|
+
}
|
|
76
|
+
setIsValid(valid);
|
|
77
|
+
}, [required, minLength, maxLength]);
|
|
78
|
+
const reset = (0, _react.useCallback)(() => {
|
|
79
|
+
setValue(defaultValue);
|
|
80
|
+
setCharCount(defaultValue.length);
|
|
81
|
+
setIsValid(!required || defaultValue.length > 0);
|
|
82
|
+
setHasBeenTouched(false);
|
|
83
|
+
}, [defaultValue, required]);
|
|
84
|
+
const focus = (0, _react.useCallback)(() => {
|
|
85
|
+
textareaRef.current?.focus();
|
|
86
|
+
}, []);
|
|
87
|
+
return {
|
|
88
|
+
// State
|
|
89
|
+
value,
|
|
90
|
+
charCount,
|
|
91
|
+
isValid,
|
|
92
|
+
hasBeenTouched,
|
|
93
|
+
showError: hasBeenTouched && !isValid,
|
|
94
|
+
// Handlers
|
|
95
|
+
handleChange,
|
|
96
|
+
handleFocus,
|
|
97
|
+
handleBlur,
|
|
98
|
+
reset,
|
|
99
|
+
focus,
|
|
100
|
+
// Ref
|
|
101
|
+
textareaRef,
|
|
102
|
+
// Computed properties
|
|
103
|
+
remainingChars: maxLength ? maxLength - charCount : null,
|
|
104
|
+
isAtMaxLength: maxLength ? charCount >= maxLength : false,
|
|
105
|
+
isAtMinLength: minLength ? charCount >= minLength : true
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Validates textarea accessibility compliance
|
|
111
|
+
* Checks for proper ARIA attributes, labels, and keyboard support
|
|
112
|
+
*
|
|
113
|
+
* @param {HTMLElement} textareaElement - The textarea element to validate
|
|
114
|
+
* @returns {Object} Validation results with errors and warnings
|
|
115
|
+
*/
|
|
116
|
+
exports.useTextAreaAccessibility = useTextAreaAccessibility;
|
|
117
|
+
const validateTextAreaAccessibility = textareaElement => {
|
|
118
|
+
const results = {
|
|
119
|
+
passed: true,
|
|
120
|
+
errors: [],
|
|
121
|
+
warnings: [],
|
|
122
|
+
info: []
|
|
123
|
+
};
|
|
124
|
+
if (!textareaElement) {
|
|
125
|
+
results.passed = false;
|
|
126
|
+
results.errors.push('No textarea element provided for validation');
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check if it's actually a textarea element
|
|
131
|
+
if (textareaElement.tagName.toLowerCase() !== 'textarea') {
|
|
132
|
+
// Look for textarea within the element
|
|
133
|
+
const textarea = textareaElement.querySelector('textarea');
|
|
134
|
+
if (!textarea) {
|
|
135
|
+
results.passed = false;
|
|
136
|
+
results.errors.push('Element is not a textarea and contains no textarea element');
|
|
137
|
+
return results;
|
|
138
|
+
}
|
|
139
|
+
// Use the found textarea for validation
|
|
140
|
+
textareaElement = textarea;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 1. Check for accessible name (label, aria-label, or aria-labelledby)
|
|
144
|
+
const hasLabel = textareaElement.hasAttribute('aria-label') || textareaElement.hasAttribute('aria-labelledby') || textareaElement.labels?.length > 0;
|
|
145
|
+
if (!hasLabel) {
|
|
146
|
+
results.passed = false;
|
|
147
|
+
results.errors.push('Textarea must have an accessible name (label, aria-label, or aria-labelledby)');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 2. Check for proper ID if labelledby is used
|
|
151
|
+
if (textareaElement.hasAttribute('aria-labelledby')) {
|
|
152
|
+
const labelId = textareaElement.getAttribute('aria-labelledby');
|
|
153
|
+
const labelElement = document.getElementById(labelId);
|
|
154
|
+
if (!labelElement) {
|
|
155
|
+
results.passed = false;
|
|
156
|
+
results.errors.push(`Referenced label element with ID "${labelId}" not found`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 3. Check for required field indication
|
|
161
|
+
if (textareaElement.hasAttribute('required') || textareaElement.hasAttribute('aria-required')) {
|
|
162
|
+
if (!textareaElement.hasAttribute('aria-required')) {
|
|
163
|
+
results.warnings.push('Consider adding aria-required="true" for screen reader users');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 4. Check for error state accessibility
|
|
168
|
+
if (textareaElement.hasAttribute('aria-invalid')) {
|
|
169
|
+
const isInvalid = textareaElement.getAttribute('aria-invalid') === 'true';
|
|
170
|
+
if (isInvalid && !textareaElement.hasAttribute('aria-describedby')) {
|
|
171
|
+
results.warnings.push('Invalid textarea should have aria-describedby pointing to error message');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 5. Check for character count accessibility
|
|
176
|
+
const hasMaxLength = textareaElement.hasAttribute('maxlength');
|
|
177
|
+
if (hasMaxLength && !textareaElement.hasAttribute('aria-describedby')) {
|
|
178
|
+
results.warnings.push('Textarea with maxlength should have aria-describedby for character count info');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 6. Check for placeholder accessibility
|
|
182
|
+
if (textareaElement.hasAttribute('placeholder')) {
|
|
183
|
+
results.warnings.push('Avoid using placeholder as the only label - ensure proper labeling exists');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 7. Check for disabled state
|
|
187
|
+
if (textareaElement.disabled) {
|
|
188
|
+
results.info.push('Textarea is disabled');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 8. Check for keyboard accessibility
|
|
192
|
+
const tabIndex = textareaElement.getAttribute('tabindex');
|
|
193
|
+
if (tabIndex && parseInt(tabIndex) < 0) {
|
|
194
|
+
results.warnings.push('Textarea has negative tabindex - may not be keyboard accessible');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 9. Check for resize capability
|
|
198
|
+
const computedStyle = window.getComputedStyle(textareaElement);
|
|
199
|
+
if (computedStyle.resize === 'none') {
|
|
200
|
+
results.info.push('Textarea resize is disabled - consider allowing users to resize for better usability');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 10. Check for minimum contrast (if possible)
|
|
204
|
+
const bgColor = computedStyle.backgroundColor;
|
|
205
|
+
const textColor = computedStyle.color;
|
|
206
|
+
if (bgColor && textColor) {
|
|
207
|
+
results.info.push('Manual contrast check recommended for accessibility compliance');
|
|
208
|
+
}
|
|
209
|
+
if (results.errors.length === 0) {
|
|
210
|
+
results.info.push('Basic accessibility validation passed');
|
|
211
|
+
}
|
|
212
|
+
return results;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Character count utility for textarea accessibility
|
|
217
|
+
* Provides screen reader announcements for character limits
|
|
218
|
+
*
|
|
219
|
+
* @param {number} currentCount - Current character count
|
|
220
|
+
* @param {number} maxLength - Maximum allowed characters
|
|
221
|
+
* @param {number} warningThreshold - Threshold to start warnings (default: 0.8)
|
|
222
|
+
* @returns {Object} Character count info and announcements
|
|
223
|
+
*/
|
|
224
|
+
exports.validateTextAreaAccessibility = validateTextAreaAccessibility;
|
|
225
|
+
const getCharacterCountInfo = (currentCount, maxLength, warningThreshold = 0.8) => {
|
|
226
|
+
if (!maxLength) return null;
|
|
227
|
+
const remaining = maxLength - currentCount;
|
|
228
|
+
const percentage = currentCount / maxLength;
|
|
229
|
+
const isWarning = percentage >= warningThreshold;
|
|
230
|
+
const isError = currentCount > maxLength;
|
|
231
|
+
let announcement = '';
|
|
232
|
+
let status = 'info';
|
|
233
|
+
if (isError) {
|
|
234
|
+
announcement = `Character limit exceeded. ${Math.abs(remaining)} characters over limit.`;
|
|
235
|
+
status = 'error';
|
|
236
|
+
} else if (isWarning) {
|
|
237
|
+
announcement = `${remaining} characters remaining.`;
|
|
238
|
+
status = 'warning';
|
|
239
|
+
} else {
|
|
240
|
+
announcement = `${currentCount} of ${maxLength} characters used.`;
|
|
241
|
+
status = 'info';
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
currentCount,
|
|
245
|
+
maxLength,
|
|
246
|
+
remaining,
|
|
247
|
+
percentage,
|
|
248
|
+
isWarning,
|
|
249
|
+
isError,
|
|
250
|
+
announcement,
|
|
251
|
+
status,
|
|
252
|
+
ariaLabel: `Character count: ${announcement}`
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
exports.getCharacterCountInfo = getCharacterCountInfo;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.validateTextInputAccessibility = exports.useTextInputAccessibility = exports.getInputValidationInfo = void 0;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
var _inputValidation = require("./inputValidation");
|
|
9
|
+
/**
|
|
10
|
+
* Custom hook for managing text input state with accessibility features
|
|
11
|
+
* Provides controlled state management for text input with ARIA support
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} options - Configuration options
|
|
14
|
+
* @param {string} options.defaultValue - Default input value
|
|
15
|
+
* @param {Function} options.onChange - Change callback function
|
|
16
|
+
* @param {boolean} options.required - Whether the field is required
|
|
17
|
+
* @param {number} options.maxLength - Maximum character count
|
|
18
|
+
* @param {number} options.minLength - Minimum character count
|
|
19
|
+
* @param {string} options.type - Input type for validation
|
|
20
|
+
* @returns {Object} TextInput state and handlers
|
|
21
|
+
*/
|
|
22
|
+
const useTextInputAccessibility = (options = {}) => {
|
|
23
|
+
const {
|
|
24
|
+
defaultValue = '',
|
|
25
|
+
onChange,
|
|
26
|
+
required = false,
|
|
27
|
+
maxLength,
|
|
28
|
+
minLength,
|
|
29
|
+
type = 'text'
|
|
30
|
+
} = options;
|
|
31
|
+
const [value, setValue] = (0, _react.useState)(defaultValue);
|
|
32
|
+
const [isValid, setIsValid] = (0, _react.useState)(!required || defaultValue.length > 0);
|
|
33
|
+
const [hasBeenTouched, setHasBeenTouched] = (0, _react.useState)(false);
|
|
34
|
+
const inputRef = (0, _react.useRef)(null);
|
|
35
|
+
const validateInput = (0, _react.useCallback)((inputValue, inputType) => {
|
|
36
|
+
let valid = true;
|
|
37
|
+
if (required && !(0, _inputValidation.validateRequired)(inputValue)) valid = false;
|
|
38
|
+
if (!(0, _inputValidation.validateLength)(inputValue, minLength, maxLength)) valid = false;
|
|
39
|
+
switch (inputType) {
|
|
40
|
+
case 'email':
|
|
41
|
+
if (inputValue && !(0, _inputValidation.validateEmail)(inputValue)) valid = false;
|
|
42
|
+
break;
|
|
43
|
+
case 'tel':
|
|
44
|
+
if (inputValue && !(0, _inputValidation.validatePhone)(inputValue)) valid = false;
|
|
45
|
+
break;
|
|
46
|
+
case 'url':
|
|
47
|
+
try {
|
|
48
|
+
if (inputValue && !(inputValue.startsWith('http://') || inputValue.startsWith('https://'))) {
|
|
49
|
+
valid = false;
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
valid = false;
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
default:
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
return valid;
|
|
59
|
+
}, [type]);
|
|
60
|
+
const handleFocus = (0, _react.useCallback)(event => {
|
|
61
|
+
setHasBeenTouched(true);
|
|
62
|
+
}, []);
|
|
63
|
+
const handleBlur = (0, _react.useCallback)(event => {
|
|
64
|
+
const newValue = event.target.value;
|
|
65
|
+
const valid = validateInput(newValue, type);
|
|
66
|
+
setIsValid(valid);
|
|
67
|
+
}, [type, validateInput]);
|
|
68
|
+
const reset = (0, _react.useCallback)(() => {
|
|
69
|
+
setValue(defaultValue);
|
|
70
|
+
setIsValid(!required || defaultValue.length > 0);
|
|
71
|
+
setHasBeenTouched(false);
|
|
72
|
+
}, [defaultValue, required]);
|
|
73
|
+
const focus = (0, _react.useCallback)(() => {
|
|
74
|
+
inputRef.current?.focus();
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
// Unified onChange handler for input/textarea
|
|
78
|
+
const handleChange = (0, _react.useCallback)(event => {
|
|
79
|
+
const newValue = event.target.value;
|
|
80
|
+
setValue(newValue);
|
|
81
|
+
if (onChange) onChange(newValue);
|
|
82
|
+
}, [onChange]);
|
|
83
|
+
return {
|
|
84
|
+
// State
|
|
85
|
+
value,
|
|
86
|
+
isValid,
|
|
87
|
+
hasBeenTouched,
|
|
88
|
+
showError: hasBeenTouched && !isValid,
|
|
89
|
+
// Handlers
|
|
90
|
+
handleFocus,
|
|
91
|
+
handleBlur,
|
|
92
|
+
handleChange,
|
|
93
|
+
reset,
|
|
94
|
+
focus,
|
|
95
|
+
// Ref
|
|
96
|
+
inputRef,
|
|
97
|
+
// Computed properties
|
|
98
|
+
charCount: value.length,
|
|
99
|
+
remainingChars: maxLength ? maxLength - value.length : null,
|
|
100
|
+
isAtMaxLength: maxLength ? value.length >= maxLength : false,
|
|
101
|
+
isAtMinLength: minLength ? value.length >= minLength : true
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validates text input accessibility compliance
|
|
107
|
+
* Checks for proper ARIA attributes, labels, and keyboard support
|
|
108
|
+
*
|
|
109
|
+
* @param {HTMLElement} inputElement - The input element to validate
|
|
110
|
+
* @returns {Object} Validation results with errors and warnings
|
|
111
|
+
*/
|
|
112
|
+
exports.useTextInputAccessibility = useTextInputAccessibility;
|
|
113
|
+
const validateTextInputAccessibility = inputElement => {
|
|
114
|
+
const results = {
|
|
115
|
+
passed: true,
|
|
116
|
+
errors: [],
|
|
117
|
+
warnings: [],
|
|
118
|
+
info: []
|
|
119
|
+
};
|
|
120
|
+
if (!inputElement) {
|
|
121
|
+
results.passed = false;
|
|
122
|
+
results.errors.push('No input element provided for validation');
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check if it's actually an input element
|
|
127
|
+
if (inputElement.tagName.toLowerCase() !== 'input') {
|
|
128
|
+
const input = inputElement.querySelector('input');
|
|
129
|
+
if (!input) {
|
|
130
|
+
results.passed = false;
|
|
131
|
+
results.errors.push('Element is not an input and contains no input element');
|
|
132
|
+
return results;
|
|
133
|
+
}
|
|
134
|
+
inputElement = input;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 1. Check for accessible name
|
|
138
|
+
const hasLabel = inputElement.hasAttribute('aria-label') || inputElement.hasAttribute('aria-labelledby') || inputElement.labels?.length > 0;
|
|
139
|
+
if (!hasLabel) {
|
|
140
|
+
results.passed = false;
|
|
141
|
+
results.errors.push('Input must have an accessible name (label, aria-label, or aria-labelledby)');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 2. Check for proper ID if labelledby is used
|
|
145
|
+
if (inputElement.hasAttribute('aria-labelledby')) {
|
|
146
|
+
const labelId = inputElement.getAttribute('aria-labelledby');
|
|
147
|
+
const labelElement = document.getElementById(labelId);
|
|
148
|
+
if (!labelElement) {
|
|
149
|
+
results.passed = false;
|
|
150
|
+
results.errors.push(`Referenced label element with ID "${labelId}" not found`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 3. Check for required field indication
|
|
155
|
+
if (inputElement.hasAttribute('required') || inputElement.hasAttribute('aria-required')) {
|
|
156
|
+
if (!inputElement.hasAttribute('aria-required')) {
|
|
157
|
+
results.warnings.push('Consider adding aria-required="true" for screen reader users');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 4. Check for error state accessibility
|
|
162
|
+
if (inputElement.hasAttribute('aria-invalid')) {
|
|
163
|
+
const isInvalid = inputElement.getAttribute('aria-invalid') === 'true';
|
|
164
|
+
if (isInvalid && !inputElement.hasAttribute('aria-describedby')) {
|
|
165
|
+
results.warnings.push('Invalid input should have aria-describedby pointing to error message');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 5. Check input type appropriateness
|
|
170
|
+
const inputType = inputElement.getAttribute('type') || 'text';
|
|
171
|
+
if (inputType === 'text' && inputElement.getAttribute('name')) {
|
|
172
|
+
const name = inputElement.getAttribute('name').toLowerCase();
|
|
173
|
+
if (name.includes('email')) {
|
|
174
|
+
results.warnings.push('Consider using type="email" for email inputs');
|
|
175
|
+
} else if (name.includes('phone') || name.includes('tel')) {
|
|
176
|
+
results.warnings.push('Consider using type="tel" for phone inputs');
|
|
177
|
+
} else if (name.includes('url') || name.includes('website')) {
|
|
178
|
+
results.warnings.push('Consider using type="url" for URL inputs');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 6. Check for placeholder accessibility
|
|
183
|
+
if (inputElement.hasAttribute('placeholder')) {
|
|
184
|
+
results.warnings.push('Avoid using placeholder as the only label - ensure proper labeling exists');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 7. Check for disabled state
|
|
188
|
+
if (inputElement.disabled) {
|
|
189
|
+
results.info.push('Input is disabled');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 8. Check for keyboard accessibility
|
|
193
|
+
const tabIndex = inputElement.getAttribute('tabindex');
|
|
194
|
+
if (tabIndex && parseInt(tabIndex) < 0) {
|
|
195
|
+
results.warnings.push('Input has negative tabindex - may not be keyboard accessible');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 9. Check autocomplete attributes
|
|
199
|
+
const autocomplete = inputElement.getAttribute('autocomplete');
|
|
200
|
+
if (!autocomplete && ['email', 'tel', 'url'].includes(inputType)) {
|
|
201
|
+
results.info.push(`Consider adding autocomplete attribute for ${inputType} inputs`);
|
|
202
|
+
}
|
|
203
|
+
if (results.errors.length === 0) {
|
|
204
|
+
results.info.push('Basic accessibility validation passed');
|
|
205
|
+
}
|
|
206
|
+
return results;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Input validation utility for common input types
|
|
211
|
+
* Provides validation messages and patterns
|
|
212
|
+
*
|
|
213
|
+
* @param {string} value - Current input value
|
|
214
|
+
* @param {string} type - Input type
|
|
215
|
+
* @param {Object} options - Validation options
|
|
216
|
+
* @returns {Object} Validation result with message and validity
|
|
217
|
+
*/
|
|
218
|
+
exports.validateTextInputAccessibility = validateTextInputAccessibility;
|
|
219
|
+
const getInputValidationInfo = (value, type, options = {}) => {
|
|
220
|
+
const {
|
|
221
|
+
required = false,
|
|
222
|
+
minLength,
|
|
223
|
+
maxLength
|
|
224
|
+
} = options;
|
|
225
|
+
let isValid = true;
|
|
226
|
+
let message = '';
|
|
227
|
+
let status = 'valid';
|
|
228
|
+
|
|
229
|
+
// Required validation
|
|
230
|
+
if (required && !value.trim()) {
|
|
231
|
+
isValid = false;
|
|
232
|
+
message = 'This field is required';
|
|
233
|
+
status = 'error';
|
|
234
|
+
return {
|
|
235
|
+
isValid,
|
|
236
|
+
message,
|
|
237
|
+
status
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Length validation
|
|
242
|
+
if (minLength && value.length < minLength) {
|
|
243
|
+
isValid = false;
|
|
244
|
+
message = `Minimum ${minLength} characters required`;
|
|
245
|
+
status = 'error';
|
|
246
|
+
} else if (maxLength && value.length > maxLength) {
|
|
247
|
+
isValid = false;
|
|
248
|
+
message = `Maximum ${maxLength} characters allowed`;
|
|
249
|
+
status = 'error';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Type-specific validation
|
|
253
|
+
if (value && isValid) {
|
|
254
|
+
switch (type) {
|
|
255
|
+
case 'email':
|
|
256
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
257
|
+
if (!emailRegex.test(value)) {
|
|
258
|
+
isValid = false;
|
|
259
|
+
message = 'Please enter a valid email address';
|
|
260
|
+
status = 'error';
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
case 'tel':
|
|
264
|
+
const phoneRegex = /^[\+]?[\s\-\(\)]?[\d\s\-\(\)]{10,}$/;
|
|
265
|
+
if (!phoneRegex.test(value)) {
|
|
266
|
+
isValid = false;
|
|
267
|
+
message = 'Please enter a valid phone number';
|
|
268
|
+
status = 'error';
|
|
269
|
+
}
|
|
270
|
+
break;
|
|
271
|
+
case 'url':
|
|
272
|
+
if (!value.startsWith('http://') && !value.startsWith('https://')) {
|
|
273
|
+
isValid = false;
|
|
274
|
+
message = 'Please enter a valid URL (starting with http:// or https://)';
|
|
275
|
+
status = 'error';
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
case 'number':
|
|
279
|
+
if (isNaN(Number(value))) {
|
|
280
|
+
isValid = false;
|
|
281
|
+
message = 'Please enter a valid number';
|
|
282
|
+
status = 'error';
|
|
283
|
+
}
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
isValid,
|
|
289
|
+
message,
|
|
290
|
+
status,
|
|
291
|
+
charCount: value.length,
|
|
292
|
+
remainingChars: maxLength ? maxLength - value.length : null
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
exports.getInputValidationInfo = getInputValidationInfo;
|