@proyecto-viviana/solidaria-components 0.2.9 → 0.3.0

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 (222) hide show
  1. package/README.md +39 -272
  2. package/dist/ActionBar.d.ts +21 -13
  3. package/dist/ActionBar.d.ts.map +1 -1
  4. package/dist/ActionGroup.d.ts +8 -8
  5. package/dist/ActionGroup.d.ts.map +1 -1
  6. package/dist/Alert.d.ts +5 -5
  7. package/dist/Alert.d.ts.map +1 -1
  8. package/dist/Autocomplete.d.ts +5 -5
  9. package/dist/Autocomplete.d.ts.map +1 -1
  10. package/dist/Breadcrumbs.d.ts +18 -7
  11. package/dist/Breadcrumbs.d.ts.map +1 -1
  12. package/dist/Button.d.ts +24 -5
  13. package/dist/Button.d.ts.map +1 -1
  14. package/dist/Calendar.d.ts +38 -7
  15. package/dist/Calendar.d.ts.map +1 -1
  16. package/dist/Checkbox.d.ts +32 -7
  17. package/dist/Checkbox.d.ts.map +1 -1
  18. package/dist/Collection.d.ts +19 -14
  19. package/dist/Collection.d.ts.map +1 -1
  20. package/dist/Color.d.ts +103 -14
  21. package/dist/Color.d.ts.map +1 -1
  22. package/dist/ColorEditor.d.ts +6 -6
  23. package/dist/ColorEditor.d.ts.map +1 -1
  24. package/dist/ComboBox.d.ts +85 -19
  25. package/dist/ComboBox.d.ts.map +1 -1
  26. package/dist/ContextualHelpTrigger.d.ts +2 -2
  27. package/dist/ContextualHelpTrigger.d.ts.map +1 -1
  28. package/dist/DateField.d.ts +8 -6
  29. package/dist/DateField.d.ts.map +1 -1
  30. package/dist/DatePicker.d.ts +53 -22
  31. package/dist/DatePicker.d.ts.map +1 -1
  32. package/dist/DateRangePickerContext.d.ts +30 -0
  33. package/dist/DateRangePickerContext.d.ts.map +1 -0
  34. package/dist/Dialog.d.ts +5 -5
  35. package/dist/Dialog.d.ts.map +1 -1
  36. package/dist/Disclosure.d.ts +23 -5
  37. package/dist/Disclosure.d.ts.map +1 -1
  38. package/dist/DragAndDrop.d.ts +6 -6
  39. package/dist/DragAndDrop.d.ts.map +1 -1
  40. package/dist/DragPreview.d.ts +2 -2
  41. package/dist/DragPreview.d.ts.map +1 -1
  42. package/dist/DropZone.d.ts +4 -4
  43. package/dist/DropZone.d.ts.map +1 -1
  44. package/dist/FieldError.d.ts +9 -5
  45. package/dist/FieldError.d.ts.map +1 -1
  46. package/dist/FileTrigger.d.ts +3 -3
  47. package/dist/FileTrigger.d.ts.map +1 -1
  48. package/dist/Focusable.d.ts +2 -2
  49. package/dist/Focusable.d.ts.map +1 -1
  50. package/dist/Form.d.ts +18 -4
  51. package/dist/Form.d.ts.map +1 -1
  52. package/dist/GridList.d.ts +32 -12
  53. package/dist/GridList.d.ts.map +1 -1
  54. package/dist/HiddenDateInput.d.ts +26 -0
  55. package/dist/HiddenDateInput.d.ts.map +1 -0
  56. package/dist/HiddenTimeInput.d.ts +25 -0
  57. package/dist/HiddenTimeInput.d.ts.map +1 -0
  58. package/dist/Icon.d.ts +5 -5
  59. package/dist/Icon.d.ts.map +1 -1
  60. package/dist/Keyboard.d.ts +1 -1
  61. package/dist/Landmark.d.ts +3 -3
  62. package/dist/Landmark.d.ts.map +1 -1
  63. package/dist/Link.d.ts +10 -4
  64. package/dist/Link.d.ts.map +1 -1
  65. package/dist/ListBox.d.ts +32 -12
  66. package/dist/ListBox.d.ts.map +1 -1
  67. package/dist/ListDropTargetDelegate.d.ts +6 -6
  68. package/dist/ListDropTargetDelegate.d.ts.map +1 -1
  69. package/dist/Menu.d.ts +65 -14
  70. package/dist/Menu.d.ts.map +1 -1
  71. package/dist/Meter.d.ts +3 -3
  72. package/dist/Meter.d.ts.map +1 -1
  73. package/dist/Modal.d.ts +5 -5
  74. package/dist/Modal.d.ts.map +1 -1
  75. package/dist/NumberField.d.ts +8 -12
  76. package/dist/NumberField.d.ts.map +1 -1
  77. package/dist/Popover.d.ts +28 -5
  78. package/dist/Popover.d.ts.map +1 -1
  79. package/dist/Pressable.d.ts +2 -2
  80. package/dist/Pressable.d.ts.map +1 -1
  81. package/dist/ProgressBar.d.ts +5 -3
  82. package/dist/ProgressBar.d.ts.map +1 -1
  83. package/dist/RadioGroup.d.ts +43 -9
  84. package/dist/RadioGroup.d.ts.map +1 -1
  85. package/dist/RangeCalendar.d.ts +34 -7
  86. package/dist/RangeCalendar.d.ts.map +1 -1
  87. package/dist/RouterProvider.d.ts +2 -2
  88. package/dist/RouterProvider.d.ts.map +1 -1
  89. package/dist/SearchField.d.ts +23 -20
  90. package/dist/SearchField.d.ts.map +1 -1
  91. package/dist/Select.d.ts +41 -11
  92. package/dist/Select.d.ts.map +1 -1
  93. package/dist/SelectionIndicator.d.ts +3 -3
  94. package/dist/SelectionIndicator.d.ts.map +1 -1
  95. package/dist/Separator.d.ts +9 -3
  96. package/dist/Separator.d.ts.map +1 -1
  97. package/dist/SharedElementTransition.d.ts +6 -4
  98. package/dist/SharedElementTransition.d.ts.map +1 -1
  99. package/dist/Slider.d.ts +12 -8
  100. package/dist/Slider.d.ts.map +1 -1
  101. package/dist/StepList.d.ts +90 -0
  102. package/dist/StepList.d.ts.map +1 -0
  103. package/dist/Switch.d.ts +11 -5
  104. package/dist/Switch.d.ts.map +1 -1
  105. package/dist/Table.d.ts +187 -23
  106. package/dist/Table.d.ts.map +1 -1
  107. package/dist/Tabs.d.ts +45 -9
  108. package/dist/Tabs.d.ts.map +1 -1
  109. package/dist/TagGroup.d.ts +12 -10
  110. package/dist/TagGroup.d.ts.map +1 -1
  111. package/dist/Text.d.ts +2 -2
  112. package/dist/TextField.d.ts +15 -11
  113. package/dist/TextField.d.ts.map +1 -1
  114. package/dist/TimeField.d.ts +6 -6
  115. package/dist/TimeField.d.ts.map +1 -1
  116. package/dist/Toast.d.ts +29 -14
  117. package/dist/Toast.d.ts.map +1 -1
  118. package/dist/ToggleButton.d.ts +11 -5
  119. package/dist/ToggleButton.d.ts.map +1 -1
  120. package/dist/ToggleButtonGroup.d.ts +7 -7
  121. package/dist/ToggleButtonGroup.d.ts.map +1 -1
  122. package/dist/Toolbar.d.ts +7 -3
  123. package/dist/Toolbar.d.ts.map +1 -1
  124. package/dist/Tooltip.d.ts +50 -8
  125. package/dist/Tooltip.d.ts.map +1 -1
  126. package/dist/Tree.d.ts +66 -17
  127. package/dist/Tree.d.ts.map +1 -1
  128. package/dist/Virtualizer.d.ts +12 -12
  129. package/dist/Virtualizer.d.ts.map +1 -1
  130. package/dist/VirtualizerLayouts.d.ts +2 -2
  131. package/dist/VirtualizerLayouts.d.ts.map +1 -1
  132. package/dist/VisuallyHidden.d.ts +1 -1
  133. package/dist/VisuallyHidden.d.ts.map +1 -1
  134. package/dist/contexts.d.ts +5 -1
  135. package/dist/contexts.d.ts.map +1 -1
  136. package/dist/index.d.ts +73 -71
  137. package/dist/index.d.ts.map +1 -1
  138. package/dist/index.js +23247 -18564
  139. package/dist/index.js.map +1 -1
  140. package/dist/index.jsx +18110 -0
  141. package/dist/index.jsx.map +1 -0
  142. package/dist/useDragAndDrop.d.ts +13 -13
  143. package/dist/useDragAndDrop.d.ts.map +1 -1
  144. package/dist/utils.d.ts +2 -2
  145. package/dist/utils.d.ts.map +1 -1
  146. package/dist/virtualizer/Layout.d.ts +1 -1
  147. package/dist/virtualizer/Layout.d.ts.map +1 -1
  148. package/package.json +31 -32
  149. package/src/ActionBar.tsx +75 -72
  150. package/src/ActionGroup.tsx +53 -61
  151. package/src/Alert.tsx +17 -42
  152. package/src/Autocomplete.tsx +39 -44
  153. package/src/Breadcrumbs.tsx +149 -80
  154. package/src/Button.tsx +267 -70
  155. package/src/Calendar.tsx +218 -138
  156. package/src/Checkbox.tsx +413 -121
  157. package/src/Collection.tsx +67 -58
  158. package/src/Color.tsx +803 -380
  159. package/src/ColorEditor.tsx +131 -149
  160. package/src/ComboBox.tsx +414 -249
  161. package/src/ContextualHelpTrigger.tsx +86 -74
  162. package/src/DateField.tsx +185 -91
  163. package/src/DatePicker.tsx +524 -213
  164. package/src/DateRangePickerContext.tsx +44 -0
  165. package/src/Dialog.tsx +156 -118
  166. package/src/Disclosure.tsx +127 -80
  167. package/src/DragAndDrop.tsx +60 -54
  168. package/src/DragPreview.tsx +13 -11
  169. package/src/DropZone.tsx +42 -22
  170. package/src/FieldError.tsx +45 -23
  171. package/src/FileTrigger.tsx +19 -19
  172. package/src/Focusable.tsx +21 -24
  173. package/src/Form.tsx +71 -16
  174. package/src/GridList.tsx +273 -197
  175. package/src/HiddenDateInput.tsx +153 -0
  176. package/src/HiddenTimeInput.tsx +133 -0
  177. package/src/Icon.tsx +22 -43
  178. package/src/Keyboard.tsx +3 -3
  179. package/src/Landmark.tsx +37 -63
  180. package/src/Link.tsx +125 -75
  181. package/src/ListBox.tsx +332 -233
  182. package/src/ListDropTargetDelegate.ts +81 -80
  183. package/src/Menu.tsx +1023 -274
  184. package/src/Meter.tsx +38 -56
  185. package/src/Modal.tsx +243 -175
  186. package/src/NumberField.tsx +139 -143
  187. package/src/Popover.tsx +386 -233
  188. package/src/Pressable.tsx +21 -21
  189. package/src/ProgressBar.tsx +48 -57
  190. package/src/RadioGroup.tsx +524 -122
  191. package/src/RangeCalendar.tsx +157 -90
  192. package/src/RouterProvider.tsx +30 -47
  193. package/src/SearchField.tsx +362 -143
  194. package/src/Select.tsx +656 -233
  195. package/src/SelectionIndicator.tsx +18 -15
  196. package/src/Separator.tsx +47 -49
  197. package/src/SharedElementTransition.tsx +103 -97
  198. package/src/Slider.tsx +138 -98
  199. package/src/StepList.tsx +272 -0
  200. package/src/Switch.tsx +93 -46
  201. package/src/Table.tsx +1308 -342
  202. package/src/Tabs.tsx +324 -103
  203. package/src/TagGroup.tsx +139 -126
  204. package/src/Text.tsx +3 -3
  205. package/src/TextField.tsx +389 -79
  206. package/src/TimeField.tsx +136 -76
  207. package/src/Toast.tsx +209 -157
  208. package/src/ToggleButton.tsx +47 -37
  209. package/src/ToggleButtonGroup.tsx +39 -34
  210. package/src/Toolbar.tsx +54 -69
  211. package/src/Tooltip.tsx +387 -119
  212. package/src/Tree.tsx +651 -368
  213. package/src/Virtualizer.tsx +208 -180
  214. package/src/VirtualizerLayouts.ts +45 -30
  215. package/src/VisuallyHidden.tsx +19 -19
  216. package/src/contexts.ts +29 -37
  217. package/src/index.ts +110 -195
  218. package/src/useDragAndDrop.ts +87 -71
  219. package/src/utils.tsx +40 -55
  220. package/src/virtualizer/Layout.ts +14 -22
  221. package/dist/index.ssr.js +0 -16996
  222. package/dist/index.ssr.js.map +0 -1
