@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
package/src/TextField.tsx CHANGED
@@ -10,15 +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,
24
+ mergeProps,
19
25
  type AriaTextFieldProps,
20
- } from '@proyecto-viviana/solidaria';
21
- 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";
22
34
  import {
23
35
  type RenderChildren,
24
36
  type ClassNameOrFunction,
@@ -26,11 +38,7 @@ import {
26
38
  type SlotProps,
27
39
  useRenderProps,
28
40
  filterDOMProps,
29
- } from './utils';
30
-
31
- // ============================================
32
- // TYPES
33
- // ============================================
41
+ } from "./utils";
34
42
 
35
43
  export interface TextFieldRenderProps {
36
44
  /** Whether the text field is disabled. */
@@ -49,34 +57,85 @@ export interface TextFieldRenderProps {
49
57
  isFocusVisible: boolean;
50
58
  }
51
59
 
52
- export interface TextFieldProps
53
- extends Omit<AriaTextFieldProps, 'children'>,
54
- SlotProps {
60
+ export interface TextFieldProps extends Omit<AriaTextFieldProps, "children">, SlotProps {
55
61
  /** The children of the component. A function may be provided to receive render props. */
56
62
  children?: RenderChildren<TextFieldRenderProps>;
57
63
  /** The CSS className for the element. */
58
64
  class?: ClassNameOrFunction<TextFieldRenderProps>;
59
65
  /** The inline style for the element. */
60
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;
61
72
  }
62
73
 
63
- // ============================================
64
- // CONTEXT
65
- // ============================================
66
-
67
74
  export interface TextFieldContextValue {
68
- labelProps: JSX.LabelHTMLAttributes<HTMLLabelElement>;
69
- inputProps: JSX.InputHTMLAttributes<HTMLInputElement>;
70
- descriptionProps: JSX.HTMLAttributes<HTMLElement>;
71
- errorMessageProps: JSX.HTMLAttributes<HTMLElement>;
72
- 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;
73
82
  }
74
83
 
75
84
  export const TextFieldContext = createContext<TextFieldContextValue | null>(null);
85
+ export const LabelContext = TextFieldContext;
86
+ export const InputContext = TextFieldContext;
87
+ export const TextAreaContext = TextFieldContext;
88
+ export const FieldInputContext = TextFieldContext;
89
+
90
+ function withFormValidationBehavior(
91
+ props: TextFieldProps,
92
+ formContext: FormProps | null,
93
+ ): TextFieldProps {
94
+ if (!formContext?.validationBehavior) {
95
+ return props;
96
+ }
76
97
 
77
- // ============================================
78
- // SUB-COMPONENTS
79
- // ============================================
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
+ }
80
139
 
81
140
  export interface LabelProps extends JSX.LabelHTMLAttributes<HTMLLabelElement> {
82
141
  children?: JSX.Element;
@@ -92,16 +151,49 @@ export function Label(props: LabelProps): JSX.Element {
92
151
  // Merge context labelProps with local props (local props take precedence)
93
152
  const mergedProps = () => {
94
153
  if (context) {
95
- const { ref: _ref, ...contextLabelProps } = context.labelProps as Record<string, unknown>;
96
- 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;
97
167
  }
98
- return props;
168
+ return props.class == null ? { ...props, class: "solidaria-Label" } : props;
99
169
  };
100
170
 
101
171
  return <label {...mergedProps()}>{props.children}</label>;
102
172
  }
103
173
 
104
- 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
+ }
105
197
 
106
198
  /**
107
199
  * An input element that automatically wires up to the parent TextField context.
@@ -110,21 +202,85 @@ export interface InputProps extends Omit<JSX.InputHTMLAttributes<HTMLInputElemen
110
202
  */
111
203
  export function Input(props: InputProps): JSX.Element {
112
204
  const context = useContext(TextFieldContext);
205
+ let inputElement: HTMLInputElement | undefined;
113
206
 
114
207
  // Merge context inputProps with local props (local props take precedence)
115
208
  const mergedProps = () => {
116
209
  if (context) {
117
210
  // Remove ref from context props to avoid conflicts
118
- const { ref: _ref, ...contextInputProps } = context.inputProps as Record<string, unknown>;
119
- 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;
120
220
  }
121
- return props;
221
+ return props.class == null ? { ...props, class: "solidaria-Input" } : props;
122
222
  };
123
223
 
124
- 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
+ );
125
278
  }
126
279
 
127
- export interface TextAreaProps extends Omit<JSX.TextareaHTMLAttributes<HTMLTextAreaElement>, 'children'> {}
280
+ export interface TextAreaProps extends Omit<
281
+ JSX.TextareaHTMLAttributes<HTMLTextAreaElement>,
282
+ "children"
283
+ > {}
128
284
 
129
285
  /**
130
286
  * A textarea element that automatically wires up to the parent TextField context.
@@ -133,23 +289,81 @@ export interface TextAreaProps extends Omit<JSX.TextareaHTMLAttributes<HTMLTextA
133
289
  */
134
290
  export function TextArea(props: TextAreaProps): JSX.Element {
135
291
  const context = useContext(TextFieldContext);
292
+ let textAreaElement: HTMLTextAreaElement | undefined;
136
293
 
137
294
  // Merge context inputProps with local props (local props take precedence)
138
295
  // Note: TextArea uses inputProps from context since it's an input variant
139
296
  const mergedProps = () => {
140
297
  if (context) {
141
- const { ref: _ref, type: _type, ...contextInputProps } = context.inputProps as Record<string, unknown>;
142
- 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;
143
308
  }
144
- return props;
309
+ return props.class == null ? { ...props, class: "solidaria-TextArea" } : props;
145
310
  };
146
311
 
147
- return <textarea {...mergedProps()} />;
148
- }
312
+ onMount(() => {
313
+ const element = textAreaElement;
314
+ if (!element) {
315
+ return;
316
+ }
149
317
 
150
- // ============================================
151
- // COMPONENT
152
- // ============================================
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
+ });
353
+
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
+ }
153
367
 
