@zeno-lib/forms 0.6.0 → 0.7.1

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 (59) hide show
  1. package/dist/create-zeno-form.d.mts +19 -4
  2. package/dist/create-zeno-form.mjs +29 -6
  3. package/dist/form-element.d.mts +16 -3
  4. package/dist/form-element.mjs +38 -18
  5. package/dist/index.d.mts +2 -1
  6. package/dist/index.mjs +2 -1
  7. package/dist/lib/date-value.d.mts +24 -0
  8. package/dist/lib/date-value.mjs +37 -0
  9. package/dist/lib/default-props.d.mts +7 -0
  10. package/dist/lib/default-props.mjs +13 -0
  11. package/dist/lib/empty-value.mjs +25 -1
  12. package/dist/lib/form-disabled.d.mts +28 -0
  13. package/dist/lib/form-disabled.mjs +54 -0
  14. package/dist/lib/item-options.d.mts +45 -0
  15. package/dist/lib/item-options.mjs +70 -0
  16. package/dist/lib/schema-required.mjs +17 -0
  17. package/dist/lib/use-field-control.d.mts +7 -0
  18. package/dist/lib/use-field-control.mjs +4 -1
  19. package/package.json +1 -1
  20. package/src/create-form.tsx +34 -29
  21. package/src/create-zeno-form.tsx +124 -51
  22. package/src/date-picker-field.test.tsx +108 -0
  23. package/src/default-props.test-d.ts +66 -0
  24. package/src/default-props.test.tsx +165 -0
  25. package/src/fields/checkbox-field.tsx +5 -0
  26. package/src/fields/checkbox-group-field.tsx +6 -2
  27. package/src/fields/combobox-field.tsx +107 -40
  28. package/src/fields/date-picker-field.tsx +34 -11
  29. package/src/fields/field-shell.tsx +8 -1
  30. package/src/fields/input-field.tsx +2 -0
  31. package/src/fields/multi-select-field.tsx +100 -42
  32. package/src/fields/otp-field.tsx +8 -2
  33. package/src/fields/radio-group-field.tsx +9 -1
  34. package/src/fields/reset-button.tsx +4 -0
  35. package/src/fields/select-field.tsx +9 -1
  36. package/src/fields/slider-field.tsx +9 -1
  37. package/src/fields/submit-button.tsx +3 -1
  38. package/src/fields/switch-field.tsx +5 -0
  39. package/src/fields/textarea-field.tsx +2 -0
  40. package/src/form-dialog.test.tsx +255 -4
  41. package/src/form-dialog.tsx +187 -51
  42. package/src/form-disabled.test.tsx +329 -0
  43. package/src/form-element.tsx +64 -29
  44. package/src/grouped-options.test-d.ts +59 -0
  45. package/src/grouped-options.test.tsx +251 -0
  46. package/src/index.ts +1 -0
  47. package/src/lib/date-value.test.ts +14 -0
  48. package/src/lib/date-value.ts +70 -0
  49. package/src/lib/default-props.ts +19 -0
  50. package/src/lib/empty-value.test.ts +19 -0
  51. package/src/lib/empty-value.ts +45 -1
  52. package/src/lib/form-disabled.ts +95 -0
  53. package/src/lib/item-options.test.ts +11 -0
  54. package/src/lib/item-options.ts +165 -0
  55. package/src/lib/schema-required.test.ts +9 -0
  56. package/src/lib/schema-required.ts +33 -0
  57. package/src/lib/use-field-control.ts +11 -1
  58. package/src/with-form.test-d.ts +76 -0
  59. package/src/with-form.test.tsx +60 -0
@@ -14,6 +14,11 @@ type StandardSchema<T> = {
14
14
  };
15
15
  type PartialFormData<T> = T extends readonly unknown[] ? T : T extends Date | RegExp | ((...args: never[]) => unknown) ? T : T extends object ? { [K in keyof T]?: PartialFormData<T[K]>; } : T;
