@zeno-lib/forms 0.0.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 (48) hide show
  1. package/package.json +47 -0
  2. package/src/addons/index.ts +2 -0
  3. package/src/addons/validation-spinner.tsx +25 -0
  4. package/src/fields/checkbox-field.tsx +80 -0
  5. package/src/fields/combobox-field.tsx +189 -0
  6. package/src/fields/date-picker-field.tsx +141 -0
  7. package/src/fields/email-field.tsx +21 -0
  8. package/src/fields/index.ts +16 -0
  9. package/src/fields/input-field.tsx +95 -0
  10. package/src/fields/number-field.tsx +33 -0
  11. package/src/fields/otp-field.tsx +111 -0
  12. package/src/fields/password-field.tsx +20 -0
  13. package/src/fields/radio-group-field.tsx +105 -0
  14. package/src/fields/reset-button.tsx +29 -0
  15. package/src/fields/select-field.tsx +113 -0
  16. package/src/fields/slider-field.tsx +103 -0
  17. package/src/fields/submit-button.tsx +31 -0
  18. package/src/fields/switch-field.tsx +80 -0
  19. package/src/fields/textarea-field.tsx +91 -0
  20. package/src/form.tsx +105 -0
  21. package/src/lib/apply-validation-error.test.ts +97 -0
  22. package/src/lib/apply-validation-error.ts +31 -0
  23. package/src/lib/aria.ts +7 -0
  24. package/src/lib/contexts.ts +9 -0
  25. package/src/lib/required-indicator.tsx +22 -0
  26. package/src/lib/schema-defaults.test.ts +195 -0
  27. package/src/lib/schema-defaults.ts +127 -0
  28. package/src/lib/schema-required.test.ts +132 -0
  29. package/src/lib/schema-required.ts +64 -0
  30. package/src/lib/use-is-invalid.ts +109 -0
  31. package/src/lib/use-unsaved-changes-warning.ts +57 -0
  32. package/src/lib/validation-error.test.ts +41 -0
  33. package/src/lib/validation-error.ts +30 -0
  34. package/src/lib/validation-logic.test.ts +261 -0
  35. package/src/lib/validation-logic.ts +192 -0
  36. package/src/lib/validation-modes.test-d.ts +9 -0
  37. package/src/lib/validation-modes.test.ts +72 -0
  38. package/src/lib/validation-modes.ts +73 -0
  39. package/src/tanstack.ts +2 -0
  40. package/src/use-app-fields.test-d.ts +112 -0
  41. package/src/use-app-fields.test.tsx +163 -0
  42. package/src/use-app-fields.tsx +272 -0
  43. package/src/use-form.cascade.test.tsx +83 -0
  44. package/src/use-form.test-d.ts +153 -0
  45. package/src/use-form.test.tsx +348 -0
  46. package/src/use-form.tsx +450 -0
  47. package/src/use-form.unsaved-warning.test.tsx +192 -0
  48. package/src/use-form.validation-modes.test.tsx +149 -0
