@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,21 +9,28 @@ import {
9
9
  type JSX,
10
10
  createContext,
11
11
  createMemo,
12
+ onCleanup,
13
+ onMount,
12
14
  splitProps,
13
15
  useContext,
14
16
  Show,
15
- } from 'solid-js';
17
+ } from "solid-js";
16
18
  import {
17
19
  createSearchField,
18
20
  createFocusRing,
19
21
  createHover,
20
22
  createPress,
23
+ mergeProps,
21
24
  type AriaSearchFieldProps,
22
- } from '@proyecto-viviana/solidaria';
25
+ } from "@proyecto-viviana/solidaria";
23
26
  import {
24
27
  createSearchFieldState,
28
+ VALID_VALIDITY_STATE,
25
29
  type SearchFieldState,
26
- } from '@proyecto-viviana/solid-stately';
30
+ type ValidationResult,
31
+ } from "@proyecto-viviana/solid-stately";
32
+ import { FormContext, type FormProps } from "./Form";
33
+ import { FieldErrorContext, type FieldErrorContextValue } from "./FieldError";
27
34
  import {
28
35
  type RenderChildren,
29
36
  type ClassNameOrFunction,
@@ -31,11 +38,7 @@ import {
31
38
  type SlotProps,
32
39
  useRenderProps,
33
40
  filterDOMProps,
34
- } from './utils';
35
-
36
- // ============================================
37
- // TYPES
38
- // ============================================
41
+ } from "./utils";
39
42
 
