@proyecto-viviana/solidaria-components 0.2.9 → 0.3.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 (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 +23253 -18564
  139. package/dist/index.js.map +1 -1
  140. package/dist/index.jsx +18116 -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 +251 -176
  186. package/src/NumberField.tsx +139 -143
  187. package/src/Popover.tsx +396 -234
  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 +216 -158
  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 +49 -60
  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
package/src/TextField.tsx CHANGED
@@ -10,16 +10,27 @@ import {
10
10
  createContext,
11
11
  useContext,
12
12
  createMemo,
13
+ createSignal,
14
+ createEffect,
15
+ onCleanup,
16
+ onMount,
13
17
  splitProps,
14
- } from 'solid-js';
18
+ untrack,
19
+ } from "solid-js";
15
20
  import {
16
21
  createTextField,
17
22
  createFocusRing,
18
23
  createHover,
19
24
  mergeProps,
20
25
  type AriaTextFieldProps,
21
- } from '@proyecto-viviana/solidaria';
22
- import { createTextFieldState } from '@proyecto-viviana/solid-stately';
26
+ } from "@proyecto-viviana/solidaria";
27
+ import {
28
+ createTextFieldState,
29
+ VALID_VALIDITY_STATE,
30
+ type ValidationResult,
31
+ } from "@proyecto-viviana/solid-stately";
32
+ import { FormContext, type FormProps } from "./Form";
33
+ import { FieldErrorContext, type FieldErrorContextValue } from "./FieldError";
23
34
  import {
24
35
  type RenderChildren,
25
36
  type ClassNameOrFunction,
@@ -27,11 +38,7 @@ import {
27
38
  type SlotProps,
28
39
  useRenderProps,
29
40
  filterDOMProps,
30
- } from './utils';
31
-
32
- // ============================================
33
- // TYPES
34
- // ============================================
41
+ } from "./utils";
35
42
 