16
16
  type ZenoFormExtras<TFormData> = {
17
+ /**
18
+ * Disable every field of the form, its submit button and `FormDialog`'s
19
+ * Save. A field's own `disabled` still applies.
20
+ */
21
+ disabled?: boolean;
17
22
  /**
18
23
  * If `true`, shipped fields skip rendering their inline `<FieldError>`
19
24
  * message. Fields still flip `data-invalid` / `aria-invalid`.
@@ -44,9 +49,11 @@ type SchemaPathExtras<TFormData> = {
44
49
  type ManualPathExtras = {
45
50
  schema?: never;
46
51
  };
47
- type SchemaFormOptions<TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnDynamic extends undefined | FormValidateOrFn<TFormData>, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TSubmitMeta> = Omit<FormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>, "defaultValues" | "validators" | "validationLogic"> & SchemaPathExtras<TFormData> & ZenoFormExtras<TFormData>;
48
- type ManualFormOptions<TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnDynamic extends undefined | FormValidateOrFn<TFormData>, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TSubmitMeta> = Omit<FormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>, "defaultValues"> & ManualPathExtras & ZenoFormExtras<TFormData>;
49
- type UseFormOptions<TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnDynamic extends undefined | FormValidateOrFn<TFormData>, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TSubmitMeta> = SchemaFormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta> | ManualFormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>;
52
+ type SyncValidator<TFormData> = undefined | FormValidateOrFn<TFormData>;
53
+ type AsyncValidator<TFormData> = undefined | FormAsyncValidateOrFn<TFormData>;
54
+ type SchemaFormOptions<TFormData, TOnMount extends SyncValidator<TFormData>, TOnChange extends SyncValidator<TFormData>, TOnChangeAsync extends AsyncValidator<TFormData>, TOnBlur extends SyncValidator<TFormData>, TOnBlurAsync extends AsyncValidator<TFormData>, TOnSubmit extends SyncValidator<TFormData>, TOnSubmitAsync extends AsyncValidator<TFormData>, TOnDynamic extends SyncValidator<TFormData>, TOnDynamicAsync extends AsyncValidator<TFormData>, TOnServer extends AsyncValidator<TFormData>, TSubmitMeta> = Omit<FormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>, "defaultValues" | "validators" | "validationLogic"> & SchemaPathExtras<TFormData> & ZenoFormExtras<TFormData>;
55
+ type ManualFormOptions<TFormData, TOnMount extends SyncValidator<TFormData>, TOnChange extends SyncValidator<TFormData>, TOnChangeAsync extends AsyncValidator<TFormData>, TOnBlur extends SyncValidator<TFormData>, TOnBlurAsync extends AsyncValidator<TFormData>, TOnSubmit extends SyncValidator<TFormData>, TOnSubmitAsync extends AsyncValidator<TFormData>, TOnDynamic extends SyncValidator<TFormData>, TOnDynamicAsync extends AsyncValidator<TFormData>, TOnServer extends AsyncValidator<TFormData>, TSubmitMeta> = Omit<FormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>, "defaultValues"> & ManualPathExtras & ZenoFormExtras<TFormData>;
56
+ type UseFormOptions<TFormData, TOnMount extends SyncValidator<TFormData>, TOnChange extends SyncValidator<TFormData>, TOnChangeAsync extends AsyncValidator<TFormData>, TOnBlur extends SyncValidator<TFormData>, TOnBlurAsync extends AsyncValidator<TFormData>, TOnSubmit extends SyncValidator<TFormData>, TOnSubmitAsync extends AsyncValidator<TFormData>, TOnDynamic extends SyncValidator<TFormData>, TOnDynamicAsync extends AsyncValidator<TFormData>, TOnServer extends AsyncValidator<TFormData>, TSubmitMeta> = SchemaFormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta> | ManualFormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>;
50
57
  type FormDataOf<F> = F extends {
51
58
  state: {
52
59
  values: infer V;
@@ -93,9 +100,17 @@ type WithControlValue<P, V> = P extends {
93
100
  type FieldWrappers<FC extends Record<string, AnyComponent>, T> = { [K in keyof FC]: <N extends DeepKeys<T>>(props: WithControlValue<ComponentProps<FC[K]>, DeepValue<T, N>> & {
94
101
  name: N;
95
102
  } & WithValidators) => ReactNode; };
103
+ type OptionalProps<P> = P extends unknown ? { [K in keyof P as Pick<P, K> extends Required<Pick<P, K>> ? never : K]?: P[K]; } : never;
104
+ type DefaultProps<C extends Record<string, AnyComponent>> = { [K in keyof C]?: OptionalProps<ComponentProps<C[K]>>; };
96
105
  type CreateZenoFormConfig<FC extends Record<string, AnyComponent>, FMC extends Record<string, AnyComponent>> = {
97
106
  fieldComponents: FC;
98
107
  formComponents: FMC;
108
+ /**
109
+ * Default props for field and form components, keyed by component name,
110
+ * e.g. `{ MoneyField: { currency: "CHF", locale: "de-CH" } }`. A prop
111
+ * passed at the call site wins; one passed as `undefined` keeps the default.
112
+ */
113
+ defaultProps?: DefaultProps<NoInfer<FC> & NoInfer<FMC>>;
99
114
  };
