@abstraks-dev/ui-library 1.0.1

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