@proyecto-viviana/solid-stately 0.2.4 → 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 (243) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -283
  3. package/dist/autocomplete/createAutocompleteState.d.ts +3 -2
  4. package/dist/autocomplete/index.d.ts +1 -1
  5. package/dist/calendar/createCalendarState.d.ts +22 -7
  6. package/dist/calendar/createDateFieldState.d.ts +30 -7
  7. package/dist/calendar/createDatePickerState.d.ts +60 -0
  8. package/dist/calendar/createRangeCalendarState.d.ts +27 -8
  9. package/dist/calendar/createTimeFieldState.d.ts +34 -7
  10. package/dist/calendar/index.d.ts +7 -6
  11. package/dist/checkbox/createCheckboxGroupState.d.ts +12 -3
  12. package/dist/checkbox/index.d.ts +1 -1
  13. package/dist/collections/ListCollection.d.ts +1 -1
  14. package/dist/collections/createListState.d.ts +8 -8
  15. package/dist/collections/createMenuState.d.ts +7 -7
  16. package/dist/collections/createSelectionState.d.ts +5 -5
  17. package/dist/collections/index.d.ts +5 -5
  18. package/dist/collections/types.d.ts +23 -12
  19. package/dist/color/Color.d.ts +1 -1
  20. package/dist/color/createColorAreaState.d.ts +4 -2
  21. package/dist/color/createColorFieldState.d.ts +38 -12
  22. package/dist/color/createColorSliderState.d.ts +9 -2
  23. package/dist/color/createColorWheelState.d.ts +13 -2
  24. package/dist/color/getColorChannels.d.ts +20 -0
  25. package/dist/color/index.d.ts +6 -6
  26. package/dist/color/types.d.ts +4 -4
  27. package/dist/combobox/createComboBoxState.d.ts +27 -13
  28. package/dist/combobox/index.d.ts +1 -1
  29. package/dist/data/createAsyncList.d.ts +111 -0
  30. package/dist/data/createListData.d.ts +65 -0
  31. package/dist/data/createTreeData.d.ts +65 -0
  32. package/dist/data/index.d.ts +3 -0
  33. package/dist/datepicker/index.d.ts +8 -0
  34. package/dist/disclosure/createDisclosureState.d.ts +2 -2
  35. package/dist/disclosure/index.d.ts +1 -1
  36. package/dist/dnd/createDragState.d.ts +2 -2
  37. package/dist/dnd/createDraggableCollectionState.d.ts +2 -2
  38. package/dist/dnd/createDropState.d.ts +2 -2
  39. package/dist/dnd/createDroppableCollectionState.d.ts +3 -3
  40. package/dist/dnd/index.d.ts +5 -5
  41. package/dist/dnd/types.d.ts +16 -16
  42. package/dist/form/createFormValidationState.d.ts +3 -3
  43. package/dist/form/index.d.ts +1 -1
  44. package/dist/grid/createGridState.d.ts +2 -2
  45. package/dist/grid/index.d.ts +2 -2
  46. package/dist/grid/types.d.ts +17 -11
  47. package/dist/index.d.ts +31 -25
  48. package/dist/index.js +7990 -6264
  49. package/dist/index.js.map +1 -7
  50. package/dist/menu/index.d.ts +8 -0
  51. package/dist/numberfield/createNumberFieldState.d.ts +2 -2
  52. package/dist/numberfield/index.d.ts +1 -1
  53. package/dist/overlays/createOverlayTriggerState.d.ts +2 -2
  54. package/dist/overlays/index.d.ts +1 -1
  55. package/dist/radio/createRadioGroupState.d.ts +13 -4
  56. package/dist/radio/index.d.ts +1 -1
  57. package/dist/searchfield/createSearchFieldState.d.ts +2 -2
  58. package/dist/searchfield/index.d.ts +2 -2
  59. package/dist/select/createSelectState.d.ts +20 -3
  60. package/dist/select/index.d.ts +1 -1
  61. package/dist/selection/index.d.ts +11 -0
  62. package/dist/slider/createSliderState.d.ts +3 -3
  63. package/dist/slider/index.d.ts +2 -2
  64. package/dist/steplist/createStepListState.d.ts +48 -0
  65. package/dist/steplist/index.d.ts +1 -0
  66. package/dist/table/TableCollection.d.ts +3 -3
  67. package/dist/table/createTableColumnResizeState.d.ts +54 -0
  68. package/dist/table/createTableState.d.ts +2 -2
  69. package/dist/table/index.d.ts +5 -3
  70. package/dist/table/types.d.ts +10 -10
  71. package/dist/tabs/createTabListState.d.ts +5 -5
  72. package/dist/tabs/index.d.ts +1 -1
  73. package/dist/textfield/createTextFieldState.d.ts +2 -2
  74. package/dist/textfield/index.d.ts +1 -1
  75. package/dist/toast/createToastState.d.ts +15 -3
  76. package/dist/toast/index.d.ts +1 -1
  77. package/dist/toggle/createToggleGroupState.d.ts +45 -0
  78. package/dist/toggle/createToggleState.d.ts +2 -2
  79. package/dist/toggle/index.d.ts +2 -1
  80. package/dist/tooltip/createTooltipTriggerState.d.ts +2 -2
  81. package/dist/tooltip/index.d.ts +1 -1
  82. package/dist/tree/TreeCollection.d.ts +5 -4
  83. package/dist/tree/createTreeState.d.ts +2 -2
  84. package/dist/tree/index.d.ts +3 -3
  85. package/dist/tree/types.d.ts +19 -13
  86. package/dist/utils/index.d.ts +1 -1
  87. package/dist/utils/reactivity.d.ts +1 -1
  88. package/package.json +30 -30
  89. package/src/autocomplete/createAutocompleteState.d.ts +19 -17
  90. package/src/autocomplete/createAutocompleteState.ts +25 -32
  91. package/src/autocomplete/index.d.ts +6 -2
  92. package/src/autocomplete/index.ts +1 -1
  93. package/src/calendar/createCalendarState.d.ts +139 -117
  94. package/src/calendar/createCalendarState.ts +221 -74
  95. package/src/calendar/createDateFieldState.d.ts +130 -95
  96. package/src/calendar/createDateFieldState.ts +286 -151
  97. package/src/calendar/createDatePickerState.ts +347 -0
  98. package/src/calendar/createRangeCalendarState.d.ts +154 -129
  99. package/src/calendar/createRangeCalendarState.ts +293 -79
  100. package/src/calendar/createTimeFieldState.d.ts +81 -79
  101. package/src/calendar/createTimeFieldState.ts +187 -108
  102. package/src/calendar/index.d.ts +80 -7
  103. package/src/calendar/index.ts +21 -12
  104. package/src/checkbox/createCheckboxGroupState.d.ts +57 -55
  105. package/src/checkbox/createCheckboxGroupState.ts +57 -30
  106. package/src/checkbox/index.d.ts +6 -2
  107. package/src/checkbox/index.ts +1 -1
  108. package/src/collections/ListCollection.d.ts +25 -22
  109. package/src/collections/ListCollection.ts +152 -146
  110. package/src/collections/createListState.d.ts +73 -58
  111. package/src/collections/createListState.ts +267 -264
  112. package/src/collections/createMenuState.d.ts +36 -31
  113. package/src/collections/createMenuState.ts +112 -106
  114. package/src/collections/createSelectionState.d.ts +78 -65
  115. package/src/collections/createSelectionState.ts +340 -336
  116. package/src/collections/index.d.ts +38 -6
  117. package/src/collections/index.ts +41 -46
  118. package/src/collections/types.d.ts +101 -101
  119. package/src/collections/types.ts +181 -169
  120. package/src/color/Color.d.ts +20 -5
  121. package/src/color/Color.ts +1054 -951
  122. package/src/color/createColorAreaState.d.ts +67 -65
  123. package/src/color/createColorAreaState.ts +301 -293
  124. package/src/color/createColorFieldState.d.ts +46 -44
  125. package/src/color/createColorFieldState.ts +428 -292
  126. package/src/color/createColorSliderState.d.ts +58 -56
  127. package/src/color/createColorSliderState.ts +272 -241
  128. package/src/color/createColorWheelState.d.ts +42 -40
  129. package/src/color/createColorWheelState.ts +248 -211
  130. package/src/color/getColorChannels.ts +36 -0
  131. package/src/color/index.d.ts +36 -7
  132. package/src/color/index.ts +44 -47
  133. package/src/color/types.d.ts +68 -63
  134. package/src/color/types.ts +119 -127
  135. package/src/combobox/createComboBoxState.d.ts +107 -105
  136. package/src/combobox/createComboBoxState.ts +790 -703
  137. package/src/combobox/index.d.ts +10 -2
  138. package/src/combobox/index.ts +13 -13
  139. package/src/data/createAsyncList.ts +403 -0
  140. package/src/data/createListData.ts +298 -0
  141. package/src/data/createTreeData.ts +646 -0
  142. package/src/data/index.ts +25 -0
  143. package/src/datepicker/index.ts +32 -0
  144. package/src/disclosure/createDisclosureState.d.ts +47 -43
  145. package/src/disclosure/createDisclosureState.ts +9 -23
  146. package/src/disclosure/index.d.ts +10 -2
  147. package/src/disclosure/index.ts +1 -1
  148. package/src/dnd/createDragState.d.ts +51 -42
  149. package/src/dnd/createDragState.ts +151 -153
  150. package/src/dnd/createDraggableCollectionState.d.ts +52 -41
  151. package/src/dnd/createDraggableCollectionState.ts +158 -165
  152. package/src/dnd/createDropState.d.ts +64 -45
  153. package/src/dnd/createDropState.ts +202 -212
  154. package/src/dnd/createDroppableCollectionState.d.ts +88 -59
  155. package/src/dnd/createDroppableCollectionState.ts +354 -357
  156. package/src/dnd/index.d.ts +54 -6
  157. package/src/dnd/index.ts +63 -76
  158. package/src/dnd/types.d.ts +167 -156
  159. package/src/dnd/types.ts +316 -317
  160. package/src/form/createFormValidationState.d.ts +47 -45
  161. package/src/form/createFormValidationState.ts +356 -389
  162. package/src/form/index.d.ts +16 -2
  163. package/src/form/index.ts +15 -15
  164. package/src/grid/createGridState.d.ts +7 -4
  165. package/src/grid/createGridState.ts +44 -51
  166. package/src/grid/index.d.ts +3 -3
  167. package/src/grid/index.ts +2 -8
  168. package/src/grid/types.d.ts +136 -129
  169. package/src/grid/types.ts +24 -17
  170. package/src/index.d.ts +310 -26
  171. package/src/index.ts +87 -79
  172. package/src/menu/index.ts +17 -0
  173. package/src/numberfield/createNumberFieldState.d.ts +56 -54
  174. package/src/numberfield/createNumberFieldState.ts +422 -383
  175. package/src/numberfield/index.d.ts +6 -2
  176. package/src/numberfield/index.ts +5 -5
  177. package/src/overlays/createOverlayTriggerState.d.ts +22 -20
  178. package/src/overlays/createOverlayTriggerState.ts +66 -67
  179. package/src/overlays/index.d.ts +6 -2
  180. package/src/overlays/index.ts +5 -5
  181. package/src/radio/createRadioGroupState.d.ts +54 -52
  182. package/src/radio/createRadioGroupState.ts +66 -30
  183. package/src/radio/index.d.ts +7 -2
  184. package/src/radio/index.ts +1 -1
  185. package/src/searchfield/createSearchFieldState.d.ts +16 -14
  186. package/src/searchfield/createSearchFieldState.ts +56 -62
  187. package/src/searchfield/index.d.ts +3 -3
  188. package/src/searchfield/index.ts +2 -5
  189. package/src/select/createSelectState.d.ts +63 -61
  190. package/src/select/createSelectState.ts +290 -181
  191. package/src/select/index.d.ts +2 -2
  192. package/src/select/index.ts +1 -5
  193. package/src/selection/index.ts +27 -0
  194. package/src/slider/createSliderState.d.ts +60 -60
  195. package/src/slider/createSliderState.ts +233 -211
  196. package/src/slider/index.d.ts +3 -3
  197. package/src/slider/index.ts +2 -6
  198. package/src/ssr/index.d.ts +1 -1
  199. package/src/ssr/index.ts +2 -2
  200. package/src/steplist/createStepListState.ts +191 -0
  201. package/src/steplist/index.ts +5 -0
  202. package/src/table/TableCollection.d.ts +42 -40
  203. package/src/table/TableCollection.ts +61 -64
  204. package/src/table/createTableColumnResizeState.ts +242 -0
  205. package/src/table/createTableState.d.ts +7 -4
  206. package/src/table/createTableState.ts +10 -7
  207. package/src/table/index.d.ts +14 -4
  208. package/src/table/index.ts +10 -3
  209. package/src/table/types.d.ts +98 -92
  210. package/src/table/types.ts +17 -13
  211. package/src/tabs/createTabListState.d.ts +59 -57
  212. package/src/tabs/createTabListState.ts +58 -39
  213. package/src/tabs/index.d.ts +8 -2
  214. package/src/tabs/index.ts +1 -1
  215. package/src/textfield/createTextFieldState.d.ts +16 -14
  216. package/src/textfield/createTextFieldState.ts +7 -18
  217. package/src/textfield/index.d.ts +6 -2
  218. package/src/textfield/index.ts +1 -1
  219. package/src/toast/createToastState.d.ts +92 -81
  220. package/src/toast/createToastState.ts +56 -44
  221. package/src/toast/index.d.ts +12 -2
  222. package/src/toast/index.ts +1 -1
  223. package/src/toggle/createToggleGroupState.ts +117 -0
  224. package/src/toggle/createToggleState.d.ts +19 -19
  225. package/src/toggle/createToggleState.ts +2 -16
  226. package/src/toggle/index.d.ts +2 -2
  227. package/src/toggle/index.ts +6 -4
  228. package/src/tooltip/createTooltipTriggerState.d.ts +17 -17
  229. package/src/tooltip/createTooltipTriggerState.ts +184 -183
  230. package/src/tooltip/index.d.ts +7 -2
  231. package/src/tooltip/index.ts +6 -6
  232. package/src/tree/TreeCollection.d.ts +26 -23
  233. package/src/tree/TreeCollection.ts +215 -175
  234. package/src/tree/createTreeState.d.ts +7 -4
  235. package/src/tree/createTreeState.ts +390 -392
  236. package/src/tree/index.d.ts +10 -4
  237. package/src/tree/index.ts +13 -13
  238. package/src/tree/types.d.ts +131 -126
  239. package/src/tree/types.ts +183 -174
  240. package/src/utils/index.d.ts +2 -2
  241. package/src/utils/index.ts +1 -1
  242. package/src/utils/reactivity.d.ts +2 -2
  243. package/src/utils/reactivity.ts +3 -3