100
115
  /**
101
116
  * Build the wired Zeno form API from the caller's field + form components.
@@ -108,7 +123,7 @@ type CreateZenoFormConfig<FC extends Record<string, AnyComponent>, FMC extends R
108
123
  export declare function createZenoForm<FC extends Record<string, AnyComponent>, FMC extends Record<string, AnyComponent>>(config: CreateZenoFormConfig<FC, FMC>): {
109
124
  useAppFields: <TForm extends AnyAppForm>(form: TForm) => FieldWrappers<FC, FormDataOf<TForm>>;
110
125
  useAppForm: <TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnDynamic extends undefined | FormValidateOrFn<TFormData>, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TSubmitMeta>(props: FormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>) => import("@tanstack/react-form").AppFieldExtendedReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, FC, FMC>;
111
- useForm: <TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData> = undefined, TOnChange extends undefined | FormValidateOrFn<TFormData> = undefined, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined, TOnBlur extends undefined | FormValidateOrFn<TFormData> = undefined, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined, TOnSubmit extends undefined | FormValidateOrFn<TFormData> = undefined, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined, TOnDynamic extends undefined | FormValidateOrFn<TFormData> = undefined, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData> = undefined, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData> = undefined, TSubmitMeta = never>(options: UseFormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>) => import("@tanstack/react-form").FormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta> & import("@tanstack/react-form").ReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta> & NoInfer<FMC> & {
126
+ useForm: <TFormData, TOnMount extends SyncValidator<TFormData> = SyncValidator<TFormData>, TOnChange extends SyncValidator<TFormData> = SyncValidator<TFormData>, TOnChangeAsync extends AsyncValidator<TFormData> = AsyncValidator<TFormData>, TOnBlur extends SyncValidator<TFormData> = SyncValidator<TFormData>, TOnBlurAsync extends AsyncValidator<TFormData> = AsyncValidator<TFormData>, TOnSubmit extends SyncValidator<TFormData> = SyncValidator<TFormData>, TOnSubmitAsync extends AsyncValidator<TFormData> = AsyncValidator<TFormData>, TOnDynamic extends SyncValidator<TFormData> = SyncValidator<TFormData>, TOnDynamicAsync extends AsyncValidator<TFormData> = AsyncValidator<TFormData>, TOnServer extends AsyncValidator<TFormData> = AsyncValidator<TFormData>, TSubmitMeta = unknown>(options: UseFormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>) => import("@tanstack/react-form").FormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta> & import("@tanstack/react-form").ReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta> & NoInfer<FMC> & {
112
127
  AppField: import("@tanstack/react-form").FieldComponent<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, NoInfer<FC>>;
113
128
  AppForm: import("react").ComponentType<import("react").PropsWithChildren<{}>>;
114
129
  } & FieldWrappers<FC, FormDataOf<import("@tanstack/react-form").AppFieldExtendedReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, FC, FMC>>> & FMC;
@@ -1,10 +1,12 @@
1
1
  "use client";
2
2
  import { applyValidationError } from "./lib/apply-validation-error.mjs";
3
3
  import { fieldContext, formContext } from "./lib/contexts.mjs";
4
+ import { withDefaultProps } from "./lib/default-props.mjs";
4
5
  import { deepMergeDefaults, extractZodDefaults } from "./lib/schema-defaults.mjs";
5
6
  import { getRequiredPaths } from "./lib/schema-required.mjs";
6
7
  import { DEFAULT_VALIDATION_MODE, setFormZenoState } from "./lib/validation-modes.mjs";
7
8
  import { withEmptyStartValues } from "./lib/empty-value.mjs";
9
+ import { useDisabledOption } from "./lib/form-disabled.mjs";
8
10
  import { setSubmitError } from "./lib/submit-error.mjs";
9
11
  import { useRebasedDefaultValues } from "./lib/use-rebased-default-values.mjs";
10
12
  import { useUnsavedChangesWarning } from "./lib/use-unsaved-changes-warning.mjs";
@@ -31,6 +33,24 @@ function withMountListener(listeners, onMount) {
31
33
  }
32
34
  };
33
35
  }
36
+ const REACT_TYPE_KEYS = /* @__PURE__ */ new Set([
37
+ "$$typeof",
38
+ "compare",
39
+ "render",
40
+ "type"
41
+ ]);
42
+ function withComponentDefaults(components, defaultProps) {
43
+ const wrapped = { ...components };
44
+ for (const [name, defaults] of Object.entries(defaultProps ?? {})) {
45
+ const Component = components[name];
46
+ if (Component === void 0 || defaults === void 0) continue;
47
+ const Defaulted = (props) => /* @__PURE__ */ jsx(Component, { ...withDefaultProps(defaults, props) });
48
+ for (const [key, value] of Object.entries(Component)) if (!REACT_TYPE_KEYS.has(key)) Object.assign(Defaulted, { [key]: value });
49
+ Defaulted.displayName = name;
50
+ wrapped[name] = Defaulted;
51
+ }
52
+ return wrapped;
53
+ }
34
54
  /**
35
55
  * Build the wired Zeno form API from the caller's field + form components.
36
56
  *
@@ -40,18 +60,20 @@ function withMountListener(listeners, onMount) {
40
60
  * `useAppFields` wrappers (prop types are inferred from the injected components).
41
61
  */