@@ -9,29 +9,35 @@ import {
9
9
  type JSX,
10
10
  type ParentProps,
11
11
  createContext,
12
- useContext,
13
12
  createMemo,
13
+ createSignal,
14
+ createUniqueId,
14
15
  splitProps,
16
+ useContext,
15
17
  Show,
16
- } from 'solid-js';
18
+ } from "solid-js";
17
19
  import {
18
20
  createRadio,
19
21
  createRadioGroup,
20
22
  createFocusRing,
21
23
  createHover,
24
+ mergeProps,
22
25
  type AriaRadioProps,
23
26
  type AriaRadioGroupProps,
24
- } from '@proyecto-viviana/solidaria';
27
+ } from "@proyecto-viviana/solidaria";
25
28
  import {
26
29
  createRadioGroupState,
30
+ VALID_VALIDITY_STATE,
27
31
  type RadioGroupState,
28
32
  type RadioGroupProps as RadioGroupStateProps,
29
- } from '@proyecto-viviana/solid-stately';
30
- import { VisuallyHidden } from './VisuallyHidden';
33
+ type ValidationResult,
34
+ } from "@proyecto-viviana/solid-stately";
35
+ import { FieldErrorContext, type FieldErrorContextValue } from "./FieldError";
36
+ import { VisuallyHidden } from "./VisuallyHidden";
31
37
  import {
32
38
  SelectionIndicatorContext,
33
39
  type SelectionIndicatorContextValue,
34
- } from './SelectionIndicator';
40
+ } from "./SelectionIndicator";
35
41
  import {
36
42
  type RenderChildren,
37
43
  type ClassNameOrFunction,
@@ -39,13 +45,46 @@ import {
39
45
  type SlotProps,
40
46
  useRenderProps,
41
47
  filterDOMProps,
42
- } from './utils';
48
+ } from "./utils";
43
49
 