@@ -1,389 +1,356 @@
1
- /**
2
- * createFormValidationState for solid-stately
3
- *
4
- * Manages form validation state including realtime and displayed validation results.
5
- * Supports both ARIA (live) and native (on submit) validation behaviors.
6
- *
7
- * Port of react-stately's useFormValidationState.
8
- */
9
-
10
- import {
11
- type Accessor,
12
- createContext,
13
- createEffect,
14
- createMemo,
15
- createSignal,
16
- useContext,
17
- } from 'solid-js';
18
-
19
- // ============================================
20
- // TYPES
21
- // ============================================
22
-
23
- /** Standard HTML ValidityState interface. */
24
- export interface ValidityState {
25
- badInput: boolean;
26
- customError: boolean;
27
- patternMismatch: boolean;
28
- rangeOverflow: boolean;
29
- rangeUnderflow: boolean;
30
- stepMismatch: boolean;
31
- tooLong: boolean;
32
- tooShort: boolean;
33
- typeMismatch: boolean;
34
- valueMissing: boolean;
35
- valid: boolean;
36
- }
37
-
38
- /** Result of validation. */
39
- export interface ValidationResult {
40
- /** Whether the value is invalid. */
41
- isInvalid: boolean;
42
- /** Details about which validation constraints failed. */
43
- validationDetails: ValidityState;
44
- /** Error messages to display. */
45
- validationErrors: string[];
46
- }
47
-
48
- /** Map of field names to their validation errors. */
49
- export type ValidationErrors = Record<string, string | string[]>;
50
-
51
- /** Validation function that returns error messages or true/false. */
52
- export type ValidationFunction<T> = (value: T) => boolean | string | string[] | null | undefined;
53
-
54
- /** Validation behavior mode. */
55
- export type ValidationBehavior = 'aria' | 'native';
56
-
57
- export interface FormValidationProps<T> {
58
- /** Whether the value is invalid (controlled). */
59
- isInvalid?: boolean;
60
- /** @deprecated Use isInvalid instead. */
61
- validationState?: 'valid' | 'invalid';
62
- /** Custom validation function. */
63
- validate?: ValidationFunction<T>;
64
- /** Validation behavior: 'aria' for realtime, 'native' for on submit. */
65
- validationBehavior?: ValidationBehavior;
66
- /** Built-in validation result from native input. */
67
- builtinValidation?: ValidationResult;
68
- /** Field name(s) for server error lookup. */
69
- name?: string | string[];
70
- /** Current field value. */
71
- value: T | null;
72
- }
73
-
74
- export interface FormValidationState {
75
- /** Realtime validation results, updated as the user edits the value. */
76
- realtimeValidation: Accessor<ValidationResult>;
77
- /** Currently displayed validation results, updated when the user commits their changes. */
78
- displayValidation: Accessor<ValidationResult>;
79
- /** Updates the current validation result. Not displayed to the user until `commitValidation` is called. */
80
- updateValidation(result: ValidationResult): void;
81
- /** Resets the displayed validation state to valid when the user resets the form. */
82
- resetValidation(): void;
83
- /** Commits the realtime validation so it is displayed to the user. */
84
- commitValidation(): void;
85
- }
86
-
87
- // ============================================
88
- // CONSTANTS
89
- // ============================================
90
-
91
- /** A valid validity state. */
92
- export const VALID_VALIDITY_STATE: ValidityState = {
93
- badInput: false,
94
- customError: false,
95
- patternMismatch: false,
96
- rangeOverflow: false,
97
- rangeUnderflow: false,
98
- stepMismatch: false,
99
- tooLong: false,
100
- tooShort: false,
101
- typeMismatch: false,
102
- valueMissing: false,
103
- valid: true,
104
- };
105
-
106
- /** A custom error validity state. */
107
- const CUSTOM_VALIDITY_STATE: ValidityState = {
108
- ...VALID_VALIDITY_STATE,
109
- customError: true,
110
- valid: false,
111
- };
112
-
113
- /** Default validation result (valid). */
114
- export const DEFAULT_VALIDATION_RESULT: ValidationResult = {
115
- isInvalid: false,
116
- validationDetails: VALID_VALIDITY_STATE,
117
- validationErrors: [],
118
- };
119
-
120
- // ============================================
121
- // CONTEXT
122
- // ============================================
123
-
124
- /** Context for server-side validation errors. */
125
- export const FormValidationContext = createContext<ValidationErrors>({});
126
-
127
- /** Private prop key for passing validation state to children. */
128
- export const privateValidationStateProp = '__formValidationState' + Date.now();
129
-
130
- // ============================================
131
- // HELPERS
132
- // ============================================
133
-
134
- function asArray<T>(v: T | T[] | undefined): T[] {
135
- if (!v) {
136
- return [];
137
- }
138
- return Array.isArray(v) ? v : [v];
139
- }
140
-
141
- function runValidate<T>(validate: ValidationFunction<T>, value: T): string[] {
142
- if (typeof validate === 'function') {
143
- const e = validate(value);
144
- if (e && typeof e !== 'boolean') {
145
- return asArray(e);
146
- }
147
- }
148
- return [];
149
- }
150
-
151
- function getValidationResult(errors: string[]): ValidationResult | null {
152
- return errors.length
153
- ? {
154
- isInvalid: true,
155
- validationErrors: errors,
156
- validationDetails: CUSTOM_VALIDITY_STATE,
157
- }
158
- : null;
159
- }
160
-
161
- function isEqualValidation(
162
- a: ValidationResult | null,
163
- b: ValidationResult | null
164
- ): boolean {
165
- if (a === b) {
166
- return true;
167
- }
168
- return (
169
- !!a &&
170
- !!b &&
171
- a.isInvalid === b.isInvalid &&
172
- a.validationErrors.length === b.validationErrors.length &&
173
- a.validationErrors.every((ae, i) => ae === b.validationErrors[i]) &&
174
- Object.entries(a.validationDetails).every(
175
- ([k, v]) => b.validationDetails[k as keyof ValidityState] === v
176
- )
177
- );
178
- }
179
-
180
- // ============================================
181
- // HOOK
182
- // ============================================
183
-
184
- /**
185
- * Creates form validation state for a field.
186
- *
187
- * @example
188
- * ```tsx
189
- * const validationState = createFormValidationState({
190
- * value: inputValue(),
191
- * validate: (value) => {
192
- * if (!value) return 'This field is required';
193
- * if (value.length < 3) return 'Must be at least 3 characters';
194
- * return null;
195
- * },
196
- * validationBehavior: 'aria',
197
- * });
198
- *
199
- * // Access validation state
200
- * const isInvalid = () => validationState.displayValidation().isInvalid;
201
- * const errors = () => validationState.displayValidation().validationErrors;
202
- * ```
203
- */
204
- export function createFormValidationState<T>(
205
- props: FormValidationProps<T>
206
- ): FormValidationState {
207
- const {
208
- isInvalid,
209
- validationState,
210
- name,
211
- builtinValidation: builtinValidationProp,
212
- validate,
213
- validationBehavior = 'aria',
214
- } = props;
215
-
216
- // Backward compatibility
217
- const isInvalidProp = createMemo(
218
- () => isInvalid ?? (validationState === 'invalid' ? true : undefined)
219
- );
220
-
221
- // Controlled error from isInvalid prop
222
- const controlledError = createMemo<ValidationResult | null>(() =>
223
- isInvalidProp() !== undefined
224
- ? {
225
- isInvalid: isInvalidProp()!,
226
- validationErrors: [],
227
- validationDetails: CUSTOM_VALIDITY_STATE,
228
- }
229
- : null
230
- );
231
-
232
- // Client-side validation
233
- const clientError = createMemo<ValidationResult | null>(() => {
234
- if (!validate || props.value == null) {
235
- return null;
236
- }
237
- const validateErrors = runValidate(validate, props.value);
238
- return getValidationResult(validateErrors);
239
- });
240
-
241
- // Built-in validation (skip if valid)
242
- const builtinValidation = createMemo<ValidationResult | undefined>(() => {
243
- if (builtinValidationProp?.validationDetails.valid) {
244
- return undefined;
245
- }
246
- return builtinValidationProp;
247
- });
248
-
249
- // Server errors from context
250
- const serverErrors = useContext(FormValidationContext);
251
- const serverErrorMessages = createMemo(() => {
252
- if (name) {
253
- return Array.isArray(name)
254
- ? name.flatMap((n) => asArray(serverErrors[n]))
255
- : asArray(serverErrors[name]);
256
- }
257
- return [];
258
- });
259
-
260
- // Track server errors clearing
261
- const [lastServerErrors, setLastServerErrors] = createSignal(serverErrors);
262
- const [isServerErrorCleared, setServerErrorCleared] = createSignal(false);
263
-
264
- createEffect(() => {
265
- if (serverErrors !== lastServerErrors()) {
266
- setLastServerErrors(serverErrors);
267
- setServerErrorCleared(false);
268
- }
269
- });
270
-
271
- const serverError = createMemo<ValidationResult | null>(() =>
272
- getValidationResult(isServerErrorCleared() ? [] : serverErrorMessages())
273
- );
274
-
275
- // Track validation state
276
- const [currentValidity, setCurrentValidity] = createSignal(
277
- DEFAULT_VALIDATION_RESULT
278
- );
279
- const [commitQueued, setCommitQueued] = createSignal(false);
280
-
281
- let nextValidation = DEFAULT_VALIDATION_RESULT;
282
- let lastError = DEFAULT_VALIDATION_RESULT;
283
-
284
- // Commit validation effect
285
- createEffect(() => {
286
- if (!commitQueued()) {
287
- return;
288
- }
289
- setCommitQueued(false);
290
- const error = clientError() || builtinValidation() || nextValidation;
291
- if (!isEqualValidation(error, lastError)) {
292
- lastError = error;
293
- setCurrentValidity(error);
294
- }
295
- });
296
-
297
- // Realtime validation (for native input state)
298
- const realtimeValidation = createMemo<ValidationResult>(
299
- () =>
300
- controlledError() ||
301
- serverError() ||
302
- clientError() ||
303
- builtinValidation() ||
304
- DEFAULT_VALIDATION_RESULT
305
- );
306
-
307
- // Display validation (what the user sees)
308
- const displayValidation = createMemo<ValidationResult>(() => {
309
- if (validationBehavior === 'native') {
310
- return (
311
- controlledError() || serverError() || currentValidity()
312
- );
313
- }
314
- return (
315
- controlledError() ||
316
- serverError() ||
317
- clientError() ||
318
- builtinValidation() ||
319
- currentValidity()
320
- );
321
- });
322
-
323
- return {
324
- realtimeValidation,
325
- displayValidation,
326
- updateValidation(value: ValidationResult) {
327
- // If validationBehavior is 'aria', update in realtime. Otherwise, store until commit.
328
- if (
329
- validationBehavior === 'aria' &&
330
- !isEqualValidation(currentValidity(), value)
331
- ) {
332
- setCurrentValidity(value);
333
- } else {
334
- nextValidation = value;
335
- }
336
- },
337
- resetValidation() {
338
- // Update the currently displayed validation state to valid on form reset.
339
- const error = DEFAULT_VALIDATION_RESULT;
340
- if (!isEqualValidation(error, lastError)) {
341
- lastError = error;
342
- setCurrentValidity(error);
343
- }
344
- // Do not commit validation after the next render for native behavior.
345
- if (validationBehavior === 'native') {
346
- setCommitQueued(false);
347
- }
348
- setServerErrorCleared(true);
349
- },
350
- commitValidation() {
351
- // Commit validation state so the user sees it on blur/change/submit.
352
- if (validationBehavior === 'native') {
353
- setCommitQueued(true);
354
- }
355
- setServerErrorCleared(true);
356
- },
357
- };
358
- }
359
-
360
- /**
361
- * Merges multiple validation results into one.
362
- */
363
- export function mergeValidation(
364
- ...results: ValidationResult[]
365
- ): ValidationResult {
366
- const errors = new Set<string>();
367
- let isInvalid = false;
368
- const validationDetails: ValidityState = { ...VALID_VALIDITY_STATE };
369
-
370
- for (const v of results) {
371
- for (const e of v.validationErrors) {
372
- errors.add(e);
373
- }
374
- isInvalid = isInvalid || v.isInvalid;
375
- for (const key in validationDetails) {
376
- const k = key as keyof ValidityState;
377
- (validationDetails as unknown as Record<string, boolean>)[k] =
378
- (validationDetails as unknown as Record<string, boolean>)[k] ||
379
- (v.validationDetails as unknown as Record<string, boolean>)[k];
380
- }
381
- }
382
-
383
- validationDetails.valid = !isInvalid;
384
- return {
385
- isInvalid,
386
- validationErrors: [...errors],
387
- validationDetails,
388
- };
389
- }
1
+ /**
2
+ * createFormValidationState for solid-stately
3
+ *
4
+ * Manages form validation state including realtime and displayed validation results.
5
+ * Supports both ARIA (live) and native (on submit) validation behaviors.
6
+ *
7
+ * Port of react-stately's useFormValidationState.
8
+ */
9
+
10
+ import {
11
+ type Accessor,
12
+ createContext,
13
+ createEffect,
14
+ createMemo,
15
+ createSignal,
16
+ useContext,
17
+ } from "solid-js";
18
+
19
+ /** Standard HTML ValidityState interface. */
20
+ export interface ValidityState {
21
+ badInput: boolean;
22
+ customError: boolean;
23
+ patternMismatch: boolean;
24
+ rangeOverflow: boolean;
25
+ rangeUnderflow: boolean;
26
+ stepMismatch: boolean;
27
+ tooLong: boolean;
28
+ tooShort: boolean;
29
+ typeMismatch: boolean;
30
+ valueMissing: boolean;
31
+ valid: boolean;
32
+ }
33
+
34
+ /** Result of validation. */
35
+ export interface ValidationResult {
36
+ /** Whether the value is invalid. */
37
+ isInvalid: boolean;
38
+ /** Details about which validation constraints failed. */
39
+ validationDetails: ValidityState;
40
+ /** Error messages to display. */
41
+ validationErrors: string[];
42
+ }
43
+
44
+ /** Map of field names to their validation errors. */
45
+ export type ValidationErrors = Record<string, string | string[]>;
46
+
47
+ /** Validation function that returns error messages or true/false. */
48
+ export type ValidationFunction<T> = (value: T) => boolean | string | string[] | null | undefined;
49
+
50
+ /** Validation behavior mode. */
51
+ export type ValidationBehavior = "aria" | "native";
52
+
53
+ export interface FormValidationProps<T> {
54
+ /** Whether the value is invalid (controlled). */
55
+ isInvalid?: boolean;
56
+ /** @deprecated Use isInvalid instead. */
57
+ validationState?: "valid" | "invalid";
58
+ /** Custom validation function. */
59
+ validate?: ValidationFunction<T>;
60
+ /** Validation behavior: 'aria' for realtime, 'native' for on submit. */
61
+ validationBehavior?: ValidationBehavior;
62
+ /** Built-in validation result from native input. */
63
+ builtinValidation?: ValidationResult;
64
+ /** Field name(s) for server error lookup. */
65
+ name?: string | string[];
66
+ /** Current field value. */
67
+ value: T | null;
68
+ }
69
+
70
+ export interface FormValidationState {
71
+ /** Realtime validation results, updated as the user edits the value. */
72
+ realtimeValidation: Accessor<ValidationResult>;
73
+ /** Currently displayed validation results, updated when the user commits their changes. */
74
+ displayValidation: Accessor<ValidationResult>;
75
+ /** Updates the current validation result. Not displayed to the user until `commitValidation` is called. */
76
+ updateValidation(result: ValidationResult): void;
77
+ /** Resets the displayed validation state to valid when the user resets the form. */
78
+ resetValidation(): void;
79
+ /** Commits the realtime validation so it is displayed to the user. */
80
+ commitValidation(): void;
81
+ }
82
+
83
+ /** A valid validity state. */
84
+ export const VALID_VALIDITY_STATE: ValidityState = {
85
+ badInput: false,
86
+ customError: false,
87
+ patternMismatch: false,
88
+ rangeOverflow: false,
89
+ rangeUnderflow: false,
90
+ stepMismatch: false,
91
+ tooLong: false,
92
+ tooShort: false,
93
+ typeMismatch: false,
94
+ valueMissing: false,
95
+ valid: true,
96
+ };
97
+
98
+ /** A custom error validity state. */
99
+ const CUSTOM_VALIDITY_STATE: ValidityState = {
100
+ ...VALID_VALIDITY_STATE,
101
+ customError: true,
102
+ valid: false,
103
+ };
104
+
105
+ /** Default validation result (valid). */
106
+ export const DEFAULT_VALIDATION_RESULT: ValidationResult = {
107
+ isInvalid: false,
108
+ validationDetails: VALID_VALIDITY_STATE,
109
+ validationErrors: [],
110
+ };
111
+
112
+ /** Context for server-side validation errors. */
113
+ export const FormValidationContext = createContext<ValidationErrors>({});
114
+
115
+ /** Private prop key for passing validation state to children. */
116
+ export const privateValidationStateProp = "__formValidationState" + Date.now();
117
+
118
+ function asArray<T>(v: T | T[] | undefined): T[] {
119
+ if (!v) {
120
+ return [];
121
+ }
122
+ return Array.isArray(v) ? v : [v];
123
+ }
124
+
125
+ function runValidate<T>(validate: ValidationFunction<T>, value: T): string[] {
126
+ if (typeof validate === "function") {
127
+ const e = validate(value);
128
+ if (e && typeof e !== "boolean") {
129
+ return asArray(e);
130
+ }
131
+ }
132
+ return [];
133
+ }
134
+
135
+ function getValidationResult(errors: string[]): ValidationResult | null {
136
+ return errors.length
137
+ ? {
138
+ isInvalid: true,
139
+ validationErrors: errors,
140
+ validationDetails: CUSTOM_VALIDITY_STATE,
141
+ }
142
+ : null;
143
+ }
144
+
145
+ function isEqualValidation(a: ValidationResult | null, b: ValidationResult | null): boolean {
146
+ if (a === b) {
147
+ return true;
148
+ }
149
+ return (
150
+ !!a &&
151
+ !!b &&
152
+ a.isInvalid === b.isInvalid &&
153
+ a.validationErrors.length === b.validationErrors.length &&
154
+ a.validationErrors.every((ae, i) => ae === b.validationErrors[i]) &&
155
+ Object.entries(a.validationDetails).every(
156
+ ([k, v]) => b.validationDetails[k as keyof ValidityState] === v,
157
+ )
158
+ );
159
+ }
160
+
161
+ /**
162
+ * Creates form validation state for a field.
163
+ *
164
+ * @example
165
+ * ```tsx
166
+ * const validationState = createFormValidationState({
167
+ * value: inputValue(),
168
+ * validate: (value) => {
169
+ * if (!value) return 'This field is required';
170
+ * if (value.length < 3) return 'Must be at least 3 characters';
171
+ * return null;
172
+ * },
173
+ * validationBehavior: 'aria',
174
+ * });
175
+ *
176
+ * // Access validation state
177
+ * const isInvalid = () => validationState.displayValidation().isInvalid;
178
+ * const errors = () => validationState.displayValidation().validationErrors;
179
+ * ```
180
+ */
181
+ export function createFormValidationState<T>(props: FormValidationProps<T>): FormValidationState {
182
+ const validationBehavior = () => props.validationBehavior ?? "aria";
183
+
184
+ // Backward compatibility
185
+ const isInvalidProp = createMemo(
186
+ () => props.isInvalid ?? (props.validationState === "invalid" ? true : undefined),
187
+ );
188
+
189
+ // Controlled error from isInvalid prop
190
+ const controlledError = createMemo<ValidationResult | null>(() =>
191
+ isInvalidProp() !== undefined
192
+ ? {
193
+ isInvalid: isInvalidProp()!,
194
+ validationErrors: [],
195
+ validationDetails: CUSTOM_VALIDITY_STATE,
196
+ }
197
+ : null,
198
+ );
199
+
200
+ // Client-side validation
201
+ const clientError = createMemo<ValidationResult | null>(() => {
202
+ const validate = props.validate;
203
+ if (!validate || props.value == null) {
204
+ return null;
205
+ }
206
+ const validateErrors = runValidate(validate, props.value);
207
+ return getValidationResult(validateErrors);
208
+ });
209
+
210
+ // Built-in validation (skip if valid)
211
+ const builtinValidation = createMemo<ValidationResult | undefined>(() => {
212
+ const builtinValidationProp = props.builtinValidation;
213
+ if (builtinValidationProp?.validationDetails.valid) {
214
+ return undefined;
215
+ }
216
+ return builtinValidationProp;
217
+ });
218
+
219
+ // Server errors from context
220
+ const serverErrors = useContext(FormValidationContext);
221
+ const serverErrorMessages = createMemo(() => {
222
+ const name = props.name;
223
+ if (name) {
224
+ return Array.isArray(name)
225
+ ? name.flatMap((n) => asArray(serverErrors[n]))
226
+ : asArray(serverErrors[name]);
227
+ }
228
+ return [];
229
+ });
230
+
231
+ // Track server errors clearing
232
+ const [lastServerErrors, setLastServerErrors] = createSignal(serverErrors);
233
+ const [isServerErrorCleared, setServerErrorCleared] = createSignal(false);
234
+
235
+ createEffect(() => {
236
+ if (serverErrors !== lastServerErrors()) {
237
+ setLastServerErrors(serverErrors);
238
+ setServerErrorCleared(false);
239
+ }
240
+ });
241
+
242
+ const serverError = createMemo<ValidationResult | null>(() =>
243
+ getValidationResult(isServerErrorCleared() ? [] : serverErrorMessages()),
244
+ );
245
+
246
+ // Track validation state
247
+ const [currentValidity, setCurrentValidity] = createSignal(DEFAULT_VALIDATION_RESULT);
248
+ const [commitQueued, setCommitQueued] = createSignal(false);
249
+
250
+ let nextValidation = DEFAULT_VALIDATION_RESULT;
251
+ let lastError = DEFAULT_VALIDATION_RESULT;
252
+
253
+ // Commit validation effect
254
+ createEffect(() => {
255
+ if (!commitQueued()) {
256
+ return;
257
+ }
258
+ setCommitQueued(false);
259
+ const error = clientError() || builtinValidation() || nextValidation;
260
+ if (!isEqualValidation(error, lastError)) {
261
+ lastError = error;
262
+ setCurrentValidity(error);
263
+ }
264
+ });
265
+
266
+ // Realtime validation (for native input state)
267
+ const realtimeValidation = createMemo<ValidationResult>(
268
+ () =>
269
+ controlledError() ||
270
+ serverError() ||
271
+ clientError() ||
272
+ builtinValidation() ||
273
+ DEFAULT_VALIDATION_RESULT,
274
+ );
275
+
276
+ // Display validation (what the user sees)
277
+ const displayValidation = createMemo<ValidationResult>(() => {
278
+ if (validationBehavior() === "native") {
279
+ return controlledError() || serverError() || currentValidity();
280
+ }
281
+ return (
282
+ controlledError() ||
283
+ serverError() ||
284
+ clientError() ||
285
+ builtinValidation() ||
286
+ currentValidity()
287
+ );
288
+ });
289
+
290
+ return {
291
+ realtimeValidation,
292
+ displayValidation,
293
+ updateValidation(value: ValidationResult) {
294
+ // If validationBehavior is 'aria', update in realtime. Otherwise, store until commit.
295
+ if (validationBehavior() === "aria" && !isEqualValidation(currentValidity(), value)) {
296
+ setCurrentValidity(value);
297
+ } else {
298
+ nextValidation = value;
299
+ }
300
+ },
301
+ resetValidation() {
302
+ // Update the currently displayed validation state to valid on form reset.
303
+ const error = DEFAULT_VALIDATION_RESULT;
304
+ if (!isEqualValidation(error, lastError)) {
305
+ lastError = error;
306
+ setCurrentValidity(error);
307
+ }
308
+ // Do not commit validation after the next render for native behavior.
309
+ if (validationBehavior() === "native") {
310
+ setCommitQueued(false);
311
+ }
312
+ setServerErrorCleared(true);
313
+ },
314
+ commitValidation() {
315
+ // Commit validation state so the user sees it on blur/change/submit.
316
+ if (validationBehavior() === "native") {
317
+ const error = clientError() || builtinValidation() || nextValidation;
318
+ if (!isEqualValidation(error, lastError)) {
319
+ lastError = error;
320
+ setCurrentValidity(error);
321
+ }
322
+ setCommitQueued(false);
323
+ }
324
+ setServerErrorCleared(true);
325
+ },
326
+ };
327
+ }
328
+
329
+ /**
330
+ * Merges multiple validation results into one.
331
+ */
332
+ export function mergeValidation(...results: ValidationResult[]): ValidationResult {
333
+ const errors = new Set<string>();
334
+ let isInvalid = false;
335
+ const validationDetails: ValidityState = { ...VALID_VALIDITY_STATE };
336
+
337
+ for (const v of results) {
338
+ for (const e of v.validationErrors) {
339
+ errors.add(e);
340
+ }
341
+ isInvalid = isInvalid || v.isInvalid;
342
+ for (const key in validationDetails) {
343
+ const k = key as keyof ValidityState;
344
+ (validationDetails as unknown as Record<string, boolean>)[k] =
345
+ (validationDetails as unknown as Record<string, boolean>)[k] ||
346
+ (v.validationDetails as unknown as Record<string, boolean>)[k];
347
+ }
348
+ }
349
+
350
+ validationDetails.valid = !isInvalid;
351
+ return {
352
+ isInvalid,
353
+ validationErrors: [...errors],
354
+ validationDetails,
355
+ };
356
+ }