@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,298 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useHeadingId = exports.useHeadingHierarchy = exports.useHeadingAccessibility = exports.HeadingHierarchyProvider = void 0;
7
+ var _react = require("react");
8
+ // Context for tracking heading hierarchy across components
9
+ const HeadingHierarchyContext = /*#__PURE__*/(0, _react.createContext)({
10
+ registeredHeadings: [],
11
+ registerHeading: () => {},
12
+ unregisterHeading: () => {}
13
+ });
14
+
15
+ /**
16
+ * Provider component for heading hierarchy context
17
+ * Allows automatic tracking of heading levels across multiple components
18
+ *
19
+ * @param {Object} props - Component props
20
+ * @param {React.ReactNode} props.children - Child components
21
+ * @returns {JSX.Element} Context provider
22
+ *
23
+ * @example
24
+ * <HeadingHierarchyProvider>
25
+ * <YourApp />
26
+ * </HeadingHierarchyProvider>
27
+ */
28
+ const HeadingHierarchyProvider = ({
29
+ children
30
+ }) => {
31
+ const [registeredHeadings, setRegisteredHeadings] = (0, _react.useState)([]);
32
+ const registerHeading = (id, level) => {
33
+ setRegisteredHeadings(prev => {
34
+ const existing = prev.find(h => h.id === id);
35
+ if (existing) {
36
+ return prev.map(h => h.id === id ? {
37
+ ...h,
38
+ level
39
+ } : h);
40
+ }
41
+ return [...prev, {
42
+ id,
43
+ level,
44
+ timestamp: Date.now()
45
+ }];
46
+ });
47
+ };
48
+ const unregisterHeading = id => {
49
+ setRegisteredHeadings(prev => prev.filter(h => h.id !== id));
50
+ };
51
+ const value = {
52
+ registeredHeadings,
53
+ registerHeading,
54
+ unregisterHeading
55
+ };
56
+ return /*#__PURE__*/React.createElement(HeadingHierarchyContext.Provider, {
57
+ value: value
58
+ }, children);
59
+ };
60
+
61
+ /**
62
+ * Custom hook for enhanced heading accessibility features
63
+ *
64
+ * @param {Object} options - Configuration options
65
+ * @param {number} options.level - Heading level (1-6)
66
+ * @param {boolean} options.autoFocus - Whether to auto-focus the heading on mount
67
+ * @param {boolean} options.announceOnChange - Whether to announce content changes to screen readers
68
+ * @param {Function} options.onFocus - Callback when heading receives focus
69
+ * @param {boolean} options.registerWithContext - Whether to register with HeadingHierarchyContext (default: true)
70
+ * @param {string} options.id - Unique identifier for context registration (auto-generated if not provided)
71
+ * @returns {Object} Ref and accessibility properties
72
+ *
73
+ * @example
74
+ * const { headingRef, ariaProps } = useHeadingAccessibility({
75
+ * level: 1,
76
+ * autoFocus: true,
77
+ * announceOnChange: true
78
+ * });
79
+ */
80
+ exports.HeadingHierarchyProvider = HeadingHierarchyProvider;
81
+ const useHeadingAccessibility = ({
82
+ level = 1,
83
+ autoFocus = false,
84
+ announceOnChange = false,
85
+ onFocus = null,
86
+ registerWithContext = true,
87
+ id = null
88
+ } = {}) => {
89
+ const headingRef = (0, _react.useRef)(null);
90
+ const previousContent = (0, _react.useRef)('');
91
+ const headingId = (0, _react.useRef)(id || `heading-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
92
+
93
+ // Try to get context, but don't fail if not available
94
+ const hierarchyContext = (0, _react.useContext)(HeadingHierarchyContext);
95
+
96
+ // Determine if this is the primary header (first h1)
97
+ let isPrimaryHeader = false;
98
+ if (level === 1) {
99
+ if (hierarchyContext && Array.isArray(hierarchyContext.registeredHeadings) && hierarchyContext.registeredHeadings.length > 0) {
100
+ // Find the first registered h1
101
+ const firstH1 = hierarchyContext.registeredHeadings.find(h => h.level === 1);
102
+ isPrimaryHeader = firstH1 && firstH1.id === headingId.current;
103
+ } else {
104
+ // If no context, assume this is the primary header
105
+ isPrimaryHeader = true;
106
+ }
107
+ }
108
+ (0, _react.useEffect)(() => {
109
+ if (autoFocus && headingRef.current) {
110
+ // Focus the heading for immediate screen reader announcement
111
+ headingRef.current.focus();
112
+ }
113
+ }, [autoFocus]);
114
+ (0, _react.useEffect)(() => {
115
+ // Register with context if available and enabled
116
+ if (registerWithContext && hierarchyContext?.registerHeading) {
117
+ hierarchyContext.registerHeading(headingId.current, level);
118
+ }
119
+
120
+ // Cleanup: unregister when component unmounts or level changes
121
+ return () => {
122
+ if (registerWithContext && hierarchyContext?.unregisterHeading) {
123
+ hierarchyContext.unregisterHeading(headingId.current);
124
+ }
125
+ };
126
+ }, [level, registerWithContext, hierarchyContext]);
127
+ (0, _react.useEffect)(() => {
128
+ const heading = headingRef.current;
129
+ if (!heading || !announceOnChange) return;
130
+ const currentContent = heading.textContent || heading.innerText;
131
+
132
+ // Announce content changes to screen readers
133
+ if (previousContent.current !== currentContent && previousContent.current !== '') {
134
+ const announcement = document.createElement('div');
135
+ announcement.setAttribute('aria-live', 'polite');
136
+ announcement.setAttribute('aria-atomic', 'true');
137
+ announcement.className = 'sr-only';
138
+ announcement.textContent = `Heading updated: ${currentContent}`;
139
+ document.body.appendChild(announcement);
140
+
141
+ // Clean up announcement after screen reader has time to process
142
+ setTimeout(() => {
143
+ if (document.body.contains(announcement)) {
144
+ document.body.removeChild(announcement);
145
+ }
146
+ }, 1000);
147
+ }
148
+ previousContent.current = currentContent;
149
+ }, [announceOnChange, headingRef]);
150
+ const handleFocus = event => {
151
+ if (onFocus) {
152
+ onFocus(event);
153
+ }
154
+ };
155
+ const ariaProps = {
156
+ 'aria-level': level,
157
+ tabIndex: autoFocus ? 0 : undefined,
158
+ onFocus: onFocus ? handleFocus : undefined,
159
+ id: headingId.current
160
+ };
161
+ // Set role="banner" for the primary h1
162
+ if (level === 1 && isPrimaryHeader) {
163
+ ariaProps.role = 'banner';
164
+ }
165
+ return {
166
+ headingRef,
167
+ ariaProps,
168
+ headingId: headingId.current
169
+ };
170
+ };
171
+
172
+ /**
173
+ * Automatically detects existing heading levels in the DOM
174
+ * @returns {Array} Array of existing heading levels found in the document
175
+ */
176
+ exports.useHeadingAccessibility = useHeadingAccessibility;
177
+ const detectExistingHeadingLevels = () => {
178
+ const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
179
+ const levels = Array.from(headings).map(heading => {
180
+ const tagName = heading.tagName.toLowerCase();
181
+ return parseInt(tagName.charAt(1), 10);
182
+ });
183
+ return [...new Set(levels)].sort((a, b) => a - b);
184
+ };
185
+
186
+ /**
187
+ * Hook to validate heading hierarchy and warn about accessibility issues
188
+ * Automatically detects existing heading levels in the DOM to eliminate manual tracking
189
+ *
190
+ * @param {number} level - Current heading level
191
+ * @param {Object} options - Configuration options
192
+ * @param {Array} options.existingLevels - Manual override for existing levels (optional)
193
+ * @param {boolean} options.autoDetect - Whether to automatically detect existing levels (default: true)
194
+ * @returns {Object} Validation results and warnings
195
+ *
196
+ * @example
197
+ * // Automatic detection (recommended)
198
+ * const { isValidHierarchy, warnings } = useHeadingHierarchy(3);
199
+ *
200
+ * // Manual override if needed
201
+ * const { isValidHierarchy, warnings } = useHeadingHierarchy(3, { existingLevels: [1, 2] });
202
+ */
203
+ /**
204
+ * Hook to validate heading hierarchy and warn about accessibility issues
205
+ * Automatically detects existing heading levels using context or DOM scanning
206
+ *
207
+ * @param {number} level - Current heading level
208
+ * @param {Object} options - Configuration options
209
+ * @param {Array} options.existingLevels - Manual override for existing levels (optional)
210
+ * @param {boolean} options.useContext - Whether to use HeadingHierarchyContext for detection (default: true)
211
+ * @param {boolean} options.autoDetect - Whether to automatically detect existing levels via DOM (default: true when context unavailable)
212
+ * @returns {Object} Validation results and warnings
213
+ *
214
+ * @example
215
+ * // Automatic detection with context (recommended)
216
+ * const { isValidHierarchy, warnings, detectedLevels } = useHeadingHierarchy(3);
217
+ *
218
+ * // Manual override if needed
219
+ * const { isValidHierarchy, warnings } = useHeadingHierarchy(3, { existingLevels: [1, 2] });
220
+ */
221
+ const useHeadingHierarchy = (level, options = {}) => {
222
+ const {
223
+ existingLevels: manualLevels = null,
224
+ useContext: useContextDetection = true,
225
+ autoDetect = true
226
+ } = options;
227
+ const hierarchyContext = (0, _react.useContext)(HeadingHierarchyContext);
228
+ const [detectedLevels, setDetectedLevels] = (0, _react.useState)([]);
229
+ const warnings = [];
230
+ let isValidHierarchy = true;
231
+
232
+ // Auto-detect existing heading levels when component mounts or when autoDetect is enabled
233
+ (0, _react.useEffect)(() => {
234
+ if (manualLevels !== null) return; // Skip detection if manual levels provided
235
+
236
+ if (useContextDetection && hierarchyContext?.registeredHeadings) {
237
+ // Use context-based detection (preferred)
238
+ const contextLevels = hierarchyContext.registeredHeadings.map(h => h.level).filter(l => l !== level) // Exclude current level
239
+ .sort((a, b) => a - b);
240
+ setDetectedLevels([...new Set(contextLevels)]);
241
+ } else if (autoDetect) {
242
+ // Fall back to DOM scanning
243
+ const domLevels = detectExistingHeadingLevels().filter(l => l !== level); // Exclude current level to avoid self-detection
244
+ setDetectedLevels(domLevels);
245
+ }
246
+ }, [level, manualLevels, useContextDetection, autoDetect, hierarchyContext?.registeredHeadings]);
247
+
248
+ // Use manual levels if provided, otherwise use auto-detected levels
249
+ const existingLevels = manualLevels !== null ? manualLevels : detectedLevels;
250
+ if (existingLevels.length === 0 && level !== 1) {
251
+ warnings.push('Page should start with an h1 heading');
252
+ isValidHierarchy = false;
253
+ }
254
+ if (existingLevels.length > 0) {
255
+ const lastLevel = Math.max(...existingLevels);
256
+ const levelGap = level - lastLevel;
257
+ if (levelGap > 1) {
258
+ warnings.push(`Heading level gap detected: h${lastLevel} → h${level}. Consider using h${lastLevel + 1} instead.`);
259
+ isValidHierarchy = false;
260
+ }
261
+ }
262
+
263
+ // Log warnings in development
264
+ (0, _react.useEffect)(() => {
265
+ if (process.env.NODE_ENV === 'development' && warnings.length > 0) {
266
+ console.warn('Heading accessibility warnings:', warnings);
267
+ }
268
+ }, [warnings]);
269
+ return {
270
+ isValidHierarchy,
271
+ warnings,
272
+ detectedLevels: existingLevels,
273
+ detectionMethod: manualLevels !== null ? 'manual' : useContextDetection && hierarchyContext?.registeredHeadings ? 'context' : 'dom'
274
+ };
275
+ };
276
+
277
+ /**
278
+ * Hook to generate heading IDs for anchor links and table of contents
279
+ *
280
+ * @param {string} text - Heading text content
281
+ * @param {string} prefix - Optional prefix for the ID
282
+ * @returns {string} Generated heading ID
283
+ *
284
+ * @example
285
+ * const headingId = useHeadingId('Getting Started Guide', 'section');
286
+ * // Returns: 'section-getting-started-guide'
287
+ */
288
+ exports.useHeadingHierarchy = useHeadingHierarchy;
289
+ const useHeadingId = (text, prefix = '') => {
290
+ if (!text) return '';
291
+ const baseId = text.toString().toLowerCase().replace(/[^a-z0-9\s-]/g, '') // Remove special characters
292
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
293
+ .replace(/-+/g, '-') // Remove multiple consecutive hyphens
294
+ .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
295
+
296
+ return prefix ? `${prefix}-${baseId}` : baseId;
297
+ };
298
+ exports.useHeadingId = useHeadingId;
@@ -0,0 +1,260 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.validateRadioGroupAccessibility = exports.useRadioKeyboardNavigation = exports.useRadioGroup = void 0;
7
+ var _react = require("react");
8
+ /**
9
+ * Custom hook for managing radio group state with accessibility features
10
+ * Provides controlled state management for radio groups with validation
11
+ *
12
+ * @param {Object} options - Configuration options
13
+ * @param {string} options.name - Name for the radio group (required)
14
+ * @param {string} options.defaultValue - Default selected value
15
+ * @param {boolean} options.required - Whether selection is required
16
+ * @param {Function} options.onChange - Change callback function
17
+ * @param {Function} options.onValidate - Custom validation function
18
+ * @returns {Object} Radio group state and handlers
19
+ */
20
+ const useRadioGroup = (options = {}) => {
21
+ const {
22
+ name,
23
+ defaultValue = '',
24
+ required = false,
25
+ onChange,
26
+ onValidate
27
+ } = options;
28
+ const [selectedValue, setSelectedValue] = (0, _react.useState)(defaultValue);
29
+ const [hasError, setHasError] = (0, _react.useState)(false);
30
+ const [errorMessage, setErrorMessage] = (0, _react.useState)('');
31
+ const [touched, setTouched] = (0, _react.useState)(false);
32
+
33
+ // Handle radio selection change
34
+ const handleChange = (0, _react.useCallback)(event => {
35
+ const value = event.target.value;
36
+ setSelectedValue(value);
37
+ setTouched(true);
38
+
39
+ // Clear error when user makes a selection
40
+ if (hasError && value) {
41
+ setHasError(false);
42
+ setErrorMessage('');
43
+ }
44
+
45
+ // Call external onChange if provided
46
+ onChange?.(event, value);
47
+ }, [hasError, onChange]);
48
+
49
+ // Validate the current selection
50
+ const validate = (0, _react.useCallback)(() => {
51
+ let isValid = true;
52
+ let message = '';
53
+
54
+ // Required field validation
55
+ if (required && !selectedValue) {
56
+ isValid = false;
57
+ message = 'Please select an option.';
58
+ }
59
+
60
+ // Custom validation
61
+ if (onValidate && selectedValue) {
62
+ const customValidation = onValidate(selectedValue);
63
+ if (customValidation !== true) {
64
+ isValid = false;
65
+ message = customValidation || 'Invalid selection.';
66
+ }
67
+ }
68
+ setHasError(!isValid);
69
+ setErrorMessage(message);
70
+ return isValid;
71
+ }, [required, selectedValue, onValidate]);
72
+
73
+ // Get props for individual radio inputs
74
+ const getRadioProps = (0, _react.useCallback)(value => ({
75
+ name,
76
+ value,
77
+ checked: selectedValue === value,
78
+ onChange: handleChange,
79
+ error: hasError,
80
+ errorText: hasError ? errorMessage : '',
81
+ 'data-radio-group': name
82
+ }), [name, selectedValue, handleChange, hasError, errorMessage]);
83
+
84
+ // Reset the group state
85
+ const reset = (0, _react.useCallback)(() => {
86
+ setSelectedValue(defaultValue);
87
+ setHasError(false);
88
+ setErrorMessage('');
89
+ setTouched(false);
90
+ }, [defaultValue]);
91
+
92
+ // Set value programmatically
93
+ const setValue = (0, _react.useCallback)(value => {
94
+ setSelectedValue(value);
95
+ setTouched(true);
96
+ if (hasError) {
97
+ setHasError(false);
98
+ setErrorMessage('');
99
+ }
100
+ }, [hasError]);
101
+ return {
102
+ // State
103
+ selectedValue,
104
+ hasError,
105
+ errorMessage,
106
+ touched,
107
+ isValid: !hasError,
108
+ // Handlers
109
+ handleChange,
110
+ validate,
111
+ reset,
112
+ setValue,
113
+ getRadioProps,
114
+ // Group props for fieldset
115
+ groupProps: {
116
+ name,
117
+ 'data-radio-group': name,
118
+ 'aria-invalid': hasError,
119
+ 'aria-required': required
120
+ }
121
+ };
122
+ };
123
+
124
+ /**
125
+ * Utility for managing keyboard navigation within radio groups
126
+ * Implements proper arrow key navigation as per ARIA standards
127
+ *
128
+ * @param {HTMLElement} groupElement - The radio group container
129
+ * @param {Object} options - Navigation options
130
+ * @param {Function} options.onChange - Optional callback when radio selection changes via keyboard
131
+ * @returns {Object} Navigation utilities
132
+ */
133
+ exports.useRadioGroup = useRadioGroup;
134
+ const useRadioKeyboardNavigation = (groupElement, options = {}) => {
135
+ const {
136
+ onChange
137
+ } = options;
138
+ const getRadioInputs = (0, _react.useCallback)(() => {
139
+ if (!groupElement) return [];
140
+ return Array.from(groupElement.querySelectorAll('input[type="radio"]:not(:disabled)'));
141
+ }, [groupElement]);
142
+ const focusRadio = (0, _react.useCallback)(radio => {
143
+ if (radio) {
144
+ radio.focus();
145
+ radio.checked = true;
146
+
147
+ // Call onChange callback if provided
148
+ if (onChange) {
149
+ // Create a synthetic event-like object for consistency
150
+ const syntheticEvent = {
151
+ target: radio,
152
+ currentTarget: radio,
153
+ type: 'change'
154
+ };
155
+ onChange(syntheticEvent, radio.value);
156
+ }
157
+ }
158
+ }, [onChange]);
159
+ const handleKeyDown = (0, _react.useCallback)(event => {
160
+ if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
161
+ return;
162
+ }
163
+ event.preventDefault();
164
+ const radios = getRadioInputs();
165
+ if (radios.length === 0) return;
166
+ const currentIndex = radios.findIndex(radio => radio === document.activeElement);
167
+ let nextIndex;
168
+ if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
169
+ nextIndex = currentIndex <= 0 ? radios.length - 1 : currentIndex - 1;
170
+ } else {
171
+ nextIndex = currentIndex >= radios.length - 1 ? 0 : currentIndex + 1;
172
+ }
173
+ focusRadio(radios[nextIndex]);
174
+ }, [getRadioInputs, focusRadio]);
175
+ return {
176
+ handleKeyDown,
177
+ focusFirst: (0, _react.useCallback)(() => {
178
+ const radios = getRadioInputs();
179
+ const checkedRadio = radios.find(radio => radio.checked);
180
+ focusRadio(checkedRadio || radios[0]);
181
+ }, [getRadioInputs, focusRadio]),
182
+ focusChecked: (0, _react.useCallback)(() => {
183
+ const radios = getRadioInputs();
184
+ const checkedRadio = radios.find(radio => radio.checked);
185
+ if (checkedRadio) checkedRadio.focus();
186
+ }, [getRadioInputs])
187
+ };
188
+ };
189
+
190
+ /**
191
+ * Accessibility testing utility for radio groups
192
+ * Validates proper ARIA attributes and keyboard navigation
193
+ *
194
+ * @param {HTMLElement} groupElement - The radio group to test
195
+ * @returns {Object} Test results
196
+ */
197
+ exports.useRadioKeyboardNavigation = useRadioKeyboardNavigation;
198
+ const validateRadioGroupAccessibility = groupElement => {
199
+ const results = {
200
+ passed: true,
201
+ errors: [],
202
+ warnings: [],
203
+ info: []
204
+ };
205
+ if (!groupElement) {
206
+ results.passed = false;
207
+ results.errors.push('No group element provided for testing');
208
+ return results;
209
+ }
210
+
211
+ // Check for fieldset/legend structure
212
+ const fieldset = groupElement.closest('fieldset') || groupElement.querySelector('fieldset');
213
+ const legend = fieldset?.querySelector('legend');
214
+ if (!fieldset) {
215
+ results.warnings.push('Radio group should be wrapped in a <fieldset> element for better accessibility');
216
+ }
217
+ if (!legend) {
218
+ results.warnings.push('Radio group should have a <legend> element to describe the group');
219
+ }
220
+
221
+ // Check radio inputs
222
+ const radios = Array.from(groupElement.querySelectorAll('input[type="radio"]'));
223
+ if (radios.length === 0) {
224
+ results.passed = false;
225
+ results.errors.push('No radio inputs found in group');
226
+ return results;
227
+ }
228
+
229
+ // Check for proper name grouping
230
+ const names = [...new Set(radios.map(radio => radio.name))];
231
+ if (names.length > 1) {
232
+ results.passed = false;
233
+ results.errors.push(`All radios in group should have same name attribute. Found: ${names.join(', ')}`);
234
+ }
235
+
236
+ // Check for labels
237
+ radios.forEach((radio, index) => {
238
+ const label = radio.closest('label') || document.querySelector(`label[for="${radio.id}"]`);
239
+ if (!label) {
240
+ results.warnings.push(`Radio ${index + 1} should have an associated label`);
241
+ }
242
+
243
+ // Check for unique values
244
+ const duplicateValues = radios.filter(r => r.value === radio.value && r !== radio);
245
+ if (duplicateValues.length > 0) {
246
+ results.errors.push(`Radio ${index + 1} has duplicate value "${radio.value}"`);
247
+ results.passed = false;
248
+ }
249
+ });
250
+
251
+ // Check for at least one enabled radio
252
+ const enabledRadios = radios.filter(radio => !radio.disabled);
253
+ if (enabledRadios.length === 0) {
254
+ results.passed = false;
255
+ results.errors.push('At least one radio should be enabled');
256
+ }
257
+ results.info.push(`Found ${radios.length} radio inputs (${enabledRadios.length} enabled)`);
258
+ return results;
259
+ };
260
+ exports.validateRadioGroupAccessibility = validateRadioGroupAccessibility;