@zeno-lib/forms 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/create-zeno-form.d.mts +27 -6
  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 +3 -1
  6. package/dist/index.mjs +3 -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/submit-action.d.mts +8 -2
  18. package/dist/lib/submit-action.mjs +4 -2
  19. package/dist/lib/use-field-control.d.mts +83 -0
  20. package/dist/lib/use-field-control.mjs +70 -0
  21. package/package.json +1 -1
  22. package/src/control-field.test.tsx +338 -0
  23. package/src/create-form.tsx +36 -28
  24. package/src/create-zeno-form.tsx +144 -52
  25. package/src/date-picker-field.test.tsx +108 -0
  26. package/src/default-props.test-d.ts +66 -0
  27. package/src/default-props.test.tsx +165 -0
  28. package/src/fields/checkbox-field.tsx +5 -0
  29. package/src/fields/checkbox-group-field.tsx +6 -2
  30. package/src/fields/combobox-field.tsx +107 -40
  31. package/src/fields/control-field.tsx +57 -0
  32. package/src/fields/date-picker-field.tsx +34 -11
  33. package/src/fields/field-shell.tsx +80 -0
  34. package/src/fields/index.ts +2 -0
  35. package/src/fields/input-field.tsx +16 -48
  36. package/src/fields/multi-select-field.tsx +100 -42
  37. package/src/fields/otp-field.tsx +8 -2
  38. package/src/fields/radio-group-field.tsx +9 -1
  39. package/src/fields/reset-button.tsx +4 -0
  40. package/src/fields/select-field.tsx +9 -1
  41. package/src/fields/slider-field.tsx +9 -1
  42. package/src/fields/submit-button.tsx +3 -1
  43. package/src/fields/switch-field.tsx +5 -0
  44. package/src/fields/textarea-field.tsx +16 -48
  45. package/src/form-dialog.test.tsx +102 -1
  46. package/src/form-dialog.tsx +143 -42
  47. package/src/form-disabled.test.tsx +329 -0
  48. package/src/form-element.tsx +64 -29
  49. package/src/grouped-options.test-d.ts +59 -0
  50. package/src/grouped-options.test.tsx +251 -0
  51. package/src/index.ts +7 -0
  52. package/src/lib/date-value.test.ts +14 -0
  53. package/src/lib/date-value.ts +70 -0
  54. package/src/lib/default-props.ts +19 -0
  55. package/src/lib/empty-value.test.ts +19 -0
  56. package/src/lib/empty-value.ts +45 -1
  57. package/src/lib/form-disabled.ts +95 -0
  58. package/src/lib/item-options.test.ts +11 -0
  59. package/src/lib/item-options.ts +165 -0
  60. package/src/lib/schema-required.test.ts +9 -0
  61. package/src/lib/schema-required.ts +33 -0
  62. package/src/lib/submit-action.ts +18 -4
  63. package/src/lib/use-field-control.ts +191 -0
  64. package/src/submit-action.test.tsx +103 -0
  65. package/src/use-app-fields.test-d.ts +51 -1
  66. package/src/with-form.test-d.ts +76 -0
  67. package/src/with-form.test.tsx +60 -0
@@ -1,23 +1,11 @@
1
1
  "use client"
2
2
 
3
- import { describedBy } from "@zeno-lib/forms/lib/aria"
4
3
  import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
5
- import {
6
- useHideFieldErrors,
7
- useIsFieldRequired,
8
- useIsFieldRequiredBySchema,
9
- useIsInvalid,
10
- } from "@zeno-lib/forms/lib/use-is-invalid"
11
- import { Children, type ComponentProps, type ReactNode, useId } from "react"
12
- import {
13
- Field,
14
- FieldDescription,
15
- FieldError,
16
- FieldLabel,
17
- } from "@/components/ui/field"
4
+ import { useFieldControl } from "@zeno-lib/forms/lib/use-field-control"
5
+ import { Children, type ComponentProps, type ReactNode } from "react"
18
6
  import { Input } from "@/components/ui/input"
19
7
  import { InputGroup, InputGroupInput } from "@/components/ui/input-group"
