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