@zeno-lib/forms 0.2.0 → 0.2.2
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.
- package/dist/create-zeno-form.d.mts +117 -0
- package/dist/create-zeno-form.mjs +121 -0
- package/dist/form-element.d.mts +13 -0
- package/dist/form-element.mjs +31 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.mjs +10 -0
- package/dist/lib/action-result.d.mts +22 -0
- package/dist/lib/action-result.mjs +32 -0
- package/dist/lib/apply-validation-error.d.mts +6 -0
- package/dist/lib/apply-validation-error.mjs +41 -0
- package/dist/lib/aria.d.mts +3 -0
- package/dist/lib/aria.mjs +7 -0
- package/dist/lib/contexts.d.mts +5 -0
- package/dist/lib/contexts.mjs +7 -0
- package/dist/lib/formatted-number.d.mts +56 -0
- package/dist/lib/formatted-number.mjs +138 -0
- package/dist/lib/schema-defaults.d.mts +17 -0
- package/dist/lib/schema-defaults.mjs +75 -0
- package/dist/lib/schema-required.d.mts +25 -0
- package/dist/lib/schema-required.mjs +72 -0
- package/dist/lib/submit-action.d.mts +64 -0
- package/dist/lib/submit-action.mjs +43 -0
- package/dist/lib/use-form-dialog.d.mts +73 -0
- package/dist/lib/use-form-dialog.mjs +90 -0
- package/dist/lib/use-formatted-number.d.mts +61 -0
- package/dist/lib/use-formatted-number.mjs +104 -0
- package/dist/lib/use-is-invalid.d.mts +7 -0
- package/dist/lib/use-is-invalid.mjs +31 -0
- package/dist/lib/use-rebased-default-values.d.mts +10 -0
- package/dist/lib/use-rebased-default-values.mjs +21 -0
- package/dist/lib/use-unsaved-changes-warning.d.mts +6 -0
- package/dist/lib/use-unsaved-changes-warning.mjs +24 -0
- package/dist/lib/validation-error.d.mts +12 -0
- package/dist/lib/validation-error.mjs +11 -0
- package/dist/lib/validation-logic.d.mts +5 -0
- package/dist/lib/validation-logic.mjs +114 -0
- package/dist/lib/validation-modes.d.mts +16 -0
- package/dist/lib/validation-modes.mjs +38 -0
- package/dist/tanstack.d.mts +1 -0
- package/dist/tanstack.mjs +2 -0
- package/package.json +42 -25
- package/src/fields/combobox-field.tsx +2 -2
- package/src/fields/date-picker-field.tsx +1 -1
- package/src/fields/number-field.tsx +1 -1
- package/src/fields/slider-field.tsx +7 -5
- package/src/form-dialog.tsx +2 -2
- package/src/lib/formatted-number.ts +5 -5
- package/src/lib/schema-defaults.ts +1 -1
- package/src/lib/schema-required.test.ts +1 -1
- package/src/lib/schema-required.ts +3 -5
- package/src/lib/validation-logic.test.ts +0 -1
- package/src/slider-field.test.tsx +30 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { evaluate } from "@tanstack/react-form";
|
|
3
|
+
import { useCallback, useRef } from "react";
|
|
4
|
+
//#region src/lib/use-rebased-default-values.ts
|
|
5
|
+
function useRebasedDefaultValues(callerDefaults) {
|
|
6
|
+
const liveForm = useRef(null);
|
|
7
|
+
const lastCallerDefaults = useRef(callerDefaults);
|
|
8
|
+
let defaultValues = callerDefaults;
|
|
9
|
+
if (evaluate(lastCallerDefaults.current, callerDefaults)) {
|
|
10
|
+
if (liveForm.current) defaultValues = liveForm.current.options.defaultValues;
|
|
11
|
+
} else lastCallerDefaults.current = callerDefaults;
|
|
12
|
+
const onMount = useCallback((props) => {
|
|
13
|
+
liveForm.current = props.formApi;
|
|
14
|
+
}, []);
|
|
15
|
+
return {
|
|
16
|
+
defaultValues,
|
|
17
|
+
onMount
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { useRebasedDefaultValues };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AnyFormApi } from "@tanstack/react-form";
|
|
2
|
+
//#region src/lib/use-unsaved-changes-warning.d.ts
|
|
3
|
+
type UnsavedChangesMode = "if-changed" | "if-touched";
|
|
4
|
+
declare function useUnsavedChangesWarning(form: AnyFormApi, enabled: boolean | UnsavedChangesMode): void;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { type UnsavedChangesMode, useUnsavedChangesWarning };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useSelector } from "@tanstack/react-form";
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
//#region src/lib/use-unsaved-changes-warning.ts
|
|
5
|
+
function useUnsavedChangesWarning(form, enabled) {
|
|
6
|
+
let mode;
|
|
7
|
+
if (enabled === false) mode = null;
|
|
8
|
+
else if (enabled === true) mode = "if-changed";
|
|
9
|
+
else mode = enabled;
|
|
10
|
+
const dirty = useSelector(form.store, (state) => mode === "if-touched" ? state.isDirty : !state.isDefaultValue);
|
|
11
|
+
const isSubmitting = useSelector(form.store, (state) => state.isSubmitting);
|
|
12
|
+
const active = mode !== null && dirty && !isSubmitting;
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!active) return;
|
|
15
|
+
const handler = (event) => {
|
|
16
|
+
event.preventDefault();
|
|
17
|
+
event.returnValue = "";
|
|
18
|
+
};
|
|
19
|
+
window.addEventListener("beforeunload", handler);
|
|
20
|
+
return () => window.removeEventListener("beforeunload", handler);
|
|
21
|
+
}, [active]);
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { useUnsavedChangesWarning };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/lib/validation-error.d.ts
|
|
2
|
+
type FieldMessage = string | readonly string[];
|
|
3
|
+
declare class ValidationError extends Error {
|
|
4
|
+
readonly fields: Readonly<Record<string, FieldMessage>>;
|
|
5
|
+
readonly formError?: string;
|
|
6
|
+
constructor(fields: Readonly<Record<string, FieldMessage>>, options?: {
|
|
7
|
+
formError?: string;
|
|
8
|
+
message?: string;
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { type FieldMessage, ValidationError };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/lib/validation-error.ts
|
|
2
|
+
var ValidationError = class extends Error {
|
|
3
|
+
constructor(fields, options = {}) {
|
|
4
|
+
super(options.message ?? "Validation failed");
|
|
5
|
+
this.name = "ValidationError";
|
|
6
|
+
this.fields = fields;
|
|
7
|
+
this.formError = options.formError;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
11
|
+
export { ValidationError };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
//#region src/lib/validation-logic.ts
|
|
3
|
+
function pickLiveValidator(validators, isAsync) {
|
|
4
|
+
const fn = isAsync ? validators?.onChangeAsync ?? validators?.onBlurAsync : validators?.onChange ?? validators?.onBlur;
|
|
5
|
+
return fn ? {
|
|
6
|
+
cause: "change",
|
|
7
|
+
fn
|
|
8
|
+
} : void 0;
|
|
9
|
+
}
|
|
10
|
+
function shouldRunFormChange(form) {
|
|
11
|
+
if (form.state.submissionAttempts > 0) return true;
|
|
12
|
+
return Object.values(form.state.fieldMeta).some((meta) => meta?.isBlurred === true);
|
|
13
|
+
}
|
|
14
|
+
function fieldLevelLogic(props) {
|
|
15
|
+
const { event, validators, form } = props;
|
|
16
|
+
const run = props.runValidation;
|
|
17
|
+
if (!validators) return run({
|
|
18
|
+
form,
|
|
19
|
+
validators: []
|
|
20
|
+
});
|
|
21
|
+
const isAsync = event.async;
|
|
22
|
+
if (event.type === "mount") return run({
|
|
23
|
+
form,
|
|
24
|
+
validators: isAsync ? [] : [{
|
|
25
|
+
cause: "mount",
|
|
26
|
+
fn: validators.onMount
|
|
27
|
+
}]
|
|
28
|
+
});
|
|
29
|
+
if (event.type === "change") return run({
|
|
30
|
+
form,
|
|
31
|
+
validators: [{
|
|
32
|
+
cause: "change",
|
|
33
|
+
fn: isAsync ? validators.onChangeAsync : validators.onChange
|
|
34
|
+
}]
|
|
35
|
+
});
|
|
36
|
+
if (event.type === "blur") return run({
|
|
37
|
+
form,
|
|
38
|
+
validators: [{
|
|
39
|
+
cause: "blur",
|
|
40
|
+
fn: isAsync ? validators.onBlurAsync : validators.onBlur
|
|
41
|
+
}]
|
|
42
|
+
});
|
|
43
|
+
if (event.type === "submit") return run({
|
|
44
|
+
form,
|
|
45
|
+
validators: [
|
|
46
|
+
{
|
|
47
|
+
cause: "change",
|
|
48
|
+
fn: isAsync ? validators.onChangeAsync : validators.onChange
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
cause: "blur",
|
|
52
|
+
fn: isAsync ? validators.onBlurAsync : validators.onBlur
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
cause: "submit",
|
|
56
|
+
fn: isAsync ? validators.onSubmitAsync : validators.onSubmit
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
});
|
|
60
|
+
return run({
|
|
61
|
+
form,
|
|
62
|
+
validators: []
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function buildSubmitValidators(validators, isAsync, liveValidator) {
|
|
66
|
+
return [
|
|
67
|
+
...liveValidator ? [liveValidator] : [],
|
|
68
|
+
{
|
|
69
|
+
cause: "submit",
|
|
70
|
+
fn: isAsync ? validators.onSubmitAsync : validators.onSubmit
|
|
71
|
+
},
|
|
72
|
+
...isAsync ? [] : [{
|
|
73
|
+
cause: "server",
|
|
74
|
+
fn: () => void 0
|
|
75
|
+
}]
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
function blurThenChangeLogicImpl(props) {
|
|
79
|
+
if (props.event.fieldName !== void 0) return fieldLevelLogic(props);
|
|
80
|
+
const { event, validators, form } = props;
|
|
81
|
+
const run = props.runValidation;
|
|
82
|
+
if (!validators) return run({
|
|
83
|
+
form,
|
|
84
|
+
validators: []
|
|
85
|
+
});
|
|
86
|
+
const isAsync = event.async;
|
|
87
|
+
const liveValidator = pickLiveValidator(validators, isAsync);
|
|
88
|
+
if (event.type === "mount") return run({
|
|
89
|
+
form,
|
|
90
|
+
validators: isAsync ? [] : [{
|
|
91
|
+
cause: "mount",
|
|
92
|
+
fn: validators.onMount
|
|
93
|
+
}]
|
|
94
|
+
});
|
|
95
|
+
if (event.type === "blur") return run({
|
|
96
|
+
form,
|
|
97
|
+
validators: liveValidator ? [liveValidator] : []
|
|
98
|
+
});
|
|
99
|
+
if (event.type === "change") return run({
|
|
100
|
+
form,
|
|
101
|
+
validators: liveValidator && shouldRunFormChange(form) ? [liveValidator] : []
|
|
102
|
+
});
|
|
103
|
+
if (event.type === "submit") return run({
|
|
104
|
+
form,
|
|
105
|
+
validators: buildSubmitValidators(validators, isAsync, liveValidator)
|
|
106
|
+
});
|
|
107
|
+
return run({
|
|
108
|
+
form,
|
|
109
|
+
validators: []
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const blurThenChangeLogic = blurThenChangeLogicImpl;
|
|
113
|
+
//#endregion
|
|
114
|
+
export { blurThenChangeLogic };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AnyFormApi } from "@tanstack/react-form";
|
|
2
|
+
//#region src/lib/validation-modes.d.ts
|
|
3
|
+
type ValidationMode = "change" | "blur" | "submit" | "blur-then-change";
|
|
4
|
+
declare const DEFAULT_VALIDATION_MODE: ValidationMode;
|
|
5
|
+
type ZenoFormState = {
|
|
6
|
+
validation: ValidationMode;
|
|
7
|
+
hideFieldErrors: boolean;
|
|
8
|
+
requiredIndicator: boolean;
|
|
9
|
+
requiredFields: ReadonlySet<string>;
|
|
10
|
+
};
|
|
11
|
+
declare function setFormZenoState(form: AnyFormApi, state: Partial<ZenoFormState>): void;
|
|
12
|
+
declare function getFormValidationMode(form: AnyFormApi): ValidationMode;
|
|
13
|
+
declare function getFormHideFieldErrors(form: AnyFormApi): boolean;
|
|
14
|
+
declare function isFieldRequired(form: AnyFormApi, name: string): boolean;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { DEFAULT_VALIDATION_MODE, type ValidationMode, getFormHideFieldErrors, getFormValidationMode, isFieldRequired, setFormZenoState };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { toRequiredPathKey } from "./schema-required.mjs";
|
|
3
|
+
//#region src/lib/validation-modes.ts
|
|
4
|
+
const DEFAULT_VALIDATION_MODE = "blur-then-change";
|
|
5
|
+
const DEFAULT_FORM_STATE = {
|
|
6
|
+
hideFieldErrors: false,
|
|
7
|
+
requiredFields: /* @__PURE__ */ new Set(),
|
|
8
|
+
requiredIndicator: true,
|
|
9
|
+
validation: DEFAULT_VALIDATION_MODE
|
|
10
|
+
};
|
|
11
|
+
const formZenoState = /* @__PURE__ */ new WeakMap();
|
|
12
|
+
function stateKey(form) {
|
|
13
|
+
return form.store;
|
|
14
|
+
}
|
|
15
|
+
function setFormZenoState(form, state) {
|
|
16
|
+
const key = stateKey(form);
|
|
17
|
+
const current = formZenoState.get(key) ?? DEFAULT_FORM_STATE;
|
|
18
|
+
formZenoState.set(key, {
|
|
19
|
+
...current,
|
|
20
|
+
...state
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function getFormZenoState(form) {
|
|
24
|
+
return formZenoState.get(stateKey(form)) ?? DEFAULT_FORM_STATE;
|
|
25
|
+
}
|
|
26
|
+
function getFormValidationMode(form) {
|
|
27
|
+
return getFormZenoState(form).validation;
|
|
28
|
+
}
|
|
29
|
+
function getFormHideFieldErrors(form) {
|
|
30
|
+
return getFormZenoState(form).hideFieldErrors;
|
|
31
|
+
}
|
|
32
|
+
function isFieldRequired(form, name) {
|
|
33
|
+
const state = getFormZenoState(form);
|
|
34
|
+
if (!state.requiredIndicator) return false;
|
|
35
|
+
return state.requiredFields.has(toRequiredPathKey(name));
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { DEFAULT_VALIDATION_MODE, getFormHideFieldErrors, getFormValidationMode, isFieldRequired, setFormZenoState };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@tanstack/react-form";
|
package/package.json
CHANGED
|
@@ -1,22 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeno-lib/forms",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"repository": {
|
|
5
|
+
"url": "https://github.com/zeno-lib/zeno.git",
|
|
6
|
+
"directory": "packages/forms"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
4
9
|
"exports": {
|
|
5
|
-
".":
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"default": "./dist/index.mjs"
|
|
14
|
+
},
|
|
6
15
|
"./create-form": "./src/create-form.tsx",
|
|
7
16
|
"./form-dialog": "./src/form-dialog.tsx",
|
|
8
|
-
"./lib/*":
|
|
9
|
-
|
|
17
|
+
"./lib/*": {
|
|
18
|
+
"types": "./dist/lib/*.d.mts",
|
|
19
|
+
"import": "./dist/lib/*.mjs",
|
|
20
|
+
"default": "./dist/lib/*.mjs"
|
|
21
|
+
},
|
|
22
|
+
"./tanstack": {
|
|
23
|
+
"types": "./dist/tanstack.d.mts",
|
|
24
|
+
"import": "./dist/tanstack.mjs",
|
|
25
|
+
"default": "./dist/tanstack.mjs"
|
|
26
|
+
}
|
|
10
27
|
},
|
|
11
28
|
"files": [
|
|
29
|
+
"dist",
|
|
12
30
|
"src"
|
|
13
31
|
],
|
|
14
|
-
"
|
|
15
|
-
"
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@tanstack/react-form": "1.33.5",
|
|
34
|
+
"@tanstack/react-form-nextjs": "1.33.5"
|
|
16
35
|
},
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "26.6.4",
|
|
38
|
+
"@types/react": "19.3.0",
|
|
39
|
+
"@types/react-dom": "19.3.0",
|
|
40
|
+
"@zeno-lib/test": "^0.1.1",
|
|
41
|
+
"@zeno-lib/typescript": "^1.1.0",
|
|
42
|
+
"@zeno-lib/ui": "^0.0.1",
|
|
43
|
+
"lucide-react": "^1.52.0",
|
|
44
|
+
"tsdown": "^0.23.0",
|
|
45
|
+
"typescript": "7.0.2",
|
|
46
|
+
"vite": "8.3.3",
|
|
47
|
+
"vitest": "5.0.3",
|
|
48
|
+
"zod": "^4.6.5"
|
|
20
49
|
},
|
|
21
50
|
"peerDependencies": {
|
|
22
51
|
"@tanstack/react-devtools": ">=0.10",
|
|
@@ -34,24 +63,12 @@
|
|
|
34
63
|
"optional": true
|
|
35
64
|
}
|
|
36
65
|
},
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"@tanstack/react-form-nextjs": "1.33.5"
|
|
40
|
-
},
|
|
41
|
-
"devDependencies": {
|
|
42
|
-
"@types/node": "25.6.0",
|
|
43
|
-
"@types/react": "19.2.14",
|
|
44
|
-
"@types/react-dom": "19.2.3",
|
|
45
|
-
"lucide-react": "^1.23.0",
|
|
46
|
-
"typescript": "7.0.2",
|
|
47
|
-
"vite": "8.0.16",
|
|
48
|
-
"vitest": "4.1.11",
|
|
49
|
-
"zod": "^4.3.6",
|
|
50
|
-
"@zeno-lib/typescript": "^1.1.0",
|
|
51
|
-
"@zeno-lib/ui": "^0.0.1",
|
|
52
|
-
"@zeno-lib/test": "^0.1.0"
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public"
|
|
53
68
|
},
|
|
54
69
|
"scripts": {
|
|
70
|
+
"build": "tsdown",
|
|
71
|
+
"dev": "tsdown --watch --no-clean",
|
|
55
72
|
"test": "vitest --run",
|
|
56
73
|
"test:watch": "vitest watch",
|
|
57
74
|
"types:check": "tsc --noEmit"
|
|
@@ -124,7 +124,7 @@ function ComboboxField<T = string>({
|
|
|
124
124
|
items={items}
|
|
125
125
|
onInputValueChange={onInputValueChange}
|
|
126
126
|
onValueChange={(next) => {
|
|
127
|
-
if (next
|
|
127
|
+
if (next === null) {
|
|
128
128
|
field.handleChange(undefined)
|
|
129
129
|
return
|
|
130
130
|
}
|
|
@@ -143,7 +143,7 @@ function ComboboxField<T = string>({
|
|
|
143
143
|
name={field.name}
|
|
144
144
|
onBlur={field.handleBlur}
|
|
145
145
|
placeholder={placeholder}
|
|
146
|
-
showClear={showClear && value
|
|
146
|
+
showClear={showClear && value !== null && value !== "" && !loading}
|
|
147
147
|
>
|
|
148
148
|
{loading && (
|
|
149
149
|
<InputGroupAddon align="inline-end">
|
|
@@ -11,7 +11,7 @@ type NumberFieldProps = Omit<
|
|
|
11
11
|
|
|
12
12
|
function NumberField(props: NumberFieldProps) {
|
|
13
13
|
const field = useFieldContext<number | undefined>()
|
|
14
|
-
const value = field.state
|
|
14
|
+
const { value } = field.state
|
|
15
15
|
const inputValue =
|
|
16
16
|
typeof value === "number" && !Number.isNaN(value) ? value : ""
|
|
17
17
|
|
|
@@ -51,12 +51,14 @@ function SliderField({
|
|
|
51
51
|
const schemaRequired = useIsFieldRequired(field)
|
|
52
52
|
const isRequired = required ?? schemaRequired
|
|
53
53
|
|
|
54
|
-
const value = field.state
|
|
54
|
+
const { value } = field.state
|
|
55
55
|
const readout = formatValue && value !== undefined ? formatValue(value) : null
|
|
56
|
+
// Not a truthiness check: a formatter may return the number 0, which must still render.
|
|
57
|
+
const hasReadout = readout !== null && readout !== undefined
|
|
56
58
|
|
|
57
59
|
return (
|
|
58
60
|
<Field data-field={field.name} data-invalid={isInvalid}>
|
|
59
|
-
{(label ||
|
|
61
|
+
{(label || hasReadout) && (
|
|
60
62
|
<FieldContent className="flex-row items-center justify-between">
|
|
61
63
|
{label && (
|
|
62
64
|
<FieldLabel htmlFor={field.name}>
|
|
@@ -64,7 +66,7 @@ function SliderField({
|
|
|
64
66
|
{isRequired && <RequiredIndicator />}
|
|
65
67
|
</FieldLabel>
|
|
66
68
|
)}
|
|
67
|
-
{
|
|
69
|
+
{hasReadout && (
|
|
68
70
|
<span className="text-muted-foreground text-sm tabular-nums">
|
|
69
71
|
{readout}
|
|
70
72
|
</span>
|
|
@@ -81,9 +83,9 @@ function SliderField({
|
|
|
81
83
|
name={field.name}
|
|
82
84
|
onBlur={field.handleBlur}
|
|
83
85
|
onValueChange={(next) => {
|
|
84
|
-
const
|
|
86
|
+
const nextValue: SliderValue =
|
|
85
87
|
typeof next === "number" ? next : Array.from(next)
|
|
86
|
-
field.handleChange(
|
|
88
|
+
field.handleChange(nextValue)
|
|
87
89
|
}}
|
|
88
90
|
value={value}
|
|
89
91
|
{...props}
|
package/src/form-dialog.tsx
CHANGED
|
@@ -84,7 +84,7 @@ function formatNumber(
|
|
|
84
84
|
useGrouping = true,
|
|
85
85
|
}: FormatNumberOptions
|
|
86
86
|
): string {
|
|
87
|
-
if (value
|
|
87
|
+
if (value === null || value === undefined || !Number.isFinite(value)) {
|
|
88
88
|
return ""
|
|
89
89
|
}
|
|
90
90
|
return normalizeMinus(
|
|
@@ -236,10 +236,10 @@ function parseLocaleNumber(text: string, locale?: string): number | null {
|
|
|
236
236
|
// the same logical place after separators were inserted or removed.
|
|
237
237
|
function countSignificant(text: string, end: number, decimal: string): number {
|
|
238
238
|
let count = 0
|
|
239
|
-
for (let i = 0; i < end && i < text.length; i
|
|
239
|
+
for (let i = 0; i < end && i < text.length; i += 1) {
|
|
240
240
|
const char = text[i] as string
|
|
241
241
|
if (DIGIT.test(char) || char === decimal || MINUS_SIGNS.test(char)) {
|
|
242
|
-
count
|
|
242
|
+
count += 1
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
245
|
return count
|
|
@@ -254,10 +254,10 @@ function caretForSignificant(
|
|
|
254
254
|
return 0
|
|
255
255
|
}
|
|
256
256
|
let count = 0
|
|
257
|
-
for (let i = 0; i < text.length; i
|
|
257
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
258
258
|
const char = text[i] as string
|
|
259
259
|
if (DIGIT.test(char) || char === decimal || char === "-") {
|
|
260
|
-
count
|
|
260
|
+
count += 1
|
|
261
261
|
if (count === significant) {
|
|
262
262
|
return i + 1
|
|
263
263
|
}
|
|
@@ -37,7 +37,7 @@ function getDef(node: SchemaNode | undefined): SchemaDef | undefined {
|
|
|
37
37
|
|
|
38
38
|
function extractDefaultForField(node: SchemaNode): ExtractResult {
|
|
39
39
|
let current: SchemaNode | undefined = node
|
|
40
|
-
for (let depth = 0; depth < MAX_WRAPPER_DEPTH; depth
|
|
40
|
+
for (let depth = 0; depth < MAX_WRAPPER_DEPTH; depth += 1) {
|
|
41
41
|
const def: SchemaDef | undefined = getDef(current)
|
|
42
42
|
if (!def?.type) {
|
|
43
43
|
return { ok: false }
|
|
@@ -166,7 +166,7 @@ describe("getRequiredPaths", () => {
|
|
|
166
166
|
const schema: StandardSchemaLike = {
|
|
167
167
|
"~standard": {
|
|
168
168
|
validate: (value) => {
|
|
169
|
-
const profile =
|
|
169
|
+
const { profile } = value as { profile?: unknown }
|
|
170
170
|
return profile === undefined
|
|
171
171
|
? { issues: [{ expected: "Object", path: [{ key: "profile" }] }] }
|
|
172
172
|
: {
|
|
@@ -50,6 +50,7 @@ const INDEX_SEGMENT = /^\d+$/
|
|
|
50
50
|
const INDEX_IN_NAME = /\[\d+\]|\.(\d+)(?=\.|\[|$)/g
|
|
51
51
|
|
|
52
52
|
function pathKey(part: PathPart): PropertyKey {
|
|
53
|
+
// biome-ignore lint/suspicious/noUnnecessaryConditions: issue paths come from third-party validators at runtime, and a non-conforming `null` segment would make `in` throw.
|
|
53
54
|
if (typeof part === "object" && part !== null && "key" in part) {
|
|
54
55
|
return part.key
|
|
55
56
|
}
|
|
@@ -93,10 +94,7 @@ function probeContainer(expected: unknown): unknown {
|
|
|
93
94
|
if (kind === "object") {
|
|
94
95
|
return {}
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
-
return [{}]
|
|
98
|
-
}
|
|
99
|
-
return
|
|
97
|
+
return kind === "array" ? [{}] : undefined
|
|
100
98
|
}
|
|
101
99
|
|
|
102
100
|
// Write `value` at `keys` inside `root`, only where nothing is set yet.
|
|
@@ -140,7 +138,7 @@ function validateSync(
|
|
|
140
138
|
function getRequiredPaths(schema: StandardSchemaLike): Set<string> {
|
|
141
139
|
const required = new Set<string>()
|
|
142
140
|
const probe: Record<PropertyKey, unknown> = {}
|
|
143
|
-
for (let depth = 0; depth < MAX_PROBE_DEPTH; depth
|
|
141
|
+
for (let depth = 0; depth < MAX_PROBE_DEPTH; depth += 1) {
|
|
144
142
|
const issues = validateSync(schema, probe)
|
|
145
143
|
if (!issues) {
|
|
146
144
|
return required
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cleanup, render, screen } from "@zeno-lib/test/testing-library"
|
|
2
|
+
import { afterEach, describe, expect, test, vi } from "vitest"
|
|
3
|
+
import { Form, FormProvider, useForm } from "./create-form"
|
|
4
|
+
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
cleanup()
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
describe("SliderField", () => {
|
|
10
|
+
function Harness() {
|
|
11
|
+
const form = useForm({ defaultValues: { volume: 0 }, onSubmit: vi.fn() })
|
|
12
|
+
const { SliderField } = form
|
|
13
|
+
return (
|
|
14
|
+
<FormProvider form={form}>
|
|
15
|
+
<Form>
|
|
16
|
+
<SliderField
|
|
17
|
+
formatValue={(value) => value}
|
|
18
|
+
label="Volume"
|
|
19
|
+
name="volume"
|
|
20
|
+
/>
|
|
21
|
+
</Form>
|
|
22
|
+
</FormProvider>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test("renders a numeric readout of 0", () => {
|
|
27
|
+
render(<Harness />)
|
|
28
|
+
expect(screen.getByText("0").tagName).toBe("SPAN")
|
|
29
|
+
})
|
|
30
|
+
})
|