@proyecto-viviana/solidaria-components 0.2.5 → 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 (225) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -272
  3. package/dist/ActionBar.d.ts +79 -0
  4. package/dist/ActionBar.d.ts.map +1 -0
  5. package/dist/ActionGroup.d.ts +74 -0
  6. package/dist/ActionGroup.d.ts.map +1 -0
  7. package/dist/Alert.d.ts +70 -0
  8. package/dist/Alert.d.ts.map +1 -0
  9. package/dist/Autocomplete.d.ts +5 -5
  10. package/dist/Autocomplete.d.ts.map +1 -1
  11. package/dist/Breadcrumbs.d.ts +27 -8
  12. package/dist/Breadcrumbs.d.ts.map +1 -1
  13. package/dist/Button.d.ts +28 -5
  14. package/dist/Button.d.ts.map +1 -1
  15. package/dist/Calendar.d.ts +51 -7
  16. package/dist/Calendar.d.ts.map +1 -1
  17. package/dist/Checkbox.d.ts +33 -8
  18. package/dist/Checkbox.d.ts.map +1 -1
  19. package/dist/Collection.d.ts +130 -0
  20. package/dist/Collection.d.ts.map +1 -0
  21. package/dist/Color.d.ts +210 -9
  22. package/dist/Color.d.ts.map +1 -1
  23. package/dist/ColorEditor.d.ts +42 -0
  24. package/dist/ColorEditor.d.ts.map +1 -0
  25. package/dist/ComboBox.d.ts +146 -16
  26. package/dist/ComboBox.d.ts.map +1 -1
  27. package/dist/ContextualHelpTrigger.d.ts +40 -0
  28. package/dist/ContextualHelpTrigger.d.ts.map +1 -0
  29. package/dist/DateField.d.ts +35 -8
  30. package/dist/DateField.d.ts.map +1 -1
  31. package/dist/DatePicker.d.ts +101 -5
  32. package/dist/DatePicker.d.ts.map +1 -1
  33. package/dist/DateRangePickerContext.d.ts +30 -0
  34. package/dist/DateRangePickerContext.d.ts.map +1 -0
  35. package/dist/Dialog.d.ts +5 -5
  36. package/dist/Dialog.d.ts.map +1 -1
  37. package/dist/Disclosure.d.ts +25 -5
  38. package/dist/Disclosure.d.ts.map +1 -1
  39. package/dist/DragAndDrop.d.ts +80 -0
  40. package/dist/DragAndDrop.d.ts.map +1 -0
  41. package/dist/DragPreview.d.ts +14 -0
  42. package/dist/DragPreview.d.ts.map +1 -0
  43. package/dist/DropZone.d.ts +27 -0
  44. package/dist/DropZone.d.ts.map +1 -0
  45. package/dist/FieldError.d.ts +27 -0
  46. package/dist/FieldError.d.ts.map +1 -0
  47. package/dist/FileTrigger.d.ts +26 -0
  48. package/dist/FileTrigger.d.ts.map +1 -0
  49. package/dist/Focusable.d.ts +27 -0
  50. package/dist/Focusable.d.ts.map +1 -0
  51. package/dist/Form.d.ts +41 -0
  52. package/dist/Form.d.ts.map +1 -0
  53. package/dist/GridList.d.ts +69 -10
  54. package/dist/GridList.d.ts.map +1 -1
  55. package/dist/HiddenDateInput.d.ts +26 -0
  56. package/dist/HiddenDateInput.d.ts.map +1 -0
  57. package/dist/HiddenTimeInput.d.ts +25 -0
  58. package/dist/HiddenTimeInput.d.ts.map +1 -0
  59. package/dist/Icon.d.ts +57 -0
  60. package/dist/Icon.d.ts.map +1 -0
  61. package/dist/Keyboard.d.ts +13 -0
  62. package/dist/Keyboard.d.ts.map +1 -0
  63. package/dist/Landmark.d.ts +3 -3
  64. package/dist/Landmark.d.ts.map +1 -1
  65. package/dist/Link.d.ts +10 -4
  66. package/dist/Link.d.ts.map +1 -1
  67. package/dist/ListBox.d.ts +73 -11
  68. package/dist/ListBox.d.ts.map +1 -1
  69. package/dist/ListDropTargetDelegate.d.ts +38 -0
  70. package/dist/ListDropTargetDelegate.d.ts.map +1 -0
  71. package/dist/Menu.d.ts +79 -10
  72. package/dist/Menu.d.ts.map +1 -1
  73. package/dist/Meter.d.ts +4 -4
  74. package/dist/Meter.d.ts.map +1 -1
  75. package/dist/Modal.d.ts +6 -4
  76. package/dist/Modal.d.ts.map +1 -1
  77. package/dist/NumberField.d.ts +10 -12
  78. package/dist/NumberField.d.ts.map +1 -1
  79. package/dist/Popover.d.ts +32 -7
  80. package/dist/Popover.d.ts.map +1 -1
  81. package/dist/Pressable.d.ts +27 -0
  82. package/dist/Pressable.d.ts.map +1 -0
  83. package/dist/ProgressBar.d.ts +6 -4
  84. package/dist/ProgressBar.d.ts.map +1 -1
  85. package/dist/RadioGroup.d.ts +43 -9
  86. package/dist/RadioGroup.d.ts.map +1 -1
  87. package/dist/RangeCalendar.d.ts +39 -7
  88. package/dist/RangeCalendar.d.ts.map +1 -1
  89. package/dist/RouterProvider.d.ts +75 -0
  90. package/dist/RouterProvider.d.ts.map +1 -0
  91. package/dist/SearchField.d.ts +23 -21
  92. package/dist/SearchField.d.ts.map +1 -1
  93. package/dist/Select.d.ts +48 -7
  94. package/dist/Select.d.ts.map +1 -1
  95. package/dist/SelectionIndicator.d.ts +30 -0
  96. package/dist/SelectionIndicator.d.ts.map +1 -0
  97. package/dist/Separator.d.ts +9 -3
  98. package/dist/Separator.d.ts.map +1 -1
  99. package/dist/SharedElementTransition.d.ts +41 -0
  100. package/dist/SharedElementTransition.d.ts.map +1 -0
  101. package/dist/Slider.d.ts +15 -8
  102. package/dist/Slider.d.ts.map +1 -1
  103. package/dist/StepList.d.ts +90 -0
  104. package/dist/StepList.d.ts.map +1 -0
  105. package/dist/Switch.d.ts +11 -5
  106. package/dist/Switch.d.ts.map +1 -1
  107. package/dist/Table.d.ts +222 -19
  108. package/dist/Table.d.ts.map +1 -1
  109. package/dist/Tabs.d.ts +47 -10
  110. package/dist/Tabs.d.ts.map +1 -1
  111. package/dist/TagGroup.d.ts +22 -10
  112. package/dist/TagGroup.d.ts.map +1 -1
  113. package/dist/Text.d.ts +10 -0
  114. package/dist/Text.d.ts.map +1 -0
  115. package/dist/TextField.d.ts +19 -11
  116. package/dist/TextField.d.ts.map +1 -1
  117. package/dist/TimeField.d.ts +32 -7
  118. package/dist/TimeField.d.ts.map +1 -1
  119. package/dist/Toast.d.ts +29 -14
  120. package/dist/Toast.d.ts.map +1 -1
  121. package/dist/ToggleButton.d.ts +36 -0
  122. package/dist/ToggleButton.d.ts.map +1 -0
  123. package/dist/ToggleButtonGroup.d.ts +33 -0
  124. package/dist/ToggleButtonGroup.d.ts.map +1 -0
  125. package/dist/Toolbar.d.ts +7 -3
  126. package/dist/Toolbar.d.ts.map +1 -1
  127. package/dist/Tooltip.d.ts +58 -7
  128. package/dist/Tooltip.d.ts.map +1 -1
  129. package/dist/Tree.d.ts +102 -11
  130. package/dist/Tree.d.ts.map +1 -1
  131. package/dist/Virtualizer.d.ts +61 -0
  132. package/dist/Virtualizer.d.ts.map +1 -0
  133. package/dist/VirtualizerLayouts.d.ts +82 -0
  134. package/dist/VirtualizerLayouts.d.ts.map +1 -0
  135. package/dist/VisuallyHidden.d.ts +4 -2
  136. package/dist/VisuallyHidden.d.ts.map +1 -1
  137. package/dist/contexts.d.ts +6 -1
  138. package/dist/contexts.d.ts.map +1 -1
  139. package/dist/index.d.ts +73 -39
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +23342 -10644
  142. package/dist/index.js.map +1 -7
  143. package/dist/index.jsx +18110 -0
  144. package/dist/index.jsx.map +1 -0
  145. package/dist/useDragAndDrop.d.ts +93 -0
  146. package/dist/useDragAndDrop.d.ts.map +1 -0
  147. package/dist/utils.d.ts +8 -2
  148. package/dist/utils.d.ts.map +1 -1
  149. package/dist/virtualizer/Layout.d.ts +79 -0
  150. package/dist/virtualizer/Layout.d.ts.map +1 -0
  151. package/package.json +33 -32
  152. package/src/ActionBar.tsx +251 -0
  153. package/src/ActionGroup.tsx +277 -0
  154. package/src/Alert.tsx +152 -0
  155. package/src/Autocomplete.tsx +39 -44
  156. package/src/Breadcrumbs.tsx +227 -72
  157. package/src/Button.tsx +315 -74
  158. package/src/Calendar.tsx +347 -141
  159. package/src/Checkbox.tsx +414 -123
  160. package/src/Collection.tsx +350 -0
  161. package/src/Color.tsx +1325 -284
  162. package/src/ColorEditor.tsx +213 -0
  163. package/src/ComboBox.tsx +644 -245
  164. package/src/ContextualHelpTrigger.tsx +195 -0
  165. package/src/DateField.tsx +274 -106
  166. package/src/DatePicker.tsx +892 -111
  167. package/src/DateRangePickerContext.tsx +44 -0
  168. package/src/Dialog.tsx +173 -104
  169. package/src/Disclosure.tsx +158 -105
  170. package/src/DragAndDrop.tsx +340 -0
  171. package/src/DragPreview.tsx +47 -0
  172. package/src/DropZone.tsx +233 -0
  173. package/src/FieldError.tsx +89 -0
  174. package/src/FileTrigger.tsx +83 -0
  175. package/src/Focusable.tsx +103 -0
  176. package/src/Form.tsx +140 -0
  177. package/src/GridList.tsx +542 -128
  178. package/src/HiddenDateInput.tsx +153 -0
  179. package/src/HiddenTimeInput.tsx +133 -0
  180. package/src/Icon.tsx +133 -0
  181. package/src/Keyboard.tsx +26 -0
  182. package/src/Landmark.tsx +37 -63
  183. package/src/Link.tsx +132 -69
  184. package/src/ListBox.tsx +656 -106
  185. package/src/ListDropTargetDelegate.ts +283 -0
  186. package/src/Menu.tsx +1234 -132
  187. package/src/Meter.tsx +44 -58
  188. package/src/Modal.tsx +262 -166
  189. package/src/NumberField.tsx +267 -151
  190. package/src/Popover.tsx +452 -343
  191. package/src/Pressable.tsx +108 -0
  192. package/src/ProgressBar.tsx +54 -59
  193. package/src/RadioGroup.tsx +533 -121
  194. package/src/RangeCalendar.tsx +249 -150
  195. package/src/RouterProvider.tsx +223 -0
  196. package/src/SearchField.tsx +460 -133
  197. package/src/Select.tsx +804 -233
  198. package/src/SelectionIndicator.tsx +108 -0
  199. package/src/Separator.tsx +47 -49
  200. package/src/SharedElementTransition.tsx +264 -0
  201. package/src/Slider.tsx +148 -98
  202. package/src/StepList.tsx +272 -0
  203. package/src/Switch.tsx +93 -46
  204. package/src/Table.tsx +1551 -225
  205. package/src/Tabs.tsx +377 -123
  206. package/src/TagGroup.tsx +233 -135
  207. package/src/Text.tsx +18 -0
  208. package/src/TextField.tsx +413 -86
  209. package/src/TimeField.tsx +232 -222
  210. package/src/Toast.tsx +306 -160
  211. package/src/ToggleButton.tsx +169 -0
  212. package/src/ToggleButtonGroup.tsx +141 -0
  213. package/src/Toolbar.tsx +61 -70
  214. package/src/Tooltip.tsx +473 -116
  215. package/src/Tree.tsx +1514 -175
  216. package/src/Virtualizer.tsx +730 -0
  217. package/src/VirtualizerLayouts.ts +280 -0
  218. package/src/VisuallyHidden.tsx +32 -38
  219. package/src/contexts.ts +29 -36
  220. package/src/index.ts +972 -620
  221. package/src/useDragAndDrop.ts +367 -0
  222. package/src/utils.tsx +69 -50
  223. package/src/virtualizer/Layout.ts +192 -0
  224. package/dist/index.ssr.js +0 -9785
  225. package/dist/index.ssr.js.map +0 -7