44
- // ============================================
45
- // TYPES
46
- // ============================================
50
+ type RefLike<T> = ((el: T) => void) | { current?: T | null } | undefined;
51
+
52
+ function assignRef<T>(ref: RefLike<T>, el: T): void {
53
+ if (!ref) return;
54
+ if (typeof ref === "function") {
55
+ ref(el);
56
+ } else {
57
+ ref.current = el;
58
+ }
59
+ }
60
+
61
+ const validValidation: ValidationResult = {
62
+ isInvalid: false,
63
+ validationDetails: VALID_VALIDITY_STATE,
64
+ validationErrors: [],
65
+ };
66
+
67
+ function getNativeValidation(input: HTMLInputElement): ValidationResult {
68
+ return {
69
+ isInvalid: !input.validity.valid,
70
+ validationDetails: {
71
+ badInput: input.validity.badInput,
72
+ customError: input.validity.customError,
73
+ patternMismatch: input.validity.patternMismatch,
74
+ rangeOverflow: input.validity.rangeOverflow,
75
+ rangeUnderflow: input.validity.rangeUnderflow,
76
+ stepMismatch: input.validity.stepMismatch,
77
+ tooLong: input.validity.tooLong,
78
+ tooShort: input.validity.tooShort,
79
+ typeMismatch: input.validity.typeMismatch,
80
+ valueMissing: input.validity.valueMissing,
81
+ valid: input.validity.valid,
82
+ },
83
+ validationErrors: input.validationMessage ? [input.validationMessage] : [],
84
+ };
85
+ }
47
86
 
48
- export type Orientation = 'horizontal' | 'vertical';
87
+ export type Orientation = "horizontal" | "vertical";
49
88
 
