@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.
Files changed (39) hide show
  1. package/dist/create-zeno-form.d.mts +8 -2
  2. package/dist/create-zeno-form.mjs +4 -0
  3. package/dist/form-element.mjs +7 -5
  4. package/dist/index.d.mts +2 -1
  5. package/dist/index.mjs +2 -1
  6. package/dist/lib/focus-first-invalid.d.mts +23 -0
  7. package/dist/lib/focus-first-invalid.mjs +37 -0
  8. package/dist/lib/submit-action.d.mts +8 -2
  9. package/dist/lib/submit-action.mjs +4 -2
  10. package/dist/lib/submit-error.d.mts +36 -0
  11. package/dist/lib/submit-error.mjs +66 -0
  12. package/dist/lib/use-field-control.d.mts +76 -0
  13. package/dist/lib/use-field-control.mjs +67 -0
  14. package/dist/lib/use-form-dialog.d.mts +15 -1
  15. package/dist/lib/use-form-dialog.mjs +20 -2
  16. package/package.json +1 -1
  17. package/src/control-field.test.tsx +338 -0
  18. package/src/create-form.tsx +5 -0
  19. package/src/create-zeno-form.tsx +32 -1
  20. package/src/fields/control-field.tsx +57 -0
  21. package/src/fields/field-shell.tsx +73 -0
  22. package/src/fields/form-error.tsx +61 -0
  23. package/src/fields/index.ts +3 -0
  24. package/src/fields/input-field.tsx +14 -48
  25. package/src/fields/textarea-field.tsx +14 -48
  26. package/src/form-dialog.test.tsx +493 -2
  27. package/src/form-dialog.tsx +217 -33
  28. package/src/form-element.tsx +13 -11
  29. package/src/form-error.test.tsx +255 -0
  30. package/src/index.ts +6 -0
  31. package/src/lib/focus-first-invalid.test.ts +22 -0
  32. package/src/lib/focus-first-invalid.ts +52 -0
  33. package/src/lib/submit-action.ts +18 -4
  34. package/src/lib/submit-error.ts +133 -0
  35. package/src/lib/use-field-control.ts +181 -0
  36. package/src/lib/use-form-dialog.ts +49 -1
  37. package/src/submit-action.test.tsx +103 -0
  38. package/src/use-app-fields.test-d.ts +51 -1
  39. package/src/use-form.focus-invalid.test.tsx +34 -0
@@ -1,6 +1,8 @@
1
- import { test } from "vitest"
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 }) {