@@ -9,25 +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";
37
+ import {
38
+ SelectionIndicatorContext,
39
+ type SelectionIndicatorContextValue,
40
+ } from "./SelectionIndicator";
31
41
  import {
32
42
  type RenderChildren,
33
43
  type ClassNameOrFunction,
@@ -35,13 +45,46 @@ import {
35
45
  type SlotProps,
36
46
  useRenderProps,
37
47
  filterDOMProps,
38
- } from './utils';
48
+ } from "./utils";
49
+
50
+ type RefLike<T> = ((el: T) => void) | { current?: T | null } | undefined;
39
51
 
40
- // ============================================
41
- // TYPES
42
- // ============================================
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
+ }
43
60
 
44
- export type Orientation = 'horizontal' | 'vertical';
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
+ }
86
+
87
+ export type Orientation = "horizontal" | "vertical";
45
88
 
46
89
  export interface RadioGroupRenderProps {
47
90
  /** The orientation of the radio group. */
@@ -80,8 +123,12 @@ export interface RadioRenderProps {
80
123
  }
81
124
 
82
125
  export interface RadioGroupProps
83
- extends Omit<AriaRadioGroupProps, 'children' | 'label' | 'description' | 'errorMessage'>,
84
- 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
+ >,
85
132
  SlotProps {
86
133
  /** The children of the component. A function may be provided to receive render props. */
87
134
  children?: RenderChildren<RadioGroupRenderProps>;
@@ -89,30 +136,56 @@ export interface RadioGroupProps
89
136
  class?: ClassNameOrFunction<RadioGroupRenderProps>;
90
137
  /** The inline style for the element. */
91
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;
92
150
  }
93
151
 
94
- export interface RadioProps
95
- extends Omit<AriaRadioProps, 'children'>,
96
- SlotProps {
152
+ export interface RadioProps extends Omit<AriaRadioProps, "children">, SlotProps {
97
153
  /** The children of the component. A function may be provided to receive render props. */
98
154
  children?: RenderChildren<RadioRenderProps>;
99
155
  /** The CSS className for the element. */
100
156
  class?: ClassNameOrFunction<RadioRenderProps>;
101
157
  /** The inline style for the element. */
102
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;
103
178
  }
104
179
 
105
- // ============================================
106
- // CONTEXT
107
- // ============================================
108
-
109
- 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);
110
184
  export const RadioGroupStateContext = createContext<RadioGroupState | null>(null);
111
- export const RadioContext = createContext<RadioProps | null>(null);
112
-
113
- // ============================================
114
- // RADIO GROUP COMPONENT
115
- // ============================================
185
+ export interface RadioContextValue extends RadioProps {
186
+ slots?: Record<string, RadioProps>;
187
+ }
188
+ export const RadioContext = createContext<RadioContextValue | null>(null);
116
189
 
117
190
  /**
118
191
  * A radio group allows a user to select a single item from a list of mutually exclusive options.
@@ -126,111 +199,307 @@ export const RadioContext = createContext<RadioProps | null>(null);
126
199
  * ```
127
200
  */
128
201
  export function RadioGroup(props: ParentProps<RadioGroupProps>): JSX.Element {
129
- const [local, ariaProps] = splitProps(props, [
130
- 'class',
131
- 'style',
132
- 'slot',
133
- ]);
134
-
135
- // Create radio group state
136
- // We pass a function that returns props so that createRadioGroupState
137
- // can access props reactively. The props object itself is a proxy in SolidJS,
138
- // so accessing props.value inside a reactive context will track changes.
139
- const state = createRadioGroupState({
140
- get value() { return props.value; },
141
- get defaultValue() { return props.defaultValue; },
142
- get onChange() { return props.onChange; },
143
- get isDisabled() { return props.isDisabled; },
144
- get isReadOnly() { return props.isReadOnly; },
145
- get isRequired() { return props.isRequired; },
146
- 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;
147
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
+ }));
148
232
 
149
233
  // Create radio group aria props
150
- 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;
151
246
 
152
- // Render props values
153
247
  const renderValues = createMemo<RadioGroupRenderProps>(() => ({
154
- orientation: (ariaProps.orientation as Orientation) ?? 'vertical',
248
+ orientation: (ariaProps.orientation as Orientation) ?? "vertical",
155
249
  isDisabled: state.isDisabled,
156
250
  isReadOnly: state.isReadOnly,
157
251
  isRequired: state.isRequired,
158
- isInvalid: groupAria.isInvalid,
252
+ isInvalid: isInvalid(),
159
253
  state,
160
254
  }));
161
255
 
162
- // Resolve render props
163
256
  const renderProps = useRenderProps(
164
257
  {
165
258
  children: props.children,
166
259
  class: local.class,
167
260
  style: local.style,
168
- defaultClassName: 'solidaria-RadioGroup',
261
+ defaultClassName: "solidaria-RadioGroup",
169
262
  },
170
- renderValues
263
+ renderValues,
171
264
  );
172
265
 
173
- // Filter DOM props
174
266
  const domProps = createMemo(() => filterDOMProps(ariaProps, { global: true }));
175
267
 
176
- // Remove ref from spread props to avoid type conflicts
177
268
  const cleanGroupProps = () => {
178
269
  const { ref: _ref, ...rest } = groupAria.radioGroupProps as Record<string, unknown>;
179
270
  return rest;
180
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
+ }
181
291
 
182
- // Resolve children - we need to pass render props if children is a function
183
- // but we use props.children directly (not renderProps.renderChildren())
184
- // to preserve SolidJS context propagation for nested components like Radio
185
- const resolvedChildren = () => {
186
- const children = props.children;
187
- if (typeof children === 'function') {
188
- 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;
189
301
  }
190
- return children;
302
+
303
+ state.updateValidation(target.validity.valid ? validValidation : getNativeValidation(target));
304
+ state.commitValidation();
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;
191
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>;
192
405
 
193
406
  return (
194
407
  <RadioGroupStateContext.Provider value={state}>
195
- <div
196
- {...domProps()}
197
- {...cleanGroupProps()}
198
- class={renderProps.class()}
199
- style={renderProps.style()}
200
- data-orientation={ariaProps.orientation ?? 'vertical'}
201
- data-disabled={state.isDisabled || undefined}
202
- data-readonly={state.isReadOnly || undefined}
203
- data-required={state.isRequired || undefined}
204
- data-invalid={groupAria.isInvalid || undefined}
205
- >
206
- {resolvedChildren()}
207
- </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>
208
433
  </RadioGroupStateContext.Provider>
209
434
  );
210
435
  }
211
436
 
212
- // ============================================
213
- // RADIO COMPONENT
214
- // ============================================
215
-
216
437
  /**
217
438
  * Internal Radio implementation that has access to RadioGroupStateContext.
218
439
  * This is rendered inside the RadioGroup's context provider.
219
440
  */
220
441
  function RadioImpl(props: { radioProps: RadioProps; state: RadioGroupState }): JSX.Element {
221
- let inputRef: HTMLInputElement | null = null;
222
- 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
+ );
223
460
 
224
- 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
+ });
225
493
 
