@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,708 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.validators = exports.useLabelAccessibility = exports.useFormValidation = exports.useFieldValidation = exports.labelTestingUtils = exports.default = void 0;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
/**
|
|
9
|
+
* Label Component Utilities
|
|
10
|
+
* Provides helper functions and hooks for Label component functionality
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Counter for generating unique IDs
|
|
15
|
+
*/
|
|
16
|
+
let idCounter = 0;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generates a robust unique ID using a combination of counter and timestamp
|
|
20
|
+
* @param {string} prefix - Optional prefix for the ID
|
|
21
|
+
* @returns {string} Unique ID
|
|
22
|
+
*/
|
|
23
|
+
const generateUniqueId = (prefix = 'id') => {
|
|
24
|
+
return `${prefix}-${++idCounter}-${Date.now().toString(36)}${Math.random().toString(36).substr(2, 4)}`;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generates a UUID v4 compatible ID (simplified version)
|
|
29
|
+
* @returns {string} UUID-like string
|
|
30
|
+
*/
|
|
31
|
+
const generateUUID = () => {
|
|
32
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
33
|
+
const r = Math.random() * 16 | 0;
|
|
34
|
+
const v = c === 'x' ? r : r & 0x3 | 0x8;
|
|
35
|
+
return v.toString(16);
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Singleton live region manager for screen reader announcements
|
|
41
|
+
*/
|
|
42
|
+
class LiveRegionManager {
|
|
43
|
+
constructor() {
|
|
44
|
+
this.liveRegion = null;
|
|
45
|
+
this.referenceCount = 0;
|
|
46
|
+
this.announcementQueue = [];
|
|
47
|
+
this.isProcessing = false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates or returns existing live region
|
|
52
|
+
* @returns {HTMLElement} Live region element
|
|
53
|
+
*/
|
|
54
|
+
getLiveRegion() {
|
|
55
|
+
if (!this.liveRegion) {
|
|
56
|
+
this.liveRegion = document.createElement('div');
|
|
57
|
+
this.liveRegion.setAttribute('aria-live', 'assertive');
|
|
58
|
+
this.liveRegion.setAttribute('aria-atomic', 'true');
|
|
59
|
+
this.liveRegion.className = 'sr-only';
|
|
60
|
+
this.liveRegion.style.cssText = `
|
|
61
|
+
position: absolute !important;
|
|
62
|
+
width: 1px !important;
|
|
63
|
+
height: 1px !important;
|
|
64
|
+
padding: 0 !important;
|
|
65
|
+
margin: -1px !important;
|
|
66
|
+
overflow: hidden !important;
|
|
67
|
+
clip: rect(0, 0, 0, 0) !important;
|
|
68
|
+
white-space: nowrap !important;
|
|
69
|
+
border: 0 !important;
|
|
70
|
+
`;
|
|
71
|
+
document.body.appendChild(this.liveRegion);
|
|
72
|
+
}
|
|
73
|
+
return this.liveRegion;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Increment reference count and return live region
|
|
78
|
+
* @returns {HTMLElement} Live region element
|
|
79
|
+
*/
|
|
80
|
+
addReference() {
|
|
81
|
+
this.referenceCount++;
|
|
82
|
+
return this.getLiveRegion();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Decrement reference count and cleanup if no references remain
|
|
87
|
+
*/
|
|
88
|
+
removeReference() {
|
|
89
|
+
this.referenceCount--;
|
|
90
|
+
if (this.referenceCount <= 0 && this.liveRegion) {
|
|
91
|
+
if (document.body.contains(this.liveRegion)) {
|
|
92
|
+
document.body.removeChild(this.liveRegion);
|
|
93
|
+
}
|
|
94
|
+
this.liveRegion = null;
|
|
95
|
+
this.referenceCount = 0;
|
|
96
|
+
this.announcementQueue = [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Announce text to screen readers with queuing to prevent conflicts
|
|
102
|
+
* @param {string} text - Text to announce
|
|
103
|
+
*/
|
|
104
|
+
async announce(text) {
|
|
105
|
+
if (!text) return;
|
|
106
|
+
this.announcementQueue.push(text);
|
|
107
|
+
if (!this.isProcessing) {
|
|
108
|
+
this.isProcessing = true;
|
|
109
|
+
await this.processQueue();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Process announcement queue to prevent conflicts
|
|
115
|
+
*/
|
|
116
|
+
async processQueue() {
|
|
117
|
+
while (this.announcementQueue.length > 0) {
|
|
118
|
+
const text = this.announcementQueue.shift();
|
|
119
|
+
const liveRegion = this.getLiveRegion();
|
|
120
|
+
|
|
121
|
+
// Clear previous announcement
|
|
122
|
+
liveRegion.textContent = '';
|
|
123
|
+
|
|
124
|
+
// Small delay to ensure screen reader picks up the change
|
|
125
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
126
|
+
|
|
127
|
+
// Set new announcement
|
|
128
|
+
liveRegion.textContent = text;
|
|
129
|
+
|
|
130
|
+
// Wait before processing next announcement
|
|
131
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
132
|
+
}
|
|
133
|
+
this.isProcessing = false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get current reference count (useful for debugging)
|
|
138
|
+
* @returns {number} Current reference count
|
|
139
|
+
*/
|
|
140
|
+
getReferenceCount() {
|
|
141
|
+
return this.referenceCount;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Singleton instance
|
|
146
|
+
const liveRegionManager = new LiveRegionManager();
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Hook for managing form field validation states
|
|
150
|
+
* @param {Object} options - Configuration options
|
|
151
|
+
* @param {boolean} options.required - Whether the field is required
|
|
152
|
+
* @param {Function} options.validator - Custom validation function
|
|
153
|
+
* @param {string} options.initialValue - Initial field value
|
|
154
|
+
* @returns {Object} Validation state and handlers
|
|
155
|
+
*/
|
|
156
|
+
const useFieldValidation = ({
|
|
157
|
+
required = false,
|
|
158
|
+
validator = null,
|
|
159
|
+
initialValue = ''
|
|
160
|
+
} = {}) => {
|
|
161
|
+
const [value, setValue] = (0, _react.useState)(initialValue);
|
|
162
|
+
const [validationState, setValidationState] = (0, _react.useState)('default');
|
|
163
|
+
const [errorText, setErrorText] = (0, _react.useState)('');
|
|
164
|
+
const [successText, setSuccessText] = (0, _react.useState)('');
|
|
165
|
+
const [touched, setTouched] = (0, _react.useState)(false);
|
|
166
|
+
const validate = (0, _react.useCallback)(inputValue => {
|
|
167
|
+
// Required validation
|
|
168
|
+
if (required && (!inputValue || inputValue.trim() === '')) {
|
|
169
|
+
return {
|
|
170
|
+
state: 'error',
|
|
171
|
+
errorText: 'This field is required',
|
|
172
|
+
successText: ''
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Custom validation
|
|
177
|
+
if (validator && inputValue) {
|
|
178
|
+
const result = validator(inputValue);
|
|
179
|
+
if (result.isValid === false) {
|
|
180
|
+
return {
|
|
181
|
+
state: 'error',
|
|
182
|
+
errorText: result.message || 'Invalid input',
|
|
183
|
+
successText: ''
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (result.isValid === true) {
|
|
187
|
+
return {
|
|
188
|
+
state: 'success',
|
|
189
|
+
errorText: '',
|
|
190
|
+
successText: result.message || 'Valid input'
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Default state for valid input
|
|
196
|
+
if (inputValue && inputValue.trim() !== '') {
|
|
197
|
+
return {
|
|
198
|
+
state: 'success',
|
|
199
|
+
errorText: '',
|
|
200
|
+
successText: ''
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
state: 'default',
|
|
205
|
+
errorText: '',
|
|
206
|
+
successText: ''
|
|
207
|
+
};
|
|
208
|
+
}, [required, validator]);
|
|
209
|
+
const handleChange = newValue => {
|
|
210
|
+
setValue(newValue);
|
|
211
|
+
if (touched) {
|
|
212
|
+
const validation = validate(newValue);
|
|
213
|
+
setValidationState(validation.state);
|
|
214
|
+
setErrorText(validation.errorText);
|
|
215
|
+
setSuccessText(validation.successText);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
const handleBlur = () => {
|
|
219
|
+
setTouched(true);
|
|
220
|
+
const validation = validate(value);
|
|
221
|
+
setValidationState(validation.state);
|
|
222
|
+
setErrorText(validation.errorText);
|
|
223
|
+
setSuccessText(validation.successText);
|
|
224
|
+
};
|
|
225
|
+
const reset = () => {
|
|
226
|
+
setValue(initialValue);
|
|
227
|
+
setValidationState('default');
|
|
228
|
+
setErrorText('');
|
|
229
|
+
setSuccessText('');
|
|
230
|
+
setTouched(false);
|
|
231
|
+
};
|
|
232
|
+
return {
|
|
233
|
+
value,
|
|
234
|
+
validationState,
|
|
235
|
+
errorText,
|
|
236
|
+
successText,
|
|
237
|
+
touched,
|
|
238
|
+
handleChange,
|
|
239
|
+
handleBlur,
|
|
240
|
+
reset,
|
|
241
|
+
isValid: validationState === 'success' || validationState === 'default' && (!required || value)
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Hook to manage label accessibility attributes
|
|
247
|
+
* @param {Object} options - Configuration options
|
|
248
|
+
* @returns {Object} Accessibility props and handlers
|
|
249
|
+
*/
|
|
250
|
+
exports.useFieldValidation = useFieldValidation;
|
|
251
|
+
const useLabelAccessibility = ({
|
|
252
|
+
labelId = '',
|
|
253
|
+
inputId = '',
|
|
254
|
+
helpText = '',
|
|
255
|
+
errorText = '',
|
|
256
|
+
successText = '',
|
|
257
|
+
required = false
|
|
258
|
+
} = {}) => {
|
|
259
|
+
const [announceText, setAnnounceText] = (0, _react.useState)('');
|
|
260
|
+
const previousErrorRef = (0, _react.useRef)('');
|
|
261
|
+
const liveRegionRef = (0, _react.useRef)(null);
|
|
262
|
+
|
|
263
|
+
// Generate unique IDs if not provided
|
|
264
|
+
const generatedLabelId = labelId || generateUniqueId('label');
|
|
265
|
+
const generatedInputId = inputId || generateUniqueId('input');
|
|
266
|
+
const helpId = helpText ? `${generatedInputId}-help` : '';
|
|
267
|
+
const errorId = errorText ? `${generatedInputId}-error` : '';
|
|
268
|
+
const successId = successText ? `${generatedInputId}-success` : '';
|
|
269
|
+
|
|
270
|
+
// Build aria-describedby
|
|
271
|
+
const ariaDescribedBy = [helpId, errorId, successId].filter(Boolean).join(' ') || undefined;
|
|
272
|
+
|
|
273
|
+
// Handle error announcements
|
|
274
|
+
(0, _react.useEffect)(() => {
|
|
275
|
+
if (errorText && errorText !== previousErrorRef.current) {
|
|
276
|
+
setAnnounceText(errorText);
|
|
277
|
+
previousErrorRef.current = errorText;
|
|
278
|
+
} else if (!errorText && previousErrorRef.current) {
|
|
279
|
+
setAnnounceText('');
|
|
280
|
+
previousErrorRef.current = '';
|
|
281
|
+
}
|
|
282
|
+
}, [errorText]);
|
|
283
|
+
|
|
284
|
+
// Manage live region reference and announcements
|
|
285
|
+
(0, _react.useEffect)(() => {
|
|
286
|
+
// Add reference to live region manager on mount
|
|
287
|
+
liveRegionRef.current = liveRegionManager.addReference();
|
|
288
|
+
return () => {
|
|
289
|
+
// Remove reference on unmount
|
|
290
|
+
liveRegionManager.removeReference();
|
|
291
|
+
liveRegionRef.current = null;
|
|
292
|
+
};
|
|
293
|
+
}, []);
|
|
294
|
+
|
|
295
|
+
// Handle announcements
|
|
296
|
+
(0, _react.useEffect)(() => {
|
|
297
|
+
if (announceText) {
|
|
298
|
+
liveRegionManager.announce(announceText);
|
|
299
|
+
}
|
|
300
|
+
}, [announceText]);
|
|
301
|
+
return {
|
|
302
|
+
labelProps: {
|
|
303
|
+
id: generatedLabelId,
|
|
304
|
+
htmlFor: generatedInputId
|
|
305
|
+
},
|
|
306
|
+
inputProps: {
|
|
307
|
+
id: generatedInputId,
|
|
308
|
+
'aria-describedby': ariaDescribedBy,
|
|
309
|
+
'aria-required': required ? 'true' : undefined,
|
|
310
|
+
'aria-invalid': errorText ? 'true' : undefined
|
|
311
|
+
},
|
|
312
|
+
helpProps: helpText ? {
|
|
313
|
+
id: helpId
|
|
314
|
+
} : {},
|
|
315
|
+
errorProps: errorText ? {
|
|
316
|
+
id: errorId,
|
|
317
|
+
role: 'alert'
|
|
318
|
+
} : {},
|
|
319
|
+
successProps: successText ? {
|
|
320
|
+
id: successId,
|
|
321
|
+
role: 'status'
|
|
322
|
+
} : {}
|
|
323
|
+
};
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Common validation functions for form fields
|
|
328
|
+
*/
|
|
329
|
+
exports.useLabelAccessibility = useLabelAccessibility;
|
|
330
|
+
const validators = exports.validators = {
|
|
331
|
+
/**
|
|
332
|
+
* Email validation
|
|
333
|
+
*/
|
|
334
|
+
email: value => {
|
|
335
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
336
|
+
return {
|
|
337
|
+
isValid: emailRegex.test(value),
|
|
338
|
+
message: emailRegex.test(value) ? 'Valid email address' : 'Please enter a valid email address'
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
/**
|
|
342
|
+
* Password validation (minimum 8 characters, mixed case, number)
|
|
343
|
+
*/
|
|
344
|
+
password: value => {
|
|
345
|
+
const hasMinLength = value.length >= 8;
|
|
346
|
+
const hasLowerCase = /[a-z]/.test(value);
|
|
347
|
+
const hasUpperCase = /[A-Z]/.test(value);
|
|
348
|
+
const hasNumber = /\d/.test(value);
|
|
349
|
+
const isValid = hasMinLength && hasLowerCase && hasUpperCase && hasNumber;
|
|
350
|
+
let message = '';
|
|
351
|
+
if (!isValid) {
|
|
352
|
+
const missing = [];
|
|
353
|
+
if (!hasMinLength) missing.push('at least 8 characters');
|
|
354
|
+
if (!hasLowerCase) missing.push('lowercase letter');
|
|
355
|
+
if (!hasUpperCase) missing.push('uppercase letter');
|
|
356
|
+
if (!hasNumber) missing.push('number');
|
|
357
|
+
message = `Password must contain ${missing.join(', ')}`;
|
|
358
|
+
} else {
|
|
359
|
+
message = 'Strong password';
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
isValid,
|
|
363
|
+
message
|
|
364
|
+
};
|
|
365
|
+
},
|
|
366
|
+
/**
|
|
367
|
+
* Phone number validation (US format)
|
|
368
|
+
*/
|
|
369
|
+
phone: value => {
|
|
370
|
+
const phoneRegex = /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/;
|
|
371
|
+
return {
|
|
372
|
+
isValid: phoneRegex.test(value),
|
|
373
|
+
message: phoneRegex.test(value) ? 'Valid phone number' : 'Please enter a valid phone number (e.g., 555-123-4567)'
|
|
374
|
+
};
|
|
375
|
+
},
|
|
376
|
+
/**
|
|
377
|
+
* URL validation
|
|
378
|
+
*/
|
|
379
|
+
url: value => {
|
|
380
|
+
try {
|
|
381
|
+
new URL(value);
|
|
382
|
+
return {
|
|
383
|
+
isValid: true,
|
|
384
|
+
message: 'Valid URL'
|
|
385
|
+
};
|
|
386
|
+
} catch {
|
|
387
|
+
return {
|
|
388
|
+
isValid: false,
|
|
389
|
+
message: 'Please enter a valid URL (e.g., https://example.com)'
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
/**
|
|
394
|
+
* Minimum length validation
|
|
395
|
+
*/
|
|
396
|
+
minLength: min => value => {
|
|
397
|
+
const isValid = value.length >= min;
|
|
398
|
+
return {
|
|
399
|
+
isValid,
|
|
400
|
+
message: isValid ? `Meets minimum length requirement` : `Must be at least ${min} characters`
|
|
401
|
+
};
|
|
402
|
+
},
|
|
403
|
+
/**
|
|
404
|
+
* Maximum length validation
|
|
405
|
+
*/
|
|
406
|
+
maxLength: max => value => {
|
|
407
|
+
const isValid = value.length <= max;
|
|
408
|
+
return {
|
|
409
|
+
isValid,
|
|
410
|
+
message: isValid ? `Within length limit` : `Must be no more than ${max} characters`
|
|
411
|
+
};
|
|
412
|
+
},
|
|
413
|
+
/**
|
|
414
|
+
* Custom regex validation
|
|
415
|
+
*/
|
|
416
|
+
regex: (pattern, message) => value => {
|
|
417
|
+
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
|
418
|
+
const isValid = regex.test(value);
|
|
419
|
+
return {
|
|
420
|
+
isValid,
|
|
421
|
+
message: isValid ? 'Valid format' : message || 'Invalid format'
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Hook for managing form validation state across multiple fields
|
|
428
|
+
* @param {Object} fields - Field configuration object
|
|
429
|
+
* @returns {Object} Form state and handlers
|
|
430
|
+
*/
|
|
431
|
+
const useFormValidation = (fields = {}) => {
|
|
432
|
+
const [formState, setFormState] = (0, _react.useState)({});
|
|
433
|
+
const [isSubmitting, setIsSubmitting] = (0, _react.useState)(false);
|
|
434
|
+
const [submitError, setSubmitError] = (0, _react.useState)('');
|
|
435
|
+
|
|
436
|
+
// Initialize form state
|
|
437
|
+
(0, _react.useEffect)(() => {
|
|
438
|
+
const initialState = {};
|
|
439
|
+
Object.keys(fields).forEach(fieldName => {
|
|
440
|
+
initialState[fieldName] = {
|
|
441
|
+
value: fields[fieldName].initialValue || '',
|
|
442
|
+
validationState: 'default',
|
|
443
|
+
errorText: '',
|
|
444
|
+
successText: '',
|
|
445
|
+
touched: false
|
|
446
|
+
};
|
|
447
|
+
});
|
|
448
|
+
setFormState(initialState);
|
|
449
|
+
}, [fields]);
|
|
450
|
+
const updateField = (fieldName, updates) => {
|
|
451
|
+
setFormState(prev => ({
|
|
452
|
+
...prev,
|
|
453
|
+
[fieldName]: {
|
|
454
|
+
...prev[fieldName],
|
|
455
|
+
...updates
|
|
456
|
+
}
|
|
457
|
+
}));
|
|
458
|
+
};
|
|
459
|
+
const validateField = (fieldName, value) => {
|
|
460
|
+
const fieldConfig = fields[fieldName];
|
|
461
|
+
if (!fieldConfig) return {
|
|
462
|
+
state: 'default',
|
|
463
|
+
errorText: '',
|
|
464
|
+
successText: ''
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// Required validation
|
|
468
|
+
if (fieldConfig.required && (!value || value.trim() === '')) {
|
|
469
|
+
return {
|
|
470
|
+
state: 'error',
|
|
471
|
+
errorText: fieldConfig.requiredMessage || 'This field is required',
|
|
472
|
+
successText: ''
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Custom validation
|
|
477
|
+
if (fieldConfig.validator && value) {
|
|
478
|
+
const result = fieldConfig.validator(value);
|
|
479
|
+
if (result.isValid === false) {
|
|
480
|
+
return {
|
|
481
|
+
state: 'error',
|
|
482
|
+
errorText: result.message || 'Invalid input',
|
|
483
|
+
successText: ''
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
if (result.isValid === true) {
|
|
487
|
+
return {
|
|
488
|
+
state: 'success',
|
|
489
|
+
errorText: '',
|
|
490
|
+
successText: result.message || ''
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
state: value ? 'success' : 'default',
|
|
496
|
+
errorText: '',
|
|
497
|
+
successText: ''
|
|
498
|
+
};
|
|
499
|
+
};
|
|
500
|
+
const handleFieldChange = (fieldName, value) => {
|
|
501
|
+
const validation = validateField(fieldName, value);
|
|
502
|
+
updateField(fieldName, {
|
|
503
|
+
value,
|
|
504
|
+
validationState: formState[fieldName]?.touched ? validation.state : 'default',
|
|
505
|
+
errorText: formState[fieldName]?.touched ? validation.errorText : '',
|
|
506
|
+
successText: formState[fieldName]?.touched ? validation.successText : ''
|
|
507
|
+
});
|
|
508
|
+
};
|
|
509
|
+
const handleFieldBlur = fieldName => {
|
|
510
|
+
const value = formState[fieldName]?.value || '';
|
|
511
|
+
const validation = validateField(fieldName, value);
|
|
512
|
+
updateField(fieldName, {
|
|
513
|
+
touched: true,
|
|
514
|
+
validationState: validation.state,
|
|
515
|
+
errorText: validation.errorText,
|
|
516
|
+
successText: validation.successText
|
|
517
|
+
});
|
|
518
|
+
};
|
|
519
|
+
const validateForm = () => {
|
|
520
|
+
let isValid = true;
|
|
521
|
+
const newFormState = {
|
|
522
|
+
...formState
|
|
523
|
+
};
|
|
524
|
+
Object.keys(fields).forEach(fieldName => {
|
|
525
|
+
const value = formState[fieldName]?.value || '';
|
|
526
|
+
const validation = validateField(fieldName, value);
|
|
527
|
+
newFormState[fieldName] = {
|
|
528
|
+
...newFormState[fieldName],
|
|
529
|
+
touched: true,
|
|
530
|
+
validationState: validation.state,
|
|
531
|
+
errorText: validation.errorText,
|
|
532
|
+
successText: validation.successText
|
|
533
|
+
};
|
|
534
|
+
if (validation.state === 'error') {
|
|
535
|
+
isValid = false;
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
setFormState(newFormState);
|
|
539
|
+
return isValid;
|
|
540
|
+
};
|
|
541
|
+
const handleSubmit = async onSubmit => {
|
|
542
|
+
setIsSubmitting(true);
|
|
543
|
+
setSubmitError('');
|
|
544
|
+
const isValid = validateForm();
|
|
545
|
+
if (isValid) {
|
|
546
|
+
try {
|
|
547
|
+
const formData = {};
|
|
548
|
+
Object.keys(formState).forEach(fieldName => {
|
|
549
|
+
formData[fieldName] = formState[fieldName].value;
|
|
550
|
+
});
|
|
551
|
+
await onSubmit(formData);
|
|
552
|
+
} catch (error) {
|
|
553
|
+
setSubmitError(error.message || 'An error occurred while submitting the form');
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
setIsSubmitting(false);
|
|
557
|
+
};
|
|
558
|
+
const resetForm = () => {
|
|
559
|
+
const resetState = {};
|
|
560
|
+
Object.keys(fields).forEach(fieldName => {
|
|
561
|
+
resetState[fieldName] = {
|
|
562
|
+
value: fields[fieldName].initialValue || '',
|
|
563
|
+
validationState: 'default',
|
|
564
|
+
errorText: '',
|
|
565
|
+
successText: '',
|
|
566
|
+
touched: false
|
|
567
|
+
};
|
|
568
|
+
});
|
|
569
|
+
setFormState(resetState);
|
|
570
|
+
setSubmitError('');
|
|
571
|
+
};
|
|
572
|
+
return {
|
|
573
|
+
formState,
|
|
574
|
+
isSubmitting,
|
|
575
|
+
submitError,
|
|
576
|
+
handleFieldChange,
|
|
577
|
+
handleFieldBlur,
|
|
578
|
+
handleSubmit,
|
|
579
|
+
resetForm,
|
|
580
|
+
isFormValid: Object.values(formState).every(field => field.validationState === 'success' || field.validationState === 'default' && !fields[Object.keys(formState).find(key => formState[key] === field)]?.required)
|
|
581
|
+
};
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Accessibility testing utilities for labels
|
|
586
|
+
*/
|
|
587
|
+
exports.useFormValidation = useFormValidation;
|
|
588
|
+
const labelTestingUtils = exports.labelTestingUtils = {
|
|
589
|
+
/**
|
|
590
|
+
* Validates label-input associations
|
|
591
|
+
*/
|
|
592
|
+
validateLabelAssociations() {
|
|
593
|
+
const labels = document.querySelectorAll('label[for]');
|
|
594
|
+
const results = [];
|
|
595
|
+
labels.forEach(label => {
|
|
596
|
+
const forValue = label.getAttribute('for');
|
|
597
|
+
const associatedInput = document.getElementById(forValue);
|
|
598
|
+
results.push({
|
|
599
|
+
labelText: label.textContent?.trim(),
|
|
600
|
+
forAttribute: forValue,
|
|
601
|
+
hasAssociatedInput: !!associatedInput,
|
|
602
|
+
inputType: associatedInput?.type || 'N/A',
|
|
603
|
+
hasAriaDescribedBy: !!associatedInput?.getAttribute('aria-describedby'),
|
|
604
|
+
isValid: !!associatedInput
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
return results;
|
|
608
|
+
},
|
|
609
|
+
/**
|
|
610
|
+
* Checks for proper required field indicators
|
|
611
|
+
*/
|
|
612
|
+
validateRequiredFields() {
|
|
613
|
+
const requiredInputs = document.querySelectorAll('input[required], select[required], textarea[required]');
|
|
614
|
+
const results = [];
|
|
615
|
+
requiredInputs.forEach(input => {
|
|
616
|
+
const id = input.id;
|
|
617
|
+
const label = document.querySelector(`label[for="${id}"]`);
|
|
618
|
+
const hasVisualIndicator = label?.textContent?.includes('*') || label?.querySelector('.required, .label__required');
|
|
619
|
+
const hasAriaRequired = input.getAttribute('aria-required') === 'true';
|
|
620
|
+
results.push({
|
|
621
|
+
inputId: id,
|
|
622
|
+
inputType: input.type,
|
|
623
|
+
hasLabel: !!label,
|
|
624
|
+
hasVisualIndicator,
|
|
625
|
+
hasAriaRequired,
|
|
626
|
+
labelText: label?.textContent?.trim() || 'No label',
|
|
627
|
+
isAccessible: hasVisualIndicator && hasAriaRequired
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
return results;
|
|
631
|
+
},
|
|
632
|
+
/**
|
|
633
|
+
* Tests validation message accessibility
|
|
634
|
+
*/
|
|
635
|
+
validateErrorMessages() {
|
|
636
|
+
const errorMessages = document.querySelectorAll('[role="alert"], .label__message--error');
|
|
637
|
+
const results = [];
|
|
638
|
+
errorMessages.forEach(message => {
|
|
639
|
+
const id = message.id;
|
|
640
|
+
const referencingInputs = document.querySelectorAll(`[aria-describedby*="${id}"]`);
|
|
641
|
+
results.push({
|
|
642
|
+
messageId: id,
|
|
643
|
+
messageText: message.textContent?.trim(),
|
|
644
|
+
hasRole: message.getAttribute('role') === 'alert',
|
|
645
|
+
hasAriaLive: !!message.getAttribute('aria-live'),
|
|
646
|
+
isReferencedByInput: referencingInputs.length > 0,
|
|
647
|
+
referencingInputCount: referencingInputs.length
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
return results;
|
|
651
|
+
},
|
|
652
|
+
/**
|
|
653
|
+
* Simulates keyboard navigation through form labels
|
|
654
|
+
*/
|
|
655
|
+
async simulateKeyboardNavigation() {
|
|
656
|
+
const labels = document.querySelectorAll('label');
|
|
657
|
+
const results = [];
|
|
658
|
+
for (let label of labels) {
|
|
659
|
+
// Simulate click on label
|
|
660
|
+
label.click();
|
|
661
|
+
const forValue = label.getAttribute('for');
|
|
662
|
+
const associatedInput = document.getElementById(forValue);
|
|
663
|
+
const isFocused = document.activeElement === associatedInput;
|
|
664
|
+
results.push({
|
|
665
|
+
labelText: label.textContent?.trim(),
|
|
666
|
+
forAttribute: forValue,
|
|
667
|
+
clickFocusesInput: isFocused,
|
|
668
|
+
focusedElementId: document.activeElement?.id || 'none',
|
|
669
|
+
isAccessible: isFocused
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// Small delay for real-world simulation
|
|
673
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
674
|
+
}
|
|
675
|
+
return results;
|
|
676
|
+
},
|
|
677
|
+
/**
|
|
678
|
+
* Checks color contrast ratios for labels
|
|
679
|
+
*/
|
|
680
|
+
checkColorContrast() {
|
|
681
|
+
const labels = document.querySelectorAll('label, .label__message');
|
|
682
|
+
const results = [];
|
|
683
|
+
labels.forEach(label => {
|
|
684
|
+
const styles = window.getComputedStyle(label);
|
|
685
|
+
const color = styles.color;
|
|
686
|
+
const backgroundColor = styles.backgroundColor;
|
|
687
|
+
|
|
688
|
+
// Note: Actual contrast calculation would require a color library
|
|
689
|
+
// This is a simplified check
|
|
690
|
+
results.push({
|
|
691
|
+
element: label.tagName,
|
|
692
|
+
className: label.className,
|
|
693
|
+
color,
|
|
694
|
+
backgroundColor,
|
|
695
|
+
// In a real implementation, calculate actual contrast ratio
|
|
696
|
+
contrastNote: 'Manual contrast checking required'
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
return results;
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
var _default = exports.default = {
|
|
703
|
+
useFieldValidation,
|
|
704
|
+
useLabelAccessibility,
|
|
705
|
+
useFormValidation,
|
|
706
|
+
validators,
|
|
707
|
+
labelTestingUtils
|
|
708
|
+
};
|