40
43
  export interface SearchFieldRenderProps {
41
44
  /** Whether the search field is empty. */
@@ -52,7 +55,7 @@ export interface SearchFieldRenderProps {
52
55
  value: string;
53
56
  }
54
57
 
55
- export interface SearchFieldProps extends Omit<AriaSearchFieldProps, 'label'>, SlotProps {
58
+ export interface SearchFieldProps extends Omit<AriaSearchFieldProps, "label">, SlotProps {
56
59
  /** The current value (controlled). */
57
60
  value?: string;
58
61
  /** The default value (uncontrolled). */
@@ -71,6 +74,8 @@ export interface SearchFieldProps extends Omit<AriaSearchFieldProps, 'label'>, S
71
74
  class?: ClassNameOrFunction<SearchFieldRenderProps>;
72
75
  /** The inline style for the element. */
73
76
  style?: StyleOrFunction<SearchFieldRenderProps>;
77
+ /** Ref for the outer search field element. */
78
+ ref?: (el: HTMLDivElement) => void;
74
79
  }
75
80
 
76
81
  export interface SearchFieldInputRenderProps {
@@ -86,7 +91,8 @@ export interface SearchFieldInputRenderProps {
86
91
  isInvalid: boolean;
87
92
  }
88
93
 
89
- export interface SearchFieldInputProps extends SlotProps {
94
+ export interface SearchFieldInputProps
95
+ extends SlotProps, Omit<JSX.InputHTMLAttributes<HTMLInputElement>, "class" | "style"> {
90
96
  /** The CSS className for the element. */
91
97
  class?: ClassNameOrFunction<SearchFieldInputRenderProps>;
92
98
  /** The inline style for the element. */
@@ -102,7 +108,10 @@ export interface SearchFieldClearButtonRenderProps {
102
108
  isDisabled: boolean;
103
109
  }
104
110
 
105
- export interface SearchFieldClearButtonProps extends SlotProps {
111
+ export interface SearchFieldClearButtonProps
112
+ extends
113
+ SlotProps,
114
+ Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, "class" | "style" | "children"> {
106
115
  /** The children of the button. */
107
116
  children?: RenderChildren<SearchFieldClearButtonRenderProps>;
108
117
  /** The CSS className for the element. */
@@ -111,49 +120,181 @@ export interface SearchFieldClearButtonProps extends SlotProps {
111
120
  style?: StyleOrFunction<SearchFieldClearButtonRenderProps>;
112
121
  }
113
122
 
114
- // ============================================
115
- // CONTEXT
116
- // ============================================
117
-
118
- interface SearchFieldContextValue {
119
- state: SearchFieldState;
120
- inputProps: JSX.InputHTMLAttributes<HTMLInputElement>;
121
- clearButtonProps: {
122
- 'aria-label': string;
123
+ interface SearchFieldContextValue extends Partial<SearchFieldProps> {
124
+ state?: SearchFieldState;
125
+ inputProps?: JSX.InputHTMLAttributes<HTMLInputElement>;
126
+ clearButtonProps?: {
127
+ "aria-label": string;
123
128
  tabIndex: number;
124
129
  disabled?: boolean;
125
130
  onMouseDown: (e: MouseEvent) => void;
126
131
  onClick: () => void;
127
132
  };
128
- labelProps: JSX.HTMLAttributes<HTMLElement>;
129
- descriptionProps: JSX.HTMLAttributes<HTMLElement>;
130
- errorMessageProps: JSX.HTMLAttributes<HTMLElement>;
131
- isDisabled: boolean;
132
- isInvalid: boolean;
133
- isRequired: boolean;
134
- isReadOnly: boolean;
135
- inputRef: HTMLInputElement | undefined;
136
- setInputRef: (el: HTMLInputElement) => void;
133
+ labelProps?: JSX.HTMLAttributes<HTMLElement>;
134
+ descriptionProps?: JSX.HTMLAttributes<HTMLElement>;
135
+ errorMessageProps?: JSX.HTMLAttributes<HTMLElement>;
136
+ isDisabled?: boolean;
137
+ isInvalid?: boolean;
138
+ isRequired?: boolean;
139
+ isReadOnly?: boolean;
140
+ setInputRef?: (el: HTMLInputElement) => void;
141
+ slots?: Record<string, SearchFieldProps>;
137
142
  }
138
143
 
139
144
  export const SearchFieldContext = createContext<SearchFieldContextValue | null>(null);
140
145
 
141
- // ============================================
142
- // COMPONENTS
143
- // ============================================
146
+ function withFormValidationBehavior(
147
+ props: SearchFieldProps,
148
+ formContext: FormProps | null,
149
+ ): SearchFieldProps {
150
+ if (!formContext?.validationBehavior) {
151
+ return props;
152
+ }
153
+
154
+ return new Proxy(props, {
155
+ get(target, property, receiver) {
156
+ const localValue = Reflect.get(target, property, receiver);
157
+ if (property === "validationBehavior" && localValue === undefined) {
158
+ return formContext.validationBehavior;
159
+ }
160
+
161
+ return localValue;
162
+ },
163
+ has(target, property) {
164
+ return (
165
+ Reflect.has(target, property) ||
166
+ (property === "validationBehavior" && formContext.validationBehavior !== undefined)
167
+ );
168
+ },
169
+ ownKeys(target) {
170
+ const keys = new Set(Reflect.ownKeys(target));
171
+ if (formContext.validationBehavior !== undefined) {
172
+ keys.add("validationBehavior");
173
+ }
174
+
175
+ return Array.from(keys);
176
+ },
177
+ getOwnPropertyDescriptor(target, property) {
178
+ const descriptor = Reflect.getOwnPropertyDescriptor(target, property);
179
+ if (descriptor) {
180
+ return descriptor;
181
+ }
182
+
183
+ if (property === "validationBehavior" && formContext.validationBehavior !== undefined) {
184
+ return {
185
+ enumerable: true,
186
+ configurable: true,
187
+ get: () => formContext.validationBehavior,
188
+ };
189
+ }
190
+
191
+ return undefined;
192
+ },
193
+ });
194
+ }
195
+
196
+ function eventWithCurrentTarget<T extends HTMLElement>(event: Event, element: T): Event {
197
+ return new Proxy(event, {
198
+ get(target, property, receiver) {
199
+ if (property === "target" || property === "currentTarget") {
200
+ return element;
201
+ }
202
+
203
+ const value = Reflect.get(target, property, receiver);
204
+ return typeof value === "function" ? value.bind(target) : value;
205
+ },
206
+ });
207
+ }
208
+
209
+ function clearDelegatedTextEntryHandlers(element: HTMLElement) {
210
+ const delegatedElement = element as HTMLElement & {
211
+ $$input?: unknown;
212
+ $$change?: unknown;
213
+ };
214
+ delete delegatedElement.$$input;
215
+ delete delegatedElement.$$change;
216
+ }
144
217
 
145
218
  /**
146
219
  * A search field allows a user to enter and clear a search query.
147
220
  */
148
221
  export function SearchField(props: SearchFieldProps): JSX.Element {
222
+ const formContext = useContext(FormContext);
223
+ const contextProps = useContext(SearchFieldContext);
224
+ const contextSlotProps = contextProps?.slots?.[props.slot ?? "default"];
225
+ const contextBaseProps = createMemo<SearchFieldProps>(() => {
226
+ if (!contextProps) return {};
227
+ const {
228
+ state: _state,
229
+ inputProps: _inputProps,
230
+ clearButtonProps: _clearButtonProps,
231
+ labelProps: _labelProps,
232
+ descriptionProps: _descriptionProps,
233
+ errorMessageProps: _errorMessageProps,
234
+ isDisabled: _isDisabled,
235
+ isInvalid: _isInvalid,
236
+ isRequired: _isRequired,
237
+ isReadOnly: _isReadOnly,
238
+ setInputRef: _setInputRef,
239
+ slots: _slots,
240
+ ...restContextProps
241
+ } = contextProps;
242
+ return restContextProps as SearchFieldProps;
243
+ });
244
+ const baseProps = (
245
+ contextProps ? mergeProps(contextBaseProps(), contextSlotProps ?? {}, props) : props
246
+ ) as SearchFieldProps;
247
+ const mergedProps = withFormValidationBehavior(baseProps, formContext);
248
+
149
249
  const [local, stateProps, ariaProps, rest] = splitProps(
150
- props,
151
- ['children', 'class', 'style', 'slot'],
152
- ['value', 'defaultValue', 'onChange', 'onSubmit', 'onClear'],
153
- ['label', 'aria-label', 'aria-labelledby', 'aria-describedby', 'isDisabled', 'isReadOnly', 'isRequired', 'isInvalid', 'description', 'errorMessage', 'id', 'autoFocus', 'name', 'placeholder', 'autoComplete', 'maxLength', 'minLength', 'pattern']
250
+ mergedProps,
251
+ ["children", "class", "style", "slot", "ref"],
252
+ ["value", "defaultValue", "onChange", "onSubmit", "onClear"],
253
+ [
254
+ "label",
255
+ "aria-label",
256
+ "aria-labelledby",
257
+ "aria-describedby",
258
+ "isDisabled",
259
+ "isReadOnly",
260
+ "isRequired",
261
+ "isInvalid",
262
+ "description",
263
+ "errorMessage",
264
+ "id",
265
+ "autoFocus",
266
+ "excludeFromTabOrder",
267
+ "name",
268
+ "form",
269
+ "validationBehavior",
270
+ "type",
271
+ "placeholder",
272
+ "autoComplete",
273
+ "inputMode",
274
+ "enterKeyHint",
275
+ "autoCorrect",
276
+ "autoCapitalize",
277
+ "spellCheck",
278
+ "maxLength",
279
+ "minLength",
280
+ "pattern",
281
+ "onFocus",
282
+ "onBlur",
283
+ "onFocusChange",
284
+ "onKeyDown",
285
+ "onKeyUp",
286
+ "onCopy",
287
+ "onCut",
288
+ "onPaste",
289
+ "onCompositionStart",
290
+ "onCompositionEnd",
291
+ "onCompositionUpdate",
292
+ "onSelect",
293
+ "onBeforeInput",
294
+ "onInput",
295
+ ],
154
296
  );
155
297
 
156
- // Create search field state
157
298
  const state = createSearchFieldState({
158
299
  get value() {
159
300
  return stateProps.value;
@@ -166,20 +307,12 @@ export function SearchField(props: SearchFieldProps): JSX.Element {
166
307
  },
167
308
  });
168
309
 
169
- // Ref for the input
170
310
  let inputRef: HTMLInputElement | undefined;
171
311
  const setInputRef = (el: HTMLInputElement) => {
172
312
  inputRef = el;
173
313
  };
174
314
 
175
- // Create search field aria props
176
- const {
177
- labelProps,
178
- inputProps,
179
- clearButtonProps,
180
- descriptionProps,
181
- errorMessageProps,
182
- } = createSearchField(
315
+ const searchFieldAria = createSearchField(
183
316
  {
184
317
  get isDisabled() {
185
318
  return ariaProps.isDisabled;
@@ -196,14 +329,14 @@ export function SearchField(props: SearchFieldProps): JSX.Element {
196
329
  get label() {
197
330
  return ariaProps.label;
198
331
  },
199
- get 'aria-label'() {
200
- return ariaProps['aria-label'];
332
+ get "aria-label"() {
333
+ return ariaProps["aria-label"];
201
334
  },
202
- get 'aria-labelledby'() {
203
- return ariaProps['aria-labelledby'];
335
+ get "aria-labelledby"() {
336
+ return ariaProps["aria-labelledby"];
204
337
  },
205
- get 'aria-describedby'() {
206
- return ariaProps['aria-describedby'];
338
+ get "aria-describedby"() {
339
+ return ariaProps["aria-describedby"];
207
340
  },
208
341
  get description() {
209
342
  return ariaProps.description;
@@ -217,12 +350,39 @@ export function SearchField(props: SearchFieldProps): JSX.Element {
217
350
  get name() {
218
351
  return ariaProps.name;
219
352
  },
353
+ get form() {
354
+ return ariaProps.form;
355
+ },
356
+ get validationBehavior() {
357
+ return ariaProps.validationBehavior;
358
+ },
359
+ get type() {
360
+ return ariaProps.type;
361
+ },
220
362
  get autoFocus() {
221
363
  return ariaProps.autoFocus;
222
364
  },
365
+ get excludeFromTabOrder() {
366
+ return ariaProps.excludeFromTabOrder;
367
+ },
223
368
  get autoComplete() {
224
369
  return ariaProps.autoComplete;
225
370
  },
371
+ get inputMode() {
372
+ return ariaProps.inputMode;
373
+ },
374
+ get enterKeyHint() {
375
+ return ariaProps.enterKeyHint;
376
+ },
377
+ get autoCorrect() {
378
+ return ariaProps.autoCorrect;
379
+ },
380
+ get autoCapitalize() {
381
+ return ariaProps.autoCapitalize;
382
+ },
383
+ get spellCheck() {
384
+ return ariaProps.spellCheck;
385
+ },
226
386
  get maxLength() {
227
387
  return ariaProps.maxLength;
228
388
  },
@@ -232,6 +392,48 @@ export function SearchField(props: SearchFieldProps): JSX.Element {
232
392
  get pattern() {
233
393
  return ariaProps.pattern;
234
394
  },
395
+ get onFocus() {
396
+ return ariaProps.onFocus;
397
+ },
398
+ get onBlur() {
399
+ return ariaProps.onBlur;
400
+ },
401
+ get onFocusChange() {
402
+ return ariaProps.onFocusChange;
403
+ },
404
+ get onKeyDown() {
405
+ return ariaProps.onKeyDown;
406
+ },
407
+ get onKeyUp() {
408
+ return ariaProps.onKeyUp;
409
+ },
410
+ get onCopy() {
411
+ return ariaProps.onCopy;
412
+ },
413
+ get onCut() {
414
+ return ariaProps.onCut;
415
+ },
416
+ get onPaste() {
417
+ return ariaProps.onPaste;
418
+ },
419
+ get onCompositionStart() {
420
+ return ariaProps.onCompositionStart;
421
+ },
422
+ get onCompositionEnd() {
423
+ return ariaProps.onCompositionEnd;
424
+ },
425
+ get onCompositionUpdate() {
426
+ return ariaProps.onCompositionUpdate;
427
+ },
428
+ get onSelect() {
429
+ return ariaProps.onSelect;
430
+ },
431
+ get onBeforeInput() {
432
+ return ariaProps.onBeforeInput;
433
+ },
434
+ get onInput() {
435
+ return ariaProps.onInput;
436
+ },
235
437
  get onSubmit() {
236
438
  return stateProps.onSubmit;
237
439
  },
@@ -240,83 +442,156 @@ export function SearchField(props: SearchFieldProps): JSX.Element {
240
442
  },
241
443
  },
242
444
  state,
243
- () => inputRef ?? null
445
+ () => inputRef ?? null,
244
446
  );
245
447
 
246
- // Render props values
247
448
  const renderValues = createMemo<SearchFieldRenderProps>(() => ({
248
- isEmpty: state.value() === '',
449
+ isEmpty: state.value() === "",
249
450
  isDisabled: ariaProps.isDisabled ?? false,
250
- isInvalid: ariaProps.isInvalid ?? false,
451
+ isInvalid: searchFieldAria.isInvalid ?? false,
251
452
  isRequired: ariaProps.isRequired ?? false,
252
453
  isReadOnly: ariaProps.isReadOnly ?? false,
253
454
  value: state.value(),
254
455
  }));
255
456
 
256
- // Resolve render props
257
457
  const renderProps = useRenderProps(
258
458
  {
259
- children: props.children,
459
+ children: local.children,
260
460
  class: local.class,
261
461
  style: local.style,
262
- defaultClassName: 'solidaria-SearchField',
462
+ defaultClassName: "solidaria-SearchField",
263
463
  },
264
- renderValues
464
+ renderValues,
265
465
  );
466
+ const childRenderValues: SearchFieldRenderProps = {
467
+ get isEmpty() {
468
+ return state.value() === "";
469
+ },
470
+ get isDisabled() {
471
+ return ariaProps.isDisabled ?? false;
472
+ },
473
+ get isInvalid() {
474
+ return searchFieldAria.isInvalid ?? false;
475
+ },
476
+ get isRequired() {
477
+ return ariaProps.isRequired ?? false;
478
+ },
479
+ get isReadOnly() {
480
+ return ariaProps.isReadOnly ?? false;
481
+ },
482
+ get value() {
483
+ return state.value();
484
+ },
485
+ };
266
486
 
267
- // Filter DOM props
268
- const domProps = createMemo(() => filterDOMProps(rest as Record<string, unknown>, { global: true }));
487
+ const domProps = createMemo(() =>
488
+ filterDOMProps(rest as Record<string, unknown>, { global: true }),
489
+ );
490
+
491
+ const fieldValidation = createMemo<ValidationResult>(() => {
492
+ const isInvalid = searchFieldAria.isInvalid;
493
+ const errorMessage = ariaProps.errorMessage;
494
+ const validationErrors = isInvalid && typeof errorMessage === "string" ? [errorMessage] : [];
495
+
496
+ return {
497
+ isInvalid,
498
+ validationErrors,
499
+ validationDetails: isInvalid
500
+ ? { ...VALID_VALIDITY_STATE, customError: true, valid: false }
501
+ : VALID_VALIDITY_STATE,
502
+ };
503
+ });
504
+ const fieldErrorContext: FieldErrorContextValue = {
505
+ get validation() {
506
+ return fieldValidation();
507
+ },
508
+ get errorMessageProps() {
509
+ return searchFieldAria.errorMessageProps;
510
+ },
511
+ };
512
+ const contextValue: SearchFieldContextValue = {
513
+ state,
514
+ get inputProps() {
515
+ return searchFieldAria.inputProps;
516
+ },
517
+ get clearButtonProps() {
518
+ return searchFieldAria.clearButtonProps;
519
+ },
520
+ get labelProps() {
521
+ return searchFieldAria.labelProps as JSX.HTMLAttributes<HTMLElement>;
522
+ },
523
+ get descriptionProps() {
524
+ return searchFieldAria.descriptionProps;
525
+ },
526
+ get errorMessageProps() {
527
+ return searchFieldAria.errorMessageProps;
528
+ },
529
+ get isDisabled() {
530
+ return ariaProps.isDisabled ?? false;
531
+ },
532
+ get isInvalid() {
533
+ return searchFieldAria.isInvalid ?? false;
534
+ },
535
+ get isRequired() {
536
+ return ariaProps.isRequired ?? false;
537
+ },
538
+ get isReadOnly() {
539
+ return ariaProps.isReadOnly ?? false;
540
+ },
541
+ setInputRef,
542
+ };
543
+ const fieldChildren = () => {
544
+ const children = local.children;
545
+ return typeof children === "function" ? children(childRenderValues) : children;
546
+ };
269
547
 
270
548
  return (
271
- <SearchFieldContext.Provider
272
- value={{
273
- state,
274
- inputProps,
275
- clearButtonProps,
276
- labelProps: labelProps as JSX.HTMLAttributes<HTMLElement>,
277
- descriptionProps,
278
- errorMessageProps,
279
- isDisabled: ariaProps.isDisabled ?? false,
280
- isInvalid: ariaProps.isInvalid ?? false,
281
- isRequired: ariaProps.isRequired ?? false,
282
- isReadOnly: ariaProps.isReadOnly ?? false,
283
- inputRef,
284
- setInputRef,
285
- }}
286
- >
287
- <div
288
- {...domProps()}
289
- class={renderProps.class()}
290
- style={renderProps.style()}
291
- data-empty={state.value() === '' || undefined}
292
- data-disabled={ariaProps.isDisabled || undefined}
293
- data-invalid={ariaProps.isInvalid || undefined}
294
- data-required={ariaProps.isRequired || undefined}
295
- data-readonly={ariaProps.isReadOnly || undefined}
296
- >
297
- {renderProps.renderChildren()}
298
- </div>
299
- </SearchFieldContext.Provider>
549
+ <FieldErrorContext.Provider value={fieldErrorContext}>
550
+ <SearchFieldContext.Provider value={contextValue}>
551
+ <div
552
+ {...domProps()}
553
+ ref={local.ref}
554
+ class={renderProps.class()}
555
+ style={renderProps.style()}
556
+ data-empty={state.value() === "" || undefined}
557
+ data-disabled={ariaProps.isDisabled || undefined}
558
+ data-invalid={searchFieldAria.isInvalid || undefined}
559
+ data-required={ariaProps.isRequired || undefined}
560
+ data-readonly={ariaProps.isReadOnly || undefined}
561
+ >
562
+ {fieldChildren()}
563
+ </div>
564
+ </SearchFieldContext.Provider>
565
+ </FieldErrorContext.Provider>
300
566
  );
301
567
  }
302
568
 
303
569
  /**
304
570
  * The label for a search field.
305
571
  */
306
- export function SearchFieldLabel(props: { children?: JSX.Element; class?: string; style?: JSX.CSSProperties }): JSX.Element {
572
+ export function SearchFieldLabel(props: {
573
+ children?: JSX.Element;
574
+ class?: string;
575
+ style?: JSX.CSSProperties;
576
+ }): JSX.Element {
307
577
  const context = useContext(SearchFieldContext);
308
578
  if (!context) {
309
- throw new Error('SearchFieldLabel must be used within a SearchField');
579
+ throw new Error("SearchFieldLabel must be used within a SearchField");
310
580
  }
311
581
 
582
+ const cleanLabelProps = () => {
583
+ const { ref: _ref, ...rest } = context.labelProps as Record<string, unknown>;
584
+ return rest;
585
+ };
586
+
312
587
  return (
313
- <span
314
- {...context.labelProps}
315
- class={props.class ?? 'solidaria-SearchField-label'}
588
+ <label
589
+ {...cleanLabelProps()}
590
+ class={props.class ?? "solidaria-SearchField-label"}
316
591
  style={props.style}
317
592
  >
318
593
  {props.children}
319
- </span>
594
+ </label>
320
595
  );
321
596
  }
322
597
 
@@ -324,43 +599,39 @@ export function SearchFieldLabel(props: { children?: JSX.Element; class?: string
324
599
  * The input element for a search field.
325
600
  */
326
601
  export function SearchFieldInput(props: SearchFieldInputProps): JSX.Element {
327
- const [local] = splitProps(props, ['class', 'style', 'slot']);
602
+ const [local, domProps] = splitProps(props, ["class", "style", "slot"]);
603
+ let inputElement: HTMLInputElement | undefined;
328
604
 
329
605
  const context = useContext(SearchFieldContext);
330
606
  if (!context) {
331
- throw new Error('SearchFieldInput must be used within a SearchField');
607
+ throw new Error("SearchFieldInput must be used within a SearchField");
332
608
  }
333
609
 
334
- // Create focus ring
335
610
  const { isFocused, isFocusVisible, focusProps } = createFocusRing();
336
611
 
337
- // Create hover
338
612
  const { isHovered, hoverProps } = createHover({
339
613
  get isDisabled() {
340
614
  return context.isDisabled;
341
615
  },
342
616
  });
343
617
 
344
- // Render props values
345
618
  const renderValues = createMemo<SearchFieldInputRenderProps>(() => ({
346
619
  isFocused: isFocused(),
347
620
  isFocusVisible: isFocusVisible(),
348
621
  isHovered: isHovered(),
349
- isDisabled: context.isDisabled,
350
- isInvalid: context.isInvalid,
622
+ isDisabled: !!context.isDisabled,
623
+ isInvalid: !!context.isInvalid,
351
624
  }));
352
625
 
353
- // Resolve render props
354
626
  const renderProps = useRenderProps(
355
627
  {
356
628
  class: local.class,
357
629
  style: local.style,
358
- defaultClassName: 'solidaria-SearchField-input',
630
+ defaultClassName: "solidaria-SearchField-input",
359
631
  },
360
- renderValues
632
+ renderValues,
361
633
  );
362
634
 
363
- // Remove ref from spread props
364
635
  const cleanInputProps = () => {
365
636
  const { ref: _ref, ...rest } = context.inputProps as Record<string, unknown>;
366
637
  return rest;
@@ -374,12 +645,67 @@ export function SearchFieldInput(props: SearchFieldInputProps): JSX.Element {
374
645
  return rest;
375
646
  };
376
647
 
648
+ const mergedInputProps = () =>
649
+ ({
650
+ ...domProps,
651
+ ...cleanInputProps(),
652
+ ...cleanFocusProps(),
653
+ ...cleanHoverProps(),
654
+ }) as Record<string, unknown>;
655
+
656
+ onMount(() => {
657
+ const element = inputElement;
658
+ if (!element) {
659
+ return;
660
+ }
661
+
662
+ const inputHandler = (event: Event) => {
663
+ const handler = mergedInputProps().onInput as
664
+ | JSX.EventHandler<HTMLInputElement, InputEvent>
665
+ | undefined;
666
+ handler?.(
667
+ eventWithCurrentTarget(event, element) as InputEvent & {
668
+ currentTarget: HTMLInputElement;
669
+ target: Element;
670
+ },
671
+ );
672
+ clearDelegatedTextEntryHandlers(element);
673
+ event.stopPropagation();
674
+ };
675
+ const changeHandler = (event: Event) => {
676
+ const handler = mergedInputProps().onChange as
677
+ | JSX.EventHandler<HTMLInputElement, Event>
678
+ | undefined;
679
+ handler?.(
680
+ eventWithCurrentTarget(event, element) as Event & {
681
+ currentTarget: HTMLInputElement;
682
+ target: Element;
683
+ },
684
+ );
685
+ clearDelegatedTextEntryHandlers(element);
686
+ event.stopPropagation();
687
+ };
688
+
689
+ element.addEventListener("input", inputHandler);
690
+ element.addEventListener("change", changeHandler);
691
+ clearDelegatedTextEntryHandlers(element);
692
+ onCleanup(() => {
693
+ element.removeEventListener("input", inputHandler);
694
+ element.removeEventListener("change", changeHandler);
695
+ });
696
+ });
697
+
377
698
  return (
378
699
  <input
379
- ref={context.setInputRef}
380
- {...cleanInputProps()}
381
- {...cleanFocusProps()}
382
- {...cleanHoverProps()}
700
+ {...mergedInputProps()}
701
+ ref={(element) => {
702
+ inputElement = element;
703
+ context.setInputRef?.(element);
704
+ const ref = (domProps as { ref?: unknown }).ref;
705
+ if (typeof ref === "function") {
706
+ ref(element);
707
+ }
708
+ }}
383
709
  class={renderProps.class()}
384
710
  style={renderProps.style()}
385
711
  data-focused={isFocused() || undefined}
@@ -395,52 +721,51 @@ export function SearchFieldInput(props: SearchFieldInputProps): JSX.Element {
395
721
  * The clear button for a search field.
396
722
  */
397
723
  export function SearchFieldClearButton(props: SearchFieldClearButtonProps): JSX.Element {
398
- const [local] = splitProps(props, ['class', 'style', 'slot']);
724
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "children"]);
399
725
 
400
726
  const context = useContext(SearchFieldContext);
401
727
  if (!context) {
402
- throw new Error('SearchFieldClearButton must be used within a SearchField');
728
+ throw new Error("SearchFieldClearButton must be used within a SearchField");
403
729
  }
404
730
 
405
- // Create press
731
+ const isDisabled = () => !!(context.isDisabled || context.isReadOnly);
732
+ const isEmpty = () => (context.state?.value() ?? "") === "";
733
+ const clear = () => {
734
+ if (!isDisabled() && !isEmpty()) {
735
+ context.clearButtonProps?.onClick();
736
+ }
737
+ };
738
+
406
739
  const { isPressed, pressProps } = createPress({
407
740
  get isDisabled() {
408
741
  return context.isDisabled || context.isReadOnly;
409
742
  },
410
- onPress: () => {
411
- context.clearButtonProps.onClick();
412
- },
743
+ onClick: clear,
744
+ onPress: clear,
413
745
  });
414
746
 
415
- // Create hover
416
747
  const { isHovered, hoverProps } = createHover({
417
748
  get isDisabled() {
418
749
  return context.isDisabled || context.isReadOnly;
419
750
  },
420
751
  });
421
752
 
422
- const isDisabled = () => context.isDisabled || context.isReadOnly;
423
- const isEmpty = () => context.state.value() === '';
424
-
425
- // Render props values
426
753
  const renderValues = createMemo<SearchFieldClearButtonRenderProps>(() => ({
427
754
  isPressed: isPressed(),
428
755
  isHovered: isHovered(),
429
756
  isDisabled: isDisabled(),
430
757
  }));
431
758
 
432
- // Resolve render props
433
759
  const renderProps = useRenderProps(
434
760
  {
435
761
  children: props.children,
436
762
  class: local.class,
437
763
  style: local.style,
438
- defaultClassName: 'solidaria-SearchField-clear',
764
+ defaultClassName: "solidaria-SearchField-clear",
439
765
  },
440
- renderValues
766
+ renderValues,
441
767
  );
442
768
 
443
- // Remove ref from spread props
444
769
  const cleanPressProps = () => {
445
770
  const { ref: _ref, ...rest } = pressProps as Record<string, unknown>;
446
771
  return rest;
@@ -450,16 +775,19 @@ export function SearchFieldClearButton(props: SearchFieldClearButtonProps): JSX.
450
775
  return rest;
451
776
  };
452
777
 
453
- // Only show clear button when there's a value
454
778
  return (
455
779
  <Show when={!isEmpty()}>
456
780
  <button
781
+ {...domProps}
457
782
  type="button"
458
- aria-label={context.clearButtonProps['aria-label']}
459
- tabIndex={context.clearButtonProps.tabIndex}
460
- disabled={context.clearButtonProps.disabled}
461
- onMouseDown={context.clearButtonProps.onMouseDown}
783
+ aria-label={context.clearButtonProps?.["aria-label"] ?? "Clear search"}
784
+ tabIndex={context.clearButtonProps?.tabIndex ?? -1}
785
+ disabled={context.clearButtonProps?.disabled}
786
+ onMouseDown={context.clearButtonProps?.onMouseDown}
462
787
  {...cleanPressProps()}
788
+ onPointerUp={clear}
789
+ onMouseUp={clear}
790
+ onClick={clear}
463
791
  {...cleanHoverProps()}
464
792
  class={renderProps.class()}
465
793
  style={renderProps.style()}
@@ -473,7 +801,6 @@ export function SearchFieldClearButton(props: SearchFieldClearButtonProps): JSX.
473
801
  );
474
802
  }
475
803
 
476
- // Attach sub-components
477
804
  SearchField.Label = SearchFieldLabel;
478
805
  SearchField.Input = SearchFieldInput;
479
806
  SearchField.ClearButton = SearchFieldClearButton;