226
494
  // Create radio aria props
227
495
  const radioAria = createRadio(
228
496
  () => ({
229
- ...ariaProps,
230
- children: typeof radioProps.children === 'function' ? true : radioProps.children,
497
+ ...inputAriaProps(),
498
+ "aria-describedby": describedBy(),
499
+ children: typeof radioProps.children === "function" ? true : radioProps.children,
231
500
  }),
232
501
  state,
233
- () => inputRef
502
+ inputElement,
234
503
  );
235
504
 
236
505
  // Create focus ring
@@ -241,9 +510,11 @@ function RadioImpl(props: { radioProps: RadioProps; state: RadioGroupState }): J
241
510
  get isDisabled() {
242
511
  return radioAria.isDisabled || state.isReadOnly;
243
512
  },
513
+ onHoverStart: local.onHoverStart,
514
+ onHoverEnd: local.onHoverEnd,
515
+ onHoverChange: local.onHoverChange,
244
516
  });
245
517
 
246
- // Render props values
247
518
  const renderValues = createMemo<RadioRenderProps>(() => ({
248
519
  isSelected: radioAria.isSelected(),
249
520
  isHovered: isHovered(),
@@ -256,18 +527,20 @@ function RadioImpl(props: { radioProps: RadioProps; state: RadioGroupState }): J
256
527
  isRequired: state.isRequired,
257
528
  }));