@@ -0,0 +1,111 @@
1
+ "use client"
2
+
3
+ import {
4
+ Field,
5
+ FieldDescription,
6
+ FieldError,
7
+ FieldLabel,
8
+ } from "@zeno-lib/ui/field"
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"
15
+ import {
16
+ useHideFieldErrors,
17
+ useIsFieldRequired,
18
+ useIsInvalid,
19
+ } from "../lib/use-is-invalid"
20
+
21
+ // Flat surface — `OTPInput`'s prop type is a discriminated union over
22
+ // `render`/`children`, which doesn't compose cleanly with `Omit`. Listing the
23
+ // knobs we forward keeps the field's typing flat and predictable.
24
+ type OtpFieldProps = {
25
+ description?: ReactNode
26
+ label?: ReactNode
27
+ /** Number of digits/characters in the code. Defaults to 6. */
28
+ maxLength?: number
29
+ /** Regex applied to each keystroke (e.g. `^[0-9]+$` for digits-only). */
30
+ pattern?: string
31
+ autoFocus?: boolean
32
+ disabled?: boolean
33
+ className?: string
34
+ containerClassName?: string
35
+ /**
36
+ * Custom slot rendering. Overrides the default single-group layout when
37
+ * you need a separator between segments.
38
+ */
39
+ children?: ReactNode
40
+ /** Force the required `*` indicator on or off. Defaults to schema-derived. */
41
+ required?: boolean
42
+ }
43
+
44
+ function OtpField({
45
+ autoFocus,
46
+ children,
47
+ className,
48
+ containerClassName,
49
+ description,
50
+ disabled,
51
+ label,
52
+ maxLength = 6,
53
+ pattern,
54
+ required,
55
+ }: OtpFieldProps) {
56
+ const field = useFieldContext<string>()
57
+ const errorId = `${field.name}-error`
58
+ const descriptionId = `${field.name}-description`
59
+ const isInvalid = useIsInvalid(field)
60
+ const hideErrors = useHideFieldErrors(field)
61
+ const showError = isInvalid && !hideErrors
62
+ const schemaRequired = useIsFieldRequired(field)
63
+ const isRequired = required ?? schemaRequired
64
+
65
+ return (
66
+ <Field data-invalid={isInvalid}>
67
+ {label && (
68
+ <FieldLabel htmlFor={field.name}>
69
+ {label}
70
+ {isRequired && <RequiredIndicator />}
71
+ </FieldLabel>
72
+ )}
73
+ <InputOTP
74
+ aria-describedby={describedBy(
75
+ [description, descriptionId],
76
+ [showError, errorId]
77
+ )}
78
+ aria-invalid={isInvalid || undefined}
79
+ autoFocus={autoFocus}
80
+ className={className}
81
+ containerClassName={containerClassName}
82
+ disabled={disabled}
83
+ id={field.name}
84
+ maxLength={maxLength}
85
+ name={field.name}
86
+ onBlur={field.handleBlur}
87
+ onChange={(value) => field.handleChange(value)}
88
+ pattern={pattern}
89
+ value={field.state.value ?? ""}
90
+ >
91
+ {children ?? (
92
+ <InputOTPGroup>
93
+ {Array.from({ length: maxLength }, (_, index) => (
94
+ // biome-ignore lint/suspicious/noArrayIndexKey: OTP slots are a fixed-length ordered list — index *is* the stable identity.
95
+ <InputOTPSlot index={index} key={index} />
96
+ ))}
97
+ </InputOTPGroup>
98
+ )}
99
+ </InputOTP>
100
+ {description && (
101
+ <FieldDescription id={descriptionId}>{description}</FieldDescription>
102
+ )}
103
+ {showError && (
104
+ <FieldError errors={field.state.meta.errors} id={errorId} />
105
+ )}
106
+ </Field>
107
+ )
108
+ }
109
+
110
+ export type { OtpFieldProps }
111
+ export { OtpField }
@@ -0,0 +1,20 @@
1
+ import { InputField, type InputFieldProps } from "./input-field"
2
+
3
+ type PasswordFieldProps = Omit<InputFieldProps, "type">
4
+
5
+ function PasswordField({
6
+ autoComplete = "current-password",
7
+ label = "Password",
8
+ ...props
9
+ }: PasswordFieldProps) {
10
+ return (
11
+ <InputField
12
+ autoComplete={autoComplete}
13
+ label={label}
14
+ type="password"
15
+ {...props}
16
+ />
17
+ )
18
+ }
19
+
20
+ export { PasswordField }
@@ -0,0 +1,105 @@
1
+ "use client"
2
+
3
+ import {
4
+ Field,
5
+ FieldDescription,
6
+ FieldError,
7
+ FieldLabel,
8
+ } from "@zeno-lib/ui/field"
9
+ import { cn } from "@zeno-lib/ui/lib/utils"
10
+ import { RadioGroup, RadioGroupItem } from "@zeno-lib/ui/radio-group"
11
+ import { type ComponentProps, type ReactNode, useId } from "react"
12
+
13
+ import { describedBy } from "../lib/aria"
14
+ import { useFieldContext } from "../lib/contexts"
15
+ import { RequiredIndicator } from "../lib/required-indicator"
16
+ import {
17
+ useHideFieldErrors,
18
+ useIsFieldRequired,
19
+ useIsInvalid,
20
+ } from "../lib/use-is-invalid"
21
+
22
+ type RadioGroupFieldProps = Omit<
23
+ ComponentProps<typeof RadioGroup>,
24
+ "name" | "onValueChange" | "value"
25
+ > & {
26
+ description?: ReactNode
27
+ label?: ReactNode
28
+ /** Force the required `*` indicator on or off. Defaults to schema-derived. */
29
+ required?: boolean
30
+ }
31
+
32
+ function RadioGroupField({
33
+ children,
34
+ description,
35
+ label,
36
+ required,
37
+ ...props
38
+ }: RadioGroupFieldProps) {
39
+ const field = useFieldContext<string>()
40
+ const errorId = `${field.name}-error`
41
+ const descriptionId = `${field.name}-description`
42
+ const isInvalid = useIsInvalid(field)
43
+ const hideErrors = useHideFieldErrors(field)
44
+ const showError = isInvalid && !hideErrors
45
+ const schemaRequired = useIsFieldRequired(field)
46
+ const isRequired = required ?? schemaRequired
47
+
48
+ return (
49
+ <Field data-invalid={isInvalid}>
50
+ {label && (
51
+ <FieldLabel>
52
+ {label}
53
+ {isRequired && <RequiredIndicator />}
54
+ </FieldLabel>
55
+ )}
56
+ <RadioGroup
57
+ aria-describedby={describedBy(
58
+ [description, descriptionId],
59
+ [showError, errorId]
60
+ )}
61
+ aria-invalid={isInvalid || undefined}
62
+ name={field.name}
63
+ onValueChange={(value) => field.handleChange(String(value))}
64
+ value={field.state.value ?? ""}
65
+ {...props}
66
+ >
67
+ {children}
68
+ </RadioGroup>
69
+ {description && (
70
+ <FieldDescription id={descriptionId}>{description}</FieldDescription>
71
+ )}
72
+ {showError && (
73
+ <FieldError errors={field.state.meta.errors} id={errorId} />
74
+ )}
75
+ </Field>
76
+ )
77
+ }
78
+
79
+ type RadioGroupFieldItemProps = Omit<
80
+ ComponentProps<typeof RadioGroupItem>,
81
+ "id"
82
+ > & {
83
+ children?: ReactNode
84
+ labelClassName?: string
85
+ }
86
+
87
+ function RadioGroupFieldItem({
88
+ children,
89
+ className,
90
+ labelClassName,
91
+ ...props
92
+ }: RadioGroupFieldItemProps) {
93
+ const id = useId()
94
+ return (
95
+ <label
96
+ className={cn("flex items-center gap-2 text-sm", labelClassName)}
97
+ htmlFor={id}
98
+ >
99
+ <RadioGroupItem className={className} id={id} {...props} />
100
+ {children}
101
+ </label>
102
+ )
103
+ }
104
+
105
+ export { RadioGroupField, RadioGroupFieldItem }
@@ -0,0 +1,29 @@
1
+ "use client"
2
+
3
+ import { Button } from "@zeno-lib/ui/button"
4
+ import type { ComponentProps, ReactNode } from "react"
5
+
6
+ import { useFormContext } from "../lib/contexts"
7
+
8
+ type ResetButtonProps = Omit<ComponentProps<typeof Button>, "onClick" | "type">
9
+
10
+ function ResetButton({
11
+ children,
12
+ variant = "outline",
13
+ ...props
14
+ }: ResetButtonProps) {
15
+ const form = useFormContext()
16
+ return (
17
+ <Button
18
+ onClick={() => form.reset()}
19
+ type="button"
20
+ variant={variant}
21
+ {...props}
22
+ >
23
+ {(children ?? "Reset") as ReactNode}
24
+ </Button>
25
+ )
26
+ }
27
+
28
+ export type { ResetButtonProps }
29
+ export { ResetButton }
@@ -0,0 +1,113 @@
1
+ "use client"
2
+
3
+ import {
4
+ Field,
5
+ FieldDescription,
6
+ FieldError,
7
+ FieldLabel,
8
+ } from "@zeno-lib/ui/field"
9
+ import {
10
+ Select,
11
+ SelectContent,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ } from "@zeno-lib/ui/select"
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"
20
+ import { RequiredIndicator } from "../lib/required-indicator"
21
+ import {
22
+ useHideFieldErrors,
23
+ useIsFieldRequired,
24
+ useIsInvalid,
25
+ } from "../lib/use-is-invalid"
26
+
27
+ type SelectFieldProps = Omit<
28
+ ComponentProps<typeof Select>,
29
+ "name" | "onValueChange" | "value" | "items"
30
+ > & {
31
+ children: ReactNode
32
+ description?: ReactNode
33
+ label?: ReactNode
34
+ placeholder?: string
35
+ /** Force the required `*` indicator on or off. Defaults to schema-derived. */
36
+ required?: boolean
37
+ triggerClassName?: string
38
+ triggerSize?: ComponentProps<typeof SelectTrigger>["size"]
39
+ }
40
+
41
+ function SelectField({
42
+ children,
43
+ description,
44
+ label,
45
+ placeholder,
46
+ required,
47
+ triggerClassName,
48
+ triggerSize,
49
+ ...props
50
+ }: SelectFieldProps) {
51
+ const field = useFieldContext()
52
+ const errorId = `${field.name}-error`
53
+ const descriptionId = `${field.name}-description`
54
+ const isInvalid = useIsInvalid(field)
55
+ const hideErrors = useHideFieldErrors(field)
56
+ const showError = isInvalid && !hideErrors
57
+ const schemaRequired = useIsFieldRequired(field)
58
+ const isRequired = required ?? schemaRequired
59
+
60
+ const items = useMemo(() => {
61
+ const map: Record<string, ReactNode> = {}
62
+ Children.forEach(children, (child) => {
63
+ if (isValidElement(child)) {
64
+ const p = child.props as { value?: unknown; children?: ReactNode }
65
+ if (p.value !== undefined) {
66
+ map[String(p.value)] = p.children
67
+ }
68
+ }
69
+ })
70
+ return map
71
+ }, [children])
72
+
73
+ return (
74
+ <Field data-invalid={isInvalid}>
75
+ {label && (
76
+ <FieldLabel htmlFor={field.name}>
77
+ {label}
78
+ {isRequired && <RequiredIndicator />}
79
+ </FieldLabel>
80
+ )}
81
+ <Select
82
+ items={items}
83
+ name={field.name}
84
+ onValueChange={(value) => field.handleChange(value)}
85
+ value={field.state.value ?? null}
86
+ {...props}
87
+ >
88
+ <SelectTrigger
89
+ aria-describedby={describedBy(
90
+ [description, descriptionId],
91
+ [showError, errorId]
92
+ )}
93
+ aria-invalid={isInvalid || undefined}
94
+ className={triggerClassName}
95
+ id={field.name}
96
+ onBlur={field.handleBlur}
97
+ size={triggerSize}
98
+ >
99
+ <SelectValue placeholder={placeholder} />
100
+ </SelectTrigger>
101
+ <SelectContent>{children}</SelectContent>
102
+ </Select>
103
+ {description && (
104
+ <FieldDescription id={descriptionId}>{description}</FieldDescription>
105
+ )}
106
+ {showError && (
107
+ <FieldError errors={field.state.meta.errors} id={errorId} />
108
+ )}
109
+ </Field>
110
+ )
111
+ }
112
+
113
+ export { SelectField }
@@ -0,0 +1,103 @@
1
+ "use client"
2
+
3
+ import {
4
+ Field,
5
+ FieldContent,
6
+ FieldDescription,
7
+ FieldError,
8
+ FieldLabel,
9
+ } from "@zeno-lib/ui/field"
10
+ import { Slider } from "@zeno-lib/ui/slider"
11
+ import type { ComponentProps, ReactNode } from "react"
12
+
13
+ import { describedBy } from "../lib/aria"
14
+ import { useFieldContext } from "../lib/contexts"
15
+ import { RequiredIndicator } from "../lib/required-indicator"
16
+ import {
17
+ useHideFieldErrors,
18
+ useIsFieldRequired,
19
+ useIsInvalid,
20
+ } from "../lib/use-is-invalid"
21
+
22
+ type SliderValue = number | number[]
23
+
24
+ type SliderFieldProps = Omit<
25
+ ComponentProps<typeof Slider>,
26
+ "name" | "onBlur" | "onValueChange" | "value"
27
+ > & {
28
+ description?: ReactNode
29
+ label?: ReactNode
30
+ /**
31
+ * Render a live numeric readout next to the label. Accepts the current
32
+ * value(s) so callers can format units (e.g. `${v}%`, `$${a}-$${b}`).
33
+ */
34
+ formatValue?: (value: SliderValue) => ReactNode
35
+ /** Force the required `*` indicator on or off. Defaults to schema-derived. */
36
+ required?: boolean
37
+ }
38
+
39
+ function SliderField({
40
+ description,
41
+ formatValue,
42
+ label,
43
+ required,
44
+ ...props
45
+ }: SliderFieldProps) {
46
+ const field = useFieldContext<SliderValue>()
47
+ const errorId = `${field.name}-error`
48
+ const descriptionId = `${field.name}-description`
49
+ const isInvalid = useIsInvalid(field)
50
+ const hideErrors = useHideFieldErrors(field)
51
+ const showError = isInvalid && !hideErrors
52
+ const schemaRequired = useIsFieldRequired(field)
53
+ const isRequired = required ?? schemaRequired
54
+
55
+ const value = field.state.value
56
+ const readout = formatValue && value !== undefined ? formatValue(value) : null
57
+
58
+ return (
59
+ <Field data-invalid={isInvalid}>
60
+ {(label || readout) && (
61
+ <FieldContent className="flex-row items-center justify-between">
62
+ {label && (
63
+ <FieldLabel htmlFor={field.name}>
64
+ {label}
65
+ {isRequired && <RequiredIndicator />}
66
+ </FieldLabel>
67
+ )}
68
+ {readout && (
69
+ <span className="text-muted-foreground text-sm tabular-nums">
70
+ {readout}
71
+ </span>
72
+ )}
73
+ </FieldContent>
74
+ )}
75
+ <Slider
76
+ aria-describedby={describedBy(
77
+ [description, descriptionId],
78
+ [showError, errorId]
79
+ )}
80
+ aria-invalid={isInvalid || undefined}
81
+ id={field.name}
82
+ name={field.name}
83
+ onBlur={field.handleBlur}
84
+ onValueChange={(next) => {
85
+ const value: SliderValue =
86
+ typeof next === "number" ? next : Array.from(next)
87
+ field.handleChange(value)
88
+ }}
89
+ value={value}
90
+ {...props}
91
+ />
92
+ {description && (
93
+ <FieldDescription id={descriptionId}>{description}</FieldDescription>
94
+ )}
95
+ {showError && (
96
+ <FieldError errors={field.state.meta.errors} id={errorId} />
97
+ )}
98
+ </Field>
99
+ )
100
+ }
101
+
102
+ export type { SliderFieldProps }
103
+ export { SliderField }
@@ -0,0 +1,31 @@
1
+ "use client"
2
+
3
+ import { Button } from "@zeno-lib/ui/button"
4
+ import { Spinner } from "@zeno-lib/ui/spinner"
5
+ import type { ComponentProps } from "react"
6
+
7
+ import { useFormContext } from "../lib/contexts"
8
+
9
+ type SubmitButtonProps = Omit<ComponentProps<typeof Button>, "type">
10
+
11
+ function SubmitButton({ children, disabled, ...props }: SubmitButtonProps) {
12
+ const form = useFormContext()
13
+ return (
14
+ <form.Subscribe
15
+ selector={(state) => [state.canSubmit, state.isSubmitting] as const}
16
+ >
17
+ {([canSubmit, isSubmitting]) => (
18
+ <Button
19
+ disabled={disabled || !canSubmit || isSubmitting}
20
+ type="submit"
21
+ {...props}
22
+ >
23
+ {isSubmitting && <Spinner />}
24
+ {children}
25
+ </Button>
26
+ )}
27
+ </form.Subscribe>
28
+ )
29
+ }
30
+
31
+ export { SubmitButton }
@@ -0,0 +1,80 @@
1
+ "use client"
2
+
3
+ import {
4
+ Field,
5
+ FieldContent,
6
+ FieldDescription,
7
+ FieldError,
8
+ FieldLabel,
9
+ } from "@zeno-lib/ui/field"
10
+ import { Switch } from "@zeno-lib/ui/switch"
11
+ import type { ComponentProps, ReactNode } from "react"
12
+
13
+ import { describedBy } from "../lib/aria"
14
+ import { useFieldContext } from "../lib/contexts"
15
+ import { RequiredIndicator } from "../lib/required-indicator"
16
+ import {
17
+ useHideFieldErrors,
18
+ useIsFieldRequired,
19
+ useIsInvalid,
20
+ } from "../lib/use-is-invalid"
21
+
22
+ type SwitchFieldProps = Omit<
23
+ ComponentProps<typeof Switch>,
24
+ "checked" | "id" | "name" | "onBlur" | "onCheckedChange"
25
+ > & {
26
+ description?: ReactNode
27
+ label?: ReactNode
28
+ /** Force the required `*` indicator on or off. Defaults to schema-derived. */
29
+ required?: boolean
30
+ }
31
+
32
+ function SwitchField({
33
+ description,
34
+ label,
35
+ required,
36
+ ...props
37
+ }: SwitchFieldProps) {
38
+ const field = useFieldContext<boolean>()
39
+ const errorId = `${field.name}-error`
40
+ const descriptionId = `${field.name}-description`
41
+ const isInvalid = useIsInvalid(field)
42
+ const hideErrors = useHideFieldErrors(field)
43
+ const showError = isInvalid && !hideErrors
44
+ const schemaRequired = useIsFieldRequired(field)
45
+ const isRequired = required ?? schemaRequired
46
+
47
+ return (
48
+ <Field data-invalid={isInvalid} orientation="horizontal">
49
+ <FieldContent>
50
+ {label && (
51
+ <FieldLabel htmlFor={field.name}>
52
+ {label}
53
+ {isRequired && <RequiredIndicator />}
54
+ </FieldLabel>
55
+ )}
56
+ {description && (
57
+ <FieldDescription id={descriptionId}>{description}</FieldDescription>
58
+ )}
59
+ {showError && (
60
+ <FieldError errors={field.state.meta.errors} id={errorId} />
61
+ )}
62
+ </FieldContent>
63
+ <Switch
64
+ aria-describedby={describedBy(
65
+ [description, descriptionId],
66
+ [showError, errorId]
67
+ )}
68
+ aria-invalid={isInvalid || undefined}
69
+ checked={field.state.value ?? false}
70
+ id={field.name}
71
+ name={field.name}
72
+ onBlur={field.handleBlur}
73
+ onCheckedChange={(value) => field.handleChange(value)}
74
+ {...props}
75
+ />
76
+ </Field>
77
+ )
78
+ }
79
+
80
+ export { SwitchField }
@@ -0,0 +1,91 @@
1
+ "use client"
2
+
3
+ import {
4
+ Field,
5
+ FieldDescription,
6
+ FieldError,
7
+ FieldLabel,
8
+ } from "@zeno-lib/ui/field"
9
+ import { InputGroup, InputGroupTextarea } from "@zeno-lib/ui/input-group"
10
+ import { Textarea } from "@zeno-lib/ui/textarea"
11
+ import { Children, type ComponentProps, type ReactNode } from "react"
12
+
13
+ import { describedBy } from "../lib/aria"
14
+ import { useFieldContext } from "../lib/contexts"
15
+ import { RequiredIndicator } from "../lib/required-indicator"
16
+ import {
17
+ useHideFieldErrors,
18
+ useIsFieldRequired,
19
+ useIsInvalid,
20
+ } from "../lib/use-is-invalid"
21
+
22
+ type TextAreaFieldProps = Omit<
23
+ ComponentProps<typeof Textarea>,
24
+ "children" | "id" | "name" | "onBlur" | "onChange" | "value"
25
+ > & {
26
+ children?: ReactNode
27
+ description?: ReactNode
28
+ label?: ReactNode
29
+ /** Force the required `*` indicator on or off. Defaults to schema-derived. */
30
+ required?: boolean
31
+ }
32
+
33
+ function TextAreaField({
34
+ children,
35
+ description,
36
+ label,
37
+ required,
38
+ ...props
39
+ }: TextAreaFieldProps) {
40
+ const field = useFieldContext<string>()
41
+ const errorId = `${field.name}-error`
42
+ const descriptionId = `${field.name}-description`
43
+ const isInvalid = useIsInvalid(field)
44
+ const hideErrors = useHideFieldErrors(field)
45
+ const showError = isInvalid && !hideErrors
46
+ const hasAddons = Children.count(children) > 0
47
+ const schemaRequired = useIsFieldRequired(field)
48
+ const isRequired = required ?? schemaRequired
49
+
50
+ const textareaProps = {
51
+ "aria-describedby": describedBy(
52
+ [description, descriptionId],
53
+ [showError, errorId]
54
+ ),
55
+ "aria-invalid": isInvalid || undefined,
56
+ id: field.name,
57
+ name: field.name,
58
+ onBlur: field.handleBlur,
59
+ onChange: (event: React.ChangeEvent<HTMLTextAreaElement>) =>
60
+ field.handleChange(event.target.value),
61
+ value: field.state.value ?? "",
62
+ ...props,
63
+ }
64
+
65
+ return (
66
+ <Field data-invalid={isInvalid}>
67
+ {label && (
68
+ <FieldLabel htmlFor={field.name}>
69
+ {label}
70
+ {isRequired && <RequiredIndicator />}
71
+ </FieldLabel>
72
+ )}
73
+ {hasAddons ? (
74
+ <InputGroup>
75
+ <InputGroupTextarea {...textareaProps} />
76
+ {children}
77
+ </InputGroup>
78
+ ) : (
79
+ <Textarea {...textareaProps} />
80
+ )}
81
+ {description && (
82
+ <FieldDescription id={descriptionId}>{description}</FieldDescription>
83
+ )}
84
+ {showError && (
85
+ <FieldError errors={field.state.meta.errors} id={errorId} />
86
+ )}
87
+ </Field>
88
+ )
89
+ }
90
+
91
+ export { TextAreaField }