@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.
Files changed (52) hide show
  1. package/dist/create-zeno-form.d.mts +117 -0
  2. package/dist/create-zeno-form.mjs +121 -0
  3. package/dist/form-element.d.mts +13 -0
  4. package/dist/form-element.mjs +31 -0
  5. package/dist/index.d.mts +11 -0
  6. package/dist/index.mjs +10 -0
  7. package/dist/lib/action-result.d.mts +22 -0
  8. package/dist/lib/action-result.mjs +32 -0
  9. package/dist/lib/apply-validation-error.d.mts +6 -0
  10. package/dist/lib/apply-validation-error.mjs +41 -0
  11. package/dist/lib/aria.d.mts +3 -0
  12. package/dist/lib/aria.mjs +7 -0
  13. package/dist/lib/contexts.d.mts +5 -0
  14. package/dist/lib/contexts.mjs +7 -0
  15. package/dist/lib/formatted-number.d.mts +56 -0
  16. package/dist/lib/formatted-number.mjs +138 -0
  17. package/dist/lib/schema-defaults.d.mts +17 -0
  18. package/dist/lib/schema-defaults.mjs +75 -0
  19. package/dist/lib/schema-required.d.mts +25 -0
  20. package/dist/lib/schema-required.mjs +72 -0
  21. package/dist/lib/submit-action.d.mts +64 -0
  22. package/dist/lib/submit-action.mjs +43 -0
  23. package/dist/lib/use-form-dialog.d.mts +73 -0
  24. package/dist/lib/use-form-dialog.mjs +90 -0
  25. package/dist/lib/use-formatted-number.d.mts +61 -0
  26. package/dist/lib/use-formatted-number.mjs +104 -0
  27. package/dist/lib/use-is-invalid.d.mts +7 -0
  28. package/dist/lib/use-is-invalid.mjs +31 -0
  29. package/dist/lib/use-rebased-default-values.d.mts +10 -0
  30. package/dist/lib/use-rebased-default-values.mjs +21 -0
  31. package/dist/lib/use-unsaved-changes-warning.d.mts +6 -0
  32. package/dist/lib/use-unsaved-changes-warning.mjs +24 -0
  33. package/dist/lib/validation-error.d.mts +12 -0
  34. package/dist/lib/validation-error.mjs +11 -0
  35. package/dist/lib/validation-logic.d.mts +5 -0
  36. package/dist/lib/validation-logic.mjs +114 -0
  37. package/dist/lib/validation-modes.d.mts +16 -0
  38. package/dist/lib/validation-modes.mjs +38 -0
  39. package/dist/tanstack.d.mts +1 -0
  40. package/dist/tanstack.mjs +2 -0
  41. package/package.json +42 -25
  42. package/src/fields/combobox-field.tsx +2 -2
  43. package/src/fields/date-picker-field.tsx +1 -1
  44. package/src/fields/number-field.tsx +1 -1
  45. package/src/fields/slider-field.tsx +7 -5
  46. package/src/form-dialog.tsx +2 -2
  47. package/src/lib/formatted-number.ts +5 -5
  48. package/src/lib/schema-defaults.ts +1 -1
  49. package/src/lib/schema-required.test.ts +1 -1
  50. package/src/lib/schema-required.ts +3 -5
  51. package/src/lib/validation-logic.test.ts +0 -1
  52. 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,5 @@
1
+ import { ValidationLogicFn } from "@tanstack/react-form";
2
+ //#region src/lib/validation-logic.d.ts
3
+ declare const blurThenChangeLogic: ValidationLogicFn;
4
+ //#endregion
5
+ export { blurThenChangeLogic };
@@ -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";
@@ -0,0 +1,2 @@
1
+ export * from "@tanstack/react-form";
2
+ export {};
package/package.json CHANGED
@@ -1,22 +1,51 @@
1
1
  {
2
2
  "name": "@zeno-lib/forms",
3
- "version": "0.2.0",
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
- ".": "./src/index.ts",
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/*": "./src/lib/*.ts",
9
- "./tanstack": "./src/tanstack.ts"
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
- "publishConfig": {
15
- "access": "public"
32
+ "dependencies": {
33
+ "@tanstack/react-form": "1.33.5",
34
+ "@tanstack/react-form-nextjs": "1.33.5"
16
35
  },
17
- "repository": {
18
- "url": "https://github.com/zeno-lib/zeno.git",
19
- "directory": "packages/forms"
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
- "dependencies": {
38
- "@tanstack/react-form": "1.33.5",
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 == null) {
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 != null && value !== "" && !loading}
146
+ showClear={showClear && value !== null && value !== "" && !loading}
147
147
  >
148
148
  {loading && (
149
149
  <InputGroupAddon align="inline-end">
@@ -68,7 +68,7 @@ function DatePickerField({
68
68
  const schemaRequired = useIsFieldRequired(field)
69
69
  const isRequired = required ?? schemaRequired
70
70
 
71
- const value = field.state.value
71
+ const { value } = field.state
72
72
  const empty = !value
73
73
 
74
74
  return (
@@ -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.value
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.value
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 || readout) && (
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
- {readout && (
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 value: SliderValue =
86
+ const nextValue: SliderValue =
85
87
  typeof next === "number" ? next : Array.from(next)
86
- field.handleChange(value)
88
+ field.handleChange(nextValue)
87
89
  }}
88
90
  value={value}
89
91
  {...props}
@@ -63,8 +63,8 @@ type DialogForm<TValues> = {
63
63
  }
64
64
  }
65
65
  readonly state: DialogFormState
66
- handleSubmit(): unknown
67
- reset(values?: TValues): void
66
+ handleSubmit: () => unknown
67
+ reset: (values?: TValues) => void
68
68
  }
69
69
 
70
70
  type FormDialogProps<TValues> = {
@@ -84,7 +84,7 @@ function formatNumber(
84
84
  useGrouping = true,
85
85
  }: FormatNumberOptions
86
86
  ): string {
87
- if (value == null || !Number.isFinite(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 = (value as { profile?: unknown }).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
- if (kind === "array") {
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
@@ -32,7 +32,6 @@ function makeRun() {
32
32
  const runValidation = vi.fn(
33
33
  (props: { form: AnyFormApi; validators: ObservedValidator[] }) => {
34
34
  observed.push(props.validators)
35
- return
36
35
  }
37
36
  )
38
37
  return { observed, runValidation }
@@ -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
+ })