258
529
 
259
- // Resolve render props
260
530
  const renderProps = useRenderProps(
261
531
  {
262
532
  children: radioProps.children,
263
533
  class: local.class,
264
534
  style: local.style,
265
- defaultClassName: 'solidaria-Radio',
535
+ defaultClassName: "solidaria-Radio",
266
536
  },
267
- renderValues
537
+ renderValues,
268
538
  );
269
539
 
270
- // Filter DOM props
540
+ const selectionIndicatorContext = createMemo<SelectionIndicatorContextValue>(() => ({
541
+ isSelected: radioAria.isSelected,
542
+ }));
543
+
271
544
  const domProps = createMemo(() => {
272
545
  const filtered = filterDOMProps(ariaProps, { global: true });
273
546
  delete (filtered as Record<string, unknown>).id;
@@ -275,7 +548,6 @@ function RadioImpl(props: { radioProps: RadioProps; state: RadioGroupState }): J
275
548
  return filtered;
276
549
  });
277
550
 
278
- // Remove ref from spread props to avoid type conflicts
279
551
  const cleanLabelProps = () => {
280
552
  const { ref: _ref1, ...rest } = radioAria.labelProps as Record<string, unknown>;
281
553
  return rest;
@@ -285,40 +557,184 @@ function RadioImpl(props: { radioProps: RadioProps; state: RadioGroupState }): J
285
557
  return rest;
286
558
  };
