@zeno-lib/forms 0.4.0 → 0.6.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 +8 -2
- package/dist/create-zeno-form.mjs +4 -0
- package/dist/form-element.mjs +7 -5
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/dist/lib/focus-first-invalid.d.mts +23 -0
- package/dist/lib/focus-first-invalid.mjs +37 -0
- package/dist/lib/submit-action.d.mts +8 -2
- package/dist/lib/submit-action.mjs +4 -2
- package/dist/lib/submit-error.d.mts +36 -0
- package/dist/lib/submit-error.mjs +66 -0
- package/dist/lib/use-field-control.d.mts +76 -0
- package/dist/lib/use-field-control.mjs +67 -0
- package/dist/lib/use-form-dialog.d.mts +15 -1
- package/dist/lib/use-form-dialog.mjs +20 -2
- package/package.json +1 -1
- package/src/control-field.test.tsx +338 -0
- package/src/create-form.tsx +5 -0
- package/src/create-zeno-form.tsx +32 -1
- package/src/fields/control-field.tsx +57 -0
- package/src/fields/field-shell.tsx +73 -0
- package/src/fields/form-error.tsx +61 -0
- package/src/fields/index.ts +3 -0
- package/src/fields/input-field.tsx +14 -48
- package/src/fields/textarea-field.tsx +14 -48
- 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 +6 -0
- package/src/lib/focus-first-invalid.test.ts +22 -0
- package/src/lib/focus-first-invalid.ts +52 -0
- package/src/lib/submit-action.ts +18 -4
- package/src/lib/submit-error.ts +133 -0
- package/src/lib/use-field-control.ts +181 -0
- package/src/lib/use-form-dialog.ts +49 -1
- package/src/submit-action.test.tsx +103 -0
- package/src/use-app-fields.test-d.ts +51 -1
- package/src/use-form.focus-invalid.test.tsx +34 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
import { expectTypeOf, test } from "vitest"
|
|
2
3
|
import { z } from "zod"
|
|
3
4
|
import { useForm } from "./create-form"
|
|
5
|
+
import { createZenoForm } from "./create-zeno-form"
|
|
4
6
|
|
|
5
7
|
const schema = z.object({
|
|
6
8
|
accept: z.boolean(),
|
|
@@ -93,3 +95,51 @@ test("listeners prop is accepted on every field wrapper", () => {
|
|
|
93
95
|
name: "email",
|
|
94
96
|
})
|
|
95
97
|
})
|
|
98
|
+
|
|
99
|
+
test("ControlField types its control with the value at `name`", () => {
|
|
100
|
+
const form = useForm({
|
|
101
|
+
onSubmit: () => undefined,
|
|
102
|
+
schema: z.object({
|
|
103
|
+
phone: z.string().nullable(),
|
|
104
|
+
rating: z.number(),
|
|
105
|
+
}),
|
|
106
|
+
})
|
|
107
|
+
form.ControlField({
|
|
108
|
+
name: "phone",
|
|
109
|
+
render: (control) => {
|
|
110
|
+
expectTypeOf(control.value).toEqualTypeOf<string | null>()
|
|
111
|
+
return null
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
form.ControlField({
|
|
115
|
+
name: "rating",
|
|
116
|
+
render: (control) => {
|
|
117
|
+
control.onChange(3)
|
|
118
|
+
// @ts-expect-error — the field holds a number
|
|
119
|
+
control.onChange("3")
|
|
120
|
+
return null
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
// @ts-expect-error — not a valid path
|
|
124
|
+
form.ControlField({ name: "nonexistent", render: () => null })
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test("a field whose `render` takes another argument keeps its own type", () => {
|
|
128
|
+
const { useForm: useListForm } = createZenoForm({
|
|
129
|
+
fieldComponents: {
|
|
130
|
+
ListField: (_: { render: (row: { id: string }) => ReactNode }) => null,
|
|
131
|
+
},
|
|
132
|
+
formComponents: {},
|
|
133
|
+
})
|
|
134
|
+
const form = useListForm({
|
|
135
|
+
onSubmit: () => undefined,
|
|
136
|
+
schema: z.object({ rows: z.array(z.string()) }),
|
|
137
|
+
})
|
|
138
|
+
form.ListField({
|
|
139
|
+
name: "rows",
|
|
140
|
+
render: (row) => {
|
|
141
|
+
expectTypeOf(row).toEqualTypeOf<{ id: string }>()
|
|
142
|
+
return null
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
})
|
|
@@ -9,6 +9,7 @@ import type { ReactNode } from "react"
|
|
|
9
9
|
import { afterEach, describe, expect, test, vi } from "vitest"
|
|
10
10
|
import { z } from "zod"
|
|
11
11
|
import { Form, FormProvider, RadioGroupFieldItem, useForm } from "./create-form"
|
|
12
|
+
import { submitAction } from "./lib/submit-action"
|
|
12
13
|
|
|
13
14
|
afterEach(() => {
|
|
14
15
|
cleanup()
|
|
@@ -103,6 +104,39 @@ describe("<Form> — focus the first invalid field on a failed submit", () => {
|
|
|
103
104
|
)
|
|
104
105
|
})
|
|
105
106
|
|
|
107
|
+
test("focuses a field error returned through submitAction", async () => {
|
|
108
|
+
const user = userEvent.setup()
|
|
109
|
+
function Harness() {
|
|
110
|
+
const form = useForm({
|
|
111
|
+
defaultValues: { email: "ada@example.com", name: "Ada" },
|
|
112
|
+
onSubmit: (submit) =>
|
|
113
|
+
submitAction(submit, () =>
|
|
114
|
+
Promise.resolve({
|
|
115
|
+
error: { fieldErrors: { name: ["Taken"] }, formErrors: [] },
|
|
116
|
+
ok: false as const,
|
|
117
|
+
})
|
|
118
|
+
),
|
|
119
|
+
schema,
|
|
120
|
+
})
|
|
121
|
+
const { EmailField, InputField, SubmitButton } = form
|
|
122
|
+
return (
|
|
123
|
+
<FormProvider form={form}>
|
|
124
|
+
<Form aria-label="Profile">
|
|
125
|
+
<EmailField name="email" />
|
|
126
|
+
<InputField label="Name" name="name" />
|
|
127
|
+
<SubmitButton>Save</SubmitButton>
|
|
128
|
+
</Form>
|
|
129
|
+
</FormProvider>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
render(<Harness />)
|
|
133
|
+
await user.click(screen.getByRole("button", { name: "Save" }))
|
|
134
|
+
expect(await screen.findByText("Taken")).toBeTruthy()
|
|
135
|
+
await waitFor(() =>
|
|
136
|
+
expect(document.activeElement).toBe(control("Profile", "name"))
|
|
137
|
+
)
|
|
138
|
+
})
|
|
139
|
+
|
|
106
140
|
test("a radio group or slider root hands focus to its tabbable child", async () => {
|
|
107
141
|
const user = userEvent.setup()
|
|
108
142
|
function Harness({ sliderFirst }: { sliderFirst: boolean }) {
|