@zeno-lib/forms 0.3.0 → 0.5.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.mjs +9 -3
- package/dist/form-element.mjs +7 -5
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -3
- package/dist/lib/empty-value.d.mts +12 -0
- package/dist/lib/empty-value.mjs +68 -0
- package/dist/lib/focus-first-invalid.d.mts +23 -0
- package/dist/lib/focus-first-invalid.mjs +37 -0
- package/dist/lib/schema-defaults.d.mts +9 -1
- package/dist/lib/schema-defaults.mjs +33 -1
- package/dist/lib/schema-required.d.mts +2 -1
- package/dist/lib/schema-required.mjs +49 -21
- package/dist/lib/submit-action.mjs +1 -1
- package/dist/lib/submit-error.d.mts +36 -0
- package/dist/lib/submit-error.mjs +66 -0
- package/dist/lib/use-form-dialog.d.mts +15 -1
- package/dist/lib/use-form-dialog.mjs +20 -2
- package/dist/lib/use-is-invalid.d.mts +2 -1
- package/dist/lib/use-is-invalid.mjs +5 -2
- package/dist/lib/validation-modes.d.mts +5 -1
- package/dist/lib/validation-modes.mjs +9 -4
- package/package.json +1 -1
- package/src/create-form.tsx +2 -0
- package/src/create-zeno-form.tsx +25 -5
- package/src/fields/checkbox-field.tsx +8 -1
- package/src/fields/combobox-field.tsx +29 -12
- package/src/fields/date-picker-field.tsx +37 -9
- package/src/fields/email-field.tsx +8 -0
- package/src/fields/form-error.tsx +61 -0
- package/src/fields/index.ts +1 -0
- package/src/fields/input-field.tsx +6 -2
- package/src/fields/money-field.tsx +3 -1
- package/src/fields/multi-select-field.tsx +8 -1
- package/src/fields/number-field.tsx +9 -7
- package/src/fields/otp-field.tsx +8 -1
- package/src/fields/percentage-field.tsx +3 -1
- package/src/fields/radio-group-field.tsx +11 -4
- package/src/fields/select-field.tsx +26 -3
- package/src/fields/switch-field.tsx +8 -1
- package/src/fields/textarea-field.tsx +8 -1
- package/src/fields/year-field.tsx +3 -1
- package/src/form-dialog.test.tsx +493 -2
- package/src/form-dialog.tsx +217 -33
- package/src/form-element.tsx +13 -11
- package/src/form-error.test.tsx +255 -0
- package/src/index.ts +1 -0
- package/src/lib/empty-value.test.ts +72 -0
- package/src/lib/empty-value.ts +130 -0
- package/src/lib/focus-first-invalid.test.ts +22 -0
- package/src/lib/focus-first-invalid.ts +52 -0
- package/src/lib/schema-defaults.ts +50 -1
- package/src/lib/schema-required.test.ts +24 -2
- package/src/lib/schema-required.ts +81 -43
- package/src/lib/submit-error.ts +133 -0
- package/src/lib/use-form-dialog.ts +49 -1
- package/src/lib/use-is-invalid.ts +14 -1
- package/src/lib/validation-modes.ts +18 -6
- package/src/use-app-fields.test.tsx +312 -2
- package/src/use-form.focus-invalid.test.tsx +34 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
|
|
4
|
+
import { getEmptyValue } from "@zeno-lib/forms/lib/empty-value"
|
|
4
5
|
import { useFormattedNumber } from "@zeno-lib/forms/lib/use-formatted-number"
|
|
5
6
|
import { InputGroupAddon, InputGroupText } from "@/components/ui/input-group"
|
|
6
7
|
import { InputField, type InputFieldProps } from "./input-field"
|
|
@@ -54,7 +55,8 @@ function PercentageField({
|
|
|
54
55
|
maximumFractionDigits: fractionDigits,
|
|
55
56
|
min,
|
|
56
57
|
onBlur: field.handleBlur,
|
|
57
|
-
onValueChange: (next) =>
|
|
58
|
+
onValueChange: (next) =>
|
|
59
|
+
field.handleChange(next ?? getEmptyValue(field, null)),
|
|
58
60
|
value: field.state.value,
|
|
59
61
|
...(isFraction ? { fromDisplay: fromPercent, toDisplay: toPercent } : {}),
|
|
60
62
|
})
|
|
@@ -5,6 +5,7 @@ import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
|
|
|
5
5
|
import {
|
|
6
6
|
useHideFieldErrors,
|
|
7
7
|
useIsFieldRequired,
|
|
8
|
+
useIsFieldRequiredBySchema,
|
|
8
9
|
useIsInvalid,
|
|
9
10
|
} from "@zeno-lib/forms/lib/use-is-invalid"
|
|
10
11
|
import { type ComponentProps, type ReactNode, useId } from "react"
|
|
@@ -24,7 +25,10 @@ type RadioGroupFieldProps = Omit<
|
|
|
24
25
|
> & {
|
|
25
26
|
description?: ReactNode
|
|
26
27
|
label?: ReactNode
|
|
27
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Mark the field required, or not, over the schema. Drives the `*` and
|
|
30
|
+
* `aria-required`.
|
|
31
|
+
*/
|
|
28
32
|
required?: boolean
|
|
29
33
|
}
|
|
30
34
|
|
|
@@ -35,7 +39,7 @@ function RadioGroupField({
|
|
|
35
39
|
required,
|
|
36
40
|
...props
|
|
37
41
|
}: RadioGroupFieldProps) {
|
|
38
|
-
const field = useFieldContext
|
|
42
|
+
const field = useFieldContext()
|
|
39
43
|
const id = useId()
|
|
40
44
|
const errorId = `${id}-error`
|
|
41
45
|
const descriptionId = `${id}-description`
|
|
@@ -44,6 +48,8 @@ function RadioGroupField({
|
|
|
44
48
|
const showError = isInvalid && !hideErrors
|
|
45
49
|
const schemaRequired = useIsFieldRequired(field)
|
|
46
50
|
const isRequired = required ?? schemaRequired
|
|
51
|
+
const schemaRequiresValue = useIsFieldRequiredBySchema(field)
|
|
52
|
+
const requiresValue = required ?? schemaRequiresValue
|
|
47
53
|
|
|
48
54
|
return (
|
|
49
55
|
<Field data-field={field.name} data-invalid={isInvalid}>
|
|
@@ -59,9 +65,10 @@ function RadioGroupField({
|
|
|
59
65
|
[showError, errorId]
|
|
60
66
|
)}
|
|
61
67
|
aria-invalid={isInvalid || undefined}
|
|
68
|
+
aria-required={requiresValue || undefined}
|
|
62
69
|
name={field.name}
|
|
63
|
-
onValueChange={(value) => field.handleChange(
|
|
64
|
-
value={field.state.value ??
|
|
70
|
+
onValueChange={(value) => field.handleChange(value)}
|
|
71
|
+
value={field.state.value ?? null}
|
|
65
72
|
{...props}
|
|
66
73
|
>
|
|
67
74
|
{children}
|
|
@@ -5,10 +5,11 @@ import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
|
|
|
5
5
|
import {
|
|
6
6
|
useHideFieldErrors,
|
|
7
7
|
useIsFieldRequired,
|
|
8
|
+
useIsFieldRequiredBySchema,
|
|
8
9
|
useIsInvalid,
|
|
9
10
|
} from "@zeno-lib/forms/lib/use-is-invalid"
|
|
10
11
|
import type { ComponentProps, ReactNode } from "react"
|
|
11
|
-
import { Children, isValidElement, useId, useMemo } from "react"
|
|
12
|
+
import { Children, isValidElement, useId, useMemo, useRef } from "react"
|
|
12
13
|
import {
|
|
13
14
|
Field,
|
|
14
15
|
FieldDescription,
|
|
@@ -31,7 +32,10 @@ type SelectFieldProps = Omit<
|
|
|
31
32
|
description?: ReactNode
|
|
32
33
|
label?: ReactNode
|
|
33
34
|
placeholder?: string
|
|
34
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Mark the field required, or not, over the schema. Drives the `*` and
|
|
37
|
+
* `aria-required`.
|
|
38
|
+
*/
|
|
35
39
|
required?: boolean
|
|
36
40
|
triggerClassName?: string
|
|
37
41
|
triggerSize?: ComponentProps<typeof SelectTrigger>["size"]
|
|
@@ -41,6 +45,7 @@ function SelectField({
|
|
|
41
45
|
children,
|
|
42
46
|
description,
|
|
43
47
|
label,
|
|
48
|
+
onOpenChange,
|
|
44
49
|
placeholder,
|
|
45
50
|
required,
|
|
46
51
|
triggerClassName,
|
|
@@ -48,6 +53,10 @@ function SelectField({
|
|
|
48
53
|
...props
|
|
49
54
|
}: SelectFieldProps) {
|
|
50
55
|
const field = useFieldContext()
|
|
56
|
+
// Opening the popup moves focus into it, so the trigger's blur would mark
|
|
57
|
+
// the field touched, and show a required error, before anything is picked.
|
|
58
|
+
// The field is touched once the popup closes, or focus leaves it closed.
|
|
59
|
+
const isOpen = useRef<boolean>(false)
|
|
51
60
|
const id = useId()
|
|
52
61
|
const errorId = `${id}-error`
|
|
53
62
|
const descriptionId = `${id}-description`
|
|
@@ -56,6 +65,8 @@ function SelectField({
|
|
|
56
65
|
const showError = isInvalid && !hideErrors
|
|
57
66
|
const schemaRequired = useIsFieldRequired(field)
|
|
58
67
|
const isRequired = required ?? schemaRequired
|
|
68
|
+
const schemaRequiresValue = useIsFieldRequiredBySchema(field)
|
|
69
|
+
const requiresValue = required ?? schemaRequiresValue
|
|
59
70
|
|
|
60
71
|
const items = useMemo(() => {
|
|
61
72
|
const map: Record<string, ReactNode> = {}
|
|
@@ -81,6 +92,13 @@ function SelectField({
|
|
|
81
92
|
<Select
|
|
82
93
|
items={items}
|
|
83
94
|
name={field.name}
|
|
95
|
+
onOpenChange={(open, eventDetails) => {
|
|
96
|
+
isOpen.current = open
|
|
97
|
+
if (!open) {
|
|
98
|
+
field.handleBlur()
|
|
99
|
+
}
|
|
100
|
+
onOpenChange?.(open, eventDetails)
|
|
101
|
+
}}
|
|
84
102
|
onValueChange={(value) => field.handleChange(value)}
|
|
85
103
|
value={field.state.value ?? null}
|
|
86
104
|
{...props}
|
|
@@ -91,9 +109,14 @@ function SelectField({
|
|
|
91
109
|
[showError, errorId]
|
|
92
110
|
)}
|
|
93
111
|
aria-invalid={isInvalid || undefined}
|
|
112
|
+
aria-required={requiresValue || undefined}
|
|
94
113
|
className={triggerClassName}
|
|
95
114
|
id={id}
|
|
96
|
-
onBlur={
|
|
115
|
+
onBlur={() => {
|
|
116
|
+
if (!isOpen.current) {
|
|
117
|
+
field.handleBlur()
|
|
118
|
+
}
|
|
119
|
+
}}
|
|
97
120
|
size={triggerSize}
|
|
98
121
|
>
|
|
99
122
|
<SelectValue placeholder={placeholder} />
|
|
@@ -5,6 +5,7 @@ import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
|
|
|
5
5
|
import {
|
|
6
6
|
useHideFieldErrors,
|
|
7
7
|
useIsFieldRequired,
|
|
8
|
+
useIsFieldRequiredBySchema,
|
|
8
9
|
useIsInvalid,
|
|
9
10
|
} from "@zeno-lib/forms/lib/use-is-invalid"
|
|
10
11
|
import { type ComponentProps, type ReactNode, useId } from "react"
|
|
@@ -24,7 +25,10 @@ type SwitchFieldProps = Omit<
|
|
|
24
25
|
> & {
|
|
25
26
|
description?: ReactNode
|
|
26
27
|
label?: ReactNode
|
|
27
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Mark the field required, or not, over the schema. Drives the `*` and
|
|
30
|
+
* `aria-required`.
|
|
31
|
+
*/
|
|
28
32
|
required?: boolean
|
|
29
33
|
}
|
|
30
34
|
|
|
@@ -43,6 +47,8 @@ function SwitchField({
|
|
|
43
47
|
const showError = isInvalid && !hideErrors
|
|
44
48
|
const schemaRequired = useIsFieldRequired(field)
|
|
45
49
|
const isRequired = required ?? schemaRequired
|
|
50
|
+
const schemaRequiresValue = useIsFieldRequiredBySchema(field)
|
|
51
|
+
const requiresValue = required ?? schemaRequiresValue
|
|
46
52
|
|
|
47
53
|
return (
|
|
48
54
|
<Field
|
|
@@ -70,6 +76,7 @@ function SwitchField({
|
|
|
70
76
|
[showError, errorId]
|
|
71
77
|
)}
|
|
72
78
|
aria-invalid={isInvalid || undefined}
|
|
79
|
+
aria-required={requiresValue || undefined}
|
|
73
80
|
checked={field.state.value ?? false}
|
|
74
81
|
id={id}
|
|
75
82
|
name={field.name}
|
|
@@ -5,6 +5,7 @@ import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
|
|
|
5
5
|
import {
|
|
6
6
|
useHideFieldErrors,
|
|
7
7
|
useIsFieldRequired,
|
|
8
|
+
useIsFieldRequiredBySchema,
|
|
8
9
|
useIsInvalid,
|
|
9
10
|
} from "@zeno-lib/forms/lib/use-is-invalid"
|
|
10
11
|
import { Children, type ComponentProps, type ReactNode, useId } from "react"
|
|
@@ -25,7 +26,10 @@ type TextAreaFieldProps = Omit<
|
|
|
25
26
|
children?: ReactNode
|
|
26
27
|
description?: ReactNode
|
|
27
28
|
label?: ReactNode
|
|
28
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Mark the field required, or not, over the schema. Drives the `*` and
|
|
31
|
+
* `aria-required`.
|
|
32
|
+
*/
|
|
29
33
|
required?: boolean
|
|
30
34
|
}
|
|
31
35
|
|
|
@@ -46,6 +50,8 @@ function TextAreaField({
|
|
|
46
50
|
const hasAddons = Children.count(children) > 0
|
|
47
51
|
const schemaRequired = useIsFieldRequired(field)
|
|
48
52
|
const isRequired = required ?? schemaRequired
|
|
53
|
+
const schemaRequiresValue = useIsFieldRequiredBySchema(field)
|
|
54
|
+
const requiresValue = required ?? schemaRequiresValue
|
|
49
55
|
|
|
50
56
|
const textareaProps = {
|
|
51
57
|
"aria-describedby": describedBy(
|
|
@@ -53,6 +59,7 @@ function TextAreaField({
|
|
|
53
59
|
[showError, errorId]
|
|
54
60
|
),
|
|
55
61
|
"aria-invalid": isInvalid || undefined,
|
|
62
|
+
"aria-required": requiresValue || undefined,
|
|
56
63
|
id,
|
|
57
64
|
name: field.name,
|
|
58
65
|
onBlur: field.handleBlur,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import { useFieldContext } from "@zeno-lib/forms/lib/contexts"
|
|
4
|
+
import { getEmptyValue } from "@zeno-lib/forms/lib/empty-value"
|
|
4
5
|
import { useFormattedNumber } from "@zeno-lib/forms/lib/use-formatted-number"
|
|
5
6
|
import { InputField, type InputFieldProps } from "./input-field"
|
|
6
7
|
|
|
@@ -36,7 +37,8 @@ function YearField({ max, min, ...props }: YearFieldProps) {
|
|
|
36
37
|
maximumFractionDigits: 0,
|
|
37
38
|
min,
|
|
38
39
|
onBlur: field.handleBlur,
|
|
39
|
-
onValueChange: (next) =>
|
|
40
|
+
onValueChange: (next) =>
|
|
41
|
+
field.handleChange(next ?? getEmptyValue(field, null)),
|
|
40
42
|
useGrouping: false,
|
|
41
43
|
value: field.state.value,
|
|
42
44
|
})
|