@coopdigital/react 1.30.1 → 1.31.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/components/Author/Author.d.ts +3 -1
- package/dist/components/Author/Author.js +1 -1
- package/dist/components/Author/index.d.ts +2 -2
- package/dist/components/Author/index.js +2 -2
- package/dist/components/Combobox/Combobox.d.ts +1 -1
- package/dist/components/Combobox/Combobox.js +29 -8
- package/dist/components/Combobox/{combobox-utils.js → combobox-internal.js} +4 -2
- package/dist/components/DateInput/DateInput.context.js +7 -0
- package/dist/components/DateInput/DateInput.d.ts +104 -0
- package/dist/components/DateInput/DateInput.js +128 -0
- package/dist/components/DateInput/dateinput-internal.d.ts +6 -0
- package/dist/components/DateInput/dateinput-internal.js +82 -0
- package/dist/components/DateInput/dateinput-utils.d.ts +66 -0
- package/dist/components/DateInput/dateinput-utils.js +91 -0
- package/dist/components/DateInput/index.d.ts +3 -0
- package/dist/components/DateInput/index.js +6 -0
- package/dist/components/Field/Field.context.js +1 -1
- package/dist/components/Field/Field.js +1 -1
- package/dist/components/FieldMarkers/{fieldmarkers-utils.js → fieldmarkers-internal.js} +1 -1
- package/dist/components/Fieldset/Fieldset.js +1 -1
- package/dist/components/Form/Form.d.ts +7 -1
- package/dist/components/Form/Form.js +2 -1
- package/dist/components/Squircle/Squircle.d.ts +3 -1
- package/dist/components/Squircle/Squircle.js +1 -1
- package/dist/components/Squircle/index.d.ts +2 -2
- package/dist/components/Squircle/index.js +2 -2
- package/dist/components/Textarea/Textarea.js +14 -4
- package/dist/components/ValidationSummary/ValidationSummary.d.ts +1 -1
- package/dist/components/ValidationSummary/ValidationSummary.js +1 -1
- package/dist/components/ValidationSummary/{validationsummary-utils.d.ts → validationsummary-internal.d.ts} +1 -1
- package/dist/components/ValidationSummary/{validationsummary-utils.js → validationsummary-internal.js} +3 -2
- package/dist/components/icons/AccountCircleFilledIcon.d.ts +2 -1
- package/dist/components/icons/AccountCircleIcon.d.ts +2 -1
- package/dist/components/icons/AccountFilledIcon.d.ts +2 -1
- package/dist/components/icons/AccountIcon.d.ts +2 -1
- package/dist/components/icons/AddIcon.d.ts +2 -1
- package/dist/components/icons/AppsFilledIcon.d.ts +2 -1
- package/dist/components/icons/AppsIcon.d.ts +2 -1
- package/dist/components/icons/ArrowDownIcon.d.ts +2 -1
- package/dist/components/icons/ArrowLeftIcon.d.ts +2 -1
- package/dist/components/icons/ArrowRightIcon.d.ts +2 -1
- package/dist/components/icons/ArrowUpIcon.d.ts +2 -1
- package/dist/components/icons/BackspaceFilledIcon.d.ts +2 -1
- package/dist/components/icons/BackspaceIcon.d.ts +2 -1
- package/dist/components/icons/BasketFilledIcon.d.ts +2 -1
- package/dist/components/icons/BasketIcon.d.ts +2 -1
- package/dist/components/icons/BellFilledIcon.d.ts +2 -1
- package/dist/components/icons/BellIcon.d.ts +2 -1
- package/dist/components/icons/BroomIcon.d.ts +2 -1
- package/dist/components/icons/CalendarFilledIcon.d.ts +2 -1
- package/dist/components/icons/CalendarIcon.d.ts +2 -1
- package/dist/components/icons/CashFilledIcon.d.ts +2 -1
- package/dist/components/icons/CashIcon.d.ts +2 -1
- package/dist/components/icons/ChevronDownIcon.d.ts +2 -1
- package/dist/components/icons/ChevronLeftIcon.d.ts +2 -1
- package/dist/components/icons/ChevronRightIcon.d.ts +2 -1
- package/dist/components/icons/ChevronUpIcon.d.ts +2 -1
- package/dist/components/icons/CircleBurgerFilledIcon.d.ts +2 -1
- package/dist/components/icons/CircleBurgerIcon.d.ts +2 -1
- package/dist/components/icons/ClipboardFilledIcon.d.ts +2 -1
- package/dist/components/icons/ClipboardIcon.d.ts +2 -1
- package/dist/components/icons/ClockFilledIcon.d.ts +2 -1
- package/dist/components/icons/ClockIcon.d.ts +2 -1
- package/dist/components/icons/CloseAltFilledIcon.d.ts +2 -1
- package/dist/components/icons/CloseAltIcon.d.ts +2 -1
- package/dist/components/icons/CogIcon.d.ts +2 -1
- package/dist/components/icons/CommunityFilledIcon.d.ts +2 -1
- package/dist/components/icons/CommunityIcon.d.ts +2 -1
- package/dist/components/icons/ComputerIcon.d.ts +2 -1
- package/dist/components/icons/ContactFilledIcon.d.ts +2 -1
- package/dist/components/icons/ContactIcon.d.ts +2 -1
- package/dist/components/icons/ContactlessFilledIcon.d.ts +2 -1
- package/dist/components/icons/ContactlessIcon.d.ts +2 -1
- package/dist/components/icons/CoopLiveIcon.d.ts +2 -1
- package/dist/components/icons/CoopLocationFilledIcon.d.ts +2 -1
- package/dist/components/icons/CoopLogoIcon.d.ts +2 -1
- package/dist/components/icons/CoopMembershipCardFilledIcon.d.ts +2 -1
- package/dist/components/icons/CreditCardFilledIcon.d.ts +2 -1
- package/dist/components/icons/CreditCardIcon.d.ts +2 -1
- package/dist/components/icons/CrossIcon.d.ts +2 -1
- package/dist/components/icons/DocumentIcon.d.ts +2 -1
- package/dist/components/icons/DownloadIcon.d.ts +2 -1
- package/dist/components/icons/EnterStoreIcon.d.ts +2 -1
- package/dist/components/icons/ExitStoreIcon.d.ts +2 -1
- package/dist/components/icons/EyeFilledIcon.d.ts +2 -1
- package/dist/components/icons/EyeIcon.d.ts +2 -1
- package/dist/components/icons/EyeStrikeFilledIcon.d.ts +2 -1
- package/dist/components/icons/EyeStrikeIcon.d.ts +2 -1
- package/dist/components/icons/FeedbackFilledIcon.d.ts +2 -1
- package/dist/components/icons/FeedbackIcon.d.ts +2 -1
- package/dist/components/icons/FilterIcon.d.ts +2 -1
- package/dist/components/icons/GiftCardFilledIcon.d.ts +2 -1
- package/dist/components/icons/GiftCardIcon.d.ts +2 -1
- package/dist/components/icons/GiftFilledIcon.d.ts +2 -1
- package/dist/components/icons/GiftIcon.d.ts +2 -1
- package/dist/components/icons/HelpFilledIcon.d.ts +2 -1
- package/dist/components/icons/HelpIcon.d.ts +2 -1
- package/dist/components/icons/HomeFilledIcon.d.ts +2 -1
- package/dist/components/icons/HomeIcon.d.ts +2 -1
- package/dist/components/icons/HowDoIAssistantIcon.d.ts +2 -1
- package/dist/components/icons/InfoIcon.d.ts +2 -1
- package/dist/components/icons/KebabIcon.d.ts +2 -1
- package/dist/components/icons/LoadingIcon.d.ts +2 -1
- package/dist/components/icons/LocationFilledIcon.d.ts +2 -1
- package/dist/components/icons/LocationIcon.d.ts +2 -1
- package/dist/components/icons/LogoutIcon.d.ts +2 -1
- package/dist/components/icons/MailIcon.d.ts +2 -1
- package/dist/components/icons/MenuIcon.d.ts +2 -1
- package/dist/components/icons/MessageIcon.d.ts +2 -1
- package/dist/components/icons/MinusIcon.d.ts +2 -1
- package/dist/components/icons/NotesFilledIcon.d.ts +2 -1
- package/dist/components/icons/NotesIcon.d.ts +2 -1
- package/dist/components/icons/OpenNewIcon.d.ts +2 -1
- package/dist/components/icons/OwnBagIcon.d.ts +2 -1
- package/dist/components/icons/PadlockIcon.d.ts +2 -1
- package/dist/components/icons/PencilIcon.d.ts +2 -1
- package/dist/components/icons/PhoneIcon.d.ts +2 -1
- package/dist/components/icons/PowerIcon.d.ts +2 -1
- package/dist/components/icons/SaveFilledIcon.d.ts +2 -1
- package/dist/components/icons/SaveIcon.d.ts +2 -1
- package/dist/components/icons/ScooterIcon.d.ts +2 -1
- package/dist/components/icons/SearchFilledIcon.d.ts +2 -1
- package/dist/components/icons/SearchIcon.d.ts +2 -1
- package/dist/components/icons/SendIcon.d.ts +2 -1
- package/dist/components/icons/SpannerIcon.d.ts +2 -1
- package/dist/components/icons/StarFilledIcon.d.ts +2 -1
- package/dist/components/icons/StarIcon.d.ts +2 -1
- package/dist/components/icons/TickAltFilledIcon.d.ts +2 -1
- package/dist/components/icons/TickAltIcon.d.ts +2 -1
- package/dist/components/icons/TickIcon.d.ts +2 -1
- package/dist/components/icons/TruckIcon.d.ts +2 -1
- package/dist/components/icons/UserWipeIcon.d.ts +2 -1
- package/dist/components/icons/VoucherFilledIcon.d.ts +2 -1
- package/dist/components/icons/VoucherIcon.d.ts +2 -1
- package/dist/components/icons/WarningCircleFilledIcon.d.ts +2 -1
- package/dist/components/icons/WarningCircleIcon.d.ts +2 -1
- package/dist/components/icons/WarningFilledIcon.d.ts +2 -1
- package/dist/components/icons/WarningIcon.d.ts +2 -1
- package/dist/components/icons/WifiIcon.d.ts +2 -1
- package/dist/components/icons/WriteIcon.d.ts +2 -1
- package/dist/hooks/index.d.ts +2 -1
- package/dist/hooks/index.js +2 -1
- package/dist/hooks/useFormReset.d.ts +6 -0
- package/dist/hooks/useFormReset.js +19 -0
- package/dist/hooks/useSlots.d.ts +2 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +6 -4
- package/dist/types/index.js +4 -0
- package/dist/utils/slots.d.ts +1 -1
- package/dist/utils/slots.js +20 -12
- package/package.json +22 -18
- package/src/components/Author/Author.tsx +1 -1
- package/src/components/Combobox/Combobox.context.ts +3 -1
- package/src/components/Combobox/Combobox.tsx +57 -24
- package/src/components/Combobox/combobox-internal.ts +60 -0
- package/src/components/Combobox/combobox-utils.ts +1 -41
- package/src/components/Combobox/index.ts +1 -0
- package/src/components/DateInput/DateInput.context.ts +25 -0
- package/src/components/DateInput/DateInput.tsx +252 -0
- package/src/components/DateInput/dateinput-internal.ts +176 -0
- package/src/components/DateInput/dateinput-utils.ts +194 -0
- package/src/components/DateInput/index.ts +6 -0
- package/src/components/Field/Field.context.ts +1 -1
- package/src/components/Field/Field.tsx +1 -1
- package/src/components/Fieldset/Fieldset.tsx +5 -1
- package/src/components/Form/Form.tsx +9 -1
- package/src/components/Squircle/Squircle.tsx +1 -1
- package/src/components/Textarea/Textarea.tsx +19 -4
- package/src/components/ValidationSummary/ValidationSummary.context.ts +1 -1
- package/src/components/ValidationSummary/ValidationSummary.tsx +1 -1
- package/src/components/ValidationSummary/{validationsummary-utils.ts → validationsummary-internal.ts} +9 -8
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useFormReset.ts +28 -0
- package/src/index.ts +1 -0
- package/src/utils/slots.ts +37 -23
- /package/src/components/FieldMarkers/{fieldmarkers-utils.ts → fieldmarkers-internal.ts} +0 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import type { ChangeEvent, FocusEvent, HTMLAttributes, JSX, ReactNode, Ref } from "react"
|
|
3
|
+
|
|
4
|
+
import clsx from "clsx"
|
|
5
|
+
import mergeRefs from "merge-refs"
|
|
6
|
+
import { useRef, useState } from "react"
|
|
7
|
+
|
|
8
|
+
import { Field } from "@/components/Field"
|
|
9
|
+
import { TextInput, type TextInputProps } from "@/components/TextInput"
|
|
10
|
+
import { useFormReset } from "@/hooks/useFormReset"
|
|
11
|
+
import { useId } from "@/hooks/useId"
|
|
12
|
+
import { StandardSizes } from "@/types"
|
|
13
|
+
import { forEachSlot } from "@/utils"
|
|
14
|
+
|
|
15
|
+
import { formatDateValue, toDateParts, toDateValue } from "./dateinput-internal"
|
|
16
|
+
import {
|
|
17
|
+
type DateInputMode,
|
|
18
|
+
type DatePart,
|
|
19
|
+
type DateParts,
|
|
20
|
+
getAutoComplete,
|
|
21
|
+
} from "./dateinput-utils"
|
|
22
|
+
import { DateInputContext, useDateInput } from "./DateInput.context"
|
|
23
|
+
|
|
24
|
+
export interface DateInputChangeEvent {
|
|
25
|
+
/** The parts as typed. */
|
|
26
|
+
parts: DateParts
|
|
27
|
+
/** The parts as typed, joined from year to day, such as `2026-2-30`.
|
|
28
|
+
*
|
|
29
|
+
* Not checked: use `validateDate` before relying on it. */
|
|
30
|
+
value: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DateInputProps extends Omit<
|
|
34
|
+
HTMLAttributes<HTMLDivElement>,
|
|
35
|
+
"defaultValue" | "onBlur" | "onChange"
|
|
36
|
+
> {
|
|
37
|
+
/** Content inside the component.
|
|
38
|
+
*
|
|
39
|
+
* This should be a set of `<DateInput.Day>`, `<DateInput.Month>` and `<DateInput.Year>`. */
|
|
40
|
+
children?: ReactNode
|
|
41
|
+
/** Specify additional CSS classes. */
|
|
42
|
+
className?: string
|
|
43
|
+
/** Specify the initial value, when not controlled. One of:
|
|
44
|
+
*
|
|
45
|
+
* - an ISO date, such as `1990-08-21`
|
|
46
|
+
* - the parts, such as `{ year: "2026" }` */
|
|
47
|
+
defaultValue?: DateParts | string
|
|
48
|
+
/** Specify the base for the part ids. Will be auto-generated if not set. */
|
|
49
|
+
id?: string
|
|
50
|
+
/** Specify the DateInput mode. */
|
|
51
|
+
mode?: DateInputMode
|
|
52
|
+
/** Specify the name.
|
|
53
|
+
*
|
|
54
|
+
* Each part is `{name}-day`, `{name}-month` and`{name}-year`.
|
|
55
|
+
*
|
|
56
|
+
* A hidden input `{name}` holds the value as typed. */
|
|
57
|
+
name?: string
|
|
58
|
+
/** Called when focus leaves the whole DateInput, not each part. */
|
|
59
|
+
onBlur?: (event: DateInputChangeEvent) => void
|
|
60
|
+
/** Called when any part changes. */
|
|
61
|
+
onChange?: (event: DateInputChangeEvent) => void
|
|
62
|
+
/** Specify a custom React ref for this component. */
|
|
63
|
+
ref?: Ref<HTMLDivElement>
|
|
64
|
+
/** Specify the component size. */
|
|
65
|
+
size?: StandardSizes
|
|
66
|
+
/** Specify the value, when controlled. One of:
|
|
67
|
+
*
|
|
68
|
+
* - an ISO date, such as `1990-08-21`
|
|
69
|
+
* - the parts, such as `{ year: "2026" }`
|
|
70
|
+
*
|
|
71
|
+
* Update it in `onChange` with the `value` or `parts` of the event. */
|
|
72
|
+
value?: DateParts | string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const PART_SLOTS: Record<string, DatePart> = {
|
|
76
|
+
DateInputDay: "day",
|
|
77
|
+
DateInputMonth: "month",
|
|
78
|
+
DateInputYear: "year",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const DateInput = ({
|
|
82
|
+
children,
|
|
83
|
+
className,
|
|
84
|
+
defaultValue,
|
|
85
|
+
id,
|
|
86
|
+
mode,
|
|
87
|
+
name,
|
|
88
|
+
onBlur,
|
|
89
|
+
onChange,
|
|
90
|
+
ref,
|
|
91
|
+
size,
|
|
92
|
+
value,
|
|
93
|
+
...props
|
|
94
|
+
}: DateInputProps): JSX.Element => {
|
|
95
|
+
const uid = useId(id)
|
|
96
|
+
|
|
97
|
+
// The parts rendered, in order
|
|
98
|
+
const rendered: DatePart[] = []
|
|
99
|
+
|
|
100
|
+
forEachSlot(children, (slot) => {
|
|
101
|
+
const part = PART_SLOTS[slot.name]
|
|
102
|
+
if (part && !rendered.includes(part)) rendered.push(part)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const defaultParts = toDateParts(defaultValue, rendered)
|
|
106
|
+
const [typed, setTyped] = useState<DateParts>(() =>
|
|
107
|
+
value === undefined ? defaultParts : toDateParts(value, rendered)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
// When controlled, show the parts as typed while they match the value,
|
|
111
|
+
// which keeps a dash typed in a part. Otherwise show the parts of the value.
|
|
112
|
+
const controlled = value !== undefined
|
|
113
|
+
const parts =
|
|
114
|
+
controlled && toDateValue(value, rendered) !== toDateValue(typed, rendered)
|
|
115
|
+
? toDateParts(value, rendered)
|
|
116
|
+
: typed
|
|
117
|
+
|
|
118
|
+
// The browser resets each part input to its default value, so reset the
|
|
119
|
+
// parts too
|
|
120
|
+
const controlRef = useRef<HTMLInputElement>(null)
|
|
121
|
+
useFormReset(controlRef, () => {
|
|
122
|
+
if (!controlled) setTyped(defaultParts)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const getEvent = (next: DateParts): DateInputChangeEvent => ({
|
|
126
|
+
parts: next,
|
|
127
|
+
value: formatDateValue(next, rendered),
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// Build on the parts shown, not typed, which still holds a change a
|
|
131
|
+
// controlling parent did not pass back
|
|
132
|
+
const changePart = (part: DatePart, partValue: string) => {
|
|
133
|
+
const next = { ...parts, [part]: partValue }
|
|
134
|
+
setTyped(next)
|
|
135
|
+
onChange?.(getEvent(next))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Only call onBlur when focus moves outside the DateInput
|
|
139
|
+
const handleBlur = (event: FocusEvent<HTMLDivElement>) => {
|
|
140
|
+
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
141
|
+
onBlur?.(getEvent(parts))
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const componentProps = {
|
|
146
|
+
...props,
|
|
147
|
+
className: clsx("coop-date-input", className),
|
|
148
|
+
onBlur: handleBlur,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<DateInputContext.Provider
|
|
153
|
+
value={{
|
|
154
|
+
changePart,
|
|
155
|
+
controlled,
|
|
156
|
+
controlPart: rendered[0],
|
|
157
|
+
controlRef,
|
|
158
|
+
defaultValue: defaultParts,
|
|
159
|
+
id: uid,
|
|
160
|
+
mode,
|
|
161
|
+
name,
|
|
162
|
+
parts,
|
|
163
|
+
size,
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
<div {...componentProps} ref={ref}>
|
|
167
|
+
{name && <input name={name} type="hidden" value={formatDateValue(parts, rendered)} />}
|
|
168
|
+
{children}
|
|
169
|
+
</div>
|
|
170
|
+
</DateInputContext.Provider>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface DateInputPartProps extends Omit<
|
|
175
|
+
TextInputProps,
|
|
176
|
+
"defaultValue" | "fit" | "prefix" | "prefixInline" | "suffix" | "suffixInline" | "type" | "value"
|
|
177
|
+
> {
|
|
178
|
+
/** Specify the part error state.
|
|
179
|
+
*
|
|
180
|
+
* Inherits error state of a `group` Fieldset when not set. */
|
|
181
|
+
error?: boolean
|
|
182
|
+
/** Specify the label. */
|
|
183
|
+
label?: ReactNode
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const DateInputPart = ({
|
|
187
|
+
autoComplete,
|
|
188
|
+
className,
|
|
189
|
+
datePart: part,
|
|
190
|
+
error,
|
|
191
|
+
id,
|
|
192
|
+
label,
|
|
193
|
+
name,
|
|
194
|
+
onChange,
|
|
195
|
+
size,
|
|
196
|
+
...props
|
|
197
|
+
}: DateInputPartProps & { datePart: DatePart; label: ReactNode }): JSX.Element => {
|
|
198
|
+
const dateInput = useDateInput()
|
|
199
|
+
|
|
200
|
+
const valueProps = dateInput?.controlled
|
|
201
|
+
? { value: dateInput.parts[part] ?? "" }
|
|
202
|
+
: { defaultValue: dateInput?.defaultValue?.[part] }
|
|
203
|
+
|
|
204
|
+
const inputProps = {
|
|
205
|
+
...valueProps,
|
|
206
|
+
autoComplete: autoComplete ?? getAutoComplete(dateInput?.mode, part),
|
|
207
|
+
className: clsx("coop-date-input--input", className),
|
|
208
|
+
"data-part": part,
|
|
209
|
+
id: id ?? (dateInput ? `${dateInput.id}-${part}` : undefined),
|
|
210
|
+
inputMode: "numeric" as const,
|
|
211
|
+
name: name ?? (dateInput?.name ? `${dateInput.name}-${part}` : undefined),
|
|
212
|
+
onChange: (event: ChangeEvent<HTMLInputElement>) => {
|
|
213
|
+
dateInput?.changePart(part, event.target.value)
|
|
214
|
+
onChange?.(event)
|
|
215
|
+
},
|
|
216
|
+
pattern: "[0-9]*",
|
|
217
|
+
size: size ?? dateInput?.size,
|
|
218
|
+
spellCheck: false,
|
|
219
|
+
...props,
|
|
220
|
+
// The first part is the control the form reset is looked up from
|
|
221
|
+
ref: dateInput?.controlPart === part ? mergeRefs(dateInput.controlRef, props.ref) : props.ref,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<Field error={error} hideErrorBar>
|
|
226
|
+
<Field.Label>{label}</Field.Label>
|
|
227
|
+
<TextInput {...inputProps} />
|
|
228
|
+
</Field>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const DateInputDay = ({ label = "Day", ...props }: DateInputPartProps): JSX.Element => (
|
|
233
|
+
<DateInputPart {...props} datePart="day" label={label} />
|
|
234
|
+
)
|
|
235
|
+
const DateInputMonth = ({ label = "Month", ...props }: DateInputPartProps): JSX.Element => (
|
|
236
|
+
<DateInputPart {...props} datePart="month" label={label} />
|
|
237
|
+
)
|
|
238
|
+
const DateInputYear = ({ label = "Year", ...props }: DateInputPartProps): JSX.Element => (
|
|
239
|
+
<DateInputPart {...props} datePart="year" label={label} />
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
DateInput.slot = { name: "DateInput" }
|
|
243
|
+
DateInputDay.slot = { name: "DateInputDay" }
|
|
244
|
+
DateInputMonth.slot = { name: "DateInputMonth" }
|
|
245
|
+
DateInputYear.slot = { name: "DateInputYear" }
|
|
246
|
+
|
|
247
|
+
DateInput.Day = DateInputDay
|
|
248
|
+
DateInput.Month = DateInputMonth
|
|
249
|
+
DateInput.Year = DateInputYear
|
|
250
|
+
|
|
251
|
+
export default DateInput
|
|
252
|
+
export { DateInput, DateInputDay, DateInputMonth, DateInputYear }
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Not exported from the DateInput entry. No React or "use client", as
|
|
2
|
+
// dateinput-utils imports it.
|
|
3
|
+
|
|
4
|
+
import { format, formatISO, isExists, isValid, parseISO } from "date-fns"
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
DATE_ERROR_MESSAGES,
|
|
8
|
+
DateErrorCode,
|
|
9
|
+
DatePart,
|
|
10
|
+
DateParts,
|
|
11
|
+
DateValidationOptions,
|
|
12
|
+
} from "./dateinput-utils"
|
|
13
|
+
|
|
14
|
+
type DefaultMessages = typeof DATE_ERROR_MESSAGES
|
|
15
|
+
|
|
16
|
+
export interface Limits {
|
|
17
|
+
max?: string
|
|
18
|
+
min?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Year first, whatever order the parts render in
|
|
22
|
+
const VALUE_ORDER: DatePart[] = ["year", "month", "day"]
|
|
23
|
+
|
|
24
|
+
// Codes that mark every part as invalid
|
|
25
|
+
export const WHOLE_DATE_ERRORS = new Set<DateErrorCode>([
|
|
26
|
+
"after-max",
|
|
27
|
+
"before-min",
|
|
28
|
+
"impossible-date",
|
|
29
|
+
"missing-date",
|
|
30
|
+
"unsupported-parts",
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
const valueParts = (parts: DatePart[]) => VALUE_ORDER.filter((part) => parts.includes(part))
|
|
34
|
+
|
|
35
|
+
// Parts as typed to a value, such as 2026-2-30. Empty if every part is empty
|
|
36
|
+
export const formatDateValue = (date: DateParts, parts: DatePart[] = VALUE_ORDER): string => {
|
|
37
|
+
const values = valueParts(parts).map((part) => date[part] ?? "")
|
|
38
|
+
|
|
39
|
+
return values.every((value) => value === "") ? "" : values.join("-")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// A piece per part. A leading -- is the ISO form of a date without a year
|
|
43
|
+
const splitValue = (value: string, used: DatePart[]) => {
|
|
44
|
+
const pieces = value.split("-")
|
|
45
|
+
const iso = !used.includes("year") && value.startsWith("--") && pieces.length === used.length + 2
|
|
46
|
+
|
|
47
|
+
return iso ? pieces.slice(2) : pieces
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// A value or ISO date to parts. Undefined when a dash typed in a part leaves
|
|
51
|
+
// it unclear which part is which
|
|
52
|
+
export const parseDateValue = (
|
|
53
|
+
value: string,
|
|
54
|
+
parts: DatePart[] = VALUE_ORDER
|
|
55
|
+
): DateParts | undefined => {
|
|
56
|
+
if (value === "") return {}
|
|
57
|
+
|
|
58
|
+
const used = valueParts(parts)
|
|
59
|
+
const pieces = splitValue(value, used)
|
|
60
|
+
if (pieces.length !== used.length) return undefined
|
|
61
|
+
|
|
62
|
+
return Object.fromEntries(used.map((part, index) => [part, pieces[index]]))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A value, ISO date or parts to a value
|
|
66
|
+
export const toDateValue = (input: DateParts | string = "", parts?: DatePart[]): string =>
|
|
67
|
+
typeof input === "string" ? input : formatDateValue(input, parts)
|
|
68
|
+
|
|
69
|
+
// A value, ISO date or parts to parts. Empty when a value cannot be split
|
|
70
|
+
export const toDateParts = (input: DateParts | string = "", parts?: DatePart[]): DateParts =>
|
|
71
|
+
typeof input === "string" ? (parseDateValue(input, parts) ?? {}) : input
|
|
72
|
+
|
|
73
|
+
export const toNumber = (value: string): number | null =>
|
|
74
|
+
/^\d+$/.test(value) ? Number(value) : null
|
|
75
|
+
|
|
76
|
+
export const isValidPart = (part: DatePart, value: string): boolean => {
|
|
77
|
+
if (part === "year") return /^\d{4}$/.test(value)
|
|
78
|
+
|
|
79
|
+
const number = toNumber(value)
|
|
80
|
+
const max = part === "day" ? 31 : 12
|
|
81
|
+
|
|
82
|
+
return number !== null && number >= 1 && number <= max
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const trimParts = (date: DateParts): Required<DateParts> => ({
|
|
86
|
+
day: date.day?.trim() ?? "",
|
|
87
|
+
month: date.month?.trim() ?? "",
|
|
88
|
+
year: date.year?.trim() ?? "",
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// An error code for each problem, in the order the parts are entered. A
|
|
92
|
+
// date with every part empty only returns missing-date. impossible-date
|
|
93
|
+
// is only checked once every part is valid.
|
|
94
|
+
export const getDateErrors = (values: Required<DateParts>, parts: DatePart[]): DateErrorCode[] => {
|
|
95
|
+
if (parts.every((part) => values[part] === "")) return ["missing-date"]
|
|
96
|
+
|
|
97
|
+
const errors: DateErrorCode[] = []
|
|
98
|
+
|
|
99
|
+
for (const part of parts) {
|
|
100
|
+
if (values[part] === "") errors.push(`missing-${part}`)
|
|
101
|
+
else if (!isValidPart(part, values[part])) errors.push(`invalid-${part}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (errors.length > 0 || !parts.includes("day") || !parts.includes("month")) {
|
|
105
|
+
return errors
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Without a year, check against a leap year, so 29 February is allowed
|
|
109
|
+
const year = parts.includes("year") ? Number(values.year) : 2000
|
|
110
|
+
|
|
111
|
+
return isExists(year, Number(values.month) - 1, Number(values.day)) ? [] : ["impossible-date"]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const getInvalidParts = (errors: DateErrorCode[], parts: DatePart[]): DatePart[] =>
|
|
115
|
+
parts.filter((part) => errors.some((code) => WHOLE_DATE_ERRORS.has(code) || code.endsWith(part)))
|
|
116
|
+
|
|
117
|
+
// Remove extra leading zeros, such as in "008", then pad to 2 digits
|
|
118
|
+
export const pad = (value: string): string => String(Number(value)).padStart(2, "0")
|
|
119
|
+
|
|
120
|
+
// The ISO 8601 string for a valid date, using the parts given. Empty
|
|
121
|
+
// when the parts have no ISO format, such as day and year.
|
|
122
|
+
export const toISODate = ({ day, month, year }: Required<DateParts>, parts: DatePart[]): string => {
|
|
123
|
+
const has = (part: DatePart) => parts.includes(part)
|
|
124
|
+
|
|
125
|
+
if (has("day") && has("month") && has("year")) return `${year}-${pad(month)}-${pad(day)}`
|
|
126
|
+
if (has("month") && has("year") && !has("day")) return `${year}-${pad(month)}`
|
|
127
|
+
if (has("day") && has("month")) return `--${pad(month)}-${pad(day)}`
|
|
128
|
+
if (parts.length === 1 && has("year")) return year
|
|
129
|
+
|
|
130
|
+
return ""
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Read a min or max limit. Throws for an invalid limit, as that is a
|
|
134
|
+
// mistake in the code. An ISO date string is read in local time.
|
|
135
|
+
export const toLimit = (limit: Date | string, name: "max" | "min"): Date => {
|
|
136
|
+
const date = typeof limit === "string" ? parseISO(limit) : limit
|
|
137
|
+
if (!isValid(date)) {
|
|
138
|
+
throw new Error(`${name} must be a valid Date or an ISO date, such as 2020-01-31`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return date
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Write a limit out for a message, to the same detail as the value, such as
|
|
145
|
+
// "1 January 2020", "January 2020" or "2020"
|
|
146
|
+
export const formatLimit = (limit: Date, length: number): string =>
|
|
147
|
+
format(limit, length === 4 ? "yyyy" : length === 7 ? "MMMM yyyy" : "d MMMM yyyy")
|
|
148
|
+
|
|
149
|
+
// Compare the value with min and max, to the same detail as the value. A
|
|
150
|
+
// day and month value has no year, so it is not compared.
|
|
151
|
+
export const getRangeError = (value: string, min?: Date, max?: Date): DateErrorCode | undefined => {
|
|
152
|
+
if (value.startsWith("--")) return undefined
|
|
153
|
+
|
|
154
|
+
const cut = (limit: Date) => formatISO(limit, { representation: "date" }).slice(0, value.length)
|
|
155
|
+
|
|
156
|
+
if (min && value < cut(min)) return "before-min"
|
|
157
|
+
if (max && value > cut(max)) return "after-max"
|
|
158
|
+
|
|
159
|
+
return undefined
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// A custom message, or the default. Only the defaults use the limits. The
|
|
163
|
+
// defaults are passed in, as they are public and live in dateinput-utils
|
|
164
|
+
export const getMessage = (
|
|
165
|
+
code: DateErrorCode,
|
|
166
|
+
label: string | undefined,
|
|
167
|
+
messages: DateValidationOptions["messages"] = {},
|
|
168
|
+
limits: Limits,
|
|
169
|
+
defaults: DefaultMessages
|
|
170
|
+
): string => {
|
|
171
|
+
const custom = messages[code]
|
|
172
|
+
if (custom !== undefined) return typeof custom === "function" ? custom(label) : custom
|
|
173
|
+
|
|
174
|
+
const fallback = defaults[code]
|
|
175
|
+
return typeof fallback === "function" ? fallback(label, limits) : fallback
|
|
176
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Validation for dates entered as separate day, month and year parts.
|
|
2
|
+
// The DateInput entry exports this to server code too, so it must not import
|
|
3
|
+
// React or use "use client".
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
formatLimit,
|
|
7
|
+
getDateErrors,
|
|
8
|
+
getInvalidParts,
|
|
9
|
+
getMessage,
|
|
10
|
+
getRangeError,
|
|
11
|
+
type Limits,
|
|
12
|
+
parseDateValue,
|
|
13
|
+
toISODate,
|
|
14
|
+
toLimit,
|
|
15
|
+
trimParts,
|
|
16
|
+
} from "./dateinput-internal"
|
|
17
|
+
|
|
18
|
+
export type DatePart = "day" | "month" | "year"
|
|
19
|
+
|
|
20
|
+
export interface DateParts {
|
|
21
|
+
day?: string
|
|
22
|
+
month?: string
|
|
23
|
+
year?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type DateInputMode = "dob" | undefined
|
|
27
|
+
|
|
28
|
+
export type DateErrorCode =
|
|
29
|
+
| "missing-date"
|
|
30
|
+
| "missing-day"
|
|
31
|
+
| "missing-month"
|
|
32
|
+
| "missing-year"
|
|
33
|
+
| "invalid-day"
|
|
34
|
+
| "invalid-month"
|
|
35
|
+
| "invalid-year"
|
|
36
|
+
| "impossible-date"
|
|
37
|
+
| "unsupported-parts"
|
|
38
|
+
| "before-min"
|
|
39
|
+
| "after-max"
|
|
40
|
+
|
|
41
|
+
/** A message, or a function that builds one from the label. */
|
|
42
|
+
export type DateErrorMessage = string | ((label?: string) => string)
|
|
43
|
+
|
|
44
|
+
export interface DateValidationOptions {
|
|
45
|
+
/** The name of the date, as it would start a sentence, such as "Date
|
|
46
|
+
* of birth".
|
|
47
|
+
*
|
|
48
|
+
* Default: `Date`. */
|
|
49
|
+
label?: string
|
|
50
|
+
/** The latest date allowed, as a `Date` or an ISO date such as `2026-12-31`.
|
|
51
|
+
*
|
|
52
|
+
* Only the parts used are compared, such as the month and year. */
|
|
53
|
+
max?: Date | string
|
|
54
|
+
/** Messages to use instead of the defaults. */
|
|
55
|
+
messages?: Partial<Record<DateErrorCode, DateErrorMessage>>
|
|
56
|
+
/** The earliest date allowed, as a `Date` or an ISO date such as `2020-01-01`.
|
|
57
|
+
*
|
|
58
|
+
* Only the parts used are compared, such as the month and year. */
|
|
59
|
+
min?: Date | string
|
|
60
|
+
/** The parts used, in the order they are entered.
|
|
61
|
+
*
|
|
62
|
+
* Default: `day`, `month`, `year`. */
|
|
63
|
+
parts?: DatePart[]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface DateValidationSuccess {
|
|
67
|
+
/** Whether the date is valid. */
|
|
68
|
+
valid: true
|
|
69
|
+
/** The date as an ISO 8601 string, such as 1990-08-21. */
|
|
70
|
+
value: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface DateValidationError {
|
|
74
|
+
/** The code for the first problem, in the order the parts are entered. */
|
|
75
|
+
error: DateErrorCode
|
|
76
|
+
/** The parts with an error. Every part is invalid when the date is
|
|
77
|
+
* empty, impossible, has no ISO format or is outside `min` and `max`. */
|
|
78
|
+
invalid: DatePart[]
|
|
79
|
+
/** The message for the error. */
|
|
80
|
+
message: string
|
|
81
|
+
/** Whether the date is valid. */
|
|
82
|
+
valid: false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type DateValidation = DateValidationSuccess | DateValidationError
|
|
86
|
+
|
|
87
|
+
export const DATE_PARTS: DatePart[] = ["day", "month", "year"]
|
|
88
|
+
|
|
89
|
+
const realDate = (label = "Date") => `${label} must be a real date`
|
|
90
|
+
|
|
91
|
+
export const DATE_ERROR_MESSAGES: Record<
|
|
92
|
+
DateErrorCode,
|
|
93
|
+
string | ((label?: string, limits?: Limits) => string)
|
|
94
|
+
> = {
|
|
95
|
+
"after-max": (label = "Date", limits) => `${label} must be on or before ${limits?.max}`,
|
|
96
|
+
"before-min": (label = "Date", limits) => `${label} must be on or after ${limits?.min}`,
|
|
97
|
+
"impossible-date": realDate,
|
|
98
|
+
"invalid-day": realDate,
|
|
99
|
+
"invalid-month": realDate,
|
|
100
|
+
"invalid-year": "Year must include 4 numbers",
|
|
101
|
+
"missing-date": "Enter a date",
|
|
102
|
+
"missing-day": (label = "Date") => `${label} must include a day`,
|
|
103
|
+
"missing-month": (label = "Date") => `${label} must include a month`,
|
|
104
|
+
"missing-year": (label = "Date") => `${label} must include a year`,
|
|
105
|
+
"unsupported-parts": realDate,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check parts and return if they make a complete, possible date with an
|
|
109
|
+
// ISO format, between min and max. A valid date has its ISO value. An
|
|
110
|
+
// invalid date has the code and message for the first problem, and the
|
|
111
|
+
// parts with an error.
|
|
112
|
+
export const validateDateParts = (
|
|
113
|
+
date: DateParts,
|
|
114
|
+
{ label, max, messages = {}, min, parts = DATE_PARTS }: DateValidationOptions = {}
|
|
115
|
+
): DateValidation => {
|
|
116
|
+
const minDate = min === undefined ? undefined : toLimit(min, "min")
|
|
117
|
+
const maxDate = max === undefined ? undefined : toLimit(max, "max")
|
|
118
|
+
const values = trimParts(date)
|
|
119
|
+
const errors = getDateErrors(values, parts)
|
|
120
|
+
let value = ""
|
|
121
|
+
|
|
122
|
+
if (errors.length === 0) {
|
|
123
|
+
value = toISODate(values, parts)
|
|
124
|
+
const rangeError = value ? getRangeError(value, minDate, maxDate) : "unsupported-parts"
|
|
125
|
+
|
|
126
|
+
if (!rangeError) return { valid: true, value }
|
|
127
|
+
errors.push(rangeError)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const [error] = errors
|
|
131
|
+
const limits = {
|
|
132
|
+
max: maxDate && value ? formatLimit(maxDate, value.length) : undefined,
|
|
133
|
+
min: minDate && value ? formatLimit(minDate, value.length) : undefined,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
error,
|
|
138
|
+
invalid: getInvalidParts(errors, parts),
|
|
139
|
+
message: getMessage(error, label, messages, limits, DATE_ERROR_MESSAGES),
|
|
140
|
+
valid: false,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Check a value, such as the DateInput value or the value a form
|
|
145
|
+
* submits, the same way as `validateDateParts`. Use the same `parts` as
|
|
146
|
+
* the DateInput. */
|
|
147
|
+
export const validateDate = (
|
|
148
|
+
value: string,
|
|
149
|
+
options: DateValidationOptions = {}
|
|
150
|
+
): DateValidation => {
|
|
151
|
+
const date = parseDateValue(value, options.parts)
|
|
152
|
+
if (date) return validateDateParts(date, options)
|
|
153
|
+
|
|
154
|
+
// A dash typed in a part leaves it unclear which part is wrong, so the
|
|
155
|
+
// whole date is
|
|
156
|
+
const { label, messages, parts = DATE_PARTS } = options
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
error: "impossible-date",
|
|
160
|
+
invalid: parts,
|
|
161
|
+
message: getMessage("impossible-date", label, messages, {}, DATE_ERROR_MESSAGES),
|
|
162
|
+
valid: false,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** As this is a more specific function for a birth of date validation, we can make some assumptions.
|
|
167
|
+
* Such as, date needs to be today or in the past and error message should reflect date of birth.
|
|
168
|
+
* Check a value, such as the DateInput value or the value a form
|
|
169
|
+
* submits, the same way as `validateDateParts`. Use the same `parts` as
|
|
170
|
+
* the DateInput. */
|
|
171
|
+
|
|
172
|
+
export const validateDob = (value: string, options: DateValidationOptions = {}): DateValidation => {
|
|
173
|
+
const { messages, ...rest } = options
|
|
174
|
+
return validateDate(value, {
|
|
175
|
+
label: "Date of birth",
|
|
176
|
+
max: new Date(),
|
|
177
|
+
messages: {
|
|
178
|
+
"after-max": "Date of birth must be today or in the past",
|
|
179
|
+
"missing-date": "Enter your date of birth",
|
|
180
|
+
...messages,
|
|
181
|
+
},
|
|
182
|
+
parts: DATE_PARTS,
|
|
183
|
+
...rest,
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const getAutoComplete = (mode: DateInputMode, part: DatePart): string | undefined => {
|
|
188
|
+
if (!mode) return undefined
|
|
189
|
+
if (mode === "dob") {
|
|
190
|
+
if (part === "day") return "bday-day"
|
|
191
|
+
if (part === "month") return "bday-month"
|
|
192
|
+
if (part === "year") return "bday-year"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import { createContext, useContext } from "react"
|
|
3
3
|
|
|
4
|
-
import { joinIds, type MarkerIds } from "@/components/FieldMarkers/fieldmarkers-
|
|
4
|
+
import { joinIds, type MarkerIds } from "@/components/FieldMarkers/fieldmarkers-internal"
|
|
5
5
|
|
|
6
6
|
export interface FieldConfig {
|
|
7
7
|
disabled: boolean
|
|
@@ -5,7 +5,7 @@ import clsx from "clsx"
|
|
|
5
5
|
import React from "react"
|
|
6
6
|
|
|
7
7
|
import { Error as BaseError, type ErrorProps } from "@/components/FieldMarkers/Error"
|
|
8
|
-
import { getMarkerIds, type Markers } from "@/components/FieldMarkers/fieldmarkers-
|
|
8
|
+
import { getMarkerIds, type Markers } from "@/components/FieldMarkers/fieldmarkers-internal"
|
|
9
9
|
import { Hint as BaseHint, type HintProps } from "@/components/FieldMarkers/Hint"
|
|
10
10
|
import { Label as BaseLabel, type LabelProps } from "@/components/FieldMarkers/Label"
|
|
11
11
|
import { useFieldset } from "@/components/Fieldset/Fieldset.context"
|
|
@@ -3,7 +3,11 @@ import clsx from "clsx"
|
|
|
3
3
|
import { type FieldsetHTMLAttributes, HTMLAttributes, type JSX, Ref } from "react"
|
|
4
4
|
|
|
5
5
|
import { Error as BaseError, type ErrorProps } from "@/components/FieldMarkers/Error"
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getMarkerIds,
|
|
8
|
+
joinIds,
|
|
9
|
+
type Markers,
|
|
10
|
+
} from "@/components/FieldMarkers/fieldmarkers-internal"
|
|
7
11
|
import { Hint as BaseHint, type HintProps } from "@/components/FieldMarkers/Hint"
|
|
8
12
|
import { Legend as BaseLegend, type LegendProps } from "@/components/FieldMarkers/Legend"
|
|
9
13
|
import { useId } from "@/hooks/useId"
|
|
@@ -2,6 +2,8 @@ import type { FormHTMLAttributes, JSX, ReactNode, Ref } from "react"
|
|
|
2
2
|
|
|
3
3
|
import clsx from "clsx"
|
|
4
4
|
|
|
5
|
+
import { StandardSizes } from "@/types"
|
|
6
|
+
|
|
5
7
|
export interface FormProps extends FormHTMLAttributes<HTMLFormElement> {
|
|
6
8
|
/** Content inside the component. */
|
|
7
9
|
children?: ReactNode
|
|
@@ -9,11 +11,17 @@ export interface FormProps extends FormHTMLAttributes<HTMLFormElement> {
|
|
|
9
11
|
className?: string
|
|
10
12
|
/** Specify a custom React ref for this component. */
|
|
11
13
|
ref?: Ref<HTMLFormElement>
|
|
14
|
+
/** Specify the spacing.
|
|
15
|
+
*
|
|
16
|
+
* Default: `md`
|
|
17
|
+
*/
|
|
18
|
+
spacing?: StandardSizes
|
|
12
19
|
}
|
|
13
20
|
|
|
14
|
-
const Form = ({ children, className, ref, ...props }: FormProps): JSX.Element => {
|
|
21
|
+
const Form = ({ children, className, ref, spacing, ...props }: FormProps): JSX.Element => {
|
|
15
22
|
const componentProps = {
|
|
16
23
|
className: clsx("coop-form", className),
|
|
24
|
+
"data-spacing": spacing ?? undefined,
|
|
17
25
|
...props,
|
|
18
26
|
}
|
|
19
27
|
|