@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,338 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
validateEmail,
|
|
4
|
+
validatePhone,
|
|
5
|
+
validateLength,
|
|
6
|
+
validateRequired,
|
|
7
|
+
} from './inputValidation';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Custom hook for managing text input state with accessibility features
|
|
11
|
+
* Provides controlled state management for text input with ARIA support
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} options - Configuration options
|
|
14
|
+
* @param {string} options.defaultValue - Default input value
|
|
15
|
+
* @param {Function} options.onChange - Change callback function
|
|
16
|
+
* @param {boolean} options.required - Whether the field is required
|
|
17
|
+
* @param {number} options.maxLength - Maximum character count
|
|
18
|
+
* @param {number} options.minLength - Minimum character count
|
|
19
|
+
* @param {string} options.type - Input type for validation
|
|
20
|
+
* @returns {Object} TextInput state and handlers
|
|
21
|
+
*/
|
|
22
|
+
export const useTextInputAccessibility = (options = {}) => {
|
|
23
|
+
const {
|
|
24
|
+
defaultValue = '',
|
|
25
|
+
onChange,
|
|
26
|
+
required = false,
|
|
27
|
+
maxLength,
|
|
28
|
+
minLength,
|
|
29
|
+
type = 'text',
|
|
30
|
+
} = options;
|
|
31
|
+
|
|
32
|
+
const [value, setValue] = useState(defaultValue);
|
|
33
|
+
const [isValid, setIsValid] = useState(!required || defaultValue.length > 0);
|
|
34
|
+
const [hasBeenTouched, setHasBeenTouched] = useState(false);
|
|
35
|
+
const inputRef = useRef(null);
|
|
36
|
+
|
|
37
|
+
const validateInput = useCallback(
|
|
38
|
+
(inputValue, inputType) => {
|
|
39
|
+
let valid = true;
|
|
40
|
+
if (required && !validateRequired(inputValue)) valid = false;
|
|
41
|
+
if (!validateLength(inputValue, minLength, maxLength)) valid = false;
|
|
42
|
+
switch (inputType) {
|
|
43
|
+
case 'email':
|
|
44
|
+
if (inputValue && !validateEmail(inputValue)) valid = false;
|
|
45
|
+
break;
|
|
46
|
+
case 'tel':
|
|
47
|
+
if (inputValue && !validatePhone(inputValue)) valid = false;
|
|
48
|
+
break;
|
|
49
|
+
case 'url':
|
|
50
|
+
try {
|
|
51
|
+
if (
|
|
52
|
+
inputValue &&
|
|
53
|
+
!(
|
|
54
|
+
inputValue.startsWith('http://') ||
|
|
55
|
+
inputValue.startsWith('https://')
|
|
56
|
+
)
|
|
57
|
+
) {
|
|
58
|
+
valid = false;
|
|
59
|
+
}
|
|
60
|
+
} catch (e) {
|
|
61
|
+
valid = false;
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
default:
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
return valid;
|
|
68
|
+
},
|
|
69
|
+
[type]
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const handleFocus = useCallback((event) => {
|
|
73
|
+
setHasBeenTouched(true);
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const handleBlur = useCallback(
|
|
77
|
+
(event) => {
|
|
78
|
+
const newValue = event.target.value;
|
|
79
|
+
const valid = validateInput(newValue, type);
|
|
80
|
+
setIsValid(valid);
|
|
81
|
+
},
|
|
82
|
+
[type, validateInput]
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const reset = useCallback(() => {
|
|
86
|
+
setValue(defaultValue);
|
|
87
|
+
setIsValid(!required || defaultValue.length > 0);
|
|
88
|
+
setHasBeenTouched(false);
|
|
89
|
+
}, [defaultValue, required]);
|
|
90
|
+
|
|
91
|
+
const focus = useCallback(() => {
|
|
92
|
+
inputRef.current?.focus();
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
// Unified onChange handler for input/textarea
|
|
96
|
+
const handleChange = useCallback(
|
|
97
|
+
(event) => {
|
|
98
|
+
const newValue = event.target.value;
|
|
99
|
+
setValue(newValue);
|
|
100
|
+
if (onChange) onChange(newValue);
|
|
101
|
+
},
|
|
102
|
+
[onChange]
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
// State
|
|
107
|
+
value,
|
|
108
|
+
isValid,
|
|
109
|
+
hasBeenTouched,
|
|
110
|
+
showError: hasBeenTouched && !isValid,
|
|
111
|
+
|
|
112
|
+
// Handlers
|
|
113
|
+
handleFocus,
|
|
114
|
+
handleBlur,
|
|
115
|
+
handleChange,
|
|
116
|
+
reset,
|
|
117
|
+
focus,
|
|
118
|
+
|
|
119
|
+
// Ref
|
|
120
|
+
inputRef,
|
|
121
|
+
|
|
122
|
+
// Computed properties
|
|
123
|
+
charCount: value.length,
|
|
124
|
+
remainingChars: maxLength ? maxLength - value.length : null,
|
|
125
|
+
isAtMaxLength: maxLength ? value.length >= maxLength : false,
|
|
126
|
+
isAtMinLength: minLength ? value.length >= minLength : true,
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validates text input accessibility compliance
|
|
132
|
+
* Checks for proper ARIA attributes, labels, and keyboard support
|
|
133
|
+
*
|
|
134
|
+
* @param {HTMLElement} inputElement - The input element to validate
|
|
135
|
+
* @returns {Object} Validation results with errors and warnings
|
|
136
|
+
*/
|
|
137
|
+
export const validateTextInputAccessibility = (inputElement) => {
|
|
138
|
+
const results = {
|
|
139
|
+
passed: true,
|
|
140
|
+
errors: [],
|
|
141
|
+
warnings: [],
|
|
142
|
+
info: [],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
if (!inputElement) {
|
|
146
|
+
results.passed = false;
|
|
147
|
+
results.errors.push('No input element provided for validation');
|
|
148
|
+
return results;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check if it's actually an input element
|
|
152
|
+
if (inputElement.tagName.toLowerCase() !== 'input') {
|
|
153
|
+
const input = inputElement.querySelector('input');
|
|
154
|
+
if (!input) {
|
|
155
|
+
results.passed = false;
|
|
156
|
+
results.errors.push(
|
|
157
|
+
'Element is not an input and contains no input element'
|
|
158
|
+
);
|
|
159
|
+
return results;
|
|
160
|
+
}
|
|
161
|
+
inputElement = input;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 1. Check for accessible name
|
|
165
|
+
const hasLabel =
|
|
166
|
+
inputElement.hasAttribute('aria-label') ||
|
|
167
|
+
inputElement.hasAttribute('aria-labelledby') ||
|
|
168
|
+
inputElement.labels?.length > 0;
|
|
169
|
+
|
|
170
|
+
if (!hasLabel) {
|
|
171
|
+
results.passed = false;
|
|
172
|
+
results.errors.push(
|
|
173
|
+
'Input must have an accessible name (label, aria-label, or aria-labelledby)'
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 2. Check for proper ID if labelledby is used
|
|
178
|
+
if (inputElement.hasAttribute('aria-labelledby')) {
|
|
179
|
+
const labelId = inputElement.getAttribute('aria-labelledby');
|
|
180
|
+
const labelElement = document.getElementById(labelId);
|
|
181
|
+
if (!labelElement) {
|
|
182
|
+
results.passed = false;
|
|
183
|
+
results.errors.push(
|
|
184
|
+
`Referenced label element with ID "${labelId}" not found`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 3. Check for required field indication
|
|
190
|
+
if (
|
|
191
|
+
inputElement.hasAttribute('required') ||
|
|
192
|
+
inputElement.hasAttribute('aria-required')
|
|
193
|
+
) {
|
|
194
|
+
if (!inputElement.hasAttribute('aria-required')) {
|
|
195
|
+
results.warnings.push(
|
|
196
|
+
'Consider adding aria-required="true" for screen reader users'
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 4. Check for error state accessibility
|
|
202
|
+
if (inputElement.hasAttribute('aria-invalid')) {
|
|
203
|
+
const isInvalid = inputElement.getAttribute('aria-invalid') === 'true';
|
|
204
|
+
if (isInvalid && !inputElement.hasAttribute('aria-describedby')) {
|
|
205
|
+
results.warnings.push(
|
|
206
|
+
'Invalid input should have aria-describedby pointing to error message'
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 5. Check input type appropriateness
|
|
212
|
+
const inputType = inputElement.getAttribute('type') || 'text';
|
|
213
|
+
if (inputType === 'text' && inputElement.getAttribute('name')) {
|
|
214
|
+
const name = inputElement.getAttribute('name').toLowerCase();
|
|
215
|
+
if (name.includes('email')) {
|
|
216
|
+
results.warnings.push('Consider using type="email" for email inputs');
|
|
217
|
+
} else if (name.includes('phone') || name.includes('tel')) {
|
|
218
|
+
results.warnings.push('Consider using type="tel" for phone inputs');
|
|
219
|
+
} else if (name.includes('url') || name.includes('website')) {
|
|
220
|
+
results.warnings.push('Consider using type="url" for URL inputs');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 6. Check for placeholder accessibility
|
|
225
|
+
if (inputElement.hasAttribute('placeholder')) {
|
|
226
|
+
results.warnings.push(
|
|
227
|
+
'Avoid using placeholder as the only label - ensure proper labeling exists'
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 7. Check for disabled state
|
|
232
|
+
if (inputElement.disabled) {
|
|
233
|
+
results.info.push('Input is disabled');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 8. Check for keyboard accessibility
|
|
237
|
+
const tabIndex = inputElement.getAttribute('tabindex');
|
|
238
|
+
if (tabIndex && parseInt(tabIndex) < 0) {
|
|
239
|
+
results.warnings.push(
|
|
240
|
+
'Input has negative tabindex - may not be keyboard accessible'
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 9. Check autocomplete attributes
|
|
245
|
+
const autocomplete = inputElement.getAttribute('autocomplete');
|
|
246
|
+
if (!autocomplete && ['email', 'tel', 'url'].includes(inputType)) {
|
|
247
|
+
results.info.push(
|
|
248
|
+
`Consider adding autocomplete attribute for ${inputType} inputs`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (results.errors.length === 0) {
|
|
253
|
+
results.info.push('Basic accessibility validation passed');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return results;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Input validation utility for common input types
|
|
261
|
+
* Provides validation messages and patterns
|
|
262
|
+
*
|
|
263
|
+
* @param {string} value - Current input value
|
|
264
|
+
* @param {string} type - Input type
|
|
265
|
+
* @param {Object} options - Validation options
|
|
266
|
+
* @returns {Object} Validation result with message and validity
|
|
267
|
+
*/
|
|
268
|
+
export const getInputValidationInfo = (value, type, options = {}) => {
|
|
269
|
+
const { required = false, minLength, maxLength } = options;
|
|
270
|
+
|
|
271
|
+
let isValid = true;
|
|
272
|
+
let message = '';
|
|
273
|
+
let status = 'valid';
|
|
274
|
+
|
|
275
|
+
// Required validation
|
|
276
|
+
if (required && !value.trim()) {
|
|
277
|
+
isValid = false;
|
|
278
|
+
message = 'This field is required';
|
|
279
|
+
status = 'error';
|
|
280
|
+
return { isValid, message, status };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Length validation
|
|
284
|
+
if (minLength && value.length < minLength) {
|
|
285
|
+
isValid = false;
|
|
286
|
+
message = `Minimum ${minLength} characters required`;
|
|
287
|
+
status = 'error';
|
|
288
|
+
} else if (maxLength && value.length > maxLength) {
|
|
289
|
+
isValid = false;
|
|
290
|
+
message = `Maximum ${maxLength} characters allowed`;
|
|
291
|
+
status = 'error';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Type-specific validation
|
|
295
|
+
if (value && isValid) {
|
|
296
|
+
switch (type) {
|
|
297
|
+
case 'email':
|
|
298
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
299
|
+
if (!emailRegex.test(value)) {
|
|
300
|
+
isValid = false;
|
|
301
|
+
message = 'Please enter a valid email address';
|
|
302
|
+
status = 'error';
|
|
303
|
+
}
|
|
304
|
+
break;
|
|
305
|
+
case 'tel':
|
|
306
|
+
const phoneRegex = /^[\+]?[\s\-\(\)]?[\d\s\-\(\)]{10,}$/;
|
|
307
|
+
if (!phoneRegex.test(value)) {
|
|
308
|
+
isValid = false;
|
|
309
|
+
message = 'Please enter a valid phone number';
|
|
310
|
+
status = 'error';
|
|
311
|
+
}
|
|
312
|
+
break;
|
|
313
|
+
case 'url':
|
|
314
|
+
if (!value.startsWith('http://') && !value.startsWith('https://')) {
|
|
315
|
+
isValid = false;
|
|
316
|
+
message =
|
|
317
|
+
'Please enter a valid URL (starting with http:// or https://)';
|
|
318
|
+
status = 'error';
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
case 'number':
|
|
322
|
+
if (isNaN(Number(value))) {
|
|
323
|
+
isValid = false;
|
|
324
|
+
message = 'Please enter a valid number';
|
|
325
|
+
status = 'error';
|
|
326
|
+
}
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
isValid,
|
|
333
|
+
message,
|
|
334
|
+
status,
|
|
335
|
+
charCount: value.length,
|
|
336
|
+
remainingChars: maxLength ? maxLength - value.length : null,
|
|
337
|
+
};
|
|
338
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom hook for typography accessibility features
|
|
5
|
+
* Manages user preferences for font size, contrast, and motion
|
|
6
|
+
*
|
|
7
|
+
* @returns {Object} Typography accessibility state and utilities
|
|
8
|
+
*/
|
|
9
|
+
export const useTypographyAccessibility = () => {
|
|
10
|
+
const [preferences, setPreferences] = useState({
|
|
11
|
+
highContrast: false,
|
|
12
|
+
reducedMotion: false,
|
|
13
|
+
fontSize: 100,
|
|
14
|
+
currentBreakpoint: 'small',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Detect user's system preferences
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const detectSystemPreferences = () => {
|
|
20
|
+
setPreferences((prev) => ({
|
|
21
|
+
...prev,
|
|
22
|
+
highContrast:
|
|
23
|
+
window.matchMedia('(prefers-contrast: high)').matches || false,
|
|
24
|
+
reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)')
|
|
25
|
+
.matches,
|
|
26
|
+
}));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
detectSystemPreferences();
|
|
30
|
+
|
|
31
|
+
// Listen for changes in system preferences
|
|
32
|
+
const contrastQuery = window.matchMedia('(prefers-contrast: high)');
|
|
33
|
+
const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
34
|
+
|
|
35
|
+
contrastQuery.addEventListener('change', detectSystemPreferences);
|
|
36
|
+
motionQuery.addEventListener('change', detectSystemPreferences);
|
|
37
|
+
|
|
38
|
+
return () => {
|
|
39
|
+
contrastQuery.removeEventListener('change', detectSystemPreferences);
|
|
40
|
+
motionQuery.removeEventListener('change', detectSystemPreferences);
|
|
41
|
+
};
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
// Detect current breakpoint
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const updateBreakpoint = () => {
|
|
47
|
+
const width = window.innerWidth;
|
|
48
|
+
let breakpoint;
|
|
49
|
+
|
|
50
|
+
if (width >= 1200) breakpoint = 'xlarge';
|
|
51
|
+
else if (width >= 992) breakpoint = 'large';
|
|
52
|
+
else if (width >= 768) breakpoint = 'medium';
|
|
53
|
+
else breakpoint = 'small';
|
|
54
|
+
|
|
55
|
+
setPreferences((prev) => ({ ...prev, currentBreakpoint: breakpoint }));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
updateBreakpoint();
|
|
59
|
+
window.addEventListener('resize', updateBreakpoint);
|
|
60
|
+
|
|
61
|
+
return () => window.removeEventListener('resize', updateBreakpoint);
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
// Utility functions
|
|
65
|
+
const toggleHighContrast = () => {
|
|
66
|
+
setPreferences((prev) => ({ ...prev, highContrast: !prev.highContrast }));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const setFontSize = (size) => {
|
|
70
|
+
setPreferences((prev) => ({
|
|
71
|
+
...prev,
|
|
72
|
+
fontSize: Math.max(50, Math.min(200, size)),
|
|
73
|
+
}));
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const getAccessibilityStyles = () => {
|
|
77
|
+
return {
|
|
78
|
+
fontSize: `${preferences.fontSize}%`,
|
|
79
|
+
filter: preferences.highContrast
|
|
80
|
+
? 'contrast(150%) brightness(120%)'
|
|
81
|
+
: 'none',
|
|
82
|
+
transition: preferences.reducedMotion ? 'none' : undefined,
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const getBreakpointLabel = () => {
|
|
87
|
+
const labels = {
|
|
88
|
+
small: 'Small screens (<768px)',
|
|
89
|
+
medium: 'Medium (768px+)',
|
|
90
|
+
large: 'Large (992px+)',
|
|
91
|
+
xlarge: 'Extra Large (1200px+)',
|
|
92
|
+
};
|
|
93
|
+
return labels[preferences.currentBreakpoint];
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
preferences,
|
|
98
|
+
toggleHighContrast,
|
|
99
|
+
setFontSize,
|
|
100
|
+
getAccessibilityStyles,
|
|
101
|
+
getBreakpointLabel,
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Utility for generating typography class names with accessibility considerations
|
|
107
|
+
*
|
|
108
|
+
* @param {string} baseClass - Base class name
|
|
109
|
+
* @param {string} size - Size variant (small, medium, large)
|
|
110
|
+
* @param {string} additionalClass - Additional class names
|
|
111
|
+
* @param {Object} preferences - Accessibility preferences
|
|
112
|
+
* @returns {string} Combined class names
|
|
113
|
+
*/
|
|
114
|
+
export const getTypographyClassNames = (
|
|
115
|
+
baseClass,
|
|
116
|
+
size = 'medium',
|
|
117
|
+
additionalClass = '',
|
|
118
|
+
preferences = {}
|
|
119
|
+
) => {
|
|
120
|
+
const classes = [baseClass];
|
|
121
|
+
|
|
122
|
+
// Add size modifier
|
|
123
|
+
if (size && size !== 'medium') {
|
|
124
|
+
classes.push(`${baseClass}--${size}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Add accessibility modifiers
|
|
128
|
+
if (preferences.highContrast) {
|
|
129
|
+
classes.push(`${baseClass}--high-contrast`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (preferences.reducedMotion) {
|
|
133
|
+
classes.push(`${baseClass}--reduced-motion`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Add additional classes
|
|
137
|
+
if (additionalClass) {
|
|
138
|
+
classes.push(additionalClass);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return classes.join(' ');
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Focus management utility for typography components
|
|
146
|
+
* Ensures proper focus indicators and keyboard navigation
|
|
147
|
+
*
|
|
148
|
+
* @param {React.RefObject} elementRef - Reference to the element
|
|
149
|
+
* @param {Object} options - Focus options
|
|
150
|
+
*/
|
|
151
|
+
export const useFocusManagement = (elementRef, options = {}) => {
|
|
152
|
+
const { onFocus, onBlur, focusOnMount = false, trapFocus = false } = options;
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const element = elementRef.current;
|
|
156
|
+
if (!element) return;
|
|
157
|
+
|
|
158
|
+
if (focusOnMount) {
|
|
159
|
+
element.focus();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const handleFocus = (event) => {
|
|
163
|
+
element.setAttribute('data-focused', 'true');
|
|
164
|
+
onFocus?.(event);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const handleBlur = (event) => {
|
|
168
|
+
element.removeAttribute('data-focused');
|
|
169
|
+
onBlur?.(event);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
element.addEventListener('focus', handleFocus);
|
|
173
|
+
element.addEventListener('blur', handleBlur);
|
|
174
|
+
|
|
175
|
+
return () => {
|
|
176
|
+
element.removeEventListener('focus', handleFocus);
|
|
177
|
+
element.removeEventListener('blur', handleBlur);
|
|
178
|
+
};
|
|
179
|
+
}, [elementRef, onFocus, onBlur, focusOnMount, trapFocus]);
|
|
180
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export const useWindowSize = () => {
|
|
4
|
+
// Initialize state with undefined width/height so server and client renders match
|
|
5
|
+
const [windowSize, setWindowSize] = useState({
|
|
6
|
+
width: undefined,
|
|
7
|
+
height: undefined,
|
|
8
|
+
});
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
// Handler to call on window resize
|
|
11
|
+
function handleResize() {
|
|
12
|
+
// Set window width/height to state
|
|
13
|
+
setWindowSize({
|
|
14
|
+
width: window.innerWidth,
|
|
15
|
+
height: window.innerHeight,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
// Add event listener
|
|
19
|
+
window.addEventListener('resize', handleResize);
|
|
20
|
+
// Call handler right away so state gets updated with initial window size
|
|
21
|
+
handleResize();
|
|
22
|
+
// Remove event listener on cleanup
|
|
23
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
24
|
+
}, []); // Empty array ensures that effect is only run on mount
|
|
25
|
+
return windowSize;
|
|
26
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation utilities for form inputs
|
|
3
|
+
* Contains common validation patterns and functions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Common email validation regex pattern
|
|
7
|
+
// This pattern checks for basic email format: user@domain.extension
|
|
8
|
+
export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
9
|
+
|
|
10
|
+
// More comprehensive email regex (RFC 5322 compliant, simplified)
|
|
11
|
+
export const EMAIL_REGEX_STRICT =
|
|
12
|
+
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
13
|
+
|
|
14
|
+
// Common validation patterns
|
|
15
|
+
export const VALIDATION_PATTERNS = {
|
|
16
|
+
email: EMAIL_REGEX,
|
|
17
|
+
phone: /^[\+]?[1-9][\d]{0,15}$/,
|
|
18
|
+
url: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/,
|
|
19
|
+
postalCode: /^[A-Za-z0-9\s-]{3,10}$/,
|
|
20
|
+
creditCard: /^[0-9]{13,19}$/,
|
|
21
|
+
strongPassword:
|
|
22
|
+
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validates an email address
|
|
27
|
+
* @param {string} email - The email address to validate
|
|
28
|
+
* @param {boolean} strict - Whether to use strict RFC 5322 validation
|
|
29
|
+
* @returns {boolean} - True if email is valid
|
|
30
|
+
*/
|
|
31
|
+
export const validateEmail = (email, strict = false) => {
|
|
32
|
+
if (!email || typeof email !== 'string') {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const pattern = strict ? EMAIL_REGEX_STRICT : EMAIL_REGEX;
|
|
36
|
+
return pattern.test(email.trim());
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validates a phone number
|
|
41
|
+
* @param {string} phone - The phone number to validate
|
|
42
|
+
* @returns {boolean} - True if phone is valid
|
|
43
|
+
*/
|
|
44
|
+
export const validatePhone = (phone) => {
|
|
45
|
+
if (!phone || typeof phone !== 'string') {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return VALIDATION_PATTERNS.phone.test(phone.replace(/[\s\-\(\)]/g, ''));
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validates a URL
|
|
53
|
+
* @param {string} url - The URL to validate
|
|
54
|
+
* @returns {boolean} - True if URL is valid
|
|
55
|
+
*/
|
|
56
|
+
export const validateUrl = (url) => {
|
|
57
|
+
if (!url || typeof url !== 'string') {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return VALIDATION_PATTERNS.url.test(url.trim());
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validates password strength
|
|
65
|
+
* @param {string} password - The password to validate
|
|
66
|
+
* @returns {boolean} - True if password meets strength requirements
|
|
67
|
+
*/
|
|
68
|
+
export const validatePasswordStrength = (password) => {
|
|
69
|
+
if (!password || typeof password !== 'string') {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return VALIDATION_PATTERNS.strongPassword.test(password);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Generic field validation function
|
|
77
|
+
* @param {string} value - The value to validate
|
|
78
|
+
* @param {Object} rules - Validation rules object
|
|
79
|
+
* @param {string} fieldName - Name of the field for error messages
|
|
80
|
+
* @returns {string|null} - Error message or null if valid
|
|
81
|
+
*/
|
|
82
|
+
export const validateField = (value, rules, fieldName) => {
|
|
83
|
+
if (!rules) return null;
|
|
84
|
+
|
|
85
|
+
// Required validation
|
|
86
|
+
if (rules.required && (!value || value.trim() === '')) {
|
|
87
|
+
return `${fieldName} is required`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Skip other validations if field is empty and not required
|
|
91
|
+
if (!value || value.trim() === '') {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Email validation
|
|
96
|
+
if (rules.email && !validateEmail(value, rules.strict)) {
|
|
97
|
+
return 'Please enter a valid email address';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Phone validation
|
|
101
|
+
if (rules.phone && !validatePhone(value)) {
|
|
102
|
+
return 'Please enter a valid phone number';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// URL validation
|
|
106
|
+
if (rules.url && !validateUrl(value)) {
|
|
107
|
+
return 'Please enter a valid URL';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Password strength validation
|
|
111
|
+
if (rules.strongPassword && !validatePasswordStrength(value)) {
|
|
112
|
+
return 'Password must contain at least 8 characters with uppercase, lowercase, number, and special character';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Minimum length validation
|
|
116
|
+
if (rules.minLength && value.length < rules.minLength) {
|
|
117
|
+
return `${fieldName} must be at least ${rules.minLength} characters`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Maximum length validation
|
|
121
|
+
if (rules.maxLength && value.length > rules.maxLength) {
|
|
122
|
+
return `${fieldName} must not exceed ${rules.maxLength} characters`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Pattern validation
|
|
126
|
+
if (rules.pattern && !rules.pattern.test(value)) {
|
|
127
|
+
return rules.patternMessage || `${fieldName} format is invalid`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
};
|