@proyecto-viviana/solid-stately 0.2.3 → 0.2.7

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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/dist/autocomplete/createAutocompleteState.d.ts +2 -1
  3. package/dist/checkbox/createCheckboxGroupState.d.ts +10 -1
  4. package/dist/collections/types.d.ts +11 -0
  5. package/dist/color/getColorChannels.d.ts +20 -0
  6. package/dist/data/createAsyncList.d.ts +111 -0
  7. package/dist/data/createListData.d.ts +65 -0
  8. package/dist/data/createTreeData.d.ts +61 -0
  9. package/dist/data/index.d.ts +3 -0
  10. package/dist/datepicker/index.d.ts +10 -0
  11. package/dist/grid/types.d.ts +5 -1
  12. package/dist/index.d.ts +6 -1
  13. package/dist/index.js +3737 -2697
  14. package/dist/index.js.map +1 -7
  15. package/dist/menu/index.d.ts +8 -0
  16. package/dist/radio/createRadioGroupState.d.ts +10 -1
  17. package/dist/select/createSelectState.d.ts +17 -0
  18. package/dist/selection/index.d.ts +11 -0
  19. package/dist/toast/createToastState.d.ts +7 -1
  20. package/dist/toggle/createToggleGroupState.d.ts +45 -0
  21. package/dist/toggle/index.d.ts +1 -0
  22. package/dist/tree/TreeCollection.d.ts +3 -2
  23. package/package.json +6 -5
  24. package/src/autocomplete/createAutocompleteState.ts +10 -11
  25. package/src/calendar/createDateFieldState.ts +24 -1
  26. package/src/checkbox/createCheckboxGroupState.ts +42 -6
  27. package/src/collections/ListCollection.ts +152 -146
  28. package/src/collections/createListState.ts +266 -264
  29. package/src/collections/createMenuState.ts +106 -106
  30. package/src/collections/createSelectionState.ts +336 -336
  31. package/src/collections/index.ts +46 -46
  32. package/src/collections/types.ts +181 -169
  33. package/src/color/Color.ts +951 -951
  34. package/src/color/createColorAreaState.ts +293 -293
  35. package/src/color/createColorFieldState.ts +292 -292
  36. package/src/color/createColorSliderState.ts +241 -241
  37. package/src/color/createColorWheelState.ts +211 -211
  38. package/src/color/getColorChannels.ts +34 -0
  39. package/src/color/index.ts +47 -47
  40. package/src/color/types.ts +127 -127
  41. package/src/combobox/createComboBoxState.ts +703 -703
  42. package/src/combobox/index.ts +13 -13
  43. package/src/data/createAsyncList.ts +377 -0
  44. package/src/data/createListData.ts +298 -0
  45. package/src/data/createTreeData.ts +433 -0
  46. package/src/data/index.ts +25 -0
  47. package/src/datepicker/index.ts +36 -0
  48. package/src/disclosure/createDisclosureState.ts +4 -4
  49. package/src/dnd/createDragState.ts +153 -153
  50. package/src/dnd/createDraggableCollectionState.ts +165 -165
  51. package/src/dnd/createDropState.ts +212 -212
  52. package/src/dnd/createDroppableCollectionState.ts +357 -357
  53. package/src/dnd/index.ts +76 -76
  54. package/src/dnd/types.ts +317 -317
  55. package/src/form/createFormValidationState.ts +389 -389
  56. package/src/form/index.ts +15 -15
  57. package/src/grid/types.ts +5 -0
  58. package/src/index.ts +49 -0
  59. package/src/menu/index.ts +19 -0
  60. package/src/numberfield/createNumberFieldState.ts +427 -383
  61. package/src/numberfield/index.ts +5 -5
  62. package/src/overlays/createOverlayTriggerState.ts +67 -67
  63. package/src/overlays/index.ts +5 -5
  64. package/src/radio/createRadioGroupState.ts +44 -6
  65. package/src/searchfield/createSearchFieldState.ts +62 -62
  66. package/src/searchfield/index.ts +5 -5
  67. package/src/select/createSelectState.ts +290 -181
  68. package/src/select/index.ts +5 -5
  69. package/src/selection/index.ts +28 -0
  70. package/src/slider/createSliderState.ts +211 -211
  71. package/src/slider/index.ts +6 -6
  72. package/src/tabs/createTabListState.ts +37 -11
  73. package/src/toast/createToastState.d.ts +6 -1
  74. package/src/toast/createToastState.ts +8 -1
  75. package/src/toggle/createToggleGroupState.ts +127 -0
  76. package/src/toggle/index.ts +6 -0
  77. package/src/tooltip/createTooltipTriggerState.ts +183 -183
  78. package/src/tooltip/index.ts +6 -6
  79. package/src/tree/TreeCollection.ts +208 -175
  80. package/src/tree/createTreeState.ts +392 -392
  81. package/src/tree/index.ts +13 -13
  82. package/src/tree/types.ts +174 -174
@@ -1,389 +1,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
- // ============================================
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
+ // ============================================
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
+ }