287
559
  const cleanInputProps = () => {
288
- 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>;
289
566
  return rest;
290
567
  };
291
568
  const cleanFocusProps = () => {
292
- 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>;
293
575
  return rest;
294
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>;
295
708
 
296
709
  return (
297
- <label
298
- {...domProps()}
299
- {...cleanLabelProps()}
300
- {...cleanHoverProps()}
301
- class={renderProps.class()}
302
- style={renderProps.style()}
303
- data-selected={radioAria.isSelected() || undefined}
304
- data-pressed={radioAria.isPressed() || undefined}
305
- data-hovered={isHovered() || undefined}
306
- data-focused={isFocused() || undefined}
307
- data-focus-visible={isFocusVisible() || undefined}
308
- data-disabled={radioAria.isDisabled || undefined}
309
- data-readonly={state.isReadOnly || undefined}
310
- data-invalid={state.isInvalid || undefined}
311
- data-required={state.isRequired || undefined}
312
- >
313
- <VisuallyHidden>
314
- <input
315
- ref={(el) => (inputRef = el)}
316
- {...cleanInputProps()}
317
- {...cleanFocusProps()}
318
- />
319
- </VisuallyHidden>
320
- {renderProps.renderChildren()}
321
- </label>
710
+ <SelectionIndicatorContext.Provider value={selectionIndicatorContext()}>
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
+ )}
737
+ </SelectionIndicatorContext.Provider>
322
738
  );
323
739
  }
324
740
 
@@ -345,11 +761,7 @@ export function Radio(props: RadioProps): JSX.Element {
345
761
  const getState = createMemo(() => useContext(RadioGroupStateContext));
346
762
 
347
763
  return (
348
- <Show
349
- when={getState()}
350
- fallback={null}
351
- keyed
352
- >
764
+ <Show when={getState()} fallback={null} keyed>
353
765
  {(state) => <RadioImpl radioProps={props} state={state} />}
354
766
  </Show>
355
767
  );