@zeno-lib/forms 0.0.0 → 0.1.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/package.json +21 -11
- package/src/addons/validation-spinner.tsx +3 -4
- package/src/create-form.tsx +49 -0
- package/src/create-zeno-form.tsx +531 -0
- package/src/fields/checkbox-field.tsx +15 -12
- package/src/fields/combobox-field.tsx +13 -14
- package/src/fields/date-picker-field.tsx +18 -15
- package/src/fields/input-field.tsx +12 -13
- package/src/fields/number-field.tsx +1 -2
- package/src/fields/otp-field.tsx +15 -12
- package/src/fields/radio-group-field.tsx +12 -13
- package/src/fields/reset-button.tsx +2 -3
- package/src/fields/select-field.tsx +12 -13
- package/src/fields/slider-field.tsx +11 -12
- package/src/fields/submit-button.tsx +3 -4
- package/src/fields/switch-field.tsx +15 -12
- package/src/fields/textarea-field.tsx +12 -13
- package/src/form-element.tsx +51 -0
- package/src/index.ts +14 -0
- package/src/lib/apply-validation-error.test.ts +1 -2
- package/src/lib/required-indicator.tsx +1 -1
- package/src/lib/schema-defaults.test.ts +1 -2
- package/src/lib/schema-required.test.ts +98 -6
- package/src/lib/schema-required.ts +126 -22
- package/src/lib/use-is-invalid.ts +4 -4
- package/src/lib/use-rebased-default-values.ts +42 -0
- package/src/lib/use-unsaved-changes-warning.ts +3 -3
- package/src/lib/validation-error.test.ts +1 -1
- package/src/lib/validation-logic.test.ts +1 -1
- package/src/lib/validation-modes.test-d.ts +1 -1
- package/src/lib/validation-modes.test.ts +12 -2
- package/src/lib/validation-modes.ts +3 -1
- package/src/use-app-fields.test-d.ts +8 -25
- package/src/use-app-fields.test.tsx +8 -32
- package/src/use-form.cascade.test.tsx +4 -6
- package/src/use-form.focus-invalid.test.tsx +198 -0
- package/src/use-form.reset-defaults.test.tsx +125 -0
- package/src/use-form.test-d.ts +2 -3
- package/src/use-form.test.tsx +12 -14
- package/src/use-form.unsaved-warning.test.tsx +4 -13
- package/src/use-form.validation-modes.test.tsx +10 -8
- package/src/form.tsx +0 -105
- package/src/use-app-fields.tsx +0 -272
- package/src/use-form.tsx +0 -450
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
+
import { describedBy } from "@zeno-lib/forms/lib/aria"
|
|
4
|
+
import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
|
|
5
|
+
import {
|
|
6
|
+
useHideFieldErrors,
|
|
7
|
+
useIsFieldRequired,
|
|
8
|
+
useIsInvalid,
|
|
9
|
+
} from "@zeno-lib/forms/lib/use-is-invalid"
|
|
10
|
+
import type { ReactNode } from "react"
|
|
3
11
|
import {
|
|
4
12
|
Combobox,
|
|
5
13
|
ComboboxContent,
|
|
@@ -7,25 +15,16 @@ import {
|
|
|
7
15
|
ComboboxInput,
|
|
8
16
|
ComboboxItem,
|
|
9
17
|
ComboboxList,
|
|
10
|
-
} from "
|
|
18
|
+
} from "@/components/ui/combobox"
|
|
11
19
|
import {
|
|
12
20
|
Field,
|
|
13
21
|
FieldDescription,
|
|
14
22
|
FieldError,
|
|
15
23
|
FieldLabel,
|
|
16
|
-
} from "
|
|
17
|
-
import { InputGroupAddon } from "
|
|
18
|
-
import { Spinner } from "
|
|
19
|
-
import type { ReactNode } from "react"
|
|
20
|
-
|
|
21
|
-
import { describedBy } from "../lib/aria"
|
|
22
|
-
import { useFieldContext } from "../lib/contexts"
|
|
24
|
+
} from "@/components/ui/field"
|
|
25
|
+
import { InputGroupAddon } from "@/components/ui/input-group"
|
|
26
|
+
import { Spinner } from "@/components/ui/spinner"
|
|
23
27
|
import { RequiredIndicator } from "../lib/required-indicator"
|
|
24
|
-
import {
|
|
25
|
-
useHideFieldErrors,
|
|
26
|
-
useIsFieldRequired,
|
|
27
|
-
useIsInvalid,
|
|
28
|
-
} from "../lib/use-is-invalid"
|
|
29
28
|
|
|
30
29
|
type ComboboxItemObject<V> = { value: V; label: string }
|
|
31
30
|
|
|
@@ -112,7 +111,7 @@ function ComboboxField<T = string>({
|
|
|
112
111
|
: ((fieldValue ?? null) as T | null)
|
|
113
112
|
|
|
114
113
|
return (
|
|
115
|
-
<Field data-invalid={isInvalid}>
|
|
114
|
+
<Field data-field={field.name} data-invalid={isInvalid}>
|
|
116
115
|
{label && (
|
|
117
116
|
<FieldLabel htmlFor={field.name}>
|
|
118
117
|
{label}
|
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { describedBy } from "@zeno-lib/forms/lib/aria"
|
|
4
|
+
import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
|
|
5
|
+
import {
|
|
6
|
+
useHideFieldErrors,
|
|
7
|
+
useIsFieldRequired,
|
|
8
|
+
useIsInvalid,
|
|
9
|
+
} from "@zeno-lib/forms/lib/use-is-invalid"
|
|
10
|
+
import type { ComponentProps, ReactNode } from "react"
|
|
11
|
+
import { buttonVariants } from "@/components/ui/button"
|
|
12
|
+
import { Calendar } from "@/components/ui/calendar"
|
|
5
13
|
import {
|
|
6
14
|
Field,
|
|
7
15
|
FieldDescription,
|
|
8
16
|
FieldError,
|
|
9
17
|
FieldLabel,
|
|
10
|
-
} from "
|
|
11
|
-
import { cn } from "@zeno-lib/ui/lib/utils"
|
|
12
|
-
import { Popover, PopoverContent, PopoverTrigger } from "@zeno-lib/ui/popover"
|
|
13
|
-
import type { ComponentProps, ReactNode } from "react"
|
|
14
|
-
|
|
15
|
-
import { describedBy } from "../lib/aria"
|
|
16
|
-
import { useFieldContext } from "../lib/contexts"
|
|
17
|
-
import { RequiredIndicator } from "../lib/required-indicator"
|
|
18
|
+
} from "@/components/ui/field"
|
|
18
19
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from "
|
|
20
|
+
Popover,
|
|
21
|
+
PopoverContent,
|
|
22
|
+
PopoverTrigger,
|
|
23
|
+
} from "@/components/ui/popover"
|
|
24
|
+
import { cn } from "@/lib/utils"
|
|
25
|
+
import { RequiredIndicator } from "../lib/required-indicator"
|
|
23
26
|
|
|
24
27
|
type CalendarProps = ComponentProps<typeof Calendar>
|
|
25
28
|
|
|
@@ -69,7 +72,7 @@ function DatePickerField({
|
|
|
69
72
|
const empty = !value
|
|
70
73
|
|
|
71
74
|
return (
|
|
72
|
-
<Field data-invalid={isInvalid}>
|
|
75
|
+
<Field data-field={field.name} data-invalid={isInvalid}>
|
|
73
76
|
{label && (
|
|
74
77
|
<FieldLabel htmlFor={field.name}>
|
|
75
78
|
{label}
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
+
import { describedBy } from "@zeno-lib/forms/lib/aria"
|
|
4
|
+
import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
|
|
5
|
+
import {
|
|
6
|
+
useHideFieldErrors,
|
|
7
|
+
useIsFieldRequired,
|
|
8
|
+
useIsInvalid,
|
|
9
|
+
} from "@zeno-lib/forms/lib/use-is-invalid"
|
|
10
|
+
import { Children, type ComponentProps, type ReactNode } from "react"
|
|
3
11
|
import {
|
|
4
12
|
Field,
|
|
5
13
|
FieldDescription,
|
|
6
14
|
FieldError,
|
|
7
15
|
FieldLabel,
|
|
8
|
-
} from "
|
|
9
|
-
import { Input } from "
|
|
10
|
-
import { InputGroup, InputGroupInput } from "
|
|
11
|
-
import { Children, type ComponentProps, type ReactNode } from "react"
|
|
12
|
-
|
|
13
|
-
import { describedBy } from "../lib/aria"
|
|
14
|
-
import { useFieldContext } from "../lib/contexts"
|
|
16
|
+
} from "@/components/ui/field"
|
|
17
|
+
import { Input } from "@/components/ui/input"
|
|
18
|
+
import { InputGroup, InputGroupInput } from "@/components/ui/input-group"
|
|
15
19
|
import { RequiredIndicator } from "../lib/required-indicator"
|
|
16
|
-
import {
|
|
17
|
-
useHideFieldErrors,
|
|
18
|
-
useIsFieldRequired,
|
|
19
|
-
useIsInvalid,
|
|
20
|
-
} from "../lib/use-is-invalid"
|
|
21
20
|
|
|
22
21
|
type InputFieldProps = Omit<
|
|
23
22
|
ComponentProps<typeof Input>,
|
|
@@ -66,7 +65,7 @@ function InputField({
|
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
return (
|
|
69
|
-
<Field data-invalid={isInvalid}>
|
|
68
|
+
<Field data-field={field.name} data-invalid={isInvalid}>
|
|
70
69
|
{label && (
|
|
71
70
|
<FieldLabel htmlFor={field.name}>
|
|
72
71
|
{label}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
+
import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
|
|
3
4
|
import type { ChangeEvent } from "react"
|
|
4
|
-
|
|
5
|
-
import { useFieldContext } from "../lib/contexts"
|
|
6
5
|
import { InputField, type InputFieldProps } from "./input-field"
|
|
7
6
|
|
|
8
7
|
type NumberFieldProps = Omit<
|
package/src/fields/otp-field.tsx
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
+
import { describedBy } from "@zeno-lib/forms/lib/aria"
|
|
4
|
+
import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
|
|
5
|
+
import {
|
|
6
|
+
useHideFieldErrors,
|
|
7
|
+
useIsFieldRequired,
|
|
8
|
+
useIsInvalid,
|
|
9
|
+
} from "@zeno-lib/forms/lib/use-is-invalid"
|
|
10
|
+
import type { ReactNode } from "react"
|
|
3
11
|
import {
|
|
4
12
|
Field,
|
|
5
13
|
FieldDescription,
|
|
6
14
|
FieldError,
|
|
7
15
|
FieldLabel,
|
|
8
|
-
} from "
|
|
9
|
-
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@zeno-lib/ui/input-otp"
|
|
10
|
-
import type { ReactNode } from "react"
|
|
11
|
-
|
|
12
|
-
import { describedBy } from "../lib/aria"
|
|
13
|
-
import { useFieldContext } from "../lib/contexts"
|
|
14
|
-
import { RequiredIndicator } from "../lib/required-indicator"
|
|
16
|
+
} from "@/components/ui/field"
|
|
15
17
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from "
|
|
18
|
+
InputOTP,
|
|
19
|
+
InputOTPGroup,
|
|
20
|
+
InputOTPSlot,
|
|
21
|
+
} from "@/components/ui/input-otp"
|
|
22
|
+
import { RequiredIndicator } from "../lib/required-indicator"
|
|
20
23
|
|
|
21
24
|
// Flat surface — `OTPInput`'s prop type is a discriminated union over
|
|
22
25
|
// `render`/`children`, which doesn't compose cleanly with `Omit`. Listing the
|
|
@@ -63,7 +66,7 @@ function OtpField({
|
|
|
63
66
|
const isRequired = required ?? schemaRequired
|
|
64
67
|
|
|
65
68
|
return (
|
|
66
|
-
<Field data-invalid={isInvalid}>
|
|
69
|
+
<Field data-field={field.name} data-invalid={isInvalid}>
|
|
67
70
|
{label && (
|
|
68
71
|
<FieldLabel htmlFor={field.name}>
|
|
69
72
|
{label}
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
+
import { describedBy } from "@zeno-lib/forms/lib/aria"
|
|
4
|
+
import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
|
|
5
|
+
import {
|
|
6
|
+
useHideFieldErrors,
|
|
7
|
+
useIsFieldRequired,
|
|
8
|
+
useIsInvalid,
|
|
9
|
+
} from "@zeno-lib/forms/lib/use-is-invalid"
|
|
10
|
+
import { type ComponentProps, type ReactNode, useId } from "react"
|
|
3
11
|
import {
|
|
4
12
|
Field,
|
|
5
13
|
FieldDescription,
|
|
6
14
|
FieldError,
|
|
7
15
|
FieldLabel,
|
|
8
|
-
} from "
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { type ComponentProps, type ReactNode, useId } from "react"
|
|
12
|
-
|
|
13
|
-
import { describedBy } from "../lib/aria"
|
|
14
|
-
import { useFieldContext } from "../lib/contexts"
|
|
16
|
+
} from "@/components/ui/field"
|
|
17
|
+
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
|
18
|
+
import { cn } from "@/lib/utils"
|
|
15
19
|
import { RequiredIndicator } from "../lib/required-indicator"
|
|
16
|
-
import {
|
|
17
|
-
useHideFieldErrors,
|
|
18
|
-
useIsFieldRequired,
|
|
19
|
-
useIsInvalid,
|
|
20
|
-
} from "../lib/use-is-invalid"
|
|
21
20
|
|
|
22
21
|
type RadioGroupFieldProps = Omit<
|
|
23
22
|
ComponentProps<typeof RadioGroup>,
|
|
@@ -46,7 +45,7 @@ function RadioGroupField({
|
|
|
46
45
|
const isRequired = required ?? schemaRequired
|
|
47
46
|
|
|
48
47
|
return (
|
|
49
|
-
<Field data-invalid={isInvalid}>
|
|
48
|
+
<Field data-field={field.name} data-invalid={isInvalid}>
|
|
50
49
|
{label && (
|
|
51
50
|
<FieldLabel>
|
|
52
51
|
{label}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useFormContext } from "@zeno-lib/forms/lib/contexts"
|
|
4
4
|
import type { ComponentProps, ReactNode } from "react"
|
|
5
|
-
|
|
6
|
-
import { useFormContext } from "../lib/contexts"
|
|
5
|
+
import { Button } from "@/components/ui/button"
|
|
7
6
|
|
|
8
7
|
type ResetButtonProps = Omit<ComponentProps<typeof Button>, "onClick" | "type">
|
|
9
8
|
|
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
+
import { describedBy } from "@zeno-lib/forms/lib/aria"
|
|
4
|
+
import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
|
|
5
|
+
import {
|
|
6
|
+
useHideFieldErrors,
|
|
7
|
+
useIsFieldRequired,
|
|
8
|
+
useIsInvalid,
|
|
9
|
+
} from "@zeno-lib/forms/lib/use-is-invalid"
|
|
10
|
+
import type { ComponentProps, ReactNode } from "react"
|
|
11
|
+
import { Children, isValidElement, useMemo } from "react"
|
|
3
12
|
import {
|
|
4
13
|
Field,
|
|
5
14
|
FieldDescription,
|
|
6
15
|
FieldError,
|
|
7
16
|
FieldLabel,
|
|
8
|
-
} from "
|
|
17
|
+
} from "@/components/ui/field"
|
|
9
18
|
import {
|
|
10
19
|
Select,
|
|
11
20
|
SelectContent,
|
|
12
21
|
SelectTrigger,
|
|
13
22
|
SelectValue,
|
|
14
|
-
} from "
|
|
15
|
-
import type { ComponentProps, ReactNode } from "react"
|
|
16
|
-
import { Children, isValidElement, useMemo } from "react"
|
|
17
|
-
|
|
18
|
-
import { describedBy } from "../lib/aria"
|
|
19
|
-
import { useFieldContext } from "../lib/contexts"
|
|
23
|
+
} from "@/components/ui/select"
|
|
20
24
|
import { RequiredIndicator } from "../lib/required-indicator"
|
|
21
|
-
import {
|
|
22
|
-
useHideFieldErrors,
|
|
23
|
-
useIsFieldRequired,
|
|
24
|
-
useIsInvalid,
|
|
25
|
-
} from "../lib/use-is-invalid"
|
|
26
25
|
|
|
27
26
|
type SelectFieldProps = Omit<
|
|
28
27
|
ComponentProps<typeof Select>,
|
|
@@ -71,7 +70,7 @@ function SelectField({
|
|
|
71
70
|
}, [children])
|
|
72
71
|
|
|
73
72
|
return (
|
|
74
|
-
<Field data-invalid={isInvalid}>
|
|
73
|
+
<Field data-field={field.name} data-invalid={isInvalid}>
|
|
75
74
|
{label && (
|
|
76
75
|
<FieldLabel htmlFor={field.name}>
|
|
77
76
|
{label}
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
+
import { describedBy } from "@zeno-lib/forms/lib/aria"
|
|
4
|
+
import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
|
|
5
|
+
import {
|
|
6
|
+
useHideFieldErrors,
|
|
7
|
+
useIsFieldRequired,
|
|
8
|
+
useIsInvalid,
|
|
9
|
+
} from "@zeno-lib/forms/lib/use-is-invalid"
|
|
10
|
+
import type { ComponentProps, ReactNode } from "react"
|
|
3
11
|
import {
|
|
4
12
|
Field,
|
|
5
13
|
FieldContent,
|
|
6
14
|
FieldDescription,
|
|
7
15
|
FieldError,
|
|
8
16
|
FieldLabel,
|
|
9
|
-
} from "
|
|
10
|
-
import { Slider } from "
|
|
11
|
-
import type { ComponentProps, ReactNode } from "react"
|
|
12
|
-
|
|
13
|
-
import { describedBy } from "../lib/aria"
|
|
14
|
-
import { useFieldContext } from "../lib/contexts"
|
|
17
|
+
} from "@/components/ui/field"
|
|
18
|
+
import { Slider } from "@/components/ui/slider"
|
|
15
19
|
import { RequiredIndicator } from "../lib/required-indicator"
|
|
16
|
-
import {
|
|
17
|
-
useHideFieldErrors,
|
|
18
|
-
useIsFieldRequired,
|
|
19
|
-
useIsInvalid,
|
|
20
|
-
} from "../lib/use-is-invalid"
|
|
21
20
|
|
|
22
21
|
type SliderValue = number | number[]
|
|
23
22
|
|
|
@@ -56,7 +55,7 @@ function SliderField({
|
|
|
56
55
|
const readout = formatValue && value !== undefined ? formatValue(value) : null
|
|
57
56
|
|
|
58
57
|
return (
|
|
59
|
-
<Field data-invalid={isInvalid}>
|
|
58
|
+
<Field data-field={field.name} data-invalid={isInvalid}>
|
|
60
59
|
{(label || readout) && (
|
|
61
60
|
<FieldContent className="flex-row items-center justify-between">
|
|
62
61
|
{label && (
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { Spinner } from "@zeno-lib/ui/spinner"
|
|
3
|
+
import { useFormContext } from "@zeno-lib/forms/lib/contexts"
|
|
5
4
|
import type { ComponentProps } from "react"
|
|
6
|
-
|
|
7
|
-
import {
|
|
5
|
+
import { Button } from "@/components/ui/button"
|
|
6
|
+
import { Spinner } from "@/components/ui/spinner"
|
|
8
7
|
|
|
9
8
|
type SubmitButtonProps = Omit<ComponentProps<typeof Button>, "type">
|
|
10
9
|
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
+
import { describedBy } from "@zeno-lib/forms/lib/aria"
|
|
4
|
+
import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
|
|
5
|
+
import {
|
|
6
|
+
useHideFieldErrors,
|
|
7
|
+
useIsFieldRequired,
|
|
8
|
+
useIsInvalid,
|
|
9
|
+
} from "@zeno-lib/forms/lib/use-is-invalid"
|
|
10
|
+
import type { ComponentProps, ReactNode } from "react"
|
|
3
11
|
import {
|
|
4
12
|
Field,
|
|
5
13
|
FieldContent,
|
|
6
14
|
FieldDescription,
|
|
7
15
|
FieldError,
|
|
8
16
|
FieldLabel,
|
|
9
|
-
} from "
|
|
10
|
-
import { Switch } from "
|
|
11
|
-
import type { ComponentProps, ReactNode } from "react"
|
|
12
|
-
|
|
13
|
-
import { describedBy } from "../lib/aria"
|
|
14
|
-
import { useFieldContext } from "../lib/contexts"
|
|
17
|
+
} from "@/components/ui/field"
|
|
18
|
+
import { Switch } from "@/components/ui/switch"
|
|
15
19
|
import { RequiredIndicator } from "../lib/required-indicator"
|
|
16
|
-
import {
|
|
17
|
-
useHideFieldErrors,
|
|
18
|
-
useIsFieldRequired,
|
|
19
|
-
useIsInvalid,
|
|
20
|
-
} from "../lib/use-is-invalid"
|
|
21
20
|
|
|
22
21
|
type SwitchFieldProps = Omit<
|
|
23
22
|
ComponentProps<typeof Switch>,
|
|
@@ -45,7 +44,11 @@ function SwitchField({
|
|
|
45
44
|
const isRequired = required ?? schemaRequired
|
|
46
45
|
|
|
47
46
|
return (
|
|
48
|
-
<Field
|
|
47
|
+
<Field
|
|
48
|
+
data-field={field.name}
|
|
49
|
+
data-invalid={isInvalid}
|
|
50
|
+
orientation="horizontal"
|
|
51
|
+
>
|
|
49
52
|
<FieldContent>
|
|
50
53
|
{label && (
|
|
51
54
|
<FieldLabel htmlFor={field.name}>
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
+
import { describedBy } from "@zeno-lib/forms/lib/aria"
|
|
4
|
+
import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
|
|
5
|
+
import {
|
|
6
|
+
useHideFieldErrors,
|
|
7
|
+
useIsFieldRequired,
|
|
8
|
+
useIsInvalid,
|
|
9
|
+
} from "@zeno-lib/forms/lib/use-is-invalid"
|
|
10
|
+
import { Children, type ComponentProps, type ReactNode } from "react"
|
|
3
11
|
import {
|
|
4
12
|
Field,
|
|
5
13
|
FieldDescription,
|
|
6
14
|
FieldError,
|
|
7
15
|
FieldLabel,
|
|
8
|
-
} from "
|
|
9
|
-
import { InputGroup, InputGroupTextarea } from "
|
|
10
|
-
import { Textarea } from "
|
|
11
|
-
import { Children, type ComponentProps, type ReactNode } from "react"
|
|
12
|
-
|
|
13
|
-
import { describedBy } from "../lib/aria"
|
|
14
|
-
import { useFieldContext } from "../lib/contexts"
|
|
16
|
+
} from "@/components/ui/field"
|
|
17
|
+
import { InputGroup, InputGroupTextarea } from "@/components/ui/input-group"
|
|
18
|
+
import { Textarea } from "@/components/ui/textarea"
|
|
15
19
|
import { RequiredIndicator } from "../lib/required-indicator"
|
|
16
|
-
import {
|
|
17
|
-
useHideFieldErrors,
|
|
18
|
-
useIsFieldRequired,
|
|
19
|
-
useIsInvalid,
|
|
20
|
-
} from "../lib/use-is-invalid"
|
|
21
20
|
|
|
22
21
|
type TextAreaFieldProps = Omit<
|
|
23
22
|
ComponentProps<typeof Textarea>,
|
|
@@ -63,7 +62,7 @@ function TextAreaField({
|
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
return (
|
|
66
|
-
<Field data-invalid={isInvalid}>
|
|
65
|
+
<Field data-field={field.name} data-invalid={isInvalid}>
|
|
67
66
|
{label && (
|
|
68
67
|
<FieldLabel htmlFor={field.name}>
|
|
69
68
|
{label}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import type { AnyFormApi } from "@tanstack/react-form"
|
|
4
|
+
import type { ComponentProps, FormEvent, ReactNode } from "react"
|
|
5
|
+
|
|
6
|
+
import { FormProvider as RawFormProvider, useFormContext } from "./lib/contexts"
|
|
7
|
+
|
|
8
|
+
type FormProviderProps = {
|
|
9
|
+
children: ReactNode
|
|
10
|
+
form: { handleSubmit: () => unknown }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function FormProvider({ children, form }: FormProviderProps) {
|
|
14
|
+
return (
|
|
15
|
+
<RawFormProvider value={form as AnyFormApi}>{children}</RawFormProvider>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type FormProps = Omit<ComponentProps<"form">, "onSubmit">
|
|
20
|
+
|
|
21
|
+
function Form({ children, className, ...props }: FormProps) {
|
|
22
|
+
const form = useFormContext()
|
|
23
|
+
return (
|
|
24
|
+
<form
|
|
25
|
+
className={className}
|
|
26
|
+
noValidate
|
|
27
|
+
onSubmit={async (event: FormEvent<HTMLFormElement>) => {
|
|
28
|
+
event.preventDefault()
|
|
29
|
+
event.stopPropagation()
|
|
30
|
+
const node = event.currentTarget
|
|
31
|
+
await Promise.resolve(form.handleSubmit()).catch(() => undefined)
|
|
32
|
+
if (!form.state.isValid) {
|
|
33
|
+
// First invalid control in *this* form; a group root (radio group,
|
|
34
|
+
// slider) hands focus to its first tabbable child.
|
|
35
|
+
const invalid = node.querySelector<HTMLElement>(
|
|
36
|
+
'[aria-invalid="true"]'
|
|
37
|
+
)
|
|
38
|
+
const targets = invalid
|
|
39
|
+
? [invalid, ...invalid.querySelectorAll<HTMLElement>("*")]
|
|
40
|
+
: []
|
|
41
|
+
targets.find((el) => el.tabIndex >= 0)?.focus()
|
|
42
|
+
}
|
|
43
|
+
}}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</form>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { Form, FormProvider }
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* biome-ignore-all lint/performance/noBarrelFile: single npm entry for the headless form API. */
|
|
2
|
+
|
|
3
|
+
export { createZenoForm } from "./create-zeno-form"
|
|
4
|
+
export { Form, FormProvider } from "./form-element"
|
|
5
|
+
export { applyValidationError } from "./lib/apply-validation-error"
|
|
6
|
+
export { useFieldContext, useFormContext } from "./lib/contexts"
|
|
7
|
+
export {
|
|
8
|
+
useHideFieldErrors,
|
|
9
|
+
useIsFieldRequired,
|
|
10
|
+
useIsInvalid,
|
|
11
|
+
} from "./lib/use-is-invalid"
|
|
12
|
+
export { type FieldMessage, ValidationError } from "./lib/validation-error"
|
|
13
|
+
export { blurThenChangeLogic } from "./lib/validation-logic"
|
|
14
|
+
export type { ValidationMode } from "./lib/validation-modes"
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { AnyFormApi } from "@tanstack/react-form"
|
|
2
|
-
import { describe, expect, test, vi } from "
|
|
3
|
-
|
|
2
|
+
import { describe, expect, test, vi } from "vitest"
|
|
4
3
|
import { applyValidationError } from "./apply-validation-error"
|
|
5
4
|
import { ValidationError } from "./validation-error"
|
|
6
5
|
|