@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.
- package/dist/create-zeno-form.d.mts +27 -6
- package/dist/create-zeno-form.mjs +29 -6
- package/dist/form-element.d.mts +16 -3
- package/dist/form-element.mjs +38 -18
- package/dist/index.d.mts +3 -1
- package/dist/index.mjs +3 -1
- package/dist/lib/date-value.d.mts +24 -0
- package/dist/lib/date-value.mjs +37 -0
- package/dist/lib/default-props.d.mts +7 -0
- package/dist/lib/default-props.mjs +13 -0
- package/dist/lib/empty-value.mjs +25 -1
- package/dist/lib/form-disabled.d.mts +28 -0
- package/dist/lib/form-disabled.mjs +54 -0
- package/dist/lib/item-options.d.mts +45 -0
- package/dist/lib/item-options.mjs +70 -0
- package/dist/lib/schema-required.mjs +17 -0
- package/dist/lib/submit-action.d.mts +8 -2
- package/dist/lib/submit-action.mjs +4 -2
- package/dist/lib/use-field-control.d.mts +83 -0
- package/dist/lib/use-field-control.mjs +70 -0
- package/package.json +1 -1
- package/src/control-field.test.tsx +338 -0
- package/src/create-form.tsx +36 -28
- package/src/create-zeno-form.tsx +144 -52
- package/src/date-picker-field.test.tsx +108 -0
- package/src/default-props.test-d.ts +66 -0
- package/src/default-props.test.tsx +165 -0
- package/src/fields/checkbox-field.tsx +5 -0
- package/src/fields/checkbox-group-field.tsx +6 -2
- package/src/fields/combobox-field.tsx +107 -40
- package/src/fields/control-field.tsx +57 -0
- package/src/fields/date-picker-field.tsx +34 -11
- package/src/fields/field-shell.tsx +80 -0
- package/src/fields/index.ts +2 -0
- package/src/fields/input-field.tsx +16 -48
- package/src/fields/multi-select-field.tsx +100 -42
- package/src/fields/otp-field.tsx +8 -2
- package/src/fields/radio-group-field.tsx +9 -1
- package/src/fields/reset-button.tsx +4 -0
- package/src/fields/select-field.tsx +9 -1
- package/src/fields/slider-field.tsx +9 -1
- package/src/fields/submit-button.tsx +3 -1
- package/src/fields/switch-field.tsx +5 -0
- package/src/fields/textarea-field.tsx +16 -48
- package/src/form-dialog.test.tsx +102 -1
- package/src/form-dialog.tsx +143 -42
- package/src/form-disabled.test.tsx +329 -0
- package/src/form-element.tsx +64 -29
- package/src/grouped-options.test-d.ts +59 -0
- package/src/grouped-options.test.tsx +251 -0
- package/src/index.ts +7 -0
- package/src/lib/date-value.test.ts +14 -0
- package/src/lib/date-value.ts +70 -0
- package/src/lib/default-props.ts +19 -0
- package/src/lib/empty-value.test.ts +19 -0
- package/src/lib/empty-value.ts +45 -1
- package/src/lib/form-disabled.ts +95 -0
- package/src/lib/item-options.test.ts +11 -0
- package/src/lib/item-options.ts +165 -0
- package/src/lib/schema-required.test.ts +9 -0
- package/src/lib/schema-required.ts +33 -0
- package/src/lib/submit-action.ts +18 -4
- package/src/lib/use-field-control.ts +191 -0
- package/src/submit-action.test.tsx +103 -0
- package/src/use-app-fields.test-d.ts +51 -1
- package/src/with-form.test-d.ts +76 -0
- 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
|
-
|
|
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 {
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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> =
|
|
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
|
|
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) =>
|
|
119
|
-
.filter((
|
|
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
|
|
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={
|
|
131
|
-
items={items}
|
|
183
|
+
disabled={isDisabled}
|
|
184
|
+
items={list.items}
|
|
132
185
|
multiple
|
|
133
|
-
onValueChange={
|
|
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((
|
|
143
|
-
<ComboboxChip key={
|
|
144
|
-
{itemLabel(
|
|
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 && !
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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 {
|
|
253
|
+
export type {
|
|
254
|
+
MultiSelectFieldProps,
|
|
255
|
+
MultiSelectItemGroup,
|
|
256
|
+
MultiSelectItemObject,
|
|
257
|
+
MultiSelectValue,
|
|
258
|
+
}
|
|
201
259
|
export { MultiSelectField }
|
package/src/fields/otp-field.tsx
CHANGED
|
@@ -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
|
|
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={
|
|
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
|
|
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
|
|
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
|
|
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={
|
|
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}
|