50
89
  export interface RadioGroupRenderProps {
51
90
  /** The orientation of the radio group. */
@@ -84,8 +123,12 @@ export interface RadioRenderProps {
84
123
  }
85
124
 
86
125
  export interface RadioGroupProps
87
- extends Omit<AriaRadioGroupProps, 'children' | 'label' | 'description' | 'errorMessage'>,
88
- Pick<RadioGroupStateProps, 'value' | 'defaultValue' | 'onChange'>,
126
+ extends
127
+ Omit<AriaRadioGroupProps, "children" | "label" | "description" | "errorMessage">,
128
+ Pick<
129
+ RadioGroupStateProps,
130
+ "value" | "defaultValue" | "onChange" | "validationState" | "validate"
131
+ >,
89
132
  SlotProps {
90
133
  /** The children of the component. A function may be provided to receive render props. */
91
134
  children?: RenderChildren<RadioGroupRenderProps>;
@@ -93,30 +136,56 @@ export interface RadioGroupProps
93
136
  class?: ClassNameOrFunction<RadioGroupRenderProps>;
94
137
  /** The inline style for the element. */
95
138
  style?: StyleOrFunction<RadioGroupRenderProps>;
139
+ /** Custom renderer for the outer radio group element. */
140
+ render?: (
141
+ props: JSX.HTMLAttributes<HTMLDivElement>,
142
+ renderProps: RadioGroupRenderProps,
143
+ ) => JSX.Element;
144
+ /** Ref for the radio group element. */
145
+ ref?: RefLike<HTMLDivElement>;
146
+ /** A description for the radio group. */
147
+ description?: JSX.Element;
148
+ /** An error message for the radio group. */
149
+ errorMessage?: JSX.Element;
96
150
  }
97
151
 
98
- export interface RadioProps
99
- extends Omit<AriaRadioProps, 'children'>,
100
- SlotProps {
152
+ export interface RadioProps extends Omit<AriaRadioProps, "children">, SlotProps {
101
153
  /** The children of the component. A function may be provided to receive render props. */
102
154
  children?: RenderChildren<RadioRenderProps>;
103
155
  /** The CSS className for the element. */
104
156
  class?: ClassNameOrFunction<RadioRenderProps>;
105
157
  /** The inline style for the element. */
106
158
  style?: StyleOrFunction<RadioRenderProps>;
159
+ /** Custom renderer for the outer radio label element. */
160
+ render?: (
161
+ props: JSX.LabelHTMLAttributes<HTMLLabelElement>,
162
+ renderProps: RadioRenderProps,
163
+ ) => JSX.Element;
164
+ /** Ref for the outer label element. */
165
+ ref?: RefLike<HTMLLabelElement>;
166
+ /** Ref for the underlying input element. */
167
+ inputRef?: RefLike<HTMLInputElement>;
168
+ /** A description for the radio. */
169
+ description?: JSX.Element;
170
+ /** An error message for the radio. */
171
+ errorMessage?: JSX.Element;
172
+ /** Handler called when hover starts. */
173
+ onHoverStart?: () => void;
174
+ /** Handler called when hover ends. */
175
+ onHoverEnd?: () => void;
176
+ /** Handler called when hover state changes. */
177
+ onHoverChange?: (isHovered: boolean) => void;
107
178
  }
108
179
 
109
- // ============================================
110
- // CONTEXT
111
- // ============================================
112
-
113
- export const RadioGroupContext = createContext<RadioGroupProps | null>(null);
180
+ export interface RadioGroupContextValue extends RadioGroupProps {
181
+ slots?: Record<string, RadioGroupProps>;
182
+ }
183
+ export const RadioGroupContext = createContext<RadioGroupContextValue | null>(null);
114
184
  export const RadioGroupStateContext = createContext<RadioGroupState | null>(null);
115
- export const RadioContext = createContext<RadioProps | null>(null);
116
-
117
- // ============================================
118
- // RADIO GROUP COMPONENT
119
- // ============================================
185
+ export interface RadioContextValue extends RadioProps {
186
+ slots?: Record<string, RadioProps>;
187
+ }
188
+ export const RadioContext = createContext<RadioContextValue | null>(null);
120
189
 
121
190
  /**
122
191
  * A radio group allows a user to select a single item from a list of mutually exclusive options.
@@ -130,111 +199,307 @@ export const RadioContext = createContext<RadioProps | null>(null);
130
199
  * ```
131
200
  */
132
201
  export function RadioGroup(props: ParentProps<RadioGroupProps>): JSX.Element {
133
- const [local, ariaProps] = splitProps(props, [
134
- 'class',
135
- 'style',
136
- 'slot',
137
- ]);
138
-
139
- // Create radio group state
140
- // We pass a function that returns props so that createRadioGroupState
141
- // can access props reactively. The props object itself is a proxy in SolidJS,
142
- // so accessing props.value inside a reactive context will track changes.
143
- const state = createRadioGroupState({
144
- get value() { return props.value; },
145
- get defaultValue() { return props.defaultValue; },
146
- get onChange() { return props.onChange; },
147
- get isDisabled() { return props.isDisabled; },
148
- get isReadOnly() { return props.isReadOnly; },
149
- get isRequired() { return props.isRequired; },
150
- get isInvalid() { return props.isInvalid; },
202
+ const contextProps = useContext(RadioGroupContext);
203
+ const contextSlotProps = contextProps?.slots?.[props.slot ?? "default"];
204
+ const contextBaseProps = createMemo<RadioGroupProps>(() => {
205
+ if (!contextProps) return {};
206
+ const { slots: _slots, ...rest } = contextProps;
207
+ return rest;
151
208
  });
209
+ const mergedProps = contextProps
210
+ ? (mergeProps(
211
+ contextBaseProps(),
212
+ contextSlotProps ?? {},
213
+ props,
214
+ ) as ParentProps<RadioGroupProps>)
215
+ : props;
216
+ const [local, ariaProps] = splitProps(mergedProps, ["class", "style", "render", "ref", "slot"]);
217
+
218
+ const state = createRadioGroupState(() => ({
219
+ value: mergedProps.value,
220
+ defaultValue: mergedProps.defaultValue,
221
+ onChange: mergedProps.onChange,
222
+ isDisabled: mergedProps.isDisabled,
223
+ isReadOnly: mergedProps.isReadOnly,
224
+ isRequired: mergedProps.isRequired,
225
+ isInvalid: mergedProps.isInvalid,
226
+ validationState: mergedProps.validationState,
227
+ validate: mergedProps.validate,
228
+ validationBehavior: mergedProps.validationBehavior,
229
+ name: mergedProps.name,
230
+ form: mergedProps.form,
231
+ }));
152
232
 
153
233
  // Create radio group aria props
154
- const groupAria = createRadioGroup(() => ariaProps, state);
234
+ const groupAria = createRadioGroup(
235
+ () => ({
236
+ ...ariaProps,
237
+ description: mergedProps.description,
238
+ errorMessage: mergedProps.errorMessage,
239
+ }),
240
+ state,
241
+ );
242
+ const isInvalid = createMemo(() => state.isInvalid);
243
+ const validation = createMemo(() => state.displayValidation());
244
+ const fallbackErrorMessageId = createUniqueId();
245
+ const errorMessageId = () => groupAria.errorMessageProps.id ?? fallbackErrorMessageId;
155
246
 
156
- // Render props values
157
247
  const renderValues = createMemo<RadioGroupRenderProps>(() => ({
158
- orientation: (ariaProps.orientation as Orientation) ?? 'vertical',
248
+ orientation: (ariaProps.orientation as Orientation) ?? "vertical",
159
249
  isDisabled: state.isDisabled,
160
250
  isReadOnly: state.isReadOnly,
161
251
  isRequired: state.isRequired,
162
- isInvalid: groupAria.isInvalid,
252
+ isInvalid: isInvalid(),
163
253
  state,
164
254
  }));
165
255
 
166
- // Resolve render props
167
256
  const renderProps = useRenderProps(
168
257
  {
169
258
  children: props.children,
170
259
  class: local.class,
171
260
  style: local.style,
172
- defaultClassName: 'solidaria-RadioGroup',
261
+ defaultClassName: "solidaria-RadioGroup",
173
262
  },
174
- renderValues
263
+ renderValues,
175
264
  );
176
265
 
177
- // Filter DOM props
178
266
  const domProps = createMemo(() => filterDOMProps(ariaProps, { global: true }));
179
267
 
180
- // Remove ref from spread props to avoid type conflicts
181
268
  const cleanGroupProps = () => {
182
269
  const { ref: _ref, ...rest } = groupAria.radioGroupProps as Record<string, unknown>;
183
270
  return rest;
184
271
  };
272
+ const handleGroupFocusIn: JSX.EventHandler<HTMLDivElement, FocusEvent> = (event) => {
273
+ (
274
+ groupAria.radioGroupProps as unknown as {
275
+ onFocus?: JSX.EventHandler<HTMLDivElement, FocusEvent>;
276
+ }
277
+ ).onFocus?.(event);
278
+ };
279
+ const handleGroupFocusOut: JSX.EventHandler<HTMLDivElement, FocusEvent> = (event) => {
280
+ (
281
+ groupAria.radioGroupProps as unknown as {
282
+ onBlur?: JSX.EventHandler<HTMLDivElement, FocusEvent>;
283
+ }
284
+ ).onBlur?.(event);
285
+ };
286
+ const handleGroupInvalidCapture: JSX.EventHandler<HTMLDivElement, Event> = (event) => {
287
+ const target = event.target;
288
+ if (!(target instanceof HTMLInputElement) || target.type !== "radio") {
289
+ return;
290
+ }
185
291
 
186
- // Resolve children - we need to pass render props if children is a function
187
- // but we use props.children directly (not renderProps.renderChildren())
188
- // to preserve SolidJS context propagation for nested components like Radio
189
- const resolvedChildren = () => {
190
- const children = props.children;
191
- if (typeof children === 'function') {
192
- return children(renderValues());
292
+ state.updateValidation(getNativeValidation(target));
293
+ state.commitValidation();
294
+ target.focus();
295
+ event.preventDefault();
296
+ };
297
+ const handleGroupChangeCapture: JSX.EventHandler<HTMLDivElement, Event> = (event) => {
298
+ const target = event.target;
299
+ if (!(target instanceof HTMLInputElement) || target.type !== "radio") {
300
+ return;
193
301
  }
194
- return children;
302
+
303
+ state.updateValidation(target.validity.valid ? validValidation : getNativeValidation(target));
304
+ state.commitValidation();
195
305
  };
306
+ const setGroupRef = (el: HTMLDivElement) => {
307
+ assignRef(local.ref, el);
308
+ };
309
+ const groupDescribedBy = () => {
310
+ const ids = [
311
+ (cleanGroupProps() as { "aria-describedby"?: string })["aria-describedby"],
312
+ mergedProps.description ? groupAria.descriptionProps.id : undefined,
313
+ isInvalid() && (mergedProps.errorMessage || validation().validationErrors.length > 0)
314
+ ? errorMessageId()
315
+ : undefined,
316
+ ]
317
+ .filter(Boolean)
318
+ .join(" ")
319
+ .split(" ")
320
+ .filter(Boolean);
321
+ return ids.length ? Array.from(new Set(ids)).join(" ") : undefined;
322
+ };
323
+ const fieldErrorContext: FieldErrorContextValue = {
324
+ get validation() {
325
+ return validation();
326
+ },
327
+ get errorMessageProps() {
328
+ return {
329
+ ...groupAria.errorMessageProps,
330
+ id: errorMessageId(),
331
+ } as JSX.HTMLAttributes<HTMLElement>;
332
+ },
333
+ };
334
+
335
+ const GroupChildren = () => {
336
+ const childRenderValues: RadioGroupRenderProps = {
337
+ get orientation() {
338
+ return (ariaProps.orientation as Orientation) ?? "vertical";
339
+ },
340
+ get isDisabled() {
341
+ return state.isDisabled;
342
+ },
343
+ get isReadOnly() {
344
+ return state.isReadOnly;
345
+ },
346
+ get isRequired() {
347
+ return state.isRequired;
348
+ },
349
+ get isInvalid() {
350
+ return isInvalid();
351
+ },
352
+ get state() {
353
+ return state;
354
+ },
355
+ };
356
+ const renderedChildren = createMemo(() => {
357
+ const children = props.children;
358
+ if (typeof children === "function") {
359
+ return children.length > 0
360
+ ? children(childRenderValues)
361
+ : (children as unknown as () => JSX.Element)();
362
+ }
363
+ return children;
364
+ });
365
+
366
+ return (
367
+ <>
368
+ {renderedChildren()}
369
+ <Show when={mergedProps.description}>
370
+ <div {...(groupAria.descriptionProps as unknown as JSX.HTMLAttributes<HTMLDivElement>)}>
371
+ {mergedProps.description}
372
+ </div>
373
+ </Show>
374
+ <Show when={isInvalid() && mergedProps.errorMessage}>
375
+ <div {...(groupAria.errorMessageProps as unknown as JSX.HTMLAttributes<HTMLDivElement>)}>
376
+ {mergedProps.errorMessage}
377
+ </div>
378
+ </Show>
379
+ </>
380
+ );
381
+ };
382
+ const groupEventProps = {
383
+ onInvalidCapture: handleGroupInvalidCapture,
384
+ onChangeCapture: handleGroupChangeCapture,
385
+ } as unknown as JSX.HTMLAttributes<HTMLDivElement>;
386
+ const customRootProps = () =>
387
+ ({
388
+ ...domProps(),
389
+ ...cleanGroupProps(),
390
+ ...groupEventProps,
391
+ ref: setGroupRef,
392
+ onFocusIn: handleGroupFocusIn,
393
+ onFocusOut: handleGroupFocusOut,
394
+ "aria-describedby": groupDescribedBy(),
395
+ class: renderProps.class(),
396
+ style: renderProps.style(),
397
+ slot: local.slot,
398
+ "data-orientation": ariaProps.orientation ?? "vertical",
399
+ "data-disabled": state.isDisabled || undefined,
400
+ "data-readonly": state.isReadOnly || undefined,
401
+ "data-required": state.isRequired || undefined,
402
+ "data-invalid": isInvalid() || undefined,
403
+ children: <GroupChildren />,
404
+ }) as unknown as JSX.HTMLAttributes<HTMLDivElement>;
196
405
 
197
406
  return (
198
407
  <RadioGroupStateContext.Provider value={state}>
199
- <div
200
- {...domProps()}
201
- {...cleanGroupProps()}
202
- class={renderProps.class()}
203
- style={renderProps.style()}
204
- data-orientation={ariaProps.orientation ?? 'vertical'}
205
- data-disabled={state.isDisabled || undefined}
206
- data-readonly={state.isReadOnly || undefined}
207
- data-required={state.isRequired || undefined}
208
- data-invalid={groupAria.isInvalid || undefined}
209
- >
210
- {resolvedChildren()}
211
- </div>
408
+ <FieldErrorContext.Provider value={fieldErrorContext}>
409
+ {local.render ? (
410
+ local.render(customRootProps(), renderValues())
411
+ ) : (
412
+ <div
413
+ {...domProps()}
414
+ {...cleanGroupProps()}
415
+ {...groupEventProps}
416
+ ref={setGroupRef}
417
+ onFocusIn={handleGroupFocusIn}
418
+ onFocusOut={handleGroupFocusOut}
419
+ aria-describedby={groupDescribedBy()}
420
+ class={renderProps.class()}
421
+ style={renderProps.style()}
422
+ slot={local.slot}
423
+ data-orientation={ariaProps.orientation ?? "vertical"}
424
+ data-disabled={state.isDisabled || undefined}
425
+ data-readonly={state.isReadOnly || undefined}
426
+ data-required={state.isRequired || undefined}
427
+ data-invalid={isInvalid() || undefined}
428
+ >
429
+ <GroupChildren />
430
+ </div>
431
+ )}
432
+ </FieldErrorContext.Provider>
212
433
  </RadioGroupStateContext.Provider>
213
434
  );
214
435
  }
215
436
 
216
- // ============================================
217
- // RADIO COMPONENT
218
- // ============================================
219
-
220
437
  /**
221
438
  * Internal Radio implementation that has access to RadioGroupStateContext.
222
439
  * This is rendered inside the RadioGroup's context provider.
223
440
  */
224
441
  function RadioImpl(props: { radioProps: RadioProps; state: RadioGroupState }): JSX.Element {
225
- let inputRef: HTMLInputElement | null = null;
226
- const { radioProps, state } = props;
442
+ const [inputElement, setInputElement] = createSignal<HTMLInputElement | null>(null);
443
+ const { state } = props;
444
+ const contextProps = useContext(RadioContext);
445
+ const contextSlotProps = contextProps?.slots?.[props.radioProps.slot ?? "default"];
446
+ const contextBaseProps = createMemo<RadioProps>(() => {
447
+ if (!contextProps) return {} as RadioProps;
448
+ const { slots: _slots, ...rest } = contextProps;
449
+ return rest as RadioProps;
450
+ });
451
+ const radioProps = contextProps
452
+ ? (mergeProps(contextBaseProps(), contextSlotProps ?? {}, props.radioProps) as RadioProps)
453
+ : props.radioProps;
454
+ const inputRefs = createMemo(
455
+ () =>
456
+ [contextBaseProps().inputRef, contextSlotProps?.inputRef, props.radioProps.inputRef].filter(
457
+ Boolean,
458
+ ) as RefLike<HTMLInputElement>[],
459
+ );
227
460
 
228
- const [local, ariaProps] = splitProps(radioProps, ['class', 'style', 'slot']);
461
+ const [local, ariaProps] = splitProps(radioProps, [
462
+ "class",
463
+ "style",
464
+ "render",
465
+ "ref",
466
+ "inputRef",
467
+ "slot",
468
+ "description",
469
+ "errorMessage",
470
+ "onHoverStart",
471
+ "onHoverEnd",
472
+ "onHoverChange",
473
+ ]);
474
+ const descriptionId = createUniqueId();
475
+ const errorMessageId = createUniqueId();
476
+ const describedBy = () => {
477
+ const ids = [
478
+ ariaProps["aria-describedby"],
479
+ local.description ? descriptionId : undefined,
480
+ state.isInvalid && local.errorMessage ? errorMessageId : undefined,
481
+ ].filter(Boolean);
482
+ return ids.length ? ids.join(" ") : undefined;
483
+ };
484
+ const inputAriaProps = createMemo(() => {
485
+ const clean: Record<string, unknown> = {};
486
+ for (const key in ariaProps as Record<string, unknown>) {
487
+ if (!key.startsWith("data-")) {
488
+ clean[key] = (ariaProps as Record<string, unknown>)[key];
489
+ }
490
+ }
491
+ return clean as typeof ariaProps;
492
+ });
229
493
 
230
494
  // Create radio aria props
231
495
  const radioAria = createRadio(
232
496
  () => ({
233
- ...ariaProps,
234
- children: typeof radioProps.children === 'function' ? true : radioProps.children,
497
+ ...inputAriaProps(),
498
+ "aria-describedby": describedBy(),
499
+ children: typeof radioProps.children === "function" ? true : radioProps.children,
235
500
  }),
236
501
  state,
237
- () => inputRef
502
+ inputElement,
238
503
  );
239
504
 
240
505
  // Create focus ring
@@ -245,9 +510,11 @@ function RadioImpl(props: { radioProps: RadioProps; state: RadioGroupState }): J
245
510
  get isDisabled() {
246
511
  return radioAria.isDisabled || state.isReadOnly;
247
512
  },
513
+ onHoverStart: local.onHoverStart,
514
+ onHoverEnd: local.onHoverEnd,
515
+ onHoverChange: local.onHoverChange,
248
516
  });
249
517
 
250
- // Render props values
251
518
  const renderValues = createMemo<RadioRenderProps>(() => ({
252
519
  isSelected: radioAria.isSelected(),
253
520
  isHovered: isHovered(),
@@ -260,22 +527,20 @@ function RadioImpl(props: { radioProps: RadioProps; state: RadioGroupState }): J
260
527
  isRequired: state.isRequired,
261
528
  }));
262
529
 
263
- // Resolve render props
264
530
  const renderProps = useRenderProps(
265
531
  {
266
532
  children: radioProps.children,
267
533
  class: local.class,
268
534
  style: local.style,
269
- defaultClassName: 'solidaria-Radio',
535
+ defaultClassName: "solidaria-Radio",
270
536
  },
271
- renderValues
537
+ renderValues,
272
538
  );
273
539
 
274
540
  const selectionIndicatorContext = createMemo<SelectionIndicatorContextValue>(() => ({
275
541
  isSelected: radioAria.isSelected,
276
542
  }));
277
543
 
278
- // Filter DOM props
279
544
  const domProps = createMemo(() => {
280
545
  const filtered = filterDOMProps(ariaProps, { global: true });
281
546
  delete (filtered as Record<string, unknown>).id;
@@ -283,7 +548,6 @@ function RadioImpl(props: { radioProps: RadioProps; state: RadioGroupState }): J
283
548
  return filtered;
284
549
  });
285
550
 
286
- // Remove ref from spread props to avoid type conflicts
287
551
  const cleanLabelProps = () => {
288
552
  const { ref: _ref1, ...rest } = radioAria.labelProps as Record<string, unknown>;
289
553
  return rest;
@@ -293,41 +557,183 @@ function RadioImpl(props: { radioProps: RadioProps; state: RadioGroupState }): J
293
557
  return rest;
294
558
  };
295
559
  const cleanInputProps = () => {
296
- const { ref: _ref3, ...rest } = radioAria.inputProps as Record<string, unknown>;
560
+ const {
561
+ ref: _ref3,
562
+ onFocus: _onFocus,
563
+ onBlur: _onBlur,
564
+ ...rest
565
+ } = radioAria.inputProps as Record<string, unknown>;
297
566
  return rest;
298
567
  };
299
568
  const cleanFocusProps = () => {
300
- const { ref: _ref4, ...rest } = focusProps as Record<string, unknown>;
569
+ const {
570
+ ref: _ref4,
571
+ onFocus: _onFocus,
572
+ onBlur: _onBlur,
573
+ ...rest
574
+ } = focusProps as Record<string, unknown>;
301
575
  return rest;
302
576
  };
577
+ const handleInputFocus: JSX.EventHandler<HTMLInputElement, FocusEvent> = (event) => {
578
+ (
579
+ radioAria.inputProps as unknown as {
580
+ onFocus?: JSX.EventHandler<HTMLInputElement, FocusEvent>;
581
+ }
582
+ ).onFocus?.(event);
583
+ (
584
+ focusProps as unknown as { onFocus?: JSX.EventHandler<HTMLInputElement, FocusEvent> }
585
+ ).onFocus?.(event);
586
+ };
587
+ const handleInputBlur: JSX.EventHandler<HTMLInputElement, FocusEvent> = (event) => {
588
+ (
589
+ radioAria.inputProps as unknown as { onBlur?: JSX.EventHandler<HTMLInputElement, FocusEvent> }
590
+ ).onBlur?.(event);
591
+ (focusProps as unknown as { onBlur?: JSX.EventHandler<HTMLInputElement, FocusEvent> }).onBlur?.(
592
+ event,
593
+ );
594
+ };
595
+ const handleLabelClick: JSX.EventHandler<HTMLLabelElement, MouseEvent> = (event) => {
596
+ (ariaProps as unknown as { onClickCapture?: (event: MouseEvent) => void }).onClickCapture?.(
597
+ event as unknown as MouseEvent,
598
+ );
599
+ (
600
+ radioAria.labelProps as unknown as {
601
+ onClick?: JSX.EventHandler<HTMLLabelElement, MouseEvent>;
602
+ }
603
+ ).onClick?.(event);
604
+ };
605
+ const handleLabelClickCapture: JSX.EventHandler<HTMLLabelElement, MouseEvent> = (event) => {
606
+ (ariaProps as unknown as { onClickCapture?: (event: MouseEvent) => void }).onClickCapture?.(
607
+ event as unknown as MouseEvent,
608
+ );
609
+ };
610
+ const handleInputClick: JSX.EventHandler<HTMLInputElement, MouseEvent> = (event) => {
611
+ (
612
+ radioAria.inputProps as unknown as {
613
+ onClick?: JSX.EventHandler<HTMLInputElement, MouseEvent>;
614
+ }
615
+ ).onClick?.(event);
616
+ };
617
+ const handleInputInvalid: JSX.EventHandler<HTMLInputElement, Event> = (event) => {
618
+ state.updateValidation(getNativeValidation(event.currentTarget));
619
+ state.commitValidation();
620
+ event.currentTarget.focus();
621
+ event.preventDefault();
622
+ };
623
+ const handleInputChange: JSX.EventHandler<HTMLInputElement, Event> = (event) => {
624
+ (
625
+ radioAria.inputProps as unknown as { onChange?: JSX.EventHandler<HTMLInputElement, Event> }
626
+ ).onChange?.(event);
627
+ state.updateValidation(
628
+ event.currentTarget.validity.valid
629
+ ? validValidation
630
+ : getNativeValidation(event.currentTarget),
631
+ );
632
+ state.commitValidation();
633
+ };
634
+ const setLabelRef = (el: HTMLLabelElement) => {
635
+ assignRef(local.ref, el);
636
+ };
637
+ const setInputRef = (el: HTMLInputElement) => {
638
+ setInputElement(el);
639
+ el.addEventListener("invalid", (event) => {
640
+ state.updateValidation(getNativeValidation(el));
641
+ state.commitValidation();
642
+ el.focus();
643
+ event.preventDefault();
644
+ });
645
+ el.addEventListener("change", () => {
646
+ state.updateValidation(el.validity.valid ? validValidation : getNativeValidation(el));
647
+ state.commitValidation();
648
+ });
649
+ for (const ref of inputRefs()) {
650
+ assignRef(ref, el);
651
+ }
652
+ };
653
+ const hiddenInput = (
654
+ <VisuallyHidden>
655
+ <input
656
+ ref={setInputRef}
657
+ {...cleanInputProps()}
658
+ {...cleanFocusProps()}
659
+ onFocus={handleInputFocus}
660
+ onBlur={handleInputBlur}
661
+ onInvalid={handleInputInvalid}
662
+ onChange={handleInputChange}
663
+ onClick={handleInputClick}
664
+ />
665
+ </VisuallyHidden>
666
+ );
667
+ const labelChildren = () => (
668
+ <>
669
+ {hiddenInput}
670
+ {renderProps.renderChildren()}
671
+ <Show when={local.description}>
672
+ <span id={descriptionId} slot="description">
673
+ {local.description}
674
+ </span>
675
+ </Show>
676
+ <Show when={state.isInvalid && local.errorMessage}>
677
+ <span id={errorMessageId} slot="errorMessage">
678
+ {local.errorMessage}
679
+ </span>
680
+ </Show>
681
+ </>
682
+ );
683
+ const customLabelProps = () =>
684
+ ({
685
+ ...domProps(),
686
+ ...cleanLabelProps(),
687
+ ...cleanHoverProps(),
688
+ ref: setLabelRef,
689
+ class: renderProps.class(),
690
+ style: renderProps.style(),
691
+ slot: local.slot,
692
+ onClick: handleLabelClick,
693
+ onClickCapture: handleLabelClickCapture,
694
+ "data-selected": radioAria.isSelected() || undefined,
695
+ "data-pressed": radioAria.isPressed() || undefined,
696
+ "data-hovered": isHovered() || undefined,
697
+ "data-focused": isFocused() || undefined,
698
+ "data-focus-visible": isFocusVisible() || undefined,
699
+ "data-disabled": radioAria.isDisabled || undefined,
700
+ "data-readonly": state.isReadOnly || undefined,
701
+ "data-invalid": state.isInvalid || undefined,
702
+ "data-required": state.isRequired || undefined,
703
+ children: labelChildren(),
704
+ }) as unknown as JSX.LabelHTMLAttributes<HTMLLabelElement>;
705
+ const labelCaptureProps = {
706
+ onClickCapture: handleLabelClickCapture,
707
+ } as unknown as JSX.LabelHTMLAttributes<HTMLLabelElement>;
303
708
 
304
709
  return (
305
710
  <SelectionIndicatorContext.Provider value={selectionIndicatorContext()}>
306
- <label
307
- {...domProps()}
308
- {...cleanLabelProps()}
309
- {...cleanHoverProps()}
310
- class={renderProps.class()}
311
- style={renderProps.style()}
312
- data-selected={radioAria.isSelected() || undefined}
313
- data-pressed={radioAria.isPressed() || undefined}
314
- data-hovered={isHovered() || undefined}
315
- data-focused={isFocused() || undefined}
316
- data-focus-visible={isFocusVisible() || undefined}
317
- data-disabled={radioAria.isDisabled || undefined}
318
- data-readonly={state.isReadOnly || undefined}
319
- data-invalid={state.isInvalid || undefined}
320
- data-required={state.isRequired || undefined}
321
- >
322
- <VisuallyHidden>
323
- <input
324
- ref={(el) => (inputRef = el)}
325
- {...cleanInputProps()}
326
- {...cleanFocusProps()}
327
- />
328
- </VisuallyHidden>
329
- {renderProps.renderChildren()}
330
- </label>
711
+ {local.render ? (
712
+ local.render(customLabelProps(), renderValues())
713
+ ) : (
714
+ <label
715
+ {...domProps()}
716
+ {...cleanLabelProps()}
717
+ {...cleanHoverProps()}
718
+ ref={setLabelRef}
719
+ class={renderProps.class()}
720
+ style={renderProps.style()}
721
+ slot={local.slot}
722
+ onClick={handleLabelClick}
723
+ {...labelCaptureProps}
724
+ data-selected={radioAria.isSelected() || undefined}
725
+ data-pressed={radioAria.isPressed() || undefined}
726
+ data-hovered={isHovered() || undefined}
727
+ data-focused={isFocused() || undefined}
728
+ data-focus-visible={isFocusVisible() || undefined}
729
+ data-disabled={radioAria.isDisabled || undefined}
730
+ data-readonly={state.isReadOnly || undefined}
731
+ data-invalid={state.isInvalid || undefined}
732
+ data-required={state.isRequired || undefined}
733
+ >
734
+ {labelChildren()}
735
+ </label>
736
+ )}
331
737
  </SelectionIndicatorContext.Provider>
332
738
  );
333
739
  }
@@ -355,11 +761,7 @@ export function Radio(props: RadioProps): JSX.Element {
355
761
  const getState = createMemo(() => useContext(RadioGroupStateContext));
356
762
 
357
763
  return (
358
- <Show
359
- when={getState()}
360
- fallback={null}
361
- keyed
362
- >
764
+ <Show when={getState()} fallback={null} keyed>
363
765
  {(state) => <RadioImpl radioProps={props} state={state} />}
364
766
  </Show>
365
767
  );