42
62
  function createZenoForm(config) {
63
+ const fieldComponents = withComponentDefaults(config.fieldComponents, config.defaultProps);
64
+ const formComponents = withComponentDefaults(config.formComponents, config.defaultProps);
43
65
  const { useAppForm, withFieldGroup, withForm } = createFormHook({
44
- fieldComponents: config.fieldComponents,
66
+ fieldComponents,
45
67
  fieldContext,
46
- formComponents: config.formComponents,
68
+ formComponents,
47
69
  formContext
48
70
  });
49
71
  function useAppFields(form) {
50
72
  return useMemo(() => {
51
73
  const Field = form.AppField;
52
74
  const wrappers = {};
53
- for (const key of Object.keys(config.fieldComponents)) {
54
- const Impl = config.fieldComponents[key];
75
+ for (const key of Object.keys(fieldComponents)) {
76
+ const Impl = fieldComponents[key];
55
77
  wrappers[key] = ({ name, validators, listeners, ...props }) => /* @__PURE__ */ jsx(Field, {
56
78
  listeners,
57
79
  name,
@@ -63,7 +85,7 @@ function createZenoForm(config) {
63
85
  }, [form]);
64
86
  }
65
87
  function useForm(options) {
66
- const { schema, validators: validatorsInput, validationLogic: userValidationLogic, hideFieldErrors = false, requiredIndicator = true, unsavedChangesWarning = false, defaultValues: userDefaultValues, onSubmit: userOnSubmit, ...rest } = options;
88
+ const { schema, validators: validatorsInput, validationLogic: userValidationLogic, disabled = false, hideFieldErrors = false, requiredIndicator = true, unsavedChangesWarning = false, defaultValues: userDefaultValues, onSubmit: userOnSubmit, ...rest } = options;
67
89
  let schemaMode;
68
90
  let resolvedValidators;
69
91
  let resolvedValidationLogic;
@@ -111,9 +133,10 @@ function createZenoForm(config) {
111
133
  schema,
112
134
  ...schemaMode === void 0 ? {} : { validation: schemaMode }
113
135
  });
136
+ useDisabledOption(form, disabled);
114
137
  useUnsavedChangesWarning(form, unsavedChangesWarning);
115
138
  const fields = useAppFields(form);
116
- return useMemo(() => Object.assign(form, fields, config.formComponents), [form, fields]);
139
+ return useMemo(() => Object.assign(form, fields, formComponents), [form, fields]);
117
140
  }
118
141
  return {
119
142
  useAppFields,
@@ -2,12 +2,25 @@ import { ComponentProps, ReactNode } from "react";
2
2
  //#region src/form-element.d.ts
3
3
  type FormProviderProps = {
4
4
  children: ReactNode;
5
+ /**
6
+ * Disable every field of the form and its submit button, as
7
+ * `useForm({ disabled })` does. A field's own `disabled` still applies.
8
+ */
9
+ disabled?: boolean;
5
10
  form: {
6
11
  handleSubmit: () => unknown;
7
12
  };
8
13
  };
9
- declare function FormProvider({ children, form }: FormProviderProps): import("react").JSX.Element;
10
- type FormProps = Omit<ComponentProps<"form">, "onSubmit">;
11
- declare function Form({ children, className, ...props }: FormProps): import("react").JSX.Element;
14
+ declare function FormProvider({ children, disabled, form }: FormProviderProps): import("react").JSX.Element;
15
+ type FormProps = Omit<ComponentProps<"form">, "onSubmit"> & {
16
+ /**
17
+ * Disable every field and button of the kit inside, and ignore a submit. A
18
+ * field's own `disabled` still applies. A button outside the `<form>`
19
+ * (`form="id"`) isn't inside: disable the form with `useForm({ disabled })`
20
+ * or `<FormProvider disabled>` to reach it.
21
+ */
22
+ disabled?: boolean;
23
+ };
24
+ declare function Form({ children, className, disabled, ...props }: FormProps): import("react").JSX.Element;
12
25
  //#endregion
13
26
  export { Form, FormProvider };
@@ -1,32 +1,52 @@
1
1
  "use client";
2
2
  import { FormProvider as FormProvider$1, useFormContext } from "./lib/contexts.mjs";
3
+ import { FormDisabledContext, useIsFormDisabled } from "./lib/form-disabled.mjs";
3
4
  import { reportSubmitError } from "./lib/submit-error.mjs";
4
5
  import { focusFirstInvalid, restoreFocus } from "./lib/focus-first-invalid.mjs";
6
+ import { useMemo } from "react";
5
7
  import { jsx } from "react/jsx-runtime";
6
8
  //#region src/form-element.tsx
7
- function FormProvider({ children, form }) {
9
+ function FormProvider({ children, disabled = false, form }) {
10
+ const { store } = form;
11
+ const scope = useMemo(() => ({
12
+ disabled,
13
+ key: store
14
+ }), [disabled, store]);
8
15
  return /* @__PURE__ */ jsx(FormProvider$1, {
9
16
  value: form,
10
- children
17
+ children: /* @__PURE__ */ jsx(FormDisabledContext, {
18
+ value: scope,
19
+ children
20
+ })
11
21
  });
12
22
  }
13
- function Form({ children, className, ...props }) {
23
+ function Form({ children, className, disabled = false, ...props }) {
14
24
  const form = useFormContext();
15
- return /* @__PURE__ */ jsx("form", {
16
- className,
17
- noValidate: true,
18
- onSubmit: async (event) => {
19
- event.preventDefault();
20
- event.stopPropagation();
21
- const node = event.currentTarget;
22
- const focused = node.ownerDocument.activeElement;
23
- await Promise.resolve(form.handleSubmit()).catch(reportSubmitError);
24
- const { isSubmitSuccessful, isValid } = form.state;
25
- if (!isValid && focusFirstInvalid(node)) return;
26
- if (!(isValid && isSubmitSuccessful)) restoreFocus(focused);
27
- },
28
- ...props,
29
- children
25
+ const isDisabled = useIsFormDisabled(form) || disabled;
26
+ const { store } = form;
27
+ const scope = useMemo(() => ({
28
+ disabled: isDisabled,
29
+ key: store
30
+ }), [isDisabled, store]);
31
+ return /* @__PURE__ */ jsx(FormDisabledContext, {
32
+ value: scope,
33
+ children: /* @__PURE__ */ jsx("form", {
34
+ className,
35
+ noValidate: true,
36
+ onSubmit: async (event) => {
37
+ event.preventDefault();
38
+ event.stopPropagation();
39
+ if (isDisabled) return;
40
+ const node = event.currentTarget;
41
+ const focused = node.ownerDocument.activeElement;
42
+ await Promise.resolve(form.handleSubmit()).catch(reportSubmitError);
43
+ const { isSubmitSuccessful, isValid } = form.state;
44
+ if (!isValid && focusFirstInvalid(node)) return;
45
+ if (!(isValid && isSubmitSuccessful)) restoreFocus(focused);
46
+ },
47
+ ...props,
48
+ children
49
+ })
30
50
  });
31
51
  }
32
52
  //#endregion
package/dist/index.d.mts CHANGED
@@ -6,8 +6,9 @@ import { ActionError, ActionIssue, ActionResult, toActionError, toFieldName } fr
6
6
  import { FieldMessage, ValidationError } from "./lib/validation-error.mjs";
7
7
  import { applyValidationError } from "./lib/apply-validation-error.mjs";
8
8
  import { useFieldContext, useFormContext } from "./lib/contexts.mjs";
9
+ import { useIsFormDisabled } from "./lib/form-disabled.mjs";
9
10
  import { SubmitActionOptions, SubmitActionSchema, applyActionError, submitAction } from "./lib/submit-action.mjs";
10
11
  import { useHideFieldErrors, useIsFieldRequired, useIsFieldRequiredBySchema, useIsInvalid } from "./lib/use-is-invalid.mjs";
11
12
  import { blurThenChangeLogic } from "./lib/validation-logic.mjs";
12
13
  import { useSelector } from "@tanstack/react-form";
13
- export { type ActionError, type ActionIssue, type ActionResult, type FieldControl, type FieldControlProps, type FieldMessage, Form, FormProvider, type SubmitActionOptions, type SubmitActionSchema, type UseFieldControlOptions, ValidationError, type ValidationMode, applyActionError, applyValidationError, blurThenChangeLogic, createZenoForm, submitAction, toActionError, toFieldName, useFieldContext, useFieldControl, useFormContext, useHideFieldErrors, useIsFieldRequired, useIsFieldRequiredBySchema, useIsInvalid, useSelector };
14
+ export { type ActionError, type ActionIssue, type ActionResult, type FieldControl, type FieldControlProps, type FieldMessage, Form, FormProvider, type SubmitActionOptions, type SubmitActionSchema, type UseFieldControlOptions, ValidationError, type ValidationMode, applyActionError, applyValidationError, blurThenChangeLogic, createZenoForm, submitAction, toActionError, toFieldName, useFieldContext, useFieldControl, useFormContext, useHideFieldErrors, useIsFieldRequired, useIsFieldRequiredBySchema, useIsFormDisabled, useIsInvalid, useSelector };
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { applyValidationError } from "./lib/apply-validation-error.mjs";
2
2
  import { useFieldContext, useFormContext } from "./lib/contexts.mjs";
3
3
  import { toActionError, toFieldName } from "./lib/action-result.mjs";
4
+ import { useIsFormDisabled } from "./lib/form-disabled.mjs";
4
5
  import { ValidationError } from "./lib/validation-error.mjs";
5
6
  import { blurThenChangeLogic } from "./lib/validation-logic.mjs";
6
7
  import { createZenoForm } from "./create-zeno-form.mjs";
@@ -9,4 +10,4 @@ import { applyActionError, submitAction } from "./lib/submit-action.mjs";
9
10
  import { useHideFieldErrors, useIsFieldRequired, useIsFieldRequiredBySchema, useIsInvalid } from "./lib/use-is-invalid.mjs";
10
11
  import { useFieldControl } from "./lib/use-field-control.mjs";
11
12
  import { useSelector } from "@tanstack/react-form";
12
- export { Form, FormProvider, ValidationError, applyActionError, applyValidationError, blurThenChangeLogic, createZenoForm, submitAction, toActionError, toFieldName, useFieldContext, useFieldControl, useFormContext, useHideFieldErrors, useIsFieldRequired, useIsFieldRequiredBySchema, useIsInvalid, useSelector };
13
+ export { Form, FormProvider, ValidationError, applyActionError, applyValidationError, blurThenChangeLogic, createZenoForm, submitAction, toActionError, toFieldName, useFieldContext, useFieldControl, useFormContext, useHideFieldErrors, useIsFieldRequired, useIsFieldRequiredBySchema, useIsFormDisabled, useIsInvalid, useSelector };
@@ -0,0 +1,24 @@
1
+ //#region src/lib/date-value.d.ts
2
+ /**
3
+ * What a date field stores for a picked day:
4
+ * - `"date"`: a `Date`, at local midnight.
5
+ * - `"iso-date"`: the day as `YYYY-MM-DD`, as `z.iso.date()` accepts and a
6
+ * `date` column stores it.
7
+ * - `"iso-datetime"`: the day's local midnight as an ISO 8601 timestamp in
8
+ * UTC (`toISOString()`), as `z.iso.datetime()` accepts and a timestamp
9
+ * column takes it.
10
+ */
11
+ type DateValueType = "date" | "iso-date" | "iso-datetime";
12
+ /**
13
+ * The day a stored value stands for, as a local `Date`, or `undefined` for an
14
+ * empty or unreadable one. Reads a `Date`, a `YYYY-MM-DD` string as that day
15
+ * in local time, and any other string `Date` parses, such as an ISO 8601
16
+ * timestamp.
17
+ */
18
+ declare function parseDateValue(value: unknown): Date | undefined;
19
+ /** The day of a local `Date` as `YYYY-MM-DD`, without going through UTC. */
20
+ declare function formatIsoDate(date: Date): string;
21
+ /** The value to store for `date`, a day picked in local time. */
22
+ declare function toDateValue(date: Date, type: DateValueType): Date | string;
23
+ //#endregion
24
+ export { type DateValueType, formatIsoDate, parseDateValue, toDateValue };
@@ -0,0 +1,37 @@
1
+ //#region src/lib/date-value.ts
2
+ const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
3
+ function pad(value, length) {
4
+ return String(value).padStart(length, "0");
5
+ }
6
+ function parseIsoDate(value) {
7
+ const match = ISO_DATE.exec(value);
8
+ if (!match) return;
9
+ const [year, month, day] = match.slice(1).map(Number);
10
+ const date = new Date(year, month - 1, day);
11
+ date.setFullYear(year, month - 1, day);
12
+ return date.getMonth() === month - 1 ? date : void 0;
13
+ }
14
+ /**
15
+ * The day a stored value stands for, as a local `Date`, or `undefined` for an
16
+ * empty or unreadable one. Reads a `Date`, a `YYYY-MM-DD` string as that day
17
+ * in local time, and any other string `Date` parses, such as an ISO 8601
18
+ * timestamp.
19
+ */
20
+ function parseDateValue(value) {
21
+ let date;
22
+ if (value instanceof Date) date = value;
23
+ else if (typeof value === "string" && value !== "") date = ISO_DATE.test(value) ? parseIsoDate(value) : new Date(value);
24
+ return date && !Number.isNaN(date.getTime()) ? date : void 0;
25
+ }
26
+ /** The day of a local `Date` as `YYYY-MM-DD`, without going through UTC. */
27
+ function formatIsoDate(date) {
28
+ return `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1, 2)}-${pad(date.getDate(), 2)}`;
29
+ }
30
+ /** The value to store for `date`, a day picked in local time. */
31
+ function toDateValue(date, type) {
32
+ if (type === "iso-date") return formatIsoDate(date);
33
+ if (type === "iso-datetime") return date.toISOString();
34
+ return date;
35
+ }
36
+ //#endregion
37
+ export { formatIsoDate, parseDateValue, toDateValue };
@@ -0,0 +1,7 @@
1
+ //#region src/lib/default-props.d.ts
2
+ /**
3
+ * The props a component renders with: `props` over `defaults`. A prop passed
4
+ * as `undefined` keeps its default, as a destructuring default would.
5
+ */
6
+ export declare function withDefaultProps<P extends object>(defaults: Partial<P> | undefined, props: P): P;
7
+ //#endregion
@@ -0,0 +1,13 @@
1
+ //#region src/lib/default-props.ts
2
+ /**
3
+ * The props a component renders with: `props` over `defaults`. A prop passed
4
+ * as `undefined` keeps its default, as a destructuring default would.
5
+ */
6
+ function withDefaultProps(defaults, props) {
7
+ if (defaults === void 0) return props;
8
+ const merged = { ...defaults };
9
+ for (const [key, value] of Object.entries(props)) if (value !== void 0) merged[key] = value;
10
+ return merged;
11
+ }
12
+ //#endregion
13
+ export { withDefaultProps };
@@ -44,14 +44,38 @@ function emptyValueFor(schema, values, name, fallback) {
44
44
  function isTypeEmpty(value) {
45
45
  return value === "" || Array.isArray(value) && value.length === 0;
46
46
  }
47
+ function isUnder(name, prefix) {
48
+ return name === prefix || name.startsWith(`${prefix}.`) || name.startsWith(`${prefix}[`);
49
+ }
50
+ function emptiableAncestor(schema, values, keys) {
51
+ for (let length = 1; length < keys.length; length += 1) {
52
+ const prefix = keys.slice(0, length);
53
+ const start = valueAt(values, prefix);
54
+ if (typeof start !== "object" || start === null || Array.isArray(start)) continue;
55
+ const empty = emptyValueFor(schema, values, toFieldName(prefix), start);
56
+ if (empty === null || empty === void 0) return {
57
+ keys: prefix,
58
+ value: empty
59
+ };
60
+ }
61
+ return null;
62
+ }
47
63
  function withEmptyStartValues(schema, defaults) {
48
64
  const issues = validateSync(schema, defaults);
49
65
  let result = defaults;
66
+ const emptied = [];
50
67
  for (const issue of issues ?? []) {
51
68
  const name = toFieldName(issue.path);
52
69
  const keys = toPathKeys(name);
70
+ if (keys.length === 0 || emptied.some((prefix) => isUnder(name, prefix))) continue;
71
+ const ancestor = emptiableAncestor(schema, result, keys);
72
+ if (ancestor) {
73
+ result = withValueAt(result, ancestor.keys, ancestor.value);
74
+ emptied.push(toFieldName(ancestor.keys));
75
+ continue;
76
+ }
53
77
  const start = valueAt(defaults, keys);
54
- if (keys.length === 0 || !isTypeEmpty(start)) continue;
78
+ if (!isTypeEmpty(start)) continue;
55
79
  const empty = emptyValueFor(schema, result, name, start);
56
80
  if (empty !== start) result = withValueAt(result, keys, empty);
57
81
  }
@@ -0,0 +1,28 @@
1
+ import { AnyFormApi } from "@tanstack/react-form";
2
+ //#region src/lib/form-disabled.d.ts
3
+ /** Apply `useForm({ disabled })` to `form`. */
4
+ declare function useDisabledOption(form: AnyFormApi, disabled: boolean): void;
5
+ type FormDisabledScope = {
6
+ readonly disabled: boolean;
7
+ /** The `store` of the form it disables. */
8
+ readonly key: object;
9
+ };
10
+ /**
11
+ * The disabled state `FormProvider` and `<Form>` give what they wrap, for
12
+ * their own form only: a field of another form rendered inside (a quick-add
13
+ * popover) keeps that form's state.
14
+ */
15
+ declare const FormDisabledContext: import("react").Context<FormDisabledScope | null>;
16
+ /**
17
+ * Whether the form is disabled, by `useForm({ disabled })`,
18
+ * `<FormProvider disabled>`, `<Form disabled>` or `FormDialog`'s `disabled`.
19
+ * Every kit field reads it, so a control the fieldset of a disabled dialog
20
+ * can't reach (a checkbox, a switch, a radio, a slider) turns off too.
21
+ *
22
+ * Reads the form of the nearest provider; inside a field, pass `field.form`.
23
+ * `useForm({ disabled })` applies even where no provider of the form is
24
+ * mounted.
25
+ */
26
+ declare function useIsFormDisabled(form?: AnyFormApi): boolean;
27
+ //#endregion
28
+ export { FormDisabledContext, useDisabledOption, useIsFormDisabled };
@@ -0,0 +1,54 @@
1
+ "use client";
2
+ import { formContext } from "./contexts.mjs";
3
+ import { createContext, useCallback, useContext, useLayoutEffect, useSyncExternalStore } from "react";
4
+ //#region src/lib/form-disabled.ts
5
+ const disabledOption = /* @__PURE__ */ new WeakMap();
6
+ const listeners = /* @__PURE__ */ new WeakMap();
7
+ function subscribe(key, listener) {
8
+ let set = listeners.get(key);
9
+ if (!set) {
10
+ set = /* @__PURE__ */ new Set();
11
+ listeners.set(key, set);
12
+ }
13
+ set.add(listener);
14
+ return () => {
15
+ set.delete(listener);
16
+ };
17
+ }
18
+ /** Apply `useForm({ disabled })` to `form`. */
19
+ function useDisabledOption(form, disabled) {
20
+ const key = form.store;
21
+ if (!disabledOption.has(key)) disabledOption.set(key, disabled);
22
+ useLayoutEffect(() => {
23
+ if (disabledOption.get(key) === disabled) return;
24
+ disabledOption.set(key, disabled);
25
+ for (const listener of listeners.get(key) ?? []) listener();
26
+ }, [key, disabled]);
27
+ }
28
+ /**
29
+ * The disabled state `FormProvider` and `<Form>` give what they wrap, for
30
+ * their own form only: a field of another form rendered inside (a quick-add
31
+ * popover) keeps that form's state.
32
+ */
33
+ const FormDisabledContext = createContext(null);
34
+ /**
35
+ * Whether the form is disabled, by `useForm({ disabled })`,
36
+ * `<FormProvider disabled>`, `<Form disabled>` or `FormDialog`'s `disabled`.
37
+ * Every kit field reads it, so a control the fieldset of a disabled dialog
38
+ * can't reach (a checkbox, a switch, a radio, a slider) turns off too.
39
+ *
40
+ * Reads the form of the nearest provider; inside a field, pass `field.form`.
41
+ * `useForm({ disabled })` applies even where no provider of the form is
42
+ * mounted.
43
+ */
44
+ function useIsFormDisabled(form) {
45
+ const scope = useContext(FormDisabledContext);
46
+ const contextForm = useContext(formContext);
47
+ const key = (form ?? contextForm)?.store;
48
+ const subscribeToKey = useCallback((listener) => key ? subscribe(key, listener) : () => null, [key]);
49
+ const getSnapshot = () => key ? disabledOption.get(key) ?? false : false;
50
+ const byOption = useSyncExternalStore(subscribeToKey, getSnapshot, getSnapshot);
51
+ return scope?.disabled === true && (key === void 0 || scope.key === key) || byOption;
52
+ }
53
+ //#endregion
54
+ export { FormDisabledContext, useDisabledOption, useIsFormDisabled };
@@ -0,0 +1,45 @@
1
+ //#region src/lib/item-options.d.ts
2
+ /** An item whose value isn't its label. */
3
+ type ItemObject<V> = {
4
+ value: V;
5
+ label: string;
6
+ };
7
+ /** Items under a heading. */
8
+ type ItemGroup<T> = {
9
+ label: string;
10
+ items: readonly T[];
11
+ };
12
+ /** An item of `items`, or of one of its groups. */
13
+ type ItemOption<T> = T extends ItemGroup<infer I> ? I : T;
14
+ /** What picking an item stores: `item.value` for an object, else the item. */
15
+ type ItemValue<T> = T extends ItemGroup<infer I> ? ItemValue<I> : T extends ItemObject<infer V> ? V : T;
16
+ /** Whether an item's value is a stored one, as `isItemEqualToValue` says. */
17
+ type ValueComparator = (itemValue: unknown, value: unknown) => boolean;
18
+ declare function isItemObject(item: unknown): item is ItemObject<unknown>;
19
+ /** What Base UI takes for a group: an object with an `items` array. */
20
+ declare function isItemGroup(item: unknown): item is ItemGroup<unknown>;
21
+ declare function itemValue(item: unknown): unknown;
22
+ declare function itemLabel(item: unknown): string;
23
+ /** A React key per value: `1` and `"1"` get two, an object gets its JSON. */
24
+ declare function valueKey(value: unknown): string;
25
+ type ItemList<T> = {
26
+ /**
27
+ * `items` as Base UI takes them. It tells a grouped list by its first entry
28
+ * alone, so once there is a group, each run of plain items beside one goes
29
+ * under a group with no heading (`label: ""`).
30
+ */
31
+ readonly items: readonly T[];
32
+ /** Every option, the grouped ones included, in order. */
33
+ readonly options: readonly ItemOption<T>[];
34
+ /** The first option of each `valueKey`, so finding a value is no scan. */
35
+ readonly byKey: ReadonlyMap<string, ItemOption<T>>;
36
+ };
37
+ declare function toItemList<T>(items: readonly T[]): ItemList<T>;
38
+ /**
39
+ * `Object.is`, then `isItemEqualToValue`, which never sees an empty value.
40
+ */
41
+ declare function isSameValue(a: unknown, b: unknown, isItemEqualToValue: ValueComparator | undefined): boolean;
42
+ /** The option whose value is `value`, if `items` has it. */
43
+ declare function findOption<T>(list: ItemList<T>, value: unknown, isItemEqualToValue: ValueComparator | undefined): ItemOption<T> | undefined;
44
+ //#endregion
45
+ export { type ItemGroup, type ItemList, type ItemObject, type ItemOption, type ItemValue, type ValueComparator, findOption, isItemGroup, isItemObject, isSameValue, itemLabel, itemValue, toItemList, valueKey };
@@ -0,0 +1,70 @@
1
+ //#region src/lib/item-options.ts
2
+ function isItemObject(item) {
3
+ return typeof item === "object" && item !== null && "value" in item && "label" in item;
4
+ }
5
+ /** What Base UI takes for a group: an object with an `items` array. */
6
+ function isItemGroup(item) {
7
+ return typeof item === "object" && item !== null && Array.isArray(item.items);
8
+ }
9
+ function itemValue(item) {
10
+ return isItemObject(item) ? item.value : item;
11
+ }
12
+ function itemLabel(item) {
13
+ return isItemObject(item) ? item.label : String(item);
14
+ }
15
+ /** A React key per value: `1` and `"1"` get two, an object gets its JSON. */
16
+ function valueKey(value) {
17
+ if (typeof value !== "object" || value === null) return `${typeof value}:${String(value)}`;
18
+ try {
19
+ return `object:${JSON.stringify(value)}`;
20
+ } catch {
21
+ return `object:${String(value)}`;
22
+ }
23
+ }
24
+ function toItemList(items) {
25
+ const hasGroup = items.some(isItemGroup);
26
+ const options = [];
27
+ const groups = [];
28
+ let run;
29
+ for (const item of items) if (isItemGroup(item)) {
30
+ for (const option of item.items) options.push(option);
31
+ groups.push(item);
32
+ run = void 0;
33
+ } else {
34
+ options.push(item);
35
+ if (hasGroup && !run) {
36
+ run = [];
37
+ groups.push({
38
+ items: run,
39
+ label: ""
40
+ });
41
+ }
42
+ run?.push(item);
43
+ }
44
+ const byKey = /* @__PURE__ */ new Map();
45
+ for (const option of options) {
46
+ const key = valueKey(itemValue(option));
47
+ if (!byKey.has(key)) byKey.set(key, option);
48
+ }
49
+ return {
50
+ byKey,
51
+ items: hasGroup ? groups : items,
52
+ options
53
+ };
54
+ }
55
+ /**
56
+ * `Object.is`, then `isItemEqualToValue`, which never sees an empty value.
57
+ */
58
+ function isSameValue(a, b, isItemEqualToValue) {
59
+ if (Object.is(a, b)) return true;
60
+ if (a === null || a === void 0 || b === null || b === void 0) return false;
61
+ return isItemEqualToValue?.(a, b) ?? false;
62
+ }
63
+ /** The option whose value is `value`, if `items` has it. */
64
+ function findOption(list, value, isItemEqualToValue) {
65
+ const hit = list.byKey.get(valueKey(value));
66
+ if (hit !== void 0 && isSameValue(itemValue(hit), value, isItemEqualToValue)) return hit;
67
+ return list.options.find((option) => isSameValue(itemValue(option), value, isItemEqualToValue));
68
+ }
69
+ //#endregion
70
+ export { findOption, isItemGroup, isItemObject, isSameValue, itemLabel, itemValue, toItemList, valueKey };
@@ -102,6 +102,22 @@ function settleEmptied(emptied, failing, probe, required, cleared) {
102
102
  next
103
103
  };
104
104
  }
105
+ function clearNullableObjects(schema, filled, probe, required, cleared) {
106
+ for (const fill of filled) {
107
+ if (isEmptyProbe(fill.value) || cleared.has(fill.path)) continue;
108
+ fillAt(probe, fill.keys, null, true);
109
+ const issues = validateSync(schema, probe);
110
+ fillAt(probe, fill.keys, fill.value, true);
111
+ const rejected = issues?.some((issue) => {
112
+ const path = joinPath(issue.path?.map(pathKey) ?? []);
113
+ return path === fill.path || path.startsWith(`${fill.path}.`) || path.startsWith(`${fill.path}[`);
114
+ });
115
+ if (issues && !rejected) {
116
+ required.delete(fill.path);
117
+ cleared.add(fill.path);
118
+ }
119
+ }
120
+ }
105
121
  function getRequiredPaths(schema) {
106
122
  const required = /* @__PURE__ */ new Set();
107
123
  const cleared = /* @__PURE__ */ new Set();
@@ -111,6 +127,7 @@ function getRequiredPaths(schema) {
111
127
  const issues = validateSync(schema, probe);
112
128
  if (!issues) return required;
113
129
  const { failing, filled } = recordPass(issues, probe, required, cleared);
130
+ clearNullableObjects(schema, filled, probe, required, cleared);
114
131
  const settled = settleEmptied(emptied, failing, probe, required, cleared);
115
132
  emptied = [...filled.filter(({ value }) => isEmptyProbe(value)), ...settled.next];
116
133
  if (filled.length === 0 && !settled.descended) break;