20
- import { RequiredIndicator } from "../lib/required-indicator"
8
+ import { FieldShell } from "./field-shell"
21
9
 
22
10
  type InputFieldProps = Omit<
23
11
  ComponentProps<typeof Input>,
@@ -36,33 +24,25 @@ type InputFieldProps = Omit<
36
24
  function InputField({
37
25
  children,
38
26
  description,
27
+ disabled,
39
28
  label,
40
29
  required,
41
30
  ...props
42
31
  }: InputFieldProps) {
43
32
  const field = useFieldContext<string>()
44
- const id = useId()
45
- const errorId = `${id}-error`
46
- const descriptionId = `${id}-description`
47
- const isInvalid = useIsInvalid(field)
48
- const hideErrors = useHideFieldErrors(field)
49
- const showError = isInvalid && !hideErrors
33
+ // The label names the input through `htmlFor`, so the input needs no
34
+ // `aria-labelledby` and the hook doesn't get `label`.
35
+ const { controlProps, ...shell } = useFieldControl<string>({
36
+ description,
37
+ disabled,
38
+ required,
39
+ })
50
40
  const hasAddons = Children.count(children) > 0
51
- const schemaRequired = useIsFieldRequired(field)
52
- const isRequired = required ?? schemaRequired
53
- const schemaRequiresValue = useIsFieldRequiredBySchema(field)
54
- const requiresValue = required ?? schemaRequiresValue
55
41
 
42
+ // A text input keeps the `""` the user typed, where `controlProps.onChange`
43
+ // would store the field's empty value.
56
44
  const inputProps = {
57
- "aria-describedby": describedBy(
58
- [description, descriptionId],
59
- [showError, errorId]
60
- ),
61
- "aria-invalid": isInvalid || undefined,
62
- "aria-required": requiresValue || undefined,
63
- id,
64
- name: field.name,
65
- onBlur: field.handleBlur,
45
+ ...controlProps,
66
46
  onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
67
47
  field.handleChange(event.target.value),
68
48
  value: field.state.value ?? "",
@@ -70,13 +50,7 @@ function InputField({
70
50
  }
71
51
 
72
52
  return (
73
- <Field data-field={field.name} data-invalid={isInvalid}>
74
- {label && (
75
- <FieldLabel htmlFor={id}>
76
- {label}
77
- {isRequired && <RequiredIndicator />}
78
- </FieldLabel>
79
- )}
53
+ <FieldShell {...shell} description={description} label={label}>
80
54
  {hasAddons ? (
81
55
  <InputGroup>
82
56
  <InputGroupInput {...inputProps} />
@@ -85,13 +59,7 @@ function InputField({
85
59
  ) : (
86
60
  <Input {...inputProps} />
87
61
  )}
88
- {description && (
89
- <FieldDescription id={descriptionId}>{description}</FieldDescription>
90
- )}
91
- {showError && (
92
- <FieldError errors={field.state.meta.errors} id={errorId} />
93
- )}
94
- </Field>
62
+ </FieldShell>
95
63
  )
96
64
  }
97
65
 
@@ -2,6 +2,21 @@
2
2
 
3
3
  import { describedBy } from "@zeno-lib/forms/lib/aria"
4
4
  import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
5
+ import { useIsFormDisabled } from "@zeno-lib/forms/lib/form-disabled"
6
+ import {
7
+ findOption,
8
+ type ItemGroup,
9
+ type ItemObject,
10
+ type ItemOption,
11
+ type ItemValue,
12
+ isItemGroup,
13
+ isSameValue,
14
+ itemLabel,
15
+ itemValue,
16
+ toItemList,
17
+ type ValueComparator,
18
+ valueKey,
19
+ } from "@zeno-lib/forms/lib/item-options"
5
20
  import {
6
21
  useHideFieldErrors,
7
22
  useIsFieldRequired,
@@ -9,16 +24,19 @@ import {
9
24
  useIsInvalid,
10
25
  } from "@zeno-lib/forms/lib/use-is-invalid"
11
26
  import { XIcon } from "lucide-react"
12
- import { type ReactNode, useId } from "react"
27
+ import { type ReactNode, useId, useMemo } from "react"
13
28
  import { Button } from "@/components/ui/button"
14
29
  import {
15
30
  Combobox,
16
31
  ComboboxChip,
17
32
  ComboboxChips,
18
33
  ComboboxChipsInput,
34
+ ComboboxCollection,
19
35
  ComboboxContent,
20
36
  ComboboxEmpty,
37
+ ComboboxGroup,
21
38
  ComboboxItem,
39
+ ComboboxLabel,
22
40
  ComboboxList,
23
41
  ComboboxValue,
24
42
  useComboboxAnchor,
@@ -31,16 +49,23 @@ import {
31
49
  } from "@/components/ui/field"
32
50
  import { RequiredIndicator } from "../lib/required-indicator"
33
51
 
34
- type MultiSelectItemObject<V> = { value: V; label: string }
52
+ type MultiSelectItemObject<V> = ItemObject<V>
53
+
54
+ /** Items under a heading. */
55
+ type MultiSelectItemGroup<T> = ItemGroup<T>
56
+
57
+ /** An item of `items`, or of one of its groups. */
58
+ type MultiSelectOption<T> = ItemOption<T>
35
59
 
36
60
  /** The value stored per selected item: `item.value` for objects, else the item. */
37
- type MultiSelectValue<T> = T extends MultiSelectItemObject<infer V> ? V : T
61
+ type MultiSelectValue<T> = ItemValue<T>
38
62
 
39
63
  type MultiSelectFieldProps<T = string> = {
40
64
  /**
41
65
  * Options shown in the dropdown. Pass plain strings or numbers (value ===
42
66
  * label), or `{ value, label }` objects. The form value is an array of the
43
- * selected values (`item.value` for objects).
67
+ * selected values (`item.value` for objects). To show them under headings,
68
+ * pass `{ label, items }` groups instead.
44
69
  */
45
70
  items: readonly T[]
46
71
  description?: ReactNode
@@ -50,7 +75,18 @@ type MultiSelectFieldProps<T = string> = {
50
75
  /** Message shown when filtering produces zero matches. */
51
76
  emptyMessage?: ReactNode
52
77
  /** Override per-row rendering. Default: the item's label. */
53
- renderItem?: (item: T) => ReactNode
78
+ renderItem?: (item: MultiSelectOption<T>) => ReactNode
79
+ /**
80
+ * Whether an item's value is a stored value. Defaults to `Object.is`. Pass
81
+ * it when the values are objects (`{ kind, id }`), which a reload or a
82
+ * refetch rebuilds: `(a, b) => a.kind === b.kind && a.id === b.id`. Never
83
+ * called with an empty value.
84
+ */
85
+ // biome-ignore lint/style/useConsistentMethodSignatures: a method's parameters are bivariant, so a comparator typed with the value fits `form.<Field>`, whose props see the items as `unknown`.
86
+ isItemEqualToValue?(
87
+ itemValue: MultiSelectValue<T>,
88
+ value: MultiSelectValue<T>
89
+ ): boolean
54
90
  /** Show a button that clears every selection. Defaults to `true`. */
55
91
  showClear?: boolean
56
92
  /** Accessible label of the clear button. Defaults to `"Clear"`. */
@@ -64,23 +100,6 @@ type MultiSelectFieldProps<T = string> = {
64
100
  required?: boolean
65
101
  }
66
102
 
67
- function isItemObject(item: unknown): item is MultiSelectItemObject<unknown> {
68
- return (
69
- typeof item === "object" &&
70
- item !== null &&
71
- "value" in item &&
72
- "label" in item
73
- )
74
- }
75
-
76
- function itemValue(item: unknown): unknown {
77
- return isItemObject(item) ? item.value : item
78
- }
79
-
80
- function itemLabel(item: unknown): string {
81
- return isItemObject(item) ? item.label : String(item)
82
- }
83
-
84
103
  /**
85
104
  * Multi-value combobox: the selection renders as removable chips and the form
86
105
  * value is an array (`[]` when empty).
@@ -91,6 +110,7 @@ function MultiSelectField<T = string>({
91
110
  description,
92
111
  disabled,
93
112
  emptyMessage = "No results.",
113
+ isItemEqualToValue,
94
114
  items,
95
115
  label,
96
116
  placeholder,
@@ -99,6 +119,7 @@ function MultiSelectField<T = string>({
99
119
  showClear = true,
100
120
  }: MultiSelectFieldProps<T>) {
101
121
  const field = useFieldContext<MultiSelectValue<T>[] | null | undefined>()
122
+ const isDisabled = useIsFormDisabled(field.form) || Boolean(disabled)
102
123
  const anchor = useComboboxAnchor()
103
124
  const id = useId()
104
125
  const errorId = `${id}-error`
@@ -111,15 +132,47 @@ function MultiSelectField<T = string>({
111
132
  const schemaRequiresValue = useIsFieldRequiredBySchema(field)
112
133
  const requiresValue = required ?? schemaRequiresValue
113
134
 
135
+ const list = useMemo(() => toItemList(items), [items])
136
+ const isEqual = isItemEqualToValue as ValueComparator | undefined
137
+
114
138
  const values = field.state.value ?? []
115
139
  // Map stored values back to item identities, in selection order, so Base UI
116
140
  // compares by reference and the chips keep the order the user picked.
117
141
  const selected = values
118
- .map((value) => items.find((item) => Object.is(itemValue(item), value)))
119
- .filter((item): item is T => item !== undefined)
142
+ .map((value) => findOption(list, value, isEqual))
143
+ .filter((option): option is MultiSelectOption<T> => option !== undefined)
144
+
145
+ // Base UI only knows the values it has a chip for. Keep the others (their
146
+ // group still loading) where they were, so picking an option doesn't drop
147
+ // them.
148
+ const onValueChange = (next: MultiSelectOption<T>[]) => {
149
+ const picked = next.map(itemValue)
150
+ const kept = values.filter(
151
+ (value) =>
152
+ findOption(list, value, isEqual) === undefined ||
153
+ picked.some((other) => isSameValue(other, value, isEqual))
154
+ )
155
+ const added = picked.filter(
156
+ (other) => !values.some((value) => isSameValue(other, value, isEqual))
157
+ )
158
+ field.handleChange([...kept, ...added] as MultiSelectValue<T>[])
159
+ }
160
+
161
+ const renderOption = (option: MultiSelectOption<T>) =>
162
+ renderItem ? (
163
+ renderItem(option)
164
+ ) : (
165
+ <ComboboxItem key={valueKey(itemValue(option))} value={option}>
166
+ {itemLabel(option)}
167
+ </ComboboxItem>
168
+ )
120
169
 
121
170
  return (
122
- <Field data-field={field.name} data-invalid={isInvalid}>
171
+ <Field
172
+ data-disabled={isDisabled || undefined}
173
+ data-field={field.name}
174
+ data-invalid={isInvalid}
175
+ >
123
176
  {label && (
124
177
  <FieldLabel htmlFor={id}>
125
178
  {label}
@@ -127,21 +180,19 @@ function MultiSelectField<T = string>({
127
180
  </FieldLabel>
128
181
  )}
129
182
  <Combobox
130
- disabled={disabled}
131
- items={items}
183
+ disabled={isDisabled}
184
+ items={list.items}
132
185
  multiple
133
- onValueChange={(next: T[]) =>
134
- field.handleChange(next.map(itemValue) as MultiSelectValue<T>[])
135
- }
186
+ onValueChange={onValueChange}
136
187
  value={selected}
137
188
  >
138
189
  <ComboboxChips className={className} ref={anchor}>
139
190
  <ComboboxValue>
140
- {(chips: T[]) => (
191
+ {(chips: MultiSelectOption<T>[]) => (
141
192
  <>
142
- {chips.map((item) => (
143
- <ComboboxChip key={String(itemValue(item))}>
144
- {itemLabel(item)}
193
+ {chips.map((option) => (
194
+ <ComboboxChip key={valueKey(itemValue(option))}>
195
+ {itemLabel(option)}
145
196
  </ComboboxChip>
146
197
  ))}
147
198
  <ComboboxChipsInput
@@ -159,7 +210,7 @@ function MultiSelectField<T = string>({
159
210
  </>
160
211
  )}
161
212
  </ComboboxValue>
162
- {showClear && selected.length > 0 && !disabled && (
213
+ {showClear && selected.length > 0 && !isDisabled && (
163
214
  <Button
164
215
  aria-label={clearLabel}
165
216
  className="ml-auto"
@@ -175,13 +226,15 @@ function MultiSelectField<T = string>({
175
226
  <ComboboxContent anchor={anchor}>
176
227
  <ComboboxEmpty>{emptyMessage}</ComboboxEmpty>
177
228
  <ComboboxList>
178
- {(item: T) =>
179
- renderItem ? (
180
- renderItem(item)
229
+ {(item: T, index: number) =>
230
+ isItemGroup(item) ? (
231
+ // Two groups can share a heading, or have none.
232
+ <ComboboxGroup items={item.items} key={index}>
233
+ {item.label && <ComboboxLabel>{item.label}</ComboboxLabel>}
234
+ <ComboboxCollection>{renderOption}</ComboboxCollection>
235
+ </ComboboxGroup>
181
236
  ) : (
182
- <ComboboxItem key={String(itemValue(item))} value={item}>
183
- {itemLabel(item)}
184
- </ComboboxItem>
237
+ renderOption(item as MultiSelectOption<T>)
185
238
  )
186
239
  }
187
240
  </ComboboxList>
@@ -197,5 +250,10 @@ function MultiSelectField<T = string>({
197
250
  )
198
251
  }
199
252
 
200
- export type { MultiSelectFieldProps, MultiSelectItemObject, MultiSelectValue }
253
+ export type {
254
+ MultiSelectFieldProps,
255
+ MultiSelectItemGroup,
256
+ MultiSelectItemObject,
257
+ MultiSelectValue,
258
+ }
201
259
  export { MultiSelectField }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { describedBy } from "@zeno-lib/forms/lib/aria"
4
4
  import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
5
+ import { useIsFormDisabled } from "@zeno-lib/forms/lib/form-disabled"
5
6
  import {
6
7
  useHideFieldErrors,
7
8
  useIsFieldRequired,
@@ -61,6 +62,7 @@ function OtpField({
61
62
  required,
62
63
  }: OtpFieldProps) {
63
64
  const field = useFieldContext<string>()
65
+ const isDisabled = useIsFormDisabled(field.form) || Boolean(disabled)
64
66
  const id = useId()
65
67
  const errorId = `${id}-error`
66
68
  const descriptionId = `${id}-description`
@@ -73,7 +75,11 @@ function OtpField({
73
75
  const requiresValue = required ?? schemaRequiresValue
74
76
 
75
77
  return (
76
- <Field data-field={field.name} data-invalid={isInvalid}>
78
+ <Field
79
+ data-disabled={isDisabled || undefined}
80
+ data-field={field.name}
81
+ data-invalid={isInvalid}
82
+ >
77
83
  {label && (
78
84
  <FieldLabel htmlFor={id}>
79
85
  {label}
@@ -90,7 +96,7 @@ function OtpField({
90
96
  autoFocus={autoFocus}
91
97
  className={className}
92
98
  containerClassName={containerClassName}
93
- disabled={disabled}
99
+ disabled={isDisabled}
94
100
  id={id}
95
101
  maxLength={maxLength}
96
102
  name={field.name}
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { describedBy } from "@zeno-lib/forms/lib/aria"
4
4
  import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
5
+ import { useIsFormDisabled } from "@zeno-lib/forms/lib/form-disabled"
5
6
  import {
6
7
  useHideFieldErrors,
7
8
  useIsFieldRequired,
@@ -35,11 +36,13 @@ type RadioGroupFieldProps = Omit<
35
36
  function RadioGroupField({
36
37
  children,
37
38
  description,
39
+ disabled,
38
40
  label,
39
41
  required,
40
42
  ...props
41
43
  }: RadioGroupFieldProps) {
42
44
  const field = useFieldContext()
45
+ const isDisabled = useIsFormDisabled(field.form) || Boolean(disabled)
43
46
  const id = useId()
44
47
  const errorId = `${id}-error`
45
48
  const descriptionId = `${id}-description`
@@ -52,7 +55,11 @@ function RadioGroupField({
52
55
  const requiresValue = required ?? schemaRequiresValue
53
56
 
54
57
  return (
55
- <Field data-field={field.name} data-invalid={isInvalid}>
58
+ <Field
59
+ data-disabled={isDisabled || undefined}
60
+ data-field={field.name}
61
+ data-invalid={isInvalid}
62
+ >
56
63
  {label && (
57
64
  <FieldLabel>
58
65
  {label}
@@ -66,6 +73,7 @@ function RadioGroupField({
66
73
  )}
67
74
  aria-invalid={isInvalid || undefined}
68
75
  aria-required={requiresValue || undefined}
76
+ disabled={isDisabled}
69
77
  name={field.name}
70
78
  onValueChange={(value) => field.handleChange(value)}
71
79
  value={field.state.value ?? null}
@@ -1,6 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import { useFormContext } from "@zeno-lib/forms/lib/contexts"
4
+ import { useIsFormDisabled } from "@zeno-lib/forms/lib/form-disabled"
4
5
  import type { ComponentProps, ReactNode } from "react"
5
6
  import { Button } from "@/components/ui/button"
6
7
 
@@ -8,12 +9,15 @@ type ResetButtonProps = Omit<ComponentProps<typeof Button>, "onClick" | "type">
8
9
 
9
10
  function ResetButton({
10
11
  children,
12
+ disabled,
11
13
  variant = "outline",
12
14
  ...props
13
15
  }: ResetButtonProps) {
14
16
  const form = useFormContext()
17
+ const isDisabled = useIsFormDisabled(form) || Boolean(disabled)
15
18
  return (
16
19
  <Button
20
+ disabled={isDisabled}
17
21
  onClick={() => form.reset()}
18
22
  type="button"
19
23
  variant={variant}
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { describedBy } from "@zeno-lib/forms/lib/aria"
4
4
  import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
5
+ import { useIsFormDisabled } from "@zeno-lib/forms/lib/form-disabled"
5
6
  import {
6
7
  useHideFieldErrors,
7
8
  useIsFieldRequired,
@@ -44,6 +45,7 @@ type SelectFieldProps = Omit<
44
45
  function SelectField({
45
46
  children,
46
47
  description,
48
+ disabled,
47
49
  label,
48
50
  onOpenChange,
49
51
  placeholder,
@@ -53,6 +55,7 @@ function SelectField({
53
55
  ...props
54
56
  }: SelectFieldProps) {
55
57
  const field = useFieldContext()
58
+ const isDisabled = useIsFormDisabled(field.form) || Boolean(disabled)
56
59
  // Opening the popup moves focus into it, so the trigger's blur would mark
57
60
  // the field touched, and show a required error, before anything is picked.
58
61
  // The field is touched once the popup closes, or focus leaves it closed.
@@ -82,7 +85,11 @@ function SelectField({
82
85
  }, [children])
83
86
 
84
87
  return (
85
- <Field data-field={field.name} data-invalid={isInvalid}>
88
+ <Field
89
+ data-disabled={isDisabled || undefined}
90
+ data-field={field.name}
91
+ data-invalid={isInvalid}
92
+ >
86
93
  {label && (
87
94
  <FieldLabel htmlFor={id}>
88
95
  {label}
@@ -90,6 +97,7 @@ function SelectField({
90
97
  </FieldLabel>
91
98
  )}
92
99
  <Select
100
+ disabled={isDisabled}
93
101
  items={items}
94
102
  name={field.name}
95
103
  onOpenChange={(open, eventDetails) => {
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { describedBy } from "@zeno-lib/forms/lib/aria"
4
4
  import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
5
+ import { useIsFormDisabled } from "@zeno-lib/forms/lib/form-disabled"
5
6
  import {
6
7
  useHideFieldErrors,
7
8
  useIsFieldRequired,
@@ -37,12 +38,14 @@ type SliderFieldProps = Omit<
37
38
 
38
39
  function SliderField({
39
40
  description,
41
+ disabled,
40
42
  formatValue,
41
43
  label,
42
44
  required,
43
45
  ...props
44
46
  }: SliderFieldProps) {
45
47
  const field = useFieldContext<SliderValue>()
48
+ const isDisabled = useIsFormDisabled(field.form) || Boolean(disabled)
46
49
  const id = useId()
47
50
  const errorId = `${id}-error`
48
51
  const descriptionId = `${id}-description`
@@ -58,7 +61,11 @@ function SliderField({
58
61
  const hasReadout = readout !== null && readout !== undefined
59
62
 
60
63
  return (
61
- <Field data-field={field.name} data-invalid={isInvalid}>
64
+ <Field
65
+ data-disabled={isDisabled || undefined}
66
+ data-field={field.name}
67
+ data-invalid={isInvalid}
68
+ >
62
69
  {(label || hasReadout) && (
63
70
  <FieldContent className="flex-row items-center justify-between">
64
71
  {label && (
@@ -80,6 +87,7 @@ function SliderField({
80
87
  [showError, errorId]
81
88
  )}
82
89
  aria-invalid={isInvalid || undefined}
90
+ disabled={isDisabled}
83
91
  id={id}
84
92
  name={field.name}
85
93
  onBlur={field.handleBlur}
@@ -1,6 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import { useFormContext } from "@zeno-lib/forms/lib/contexts"
4
+ import { useIsFormDisabled } from "@zeno-lib/forms/lib/form-disabled"
4
5
  import type { ComponentProps } from "react"
5
6
  import { Button } from "@/components/ui/button"
6
7
  import { Spinner } from "@/components/ui/spinner"
@@ -9,13 +10,14 @@ type SubmitButtonProps = Omit<ComponentProps<typeof Button>, "type">
9
10
 
10
11
  function SubmitButton({ children, disabled, ...props }: SubmitButtonProps) {
11
12
  const form = useFormContext()
13
+ const isDisabled = useIsFormDisabled(form) || Boolean(disabled)
12
14
  return (
13
15
  <form.Subscribe
14
16
  selector={(state) => [state.canSubmit, state.isSubmitting] as const}
15
17
  >
16
18
  {([canSubmit, isSubmitting]) => (
17
19
  <Button
18
- disabled={disabled || !canSubmit || isSubmitting}
20
+ disabled={isDisabled || !canSubmit || isSubmitting}
19
21
  type="submit"
20
22
  {...props}
21
23
  >
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { describedBy } from "@zeno-lib/forms/lib/aria"
4
4
  import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
5
+ import { useIsFormDisabled } from "@zeno-lib/forms/lib/form-disabled"
5
6
  import {
6
7
  useHideFieldErrors,
7
8
  useIsFieldRequired,
@@ -34,11 +35,13 @@ type SwitchFieldProps = Omit<
34
35
 
35
36
  function SwitchField({
36
37
  description,
38
+ disabled,
37
39
  label,
38
40
  required,
39
41
  ...props
40
42
  }: SwitchFieldProps) {
41
43
  const field = useFieldContext<boolean>()
44
+ const isDisabled = useIsFormDisabled(field.form) || Boolean(disabled)
42
45
  const id = useId()
43
46
  const errorId = `${id}-error`
44
47
  const descriptionId = `${id}-description`
@@ -52,6 +55,7 @@ function SwitchField({
52
55
 
53
56
  return (
54
57
  <Field
58
+ data-disabled={isDisabled || undefined}
55
59
  data-field={field.name}
56
60
  data-invalid={isInvalid}
57
61
  orientation="horizontal"
@@ -78,6 +82,7 @@ function SwitchField({
78
82
  aria-invalid={isInvalid || undefined}
79
83
  aria-required={requiresValue || undefined}
80
84
  checked={field.state.value ?? false}
85
+ disabled={isDisabled}
81
86
  id={id}
82
87
  name={field.name}
83
88
  onBlur={field.handleBlur}