36
43
  export interface TextFieldRenderProps {
37
44
  /** Whether the text field is disabled. */
@@ -50,27 +57,28 @@ export interface TextFieldRenderProps {
50
57
  isFocusVisible: boolean;
51
58
  }
52
59
 
53
- export interface TextFieldProps
54
- extends Omit<AriaTextFieldProps, 'children'>,
55
- SlotProps {
60
+ export interface TextFieldProps extends Omit<AriaTextFieldProps, "children">, SlotProps {
56
61
  /** The children of the component. A function may be provided to receive render props. */
57
62
  children?: RenderChildren<TextFieldRenderProps>;
58
63
  /** The CSS className for the element. */
59
64
  class?: ClassNameOrFunction<TextFieldRenderProps>;
60
65
  /** The inline style for the element. */
61
66
  style?: StyleOrFunction<TextFieldRenderProps>;
67
+ /** Custom renderer for the outer text field element. */
68
+ render?: (
69
+ props: JSX.HTMLAttributes<HTMLDivElement>,
70
+ renderProps: TextFieldRenderProps,
71
+ ) => JSX.Element;
62
72
  }
63
73
 
64
- // ============================================
65
- // CONTEXT
66
- // ============================================
67
-
68
74
  export interface TextFieldContextValue {
69
- labelProps: JSX.LabelHTMLAttributes<HTMLLabelElement>;
70
- inputProps: JSX.InputHTMLAttributes<HTMLInputElement>;
71
- descriptionProps: JSX.HTMLAttributes<HTMLElement>;
72
- errorMessageProps: JSX.HTMLAttributes<HTMLElement>;
73
- isInvalid: boolean;
75
+ labelProps?: JSX.LabelHTMLAttributes<HTMLLabelElement>;
76
+ inputProps?: JSX.InputHTMLAttributes<HTMLInputElement>;
77
+ descriptionProps?: JSX.HTMLAttributes<HTMLElement>;
78
+ errorMessageProps?: JSX.HTMLAttributes<HTMLElement>;
79
+ isInvalid?: boolean;
80
+ slots?: Record<string, TextFieldProps>;
81
+ inputId?: string;
74
82
  }
75
83
 
76
84
  export const TextFieldContext = createContext<TextFieldContextValue | null>(null);
@@ -79,9 +87,55 @@ export const InputContext = TextFieldContext;
79
87
  export const TextAreaContext = TextFieldContext;
80
88
  export const FieldInputContext = TextFieldContext;
81
89
 
82
- // ============================================
83
- // SUB-COMPONENTS
84
- // ============================================
90
+ function withFormValidationBehavior(
91
+ props: TextFieldProps,
92
+ formContext: FormProps | null,
93
+ ): TextFieldProps {
94
+ if (!formContext?.validationBehavior) {
95
+ return props;
96
+ }
97
+
98
+ return new Proxy(props, {
99
+ get(target, property, receiver) {
100
+ const localValue = Reflect.get(target, property, receiver);
101
+ if (property === "validationBehavior" && localValue === undefined) {
102
+ return formContext.validationBehavior;
103
+ }
104
+
105
+ return localValue;
106
+ },
107
+ has(target, property) {
108
+ return (
109
+ Reflect.has(target, property) ||
110
+ (property === "validationBehavior" && formContext.validationBehavior !== undefined)
111
+ );
112
+ },
113
+ ownKeys(target) {
114
+ const keys = new Set(Reflect.ownKeys(target));
115
+ if (formContext.validationBehavior !== undefined) {
116
+ keys.add("validationBehavior");
117
+ }
118
+
119
+ return Array.from(keys);
120
+ },
121
+ getOwnPropertyDescriptor(target, property) {
122
+ const descriptor = Reflect.getOwnPropertyDescriptor(target, property);
123
+ if (descriptor) {
124
+ return descriptor;
125
+ }
126
+
127
+ if (property === "validationBehavior" && formContext.validationBehavior !== undefined) {
128
+ return {
129
+ enumerable: true,
130
+ configurable: true,
131
+ get: () => formContext.validationBehavior,
132
+ };
133
+ }
134
+
135
+ return undefined;
136
+ },
137
+ });
138
+ }
85
139
 
86
140
  export interface LabelProps extends JSX.LabelHTMLAttributes<HTMLLabelElement> {
87
141
  children?: JSX.Element;
@@ -97,16 +151,49 @@ export function Label(props: LabelProps): JSX.Element {
97
151
  // Merge context labelProps with local props (local props take precedence)
98
152
  const mergedProps = () => {
99
153
  if (context) {
100
- const { ref: _ref, ...contextLabelProps } = context.labelProps as Record<string, unknown>;
101
- return { ...contextLabelProps, ...props };
154
+ const { ref: _ref, ...contextLabelProps } = (context.labelProps ?? {}) as Record<
155
+ string,
156
+ unknown
157
+ >;
158
+ const merged = {
159
+ ...contextLabelProps,
160
+ ...(context.inputId ? { for: context.inputId } : {}),
161
+ ...props,
162
+ } as Record<string, unknown>;
163
+ if (merged.class == null) {
164
+ merged.class = "solidaria-Label";
165
+ }
166
+ return merged;
102
167
  }
103
- return props;
168
+ return props.class == null ? { ...props, class: "solidaria-Label" } : props;
104
169
  };
105
170
 
106
171
  return <label {...mergedProps()}>{props.children}</label>;
107
172
  }
108
173
 
109
- export interface InputProps extends Omit<JSX.InputHTMLAttributes<HTMLInputElement>, 'children'> {}
174
+ export interface InputProps extends Omit<JSX.InputHTMLAttributes<HTMLInputElement>, "children"> {}
175
+
176
+ function eventWithCurrentTarget<T extends HTMLElement>(event: Event, element: T): Event {
177
+ return new Proxy(event, {
178
+ get(target, property, receiver) {
179
+ if (property === "target" || property === "currentTarget") {
180
+ return element;
181
+ }
182
+
183
+ const value = Reflect.get(target, property, receiver);
184
+ return typeof value === "function" ? value.bind(target) : value;
185
+ },
186
+ });
187
+ }
188
+
189
+ function clearDelegatedTextEntryHandlers(element: HTMLElement) {
190
+ const delegatedElement = element as HTMLElement & {
191
+ $$input?: unknown;
192
+ $$change?: unknown;
193
+ };
194
+ delete delegatedElement.$$input;
195
+ delete delegatedElement.$$change;
196
+ }
110
197
 
111
198
  /**
112
199
  * An input element that automatically wires up to the parent TextField context.
@@ -115,21 +202,85 @@ export interface InputProps extends Omit<JSX.InputHTMLAttributes<HTMLInputElemen
115
202
  */
116
203
  export function Input(props: InputProps): JSX.Element {
117
204
  const context = useContext(TextFieldContext);
205
+ let inputElement: HTMLInputElement | undefined;
118
206
 
119
207
  // Merge context inputProps with local props (local props take precedence)
120
208
  const mergedProps = () => {
121
209
  if (context) {
122
210
  // Remove ref from context props to avoid conflicts
123
- const { ref: _ref, ...contextInputProps } = context.inputProps as Record<string, unknown>;
124
- return { ...contextInputProps, ...props };
211
+ const { ref: _ref, ...contextInputProps } = (context.inputProps ?? {}) as Record<
212
+ string,
213
+ unknown
214
+ >;
215
+ const merged = { ...contextInputProps, ...props } as Record<string, unknown>;
216
+ if (merged.class == null) {
217
+ merged.class = "solidaria-Input";
218
+ }
219
+ return merged;
125
220
  }
126
- return props;
221
+ return props.class == null ? { ...props, class: "solidaria-Input" } : props;
127
222
  };
128
223
 
129
- return <input {...mergedProps()} />;
224
+ onMount(() => {
225
+ const element = inputElement;
226
+ if (!element) {
227
+ return;
228
+ }
229
+
230
+ const inputHandler = (event: Event) => {
231
+ const handler = mergedProps().onInput as
232
+ | JSX.EventHandler<HTMLInputElement, InputEvent>
233
+ | undefined;
234
+ handler?.(
235
+ eventWithCurrentTarget(event, element) as InputEvent & {
236
+ currentTarget: HTMLInputElement;
237
+ target: Element;
238
+ },
239
+ );
240
+ clearDelegatedTextEntryHandlers(element);
241
+ event.stopPropagation();
242
+ };
243
+ const changeHandler = (event: Event) => {
244
+ const handler = mergedProps().onChange as
245
+ | JSX.EventHandler<HTMLInputElement, Event>
246
+ | undefined;
247
+ handler?.(
248
+ eventWithCurrentTarget(event, element) as Event & {
249
+ currentTarget: HTMLInputElement;
250
+ target: Element;
251
+ },
252
+ );
253
+ clearDelegatedTextEntryHandlers(element);
254
+ event.stopPropagation();
255
+ };
256
+
257
+ element.addEventListener("input", inputHandler);
258
+ element.addEventListener("change", changeHandler);
259
+ clearDelegatedTextEntryHandlers(element);
260
+ onCleanup(() => {
261
+ element.removeEventListener("input", inputHandler);
262
+ element.removeEventListener("change", changeHandler);
263
+ });
264
+ });
265
+
266
+ return (
267
+ <input
268
+ {...mergedProps()}
269
+ ref={(element) => {
270
+ inputElement = element;
271
+ const ref = props.ref;
272
+ if (typeof ref === "function") {
273
+ ref(element);
274
+ }
275
+ }}
276
+ />
277
+ );
130
278
  }
131
279
 
132
- export interface TextAreaProps extends Omit<JSX.TextareaHTMLAttributes<HTMLTextAreaElement>, 'children'> {}
280
+ export interface TextAreaProps extends Omit<
281
+ JSX.TextareaHTMLAttributes<HTMLTextAreaElement>,
282
+ "children"
283
+ > {}
133
284
 
134
285
  /**
135
286
  * A textarea element that automatically wires up to the parent TextField context.
@@ -138,23 +289,81 @@ export interface TextAreaProps extends Omit<JSX.TextareaHTMLAttributes<HTMLTextA
138
289
  */
139
290
  export function TextArea(props: TextAreaProps): JSX.Element {
140
291
  const context = useContext(TextFieldContext);
292
+ let textAreaElement: HTMLTextAreaElement | undefined;
141
293
 
142
294
  // Merge context inputProps with local props (local props take precedence)
143
295
  // Note: TextArea uses inputProps from context since it's an input variant
144
296
  const mergedProps = () => {
145
297
  if (context) {
146
- const { ref: _ref, type: _type, ...contextInputProps } = context.inputProps as Record<string, unknown>;
147
- return { ...contextInputProps, ...props };
298
+ const {
299
+ ref: _ref,
300
+ type: _type,
301
+ ...contextInputProps
302
+ } = (context.inputProps ?? {}) as Record<string, unknown>;
303
+ const merged = { ...contextInputProps, ...props } as Record<string, unknown>;
304
+ if (merged.class == null) {
305
+ merged.class = "solidaria-TextArea";
306
+ }
307
+ return merged;
148
308
  }
149
- return props;
309
+ return props.class == null ? { ...props, class: "solidaria-TextArea" } : props;
150
310
  };
151
311
 
152
- return <textarea {...mergedProps()} />;
153
- }
312
+ onMount(() => {
313
+ const element = textAreaElement;
314
+ if (!element) {
315
+ return;
316
+ }
317
+
318
+ const inputHandler = (event: Event) => {
319
+ const handler = mergedProps().onInput as
320
+ | JSX.EventHandler<HTMLTextAreaElement, InputEvent>
321
+ | undefined;
322
+ handler?.(
323
+ eventWithCurrentTarget(event, element) as InputEvent & {
324
+ currentTarget: HTMLTextAreaElement;
325
+ target: Element;
326
+ },
327
+ );
328
+ clearDelegatedTextEntryHandlers(element);
329
+ event.stopPropagation();
330
+ };
331
+ const changeHandler = (event: Event) => {
332
+ const handler = mergedProps().onChange as
333
+ | JSX.EventHandler<HTMLTextAreaElement, Event>
334
+ | undefined;
335
+ handler?.(
336
+ eventWithCurrentTarget(event, element) as Event & {
337
+ currentTarget: HTMLTextAreaElement;
338
+ target: Element;
339
+ },
340
+ );
341
+ clearDelegatedTextEntryHandlers(element);
342
+ event.stopPropagation();
343
+ };
344
+
345
+ element.addEventListener("input", inputHandler);
346
+ element.addEventListener("change", changeHandler);
347
+ clearDelegatedTextEntryHandlers(element);
348
+ onCleanup(() => {
349
+ element.removeEventListener("input", inputHandler);
350
+ element.removeEventListener("change", changeHandler);
351
+ });
352
+ });
154
353
 
155
- // ============================================
156
- // COMPONENT
157
- // ============================================
354
+ return (
355
+ <textarea
356
+ {...mergedProps()}
357
+ ref={(element) => {
358
+ textAreaElement = element;
359
+ const ref = props.ref;
360
+ if (typeof ref === "function") {
361
+ ref(element);
362
+ }
363
+ }}
364
+ />
365
+ );
366
+ }
158
367
 
159
368
  /**
160
369
  * A text field allows a user to enter a plain text value with a keyboard.
@@ -175,40 +384,72 @@ export function TextArea(props: TextAreaProps): JSX.Element {
175
384
  * ```
176
385
  */
177
386
  export function TextField(props: TextFieldProps): JSX.Element {
178
- // Split props
179
- const [local, ariaProps] = splitProps(props, [
180
- 'children',
181
- 'class',
182
- 'style',
183
- 'slot',
387
+ const formContext = useContext(FormContext);
388
+ const contextProps = useContext(TextFieldContext);
389
+ const contextSlotProps = contextProps?.slots?.[props.slot ?? "default"];
390
+ const contextBaseProps = createMemo<TextFieldProps>(() => {
391
+ if (!contextProps) return {};
392
+ const {
393
+ labelProps: _labelProps,
394
+ inputProps: _inputProps,
395
+ descriptionProps: _descriptionProps,
396
+ errorMessageProps: _errorMessageProps,
397
+ isInvalid: _isInvalid,
398
+ slots: _slots,
399
+ ...rest
400
+ } = contextProps;
401
+ return rest as TextFieldProps;
402
+ });
403
+ const baseProps = (
404
+ contextProps ? mergeProps(contextBaseProps(), contextSlotProps ?? {}, props) : props
405
+ ) as TextFieldProps;
406
+ const mergedProps = withFormValidationBehavior(baseProps, formContext);
407
+
408
+ const [local, ariaProps] = splitProps(mergedProps, [
409
+ "children",
410
+ "class",
411
+ "style",
412
+ "render",
413
+ "slot",
184
414
  ]);
185
415
 
186
- // Create text field state
187
416
  // Use getters to ensure props are read lazily inside reactive contexts
188
417
  const state = createTextFieldState({
189
- get value() { return ariaProps.value; },
190
- get defaultValue() { return ariaProps.defaultValue; },
191
- get onChange() { return ariaProps.onChange; },
418
+ get value() {
419
+ return ariaProps.value;
420
+ },
421
+ get defaultValue() {
422
+ return ariaProps.defaultValue;
423
+ },
424
+ get onChange() {
425
+ return ariaProps.onChange;
426
+ },
427
+ });
428
+
429
+ const inputAriaProps = createMemo(() => {
430
+ const clean: Record<string, unknown> = {};
431
+ for (const key in ariaProps as Record<string, unknown>) {
432
+ if (!key.startsWith("data-")) {
433
+ clean[key] = (ariaProps as Record<string, unknown>)[key];
434
+ }
435
+ }
436
+ return clean as typeof ariaProps;
192
437
  });
193
438
 
194
- // Create text field aria props
195
439
  const textFieldAria = createTextField(() => ({
196
- ...ariaProps,
440
+ ...inputAriaProps(),
197
441
  value: state.value(),
198
442
  onChange: state.setValue,
199
443
  }));
200
444
 
201
- // Create focus ring
202
445
  const { isFocused, isFocusVisible, focusProps } = createFocusRing();
203
446
 
204
- // Create hover
205
447
  const { isHovered, hoverProps } = createHover({
206
448
  get isDisabled() {
207
449
  return ariaProps.isDisabled;
208
450
  },
209
451
  });
210
452
 
211
- // Render props values
212
453
  const renderValues = createMemo<TextFieldRenderProps>(() => ({
213
454
  isDisabled: ariaProps.isDisabled || false,
214
455
  isInvalid: textFieldAria.isInvalid,
@@ -219,25 +460,45 @@ export function TextField(props: TextFieldProps): JSX.Element {
219
460
  isFocusVisible: isFocusVisible(),
220
461
  }));
221
462
 
222
- // Resolve render props
223
463
  const renderProps = useRenderProps(
224
464
  {
225
- children: props.children,
465
+ children: local.children,
226
466
  class: local.class,
227
467
  style: local.style,
228
- defaultClassName: 'solidaria-TextField',
468
+ defaultClassName: "solidaria-TextField",
229
469
  },
230
- renderValues
470
+ renderValues,
231
471
  );
472
+ const childRenderValues: TextFieldRenderProps = {
473
+ get isDisabled() {
474
+ return ariaProps.isDisabled || false;
475
+ },
476
+ get isInvalid() {
477
+ return textFieldAria.isInvalid;
478
+ },
479
+ get isReadOnly() {
480
+ return ariaProps.isReadOnly || false;
481
+ },
482
+ get isRequired() {
483
+ return ariaProps.isRequired || false;
484
+ },
485
+ get isHovered() {
486
+ return isHovered();
487
+ },
488
+ get isFocused() {
489
+ return isFocused();
490
+ },
491
+ get isFocusVisible() {
492
+ return isFocusVisible();
493
+ },
494
+ };
232
495
 
233
- // Filter DOM props
234
496
  const domProps = createMemo(() => {
235
497
  const filtered = filterDOMProps(ariaProps, { global: true });
236
498
  delete (filtered as Record<string, unknown>).id;
237
499
  return filtered;
238
500
  });
239
501
 
240
- // Remove ref from spread props to avoid type conflicts
241
502
  const cleanHoverProps = () => {
242
503
  const { ref: _ref, ...rest } = hoverProps as Record<string, unknown>;
243
504
  return rest;
@@ -245,6 +506,27 @@ export function TextField(props: TextFieldProps): JSX.Element {
245
506
 
246
507
  // Context value for sub-components.
247
508
  // Use property getters so sub-components always read the latest aria/focus state.
509
+ const fieldValidation = createMemo<ValidationResult>(() => {
510
+ const isInvalid = textFieldAria.isInvalid;
511
+ const errorMessage = ariaProps.errorMessage;
512
+ const validationErrors = isInvalid && typeof errorMessage === "string" ? [errorMessage] : [];
513
+
514
+ return {
515
+ isInvalid,
516
+ validationErrors,
517
+ validationDetails: isInvalid
518
+ ? { ...VALID_VALIDITY_STATE, customError: true, valid: false }
519
+ : VALID_VALIDITY_STATE,
520
+ };
521
+ });
522
+ const fieldErrorContext: FieldErrorContextValue = {
523
+ get validation() {
524
+ return fieldValidation();
525
+ },
526
+ get errorMessageProps() {
527
+ return textFieldAria.errorMessageProps as JSX.HTMLAttributes<HTMLElement>;
528
+ },
529
+ };
248
530
  const contextValue: TextFieldContextValue = {
249
531
  get labelProps() {
250
532
  return textFieldAria.labelProps as JSX.LabelHTMLAttributes<HTMLLabelElement>;
@@ -252,7 +534,7 @@ export function TextField(props: TextFieldProps): JSX.Element {
252
534
  get inputProps() {
253
535
  return mergeProps(
254
536
  textFieldAria.inputProps as Record<string, unknown>,
255
- focusProps as Record<string, unknown>
537
+ focusProps as Record<string, unknown>,
256
538
  ) as JSX.InputHTMLAttributes<HTMLInputElement>;
257
539
  },
258
540
  get descriptionProps() {
@@ -264,25 +546,53 @@ export function TextField(props: TextFieldProps): JSX.Element {
264
546
  get isInvalid() {
265
547
  return textFieldAria.isInvalid;
266
548
  },
549
+ // Deterministic at render (createTextField generates it via createUniqueId),
550
+ // so the Label's `for` is stable across SSR + hydration. A signal set from an
551
+ // onMount effect would flip undefined->id post-mount and re-execute the Label
552
+ // template — crashing hydration if the re-run lands mid-hydration (dev).
553
+ get inputId() {
554
+ return (textFieldAria.inputProps as { id?: string }).id;
555
+ },
267
556
  };
557
+ // Resolve the render-prop children ONCE (untracked). Re-invoking it on a
558
+ // reactive update re-clones its templates; if that lands mid-hydration it
559
+ // throws a Hydration Mismatch (worst in dev, where slow unbundled modules widen
560
+ // the hydration window). The children carry their own fine-grained reactivity
561
+ // (render-value getters + <Show>s), so they update without being re-created.
562
+ const fieldChildren = untrack(() => {
563
+ const children = local.children;
564
+ return typeof children === "function" ? children(childRenderValues) : children;
565
+ });
566
+ const rootProps = () =>
567
+ ({
568
+ ...domProps(),
569
+ ...cleanHoverProps(),
570
+ class: renderProps.class(),
571
+ style: renderProps.style(),
572
+ slot: local.slot,
573
+ "data-disabled": ariaProps.isDisabled || undefined,
574
+ "data-invalid": textFieldAria.isInvalid || undefined,
575
+ "data-readonly": ariaProps.isReadOnly || undefined,
576
+ "data-required": ariaProps.isRequired || undefined,
577
+ "data-hovered": isHovered() || undefined,
578
+ "data-focused": isFocused() || undefined,
579
+ "data-focus-visible": isFocusVisible() || undefined,
580
+ }) as JSX.HTMLAttributes<HTMLDivElement>;
581
+ const customRootProps = () =>
582
+ ({
583
+ ...rootProps(),
584
+ children: fieldChildren,
585
+ }) as JSX.HTMLAttributes<HTMLDivElement>;
268
586
 
269
587
  return (
270
- <TextFieldContext.Provider value={contextValue}>
271
- <div
272
- {...domProps()}
273
- {...cleanHoverProps()}
274
- class={renderProps.class()}
275
- style={renderProps.style()}
276
- data-disabled={ariaProps.isDisabled || undefined}
277
- data-invalid={textFieldAria.isInvalid || undefined}
278
- data-readonly={ariaProps.isReadOnly || undefined}
279
- data-required={ariaProps.isRequired || undefined}
280
- data-hovered={isHovered() || undefined}
281
- data-focused={isFocused() || undefined}
282
- data-focus-visible={isFocusVisible() || undefined}
283
- >
284
- {renderProps.renderChildren()}
285
- </div>
286
- </TextFieldContext.Provider>
588
+ <FieldErrorContext.Provider value={fieldErrorContext}>
589
+ <TextFieldContext.Provider value={contextValue}>
590
+ {local.render ? (
591
+ local.render(customRootProps(), renderValues())
592
+ ) : (
593
+ <div {...rootProps()}>{fieldChildren}</div>
594
+ )}
595
+ </TextFieldContext.Provider>
596
+ </FieldErrorContext.Provider>
287
597
  );
288
598
  }