154
368
  /**
155
369
  * A text field allows a user to enter a plain text value with a keyboard.
@@ -170,40 +384,72 @@ export function TextArea(props: TextAreaProps): JSX.Element {
170
384
  * ```
171
385
  */
172
386
  export function TextField(props: TextFieldProps): JSX.Element {
173
- // Split props
174
- const [local, ariaProps] = splitProps(props, [
175
- 'children',
176
- 'class',
177
- 'style',
178
- '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",
179
414
  ]);
180
415
 
181
- // Create text field state
182
416
  // Use getters to ensure props are read lazily inside reactive contexts
183
417
  const state = createTextFieldState({
184
- get value() { return ariaProps.value; },
185
- get defaultValue() { return ariaProps.defaultValue; },
186
- 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;
187
437
  });
188
438
 
189
- // Create text field aria props
190
439
  const textFieldAria = createTextField(() => ({
191
- ...ariaProps,
440
+ ...inputAriaProps(),
192
441
  value: state.value(),
193
442
  onChange: state.setValue,
194
443
  }));
195
444
 
196
- // Create focus ring
197
445
  const { isFocused, isFocusVisible, focusProps } = createFocusRing();
198
446
 
199
- // Create hover
200
447
  const { isHovered, hoverProps } = createHover({
201
448
  get isDisabled() {
202
449
  return ariaProps.isDisabled;
203
450
  },
204
451
  });
205
452
 
206
- // Render props values
207
453
  const renderValues = createMemo<TextFieldRenderProps>(() => ({
208
454
  isDisabled: ariaProps.isDisabled || false,
209
455
  isInvalid: textFieldAria.isInvalid,
@@ -214,58 +460,139 @@ export function TextField(props: TextFieldProps): JSX.Element {
214
460
  isFocusVisible: isFocusVisible(),
215
461
  }));
216
462
 
217
- // Resolve render props
218
463
  const renderProps = useRenderProps(
219
464
  {
220
- children: props.children,
465
+ children: local.children,
221
466
  class: local.class,
222
467
  style: local.style,
223
- defaultClassName: 'solidaria-TextField',
468
+ defaultClassName: "solidaria-TextField",
224
469
  },
225
- renderValues
470
+ renderValues,
226
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
+ };
227
495
 
228
- // Filter DOM props
229
496
  const domProps = createMemo(() => {
230
497
  const filtered = filterDOMProps(ariaProps, { global: true });
231
498
  delete (filtered as Record<string, unknown>).id;
232
499
  return filtered;
233
500
  });
234
501
 
235
- // Remove ref from spread props to avoid type conflicts
236
502
  const cleanHoverProps = () => {
237
503
  const { ref: _ref, ...rest } = hoverProps as Record<string, unknown>;
238
504
  return rest;
239
505
  };
240
506
 
241
- // Context value for sub-components
242
- // Note: We create the value object directly (not in a memo) so it's available
243
- // immediately when children access the context
507
+ // Context value for sub-components.
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
+ };
244
530
  const contextValue: TextFieldContextValue = {
245
- labelProps: textFieldAria.labelProps as JSX.LabelHTMLAttributes<HTMLLabelElement>,
246
- inputProps: { ...textFieldAria.inputProps, ...focusProps } as JSX.InputHTMLAttributes<HTMLInputElement>,
247
- descriptionProps: textFieldAria.descriptionProps as JSX.HTMLAttributes<HTMLElement>,
248
- errorMessageProps: textFieldAria.errorMessageProps as JSX.HTMLAttributes<HTMLElement>,
249
- isInvalid: textFieldAria.isInvalid,
531
+ get labelProps() {
532
+ return textFieldAria.labelProps as JSX.LabelHTMLAttributes<HTMLLabelElement>;
533
+ },
534
+ get inputProps() {
535
+ return mergeProps(
536
+ textFieldAria.inputProps as Record<string, unknown>,
537
+ focusProps as Record<string, unknown>,
538
+ ) as JSX.InputHTMLAttributes<HTMLInputElement>;
539
+ },
540
+ get descriptionProps() {
541
+ return textFieldAria.descriptionProps as JSX.HTMLAttributes<HTMLElement>;
542
+ },
543
+ get errorMessageProps() {
544
+ return textFieldAria.errorMessageProps as JSX.HTMLAttributes<HTMLElement>;
545
+ },
546
+ get isInvalid() {
547
+ return textFieldAria.isInvalid;
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
+ },
250
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>;
251
586
 
252
587
  return (
253
- <TextFieldContext.Provider value={contextValue}>
254
- <div
255
- {...domProps()}
256
- {...cleanHoverProps()}
257
- class={renderProps.class()}
258
- style={renderProps.style()}
259
- data-disabled={ariaProps.isDisabled || undefined}
260
- data-invalid={textFieldAria.isInvalid || undefined}
261
- data-readonly={ariaProps.isReadOnly || undefined}
262
- data-required={ariaProps.isRequired || undefined}
263
- data-hovered={isHovered() || undefined}
264
- data-focused={isFocused() || undefined}
265
- data-focus-visible={isFocusVisible() || undefined}
266
- >
267
- {renderProps.renderChildren()}
268
- </div>
269
- </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>
270
597
  );
271
598
  }