@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,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
- useHideFieldErrors,
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 { RequiredIndicator } from "../lib/required-indicator"
8
+ import { FieldShell } from "./field-shell"
21
9
 
22
10
  type InputFieldProps = Omit<
23
11
  ComponentProps<typeof Input>,
@@ -41,28 +29,18 @@ function InputField({
41
29
  ...props
42
30
  }: InputFieldProps) {
43
31
  const field = useFieldContext<string>()
44
- const id = useId()
45
- const errorId = `${id}-error`
46
- const descriptionId = `${id}-description`
47
- const isInvalid = useIsInvalid(field)
48
- const hideErrors = useHideFieldErrors(field)
49
- const showError = isInvalid && !hideErrors
32
+ // The label names the input through `htmlFor`, so the input needs no
33
+ // `aria-labelledby` and the hook doesn't get `label`.
34
+ const { controlProps, ...shell } = useFieldControl<string>({
35
+ description,
36
+ required,
37
+ })
50
38
  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
39
 
40
+ // A text input keeps the `""` the user typed, where `controlProps.onChange`
41
+ // would store the field's empty value.
56
42
  const inputProps = {
57
- "aria-describedby": describedBy(
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,
43
+ ...controlProps,
66
44
  onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
67
45
  field.handleChange(event.target.value),
68
46
  value: field.state.value ?? "",
@@ -70,13 +48,7 @@ function InputField({
70
48
  }
71
49
 
72
50
  return (
73
- <Field data-field={field.name} data-invalid={isInvalid}>
74
- {label && (
75
- <FieldLabel htmlFor={id}>
76
- {label}
77
- {isRequired && <RequiredIndicator />}
78
- </FieldLabel>
79
- )}
51
+ <FieldShell {...shell} description={description} label={label}>
80
52
  {hasAddons ? (
81
53
  <InputGroup>
82
54
  <InputGroupInput {...inputProps} />
@@ -85,13 +57,7 @@ function InputField({
85
57
  ) : (
86
58
  <Input {...inputProps} />
87
59
  )}
88
- {description && (
89
- <FieldDescription id={descriptionId}>{description}</FieldDescription>
90
- )}
91
- {showError && (
92
- <FieldError errors={field.state.meta.errors} id={errorId} />
93
- )}
94
- </Field>
60
+ </FieldShell>
95
61
  )
96
62
  }
97
63
 
@@ -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
- useHideFieldErrors,
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 { InputGroup, InputGroupTextarea } from "@/components/ui/input-group"
19
7
  import { Textarea } from "@/components/ui/textarea"
20
- import { RequiredIndicator } from "../lib/required-indicator"
8
+ import { FieldShell } from "./field-shell"
21
9
 
22
10
  type TextAreaFieldProps = Omit<
23
11
  ComponentProps<typeof Textarea>,
@@ -41,28 +29,18 @@ function TextAreaField({
41
29
  ...props
42
30
  }: TextAreaFieldProps) {
43
31
  const field = useFieldContext<string>()
44
- const id = useId()
45
- const errorId = `${id}-error`
46
- const descriptionId = `${id}-description`
47
- const isInvalid = useIsInvalid(field)
48
- const hideErrors = useHideFieldErrors(field)
49
- const showError = isInvalid && !hideErrors
32
+ // The label names the textarea through `htmlFor`, so the textarea needs no
33
+ // `aria-labelledby` and the hook doesn't get `label`.
34
+ const { controlProps, ...shell } = useFieldControl<string>({
35
+ description,
36
+ required,
37
+ })
50
38
  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
39
 
40
+ // A textarea keeps the `""` the user typed, where `controlProps.onChange`
41
+ // would store the field's empty value.
56
42
  const textareaProps = {
57
- "aria-describedby": describedBy(
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,
43
+ ...controlProps,
66
44
  onChange: (event: React.ChangeEvent<HTMLTextAreaElement>) =>
67
45
  field.handleChange(event.target.value),
68
46
  value: field.state.value ?? "",
@@ -70,13 +48,7 @@ function TextAreaField({
70
48
  }
71
49
 
72
50
  return (
73
- <Field data-field={field.name} data-invalid={isInvalid}>
74
- {label && (
75
- <FieldLabel htmlFor={id}>
76
- {label}
77
- {isRequired && <RequiredIndicator />}
78
- </FieldLabel>
79
- )}
51
+ <FieldShell {...shell} description={description} label={label}>
80
52
  {hasAddons ? (
81
53
  <InputGroup>
82
54
  <InputGroupTextarea {...textareaProps} />
@@ -85,13 +57,7 @@ function TextAreaField({
85
57
  ) : (
86
58
  <Textarea {...textareaProps} />
87
59
  )}
88
- {description && (
89
- <FieldDescription id={descriptionId}>{description}</FieldDescription>
90
- )}
91
- {showError && (
92
- <FieldError errors={field.state.meta.errors} id={errorId} />
93
- )}
94
- </Field>
60
+ </FieldShell>
95
61
  )
96
62
  }
97
63