@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.
Files changed (176) hide show
  1. package/dist/components/Author/Author.d.ts +3 -1
  2. package/dist/components/Author/Author.js +1 -1
  3. package/dist/components/Author/index.d.ts +2 -2
  4. package/dist/components/Author/index.js +2 -2
  5. package/dist/components/Combobox/Combobox.d.ts +1 -1
  6. package/dist/components/Combobox/Combobox.js +29 -8
  7. package/dist/components/Combobox/{combobox-utils.js → combobox-internal.js} +4 -2
  8. package/dist/components/DateInput/DateInput.context.js +7 -0
  9. package/dist/components/DateInput/DateInput.d.ts +104 -0
  10. package/dist/components/DateInput/DateInput.js +128 -0
  11. package/dist/components/DateInput/dateinput-internal.d.ts +6 -0
  12. package/dist/components/DateInput/dateinput-internal.js +82 -0
  13. package/dist/components/DateInput/dateinput-utils.d.ts +66 -0
  14. package/dist/components/DateInput/dateinput-utils.js +91 -0
  15. package/dist/components/DateInput/index.d.ts +3 -0
  16. package/dist/components/DateInput/index.js +6 -0
  17. package/dist/components/Field/Field.context.js +1 -1
  18. package/dist/components/Field/Field.js +1 -1
  19. package/dist/components/FieldMarkers/{fieldmarkers-utils.js → fieldmarkers-internal.js} +1 -1
  20. package/dist/components/Fieldset/Fieldset.js +1 -1
  21. package/dist/components/Form/Form.d.ts +7 -1
  22. package/dist/components/Form/Form.js +2 -1
  23. package/dist/components/Squircle/Squircle.d.ts +3 -1
  24. package/dist/components/Squircle/Squircle.js +1 -1
  25. package/dist/components/Squircle/index.d.ts +2 -2
  26. package/dist/components/Squircle/index.js +2 -2
  27. package/dist/components/Textarea/Textarea.js +14 -4
  28. package/dist/components/ValidationSummary/ValidationSummary.d.ts +1 -1
  29. package/dist/components/ValidationSummary/ValidationSummary.js +1 -1
  30. package/dist/components/ValidationSummary/{validationsummary-utils.d.ts → validationsummary-internal.d.ts} +1 -1
  31. package/dist/components/ValidationSummary/{validationsummary-utils.js → validationsummary-internal.js} +3 -2
  32. package/dist/components/icons/AccountCircleFilledIcon.d.ts +2 -1
  33. package/dist/components/icons/AccountCircleIcon.d.ts +2 -1
  34. package/dist/components/icons/AccountFilledIcon.d.ts +2 -1
  35. package/dist/components/icons/AccountIcon.d.ts +2 -1
  36. package/dist/components/icons/AddIcon.d.ts +2 -1
  37. package/dist/components/icons/AppsFilledIcon.d.ts +2 -1
  38. package/dist/components/icons/AppsIcon.d.ts +2 -1
  39. package/dist/components/icons/ArrowDownIcon.d.ts +2 -1
  40. package/dist/components/icons/ArrowLeftIcon.d.ts +2 -1
  41. package/dist/components/icons/ArrowRightIcon.d.ts +2 -1
  42. package/dist/components/icons/ArrowUpIcon.d.ts +2 -1
  43. package/dist/components/icons/BackspaceFilledIcon.d.ts +2 -1
  44. package/dist/components/icons/BackspaceIcon.d.ts +2 -1
  45. package/dist/components/icons/BasketFilledIcon.d.ts +2 -1
  46. package/dist/components/icons/BasketIcon.d.ts +2 -1
  47. package/dist/components/icons/BellFilledIcon.d.ts +2 -1
  48. package/dist/components/icons/BellIcon.d.ts +2 -1
  49. package/dist/components/icons/BroomIcon.d.ts +2 -1
  50. package/dist/components/icons/CalendarFilledIcon.d.ts +2 -1
  51. package/dist/components/icons/CalendarIcon.d.ts +2 -1
  52. package/dist/components/icons/CashFilledIcon.d.ts +2 -1
  53. package/dist/components/icons/CashIcon.d.ts +2 -1
  54. package/dist/components/icons/ChevronDownIcon.d.ts +2 -1
  55. package/dist/components/icons/ChevronLeftIcon.d.ts +2 -1
  56. package/dist/components/icons/ChevronRightIcon.d.ts +2 -1
  57. package/dist/components/icons/ChevronUpIcon.d.ts +2 -1
  58. package/dist/components/icons/CircleBurgerFilledIcon.d.ts +2 -1
  59. package/dist/components/icons/CircleBurgerIcon.d.ts +2 -1
  60. package/dist/components/icons/ClipboardFilledIcon.d.ts +2 -1
  61. package/dist/components/icons/ClipboardIcon.d.ts +2 -1
  62. package/dist/components/icons/ClockFilledIcon.d.ts +2 -1
  63. package/dist/components/icons/ClockIcon.d.ts +2 -1
  64. package/dist/components/icons/CloseAltFilledIcon.d.ts +2 -1
  65. package/dist/components/icons/CloseAltIcon.d.ts +2 -1
  66. package/dist/components/icons/CogIcon.d.ts +2 -1
  67. package/dist/components/icons/CommunityFilledIcon.d.ts +2 -1
  68. package/dist/components/icons/CommunityIcon.d.ts +2 -1
  69. package/dist/components/icons/ComputerIcon.d.ts +2 -1
  70. package/dist/components/icons/ContactFilledIcon.d.ts +2 -1
  71. package/dist/components/icons/ContactIcon.d.ts +2 -1
  72. package/dist/components/icons/ContactlessFilledIcon.d.ts +2 -1
  73. package/dist/components/icons/ContactlessIcon.d.ts +2 -1
  74. package/dist/components/icons/CoopLiveIcon.d.ts +2 -1
  75. package/dist/components/icons/CoopLocationFilledIcon.d.ts +2 -1
  76. package/dist/components/icons/CoopLogoIcon.d.ts +2 -1
  77. package/dist/components/icons/CoopMembershipCardFilledIcon.d.ts +2 -1
  78. package/dist/components/icons/CreditCardFilledIcon.d.ts +2 -1
  79. package/dist/components/icons/CreditCardIcon.d.ts +2 -1
  80. package/dist/components/icons/CrossIcon.d.ts +2 -1
  81. package/dist/components/icons/DocumentIcon.d.ts +2 -1
  82. package/dist/components/icons/DownloadIcon.d.ts +2 -1
  83. package/dist/components/icons/EnterStoreIcon.d.ts +2 -1
  84. package/dist/components/icons/ExitStoreIcon.d.ts +2 -1
  85. package/dist/components/icons/EyeFilledIcon.d.ts +2 -1
  86. package/dist/components/icons/EyeIcon.d.ts +2 -1
  87. package/dist/components/icons/EyeStrikeFilledIcon.d.ts +2 -1
  88. package/dist/components/icons/EyeStrikeIcon.d.ts +2 -1
  89. package/dist/components/icons/FeedbackFilledIcon.d.ts +2 -1
  90. package/dist/components/icons/FeedbackIcon.d.ts +2 -1
  91. package/dist/components/icons/FilterIcon.d.ts +2 -1
  92. package/dist/components/icons/GiftCardFilledIcon.d.ts +2 -1
  93. package/dist/components/icons/GiftCardIcon.d.ts +2 -1
  94. package/dist/components/icons/GiftFilledIcon.d.ts +2 -1
  95. package/dist/components/icons/GiftIcon.d.ts +2 -1
  96. package/dist/components/icons/HelpFilledIcon.d.ts +2 -1
  97. package/dist/components/icons/HelpIcon.d.ts +2 -1
  98. package/dist/components/icons/HomeFilledIcon.d.ts +2 -1
  99. package/dist/components/icons/HomeIcon.d.ts +2 -1
  100. package/dist/components/icons/HowDoIAssistantIcon.d.ts +2 -1
  101. package/dist/components/icons/InfoIcon.d.ts +2 -1
  102. package/dist/components/icons/KebabIcon.d.ts +2 -1
  103. package/dist/components/icons/LoadingIcon.d.ts +2 -1
  104. package/dist/components/icons/LocationFilledIcon.d.ts +2 -1
  105. package/dist/components/icons/LocationIcon.d.ts +2 -1
  106. package/dist/components/icons/LogoutIcon.d.ts +2 -1
  107. package/dist/components/icons/MailIcon.d.ts +2 -1
  108. package/dist/components/icons/MenuIcon.d.ts +2 -1
  109. package/dist/components/icons/MessageIcon.d.ts +2 -1
  110. package/dist/components/icons/MinusIcon.d.ts +2 -1
  111. package/dist/components/icons/NotesFilledIcon.d.ts +2 -1
  112. package/dist/components/icons/NotesIcon.d.ts +2 -1
  113. package/dist/components/icons/OpenNewIcon.d.ts +2 -1
  114. package/dist/components/icons/OwnBagIcon.d.ts +2 -1
  115. package/dist/components/icons/PadlockIcon.d.ts +2 -1
  116. package/dist/components/icons/PencilIcon.d.ts +2 -1
  117. package/dist/components/icons/PhoneIcon.d.ts +2 -1
  118. package/dist/components/icons/PowerIcon.d.ts +2 -1
  119. package/dist/components/icons/SaveFilledIcon.d.ts +2 -1
  120. package/dist/components/icons/SaveIcon.d.ts +2 -1
  121. package/dist/components/icons/ScooterIcon.d.ts +2 -1
  122. package/dist/components/icons/SearchFilledIcon.d.ts +2 -1
  123. package/dist/components/icons/SearchIcon.d.ts +2 -1
  124. package/dist/components/icons/SendIcon.d.ts +2 -1
  125. package/dist/components/icons/SpannerIcon.d.ts +2 -1
  126. package/dist/components/icons/StarFilledIcon.d.ts +2 -1
  127. package/dist/components/icons/StarIcon.d.ts +2 -1
  128. package/dist/components/icons/TickAltFilledIcon.d.ts +2 -1
  129. package/dist/components/icons/TickAltIcon.d.ts +2 -1
  130. package/dist/components/icons/TickIcon.d.ts +2 -1
  131. package/dist/components/icons/TruckIcon.d.ts +2 -1
  132. package/dist/components/icons/UserWipeIcon.d.ts +2 -1
  133. package/dist/components/icons/VoucherFilledIcon.d.ts +2 -1
  134. package/dist/components/icons/VoucherIcon.d.ts +2 -1
  135. package/dist/components/icons/WarningCircleFilledIcon.d.ts +2 -1
  136. package/dist/components/icons/WarningCircleIcon.d.ts +2 -1
  137. package/dist/components/icons/WarningFilledIcon.d.ts +2 -1
  138. package/dist/components/icons/WarningIcon.d.ts +2 -1
  139. package/dist/components/icons/WifiIcon.d.ts +2 -1
  140. package/dist/components/icons/WriteIcon.d.ts +2 -1
  141. package/dist/hooks/index.d.ts +2 -1
  142. package/dist/hooks/index.js +2 -1
  143. package/dist/hooks/useFormReset.d.ts +6 -0
  144. package/dist/hooks/useFormReset.js +19 -0
  145. package/dist/hooks/useSlots.d.ts +2 -1
  146. package/dist/index.d.ts +6 -3
  147. package/dist/index.js +6 -4
  148. package/dist/types/index.js +4 -0
  149. package/dist/utils/slots.d.ts +1 -1
  150. package/dist/utils/slots.js +20 -12
  151. package/package.json +22 -18
  152. package/src/components/Author/Author.tsx +1 -1
  153. package/src/components/Combobox/Combobox.context.ts +3 -1
  154. package/src/components/Combobox/Combobox.tsx +57 -24
  155. package/src/components/Combobox/combobox-internal.ts +60 -0
  156. package/src/components/Combobox/combobox-utils.ts +1 -41
  157. package/src/components/Combobox/index.ts +1 -0
  158. package/src/components/DateInput/DateInput.context.ts +25 -0
  159. package/src/components/DateInput/DateInput.tsx +252 -0
  160. package/src/components/DateInput/dateinput-internal.ts +176 -0
  161. package/src/components/DateInput/dateinput-utils.ts +194 -0
  162. package/src/components/DateInput/index.ts +6 -0
  163. package/src/components/Field/Field.context.ts +1 -1
  164. package/src/components/Field/Field.tsx +1 -1
  165. package/src/components/Fieldset/Fieldset.tsx +5 -1
  166. package/src/components/Form/Form.tsx +9 -1
  167. package/src/components/Squircle/Squircle.tsx +1 -1
  168. package/src/components/Textarea/Textarea.tsx +19 -4
  169. package/src/components/ValidationSummary/ValidationSummary.context.ts +1 -1
  170. package/src/components/ValidationSummary/ValidationSummary.tsx +1 -1
  171. package/src/components/ValidationSummary/{validationsummary-utils.ts → validationsummary-internal.ts} +9 -8
  172. package/src/hooks/index.ts +1 -0
  173. package/src/hooks/useFormReset.ts +28 -0
  174. package/src/index.ts +1 -0
  175. package/src/utils/slots.ts +37 -23
  176. /package/src/components/FieldMarkers/{fieldmarkers-utils.ts → fieldmarkers-internal.ts} +0 -0
@@ -36,5 +36,7 @@ declare const Author: {
36
36
  name: string;
37
37
  };
38
38
  };
39
+ declare const AuthorImage: ({ alt, src, ...props }: AuthorImageProps) => JSX.Element;
40
+ declare const AuthorMeta: ({ date, datePrefix, name, ...props }: AuthorMetaProps) => JSX.Element;
39
41
  //#endregion
40
- export { Author, Author as default };
42
+ export { Author, Author as default, AuthorImage, AuthorMeta };
@@ -42,4 +42,4 @@ Author.Image = AuthorImage;
42
42
  Author.Meta = AuthorMeta;
43
43
  Author.slot = { name: "Author" };
44
44
  //#endregion
45
- export { Author, Author as default };
45
+ export { Author, Author as default, AuthorImage, AuthorMeta };
@@ -1,2 +1,2 @@
1
- import { Author, AuthorImageProps, AuthorMetaProps, AuthorProps } from "./Author.js";
2
- export { Author, Author as default, AuthorImageProps, AuthorMetaProps, AuthorProps };
1
+ import { Author, AuthorImage, AuthorImageProps, AuthorMeta, AuthorMetaProps, AuthorProps } from "./Author.js";
2
+ export { Author, Author as default, AuthorImage, AuthorImageProps, AuthorMeta, AuthorMetaProps, AuthorProps };
@@ -1,5 +1,5 @@
1
- import { Author } from "./Author.js";
1
+ import { Author, AuthorImage, AuthorMeta } from "./Author.js";
2
2
  //#region src/components/Author/index.ts
3
3
  var Author_default = Author;
4
4
  //#endregion
5
- export { Author, Author_default as default };
5
+ export { Author, AuthorImage, AuthorMeta, Author_default as default };
@@ -181,4 +181,4 @@ declare const ComboboxOptions: {
181
181
  };
182
182
  };
183
183
  //#endregion
184
- export { Combobox, Combobox as default, ComboboxButton, ComboboxControls, ComboboxInput, type ComboboxItem, type ComboboxOption, ComboboxOptions };
184
+ export { Combobox, Combobox as default, ComboboxButton, ComboboxControls, ComboboxInput, ComboboxOptions };
@@ -4,18 +4,19 @@ import { ChevronDownIcon } from "../icons/ChevronDownIcon.js";
4
4
  import { getSlotProp } from "../../utils/slots.js";
5
5
  import { useSlots } from "../../hooks/useSlots.js";
6
6
  import { getFieldError, useField } from "../Field/Field.context.js";
7
- import { defaultFilter, defaultIsItemDisabled, defaultItemToString, isMatchingText, normalizeOptions, resolveOption } from "./combobox-utils.js";
7
+ import { defaultFilter, defaultIsItemDisabled, defaultItemToString, defaultMaxResultsText, isMatchingText, normalizeOptions, resolveOption, shouldShowSelectionText } from "./combobox-internal.js";
8
8
  import { ComboboxContext, useComboboxContext } from "./Combobox.context.js";
9
9
  import { TextInput } from "../TextInput/TextInput.js";
10
10
  import { useDebounce } from "../../hooks/useDebounce.js";
11
+ import { useFormReset } from "../../hooks/useFormReset.js";
11
12
  import clsx from "clsx";
12
13
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
14
  import { useEffect, useMemo, useRef, useState } from "react";
14
15
  import { useCombobox } from "downshift";
16
+ import mergeRefs from "merge-refs";
15
17
  //#region src/components/Combobox/Combobox.tsx
16
18
  const DEFAULT_MAX_RESULTS = 50;
17
19
  const ANNOUNCEMENT_DELAY = 750;
18
- const defaultMaxResultsText = (shown, total) => `Showing ${shown} of ${total} results.`;
19
20
  const componentSlots = {
20
21
  ComboboxControls: null,
21
22
  ComboboxOptions: null
@@ -39,7 +40,9 @@ const Combobox = ({ allowCustom = false, children, className, defaultSelected, d
39
40
  ]);
40
41
  const visibleItems = useMemo(() => maxResults === false ? matchingItems : matchingItems.slice(0, maxResults), [matchingItems, maxResults]);
41
42
  const [initialSelectedItem] = useState(() => resolveOption(defaultSelected, items));
42
- const selectedItem = selected === void 0 ? void 0 : resolveOption(selected, items);
43
+ const defaultSelectedItem = resolveOption(defaultSelected, items);
44
+ const controlled = selected !== void 0;
45
+ const selectedItem = controlled ? resolveOption(selected, items) : void 0;
43
46
  const commit = (changes, closeMenu) => {
44
47
  const text = (changes.inputValue ?? "").trim();
45
48
  if (text === "") return {
@@ -92,6 +95,10 @@ const Combobox = ({ allowCustom = false, children, className, defaultSelected, d
92
95
  }
93
96
  };
94
97
  const downshift = useCombobox({
98
+ ...!controlled && {
99
+ defaultInputValue: toString(defaultSelectedItem),
100
+ defaultSelectedItem
101
+ },
95
102
  getItemId: (index) => visibleItems[index]?.id ?? `${uid}_item_${index}`,
96
103
  id: uid,
97
104
  initialSelectedItem,
@@ -133,25 +140,33 @@ const Combobox = ({ allowCustom = false, children, className, defaultSelected, d
133
140
  text: selectionText,
134
141
  value: selectionValue
135
142
  });
136
- const { inputValue, setInputValue } = downshift;
143
+ const { inputValue, reset, setInputValue } = downshift;
137
144
  useEffect(() => {
138
145
  const previous = previousSelection.current;
139
- previousSelection.current = {
146
+ const current = {
140
147
  text: selectionText,
141
148
  value: selectionValue
142
149
  };
143
- if (selectionValue === previous.value && selectionText !== previous.text && inputValue === previous.text) setInputValue(selectionText);
150
+ previousSelection.current = current;
151
+ if (shouldShowSelectionText(previous, current, inputValue)) setInputValue(selectionText);
144
152
  }, [
145
153
  inputValue,
146
154
  selectionText,
147
155
  selectionValue,
148
156
  setInputValue
149
157
  ]);
158
+ const inputRef = useRef(null);
159
+ useFormReset(inputRef, () => {
160
+ if (controlled) return;
161
+ reset();
162
+ setFilterText("");
163
+ });
150
164
  return /* @__PURE__ */ jsx(ComboboxContext.Provider, {
151
165
  value: {
152
166
  disabled,
153
167
  downshift,
154
168
  error: hasError,
169
+ inputRef,
155
170
  items: visibleItems,
156
171
  matchCount: matchingItems.length,
157
172
  size
@@ -177,7 +192,8 @@ const ComboboxInput = ({ error, name, size, ...props }) => {
177
192
  const disabled = combobox.disabled || props.disabled;
178
193
  const inputProps = combobox.downshift.getInputProps({
179
194
  ...props,
180
- disabled
195
+ disabled,
196
+ ref: mergeRefs(combobox.inputRef, props.ref)
181
197
  });
182
198
  return /* @__PURE__ */ jsxs(Fragment, { children: [name && /* @__PURE__ */ jsx("input", {
183
199
  disabled,
@@ -218,10 +234,15 @@ const ComboboxOptions = ({ className, maxResults: _maxResults, maxResultsText =
218
234
  };
219
235
  const message = getMessage();
220
236
  const announcement = useDebounce(message, ANNOUNCEMENT_DELAY);
237
+ const onMouseLeave = (event) => {
238
+ props.onMouseLeave?.(event);
239
+ if (!isOpen) Object.assign(event, { preventDownshiftDefault: true });
240
+ };
221
241
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("ul", {
222
242
  ...getMenuProps({
223
243
  ...props,
224
- className: clsx("coop-combobox-options", className)
244
+ className: clsx("coop-combobox-options", className),
245
+ onMouseLeave
225
246
  }),
226
247
  children: [isOpen && items.map((item, index) => {
227
248
  const { children, disabled: _disabled, icon, label, onClick, onMouseDown, onMouseMove, value, ...attributes } = item;
@@ -1,8 +1,9 @@
1
1
  import { matchSorter } from "match-sorter";
2
- //#region src/components/Combobox/combobox-utils.ts
2
+ //#region src/components/Combobox/combobox-internal.ts
3
3
  const defaultFilter = (items, inputValue) => inputValue !== "" ? matchSorter(items, inputValue, { keys: [(item) => [item.label ?? item.value]] }) : items;
4
4
  const defaultIsItemDisabled = (item) => item.disabled ?? false;
5
5
  const defaultItemToString = (item) => item?.label ?? item?.value ?? "";
6
+ const defaultMaxResultsText = (shown, total) => `Showing ${shown} of ${total} results.`;
6
7
  const normalizeOptions = (options) => options.filter((option) => option && typeof option !== "number").map((option) => typeof option === "string" ? { value: option } : option);
7
8
  const collator = new Intl.Collator(void 0, { sensitivity: "base" });
8
9
  const isMatchingText = (optionText, text) => collator.compare(optionText.trim(), text.trim()) === 0;
@@ -11,5 +12,6 @@ const resolveOption = (option, items) => {
11
12
  const item = typeof option === "string" ? { value: option } : option;
12
13
  return items.find(({ value }) => value === item.value) ?? item;
13
14
  };
15
+ const shouldShowSelectionText = (previous, current, inputValue) => current.value === previous.value && current.text !== previous.text && inputValue === previous.text;
14
16
  //#endregion
15
- export { defaultFilter, defaultIsItemDisabled, defaultItemToString, isMatchingText, normalizeOptions, resolveOption };
17
+ export { defaultFilter, defaultIsItemDisabled, defaultItemToString, defaultMaxResultsText, isMatchingText, normalizeOptions, resolveOption, shouldShowSelectionText };
@@ -0,0 +1,7 @@
1
+ "use client";
2
+ import { createContext, useContext } from "react";
3
+ //#region src/components/DateInput/DateInput.context.ts
4
+ const DateInputContext = createContext(null);
5
+ const useDateInput = () => useContext(DateInputContext);
6
+ //#endregion
7
+ export { DateInputContext, useDateInput };
@@ -0,0 +1,104 @@
1
+ import { StandardSizes } from "../../types/index.js";
2
+ import { TextInputProps } from "../TextInput/TextInput.js";
3
+ import "../TextInput/index.js";
4
+ import { DateInputMode, DateParts } from "./dateinput-utils.js";
5
+ import { HTMLAttributes, JSX, ReactNode, Ref } from "react";
6
+ //#region src/components/DateInput/DateInput.d.ts
7
+ export interface DateInputChangeEvent {
8
+ /** The parts as typed. */
9
+ parts: DateParts;
10
+ /** The parts as typed, joined from year to day, such as `2026-2-30`.
11
+ *
12
+ * Not checked: use `validateDate` before relying on it. */
13
+ value: string;
14
+ }
15
+ export interface DateInputProps extends Omit<HTMLAttributes<HTMLDivElement>, "defaultValue" | "onBlur" | "onChange"> {
16
+ /** Content inside the component.
17
+ *
18
+ * This should be a set of `<DateInput.Day>`, `<DateInput.Month>` and `<DateInput.Year>`. */
19
+ children?: ReactNode;
20
+ /** Specify additional CSS classes. */
21
+ className?: string;
22
+ /** Specify the initial value, when not controlled. One of:
23
+ *
24
+ * - an ISO date, such as `1990-08-21`
25
+ * - the parts, such as `{ year: "2026" }` */
26
+ defaultValue?: DateParts | string;
27
+ /** Specify the base for the part ids. Will be auto-generated if not set. */
28
+ id?: string;
29
+ /** Specify the DateInput mode. */
30
+ mode?: DateInputMode;
31
+ /** Specify the name.
32
+ *
33
+ * Each part is `{name}-day`, `{name}-month` and`{name}-year`.
34
+ *
35
+ * A hidden input `{name}` holds the value as typed. */
36
+ name?: string;
37
+ /** Called when focus leaves the whole DateInput, not each part. */
38
+ onBlur?: (event: DateInputChangeEvent) => void;
39
+ /** Called when any part changes. */
40
+ onChange?: (event: DateInputChangeEvent) => void;
41
+ /** Specify a custom React ref for this component. */
42
+ ref?: Ref<HTMLDivElement>;
43
+ /** Specify the component size. */
44
+ size?: StandardSizes;
45
+ /** Specify the value, when controlled. One of:
46
+ *
47
+ * - an ISO date, such as `1990-08-21`
48
+ * - the parts, such as `{ year: "2026" }`
49
+ *
50
+ * Update it in `onChange` with the `value` or `parts` of the event. */
51
+ value?: DateParts | string;
52
+ }
53
+ declare const DateInput: {
54
+ ({ children, className, defaultValue, id, mode, name, onBlur, onChange, ref, size, value, ...props }: DateInputProps): JSX.Element;
55
+ slot: {
56
+ name: string;
57
+ };
58
+ Day: {
59
+ ({ label, ...props }: DateInputPartProps): JSX.Element;
60
+ slot: {
61
+ name: string;
62
+ };
63
+ };
64
+ Month: {
65
+ ({ label, ...props }: DateInputPartProps): JSX.Element;
66
+ slot: {
67
+ name: string;
68
+ };
69
+ };
70
+ Year: {
71
+ ({ label, ...props }: DateInputPartProps): JSX.Element;
72
+ slot: {
73
+ name: string;
74
+ };
75
+ };
76
+ };
77
+ export interface DateInputPartProps extends Omit<TextInputProps, "defaultValue" | "fit" | "prefix" | "prefixInline" | "suffix" | "suffixInline" | "type" | "value"> {
78
+ /** Specify the part error state.
79
+ *
80
+ * Inherits error state of a `group` Fieldset when not set. */
81
+ error?: boolean;
82
+ /** Specify the label. */
83
+ label?: ReactNode;
84
+ }
85
+ declare const DateInputDay: {
86
+ ({ label, ...props }: DateInputPartProps): JSX.Element;
87
+ slot: {
88
+ name: string;
89
+ };
90
+ };
91
+ declare const DateInputMonth: {
92
+ ({ label, ...props }: DateInputPartProps): JSX.Element;
93
+ slot: {
94
+ name: string;
95
+ };
96
+ };
97
+ declare const DateInputYear: {
98
+ ({ label, ...props }: DateInputPartProps): JSX.Element;
99
+ slot: {
100
+ name: string;
101
+ };
102
+ };
103
+ //#endregion
104
+ export { DateInput, DateInput as default, DateInputDay, DateInputMonth, DateInputYear };
@@ -0,0 +1,128 @@
1
+ "use client";
2
+ import { useId as useId$1 } from "../../hooks/useId.js";
3
+ import { forEachSlot } from "../../utils/slots.js";
4
+ import { TextInput } from "../TextInput/TextInput.js";
5
+ import { useFormReset } from "../../hooks/useFormReset.js";
6
+ import { Field } from "../Field/Field.js";
7
+ import { formatDateValue, toDateParts, toDateValue } from "./dateinput-internal.js";
8
+ import { getAutoComplete } from "./dateinput-utils.js";
9
+ import { DateInputContext, useDateInput } from "./DateInput.context.js";
10
+ import clsx from "clsx";
11
+ import { jsx, jsxs } from "react/jsx-runtime";
12
+ import { useRef, useState } from "react";
13
+ import mergeRefs from "merge-refs";
14
+ //#region src/components/DateInput/DateInput.tsx
15
+ const PART_SLOTS = {
16
+ DateInputDay: "day",
17
+ DateInputMonth: "month",
18
+ DateInputYear: "year"
19
+ };
20
+ const DateInput = ({ children, className, defaultValue, id, mode, name, onBlur, onChange, ref, size, value, ...props }) => {
21
+ const uid = useId$1(id);
22
+ const rendered = [];
23
+ forEachSlot(children, (slot) => {
24
+ const part = PART_SLOTS[slot.name];
25
+ if (part && !rendered.includes(part)) rendered.push(part);
26
+ });
27
+ const defaultParts = toDateParts(defaultValue, rendered);
28
+ const [typed, setTyped] = useState(() => value === void 0 ? defaultParts : toDateParts(value, rendered));
29
+ const controlled = value !== void 0;
30
+ const parts = controlled && toDateValue(value, rendered) !== toDateValue(typed, rendered) ? toDateParts(value, rendered) : typed;
31
+ const controlRef = useRef(null);
32
+ useFormReset(controlRef, () => {
33
+ if (!controlled) setTyped(defaultParts);
34
+ });
35
+ const getEvent = (next) => ({
36
+ parts: next,
37
+ value: formatDateValue(next, rendered)
38
+ });
39
+ const changePart = (part, partValue) => {
40
+ const next = {
41
+ ...parts,
42
+ [part]: partValue
43
+ };
44
+ setTyped(next);
45
+ onChange?.(getEvent(next));
46
+ };
47
+ const handleBlur = (event) => {
48
+ if (!event.currentTarget.contains(event.relatedTarget)) onBlur?.(getEvent(parts));
49
+ };
50
+ const componentProps = {
51
+ ...props,
52
+ className: clsx("coop-date-input", className),
53
+ onBlur: handleBlur
54
+ };
55
+ return /* @__PURE__ */ jsx(DateInputContext.Provider, {
56
+ value: {
57
+ changePart,
58
+ controlled,
59
+ controlPart: rendered[0],
60
+ controlRef,
61
+ defaultValue: defaultParts,
62
+ id: uid,
63
+ mode,
64
+ name,
65
+ parts,
66
+ size
67
+ },
68
+ children: /* @__PURE__ */ jsxs("div", {
69
+ ...componentProps,
70
+ ref,
71
+ children: [name && /* @__PURE__ */ jsx("input", {
72
+ name,
73
+ type: "hidden",
74
+ value: formatDateValue(parts, rendered)
75
+ }), children]
76
+ })
77
+ });
78
+ };
79
+ const DateInputPart = ({ autoComplete, className, datePart: part, error, id, label, name, onChange, size, ...props }) => {
80
+ const dateInput = useDateInput();
81
+ const inputProps = {
82
+ ...dateInput?.controlled ? { value: dateInput.parts[part] ?? "" } : { defaultValue: dateInput?.defaultValue?.[part] },
83
+ autoComplete: autoComplete ?? getAutoComplete(dateInput?.mode, part),
84
+ className: clsx("coop-date-input--input", className),
85
+ "data-part": part,
86
+ id: id ?? (dateInput ? `${dateInput.id}-${part}` : void 0),
87
+ inputMode: "numeric",
88
+ name: name ?? (dateInput?.name ? `${dateInput.name}-${part}` : void 0),
89
+ onChange: (event) => {
90
+ dateInput?.changePart(part, event.target.value);
91
+ onChange?.(event);
92
+ },
93
+ pattern: "[0-9]*",
94
+ size: size ?? dateInput?.size,
95
+ spellCheck: false,
96
+ ...props,
97
+ ref: dateInput?.controlPart === part ? mergeRefs(dateInput.controlRef, props.ref) : props.ref
98
+ };
99
+ return /* @__PURE__ */ jsxs(Field, {
100
+ error,
101
+ hideErrorBar: true,
102
+ children: [/* @__PURE__ */ jsx(Field.Label, { children: label }), /* @__PURE__ */ jsx(TextInput, { ...inputProps })]
103
+ });
104
+ };
105
+ const DateInputDay = ({ label = "Day", ...props }) => /* @__PURE__ */ jsx(DateInputPart, {
106
+ ...props,
107
+ datePart: "day",
108
+ label
109
+ });
110
+ const DateInputMonth = ({ label = "Month", ...props }) => /* @__PURE__ */ jsx(DateInputPart, {
111
+ ...props,
112
+ datePart: "month",
113
+ label
114
+ });
115
+ const DateInputYear = ({ label = "Year", ...props }) => /* @__PURE__ */ jsx(DateInputPart, {
116
+ ...props,
117
+ datePart: "year",
118
+ label
119
+ });
120
+ DateInput.slot = { name: "DateInput" };
121
+ DateInputDay.slot = { name: "DateInputDay" };
122
+ DateInputMonth.slot = { name: "DateInputMonth" };
123
+ DateInputYear.slot = { name: "DateInputYear" };
124
+ DateInput.Day = DateInputDay;
125
+ DateInput.Month = DateInputMonth;
126
+ DateInput.Year = DateInputYear;
127
+ //#endregion
128
+ export { DateInput, DateInput as default, DateInputDay, DateInputMonth, DateInputYear };
@@ -0,0 +1,6 @@
1
+ //#region src/components/DateInput/dateinput-internal.d.ts
2
+ export interface Limits {
3
+ max?: string;
4
+ min?: string;
5
+ }
6
+ //#endregion
@@ -0,0 +1,82 @@
1
+ import { format, formatISO, isExists, isValid, parseISO } from "date-fns";
2
+ //#region src/components/DateInput/dateinput-internal.ts
3
+ const VALUE_ORDER = [
4
+ "year",
5
+ "month",
6
+ "day"
7
+ ];
8
+ const WHOLE_DATE_ERRORS = /* @__PURE__ */ new Set([
9
+ "after-max",
10
+ "before-min",
11
+ "impossible-date",
12
+ "missing-date",
13
+ "unsupported-parts"
14
+ ]);
15
+ const valueParts = (parts) => VALUE_ORDER.filter((part) => parts.includes(part));
16
+ const formatDateValue = (date, parts = VALUE_ORDER) => {
17
+ const values = valueParts(parts).map((part) => date[part] ?? "");
18
+ return values.every((value) => value === "") ? "" : values.join("-");
19
+ };
20
+ const splitValue = (value, used) => {
21
+ const pieces = value.split("-");
22
+ return !used.includes("year") && value.startsWith("--") && pieces.length === used.length + 2 ? pieces.slice(2) : pieces;
23
+ };
24
+ const parseDateValue = (value, parts = VALUE_ORDER) => {
25
+ if (value === "") return {};
26
+ const used = valueParts(parts);
27
+ const pieces = splitValue(value, used);
28
+ if (pieces.length !== used.length) return void 0;
29
+ return Object.fromEntries(used.map((part, index) => [part, pieces[index]]));
30
+ };
31
+ const toDateValue = (input = "", parts) => typeof input === "string" ? input : formatDateValue(input, parts);
32
+ const toDateParts = (input = "", parts) => typeof input === "string" ? parseDateValue(input, parts) ?? {} : input;
33
+ const toNumber = (value) => /^\d+$/.test(value) ? Number(value) : null;
34
+ const isValidPart = (part, value) => {
35
+ if (part === "year") return /^\d{4}$/.test(value);
36
+ const number = toNumber(value);
37
+ return number !== null && number >= 1 && number <= (part === "day" ? 31 : 12);
38
+ };
39
+ const trimParts = (date) => ({
40
+ day: date.day?.trim() ?? "",
41
+ month: date.month?.trim() ?? "",
42
+ year: date.year?.trim() ?? ""
43
+ });
44
+ const getDateErrors = (values, parts) => {
45
+ if (parts.every((part) => values[part] === "")) return ["missing-date"];
46
+ const errors = [];
47
+ for (const part of parts) if (values[part] === "") errors.push(`missing-${part}`);
48
+ else if (!isValidPart(part, values[part])) errors.push(`invalid-${part}`);
49
+ if (errors.length > 0 || !parts.includes("day") || !parts.includes("month")) return errors;
50
+ const year = parts.includes("year") ? Number(values.year) : 2e3;
51
+ return isExists(year, Number(values.month) - 1, Number(values.day)) ? [] : ["impossible-date"];
52
+ };
53
+ const getInvalidParts = (errors, parts) => parts.filter((part) => errors.some((code) => WHOLE_DATE_ERRORS.has(code) || code.endsWith(part)));
54
+ const pad = (value) => String(Number(value)).padStart(2, "0");
55
+ const toISODate = ({ day, month, year }, parts) => {
56
+ const has = (part) => parts.includes(part);
57
+ if (has("day") && has("month") && has("year")) return `${year}-${pad(month)}-${pad(day)}`;
58
+ if (has("month") && has("year") && !has("day")) return `${year}-${pad(month)}`;
59
+ if (has("day") && has("month")) return `--${pad(month)}-${pad(day)}`;
60
+ if (parts.length === 1 && has("year")) return year;
61
+ return "";
62
+ };
63
+ const toLimit = (limit, name) => {
64
+ const date = typeof limit === "string" ? parseISO(limit) : limit;
65
+ if (!isValid(date)) throw new Error(`${name} must be a valid Date or an ISO date, such as 2020-01-31`);
66
+ return date;
67
+ };
68
+ const formatLimit = (limit, length) => format(limit, length === 4 ? "yyyy" : length === 7 ? "MMMM yyyy" : "d MMMM yyyy");
69
+ const getRangeError = (value, min, max) => {
70
+ if (value.startsWith("--")) return void 0;
71
+ const cut = (limit) => formatISO(limit, { representation: "date" }).slice(0, value.length);
72
+ if (min && value < cut(min)) return "before-min";
73
+ if (max && value > cut(max)) return "after-max";
74
+ };
75
+ const getMessage = (code, label, messages = {}, limits, defaults) => {
76
+ const custom = messages[code];
77
+ if (custom !== void 0) return typeof custom === "function" ? custom(label) : custom;
78
+ const fallback = defaults[code];
79
+ return typeof fallback === "function" ? fallback(label, limits) : fallback;
80
+ };
81
+ //#endregion
82
+ export { WHOLE_DATE_ERRORS, formatDateValue, formatLimit, getDateErrors, getInvalidParts, getMessage, getRangeError, isValidPart, pad, parseDateValue, toDateParts, toDateValue, toISODate, toLimit, toNumber, trimParts };
@@ -0,0 +1,66 @@
1
+ import { Limits } from "./dateinput-internal.js";
2
+ //#region src/components/DateInput/dateinput-utils.d.ts
3
+ export type DatePart = "day" | "month" | "year";
4
+ export interface DateParts {
5
+ day?: string;
6
+ month?: string;
7
+ year?: string;
8
+ }
9
+ export type DateInputMode = "dob" | undefined;
10
+ export type DateErrorCode = "missing-date" | "missing-day" | "missing-month" | "missing-year" | "invalid-day" | "invalid-month" | "invalid-year" | "impossible-date" | "unsupported-parts" | "before-min" | "after-max";
11
+ /** A message, or a function that builds one from the label. */
12
+ export type DateErrorMessage = string | ((label?: string) => string);
13
+ export interface DateValidationOptions {
14
+ /** The name of the date, as it would start a sentence, such as "Date
15
+ * of birth".
16
+ *
17
+ * Default: `Date`. */
18
+ label?: string;
19
+ /** The latest date allowed, as a `Date` or an ISO date such as `2026-12-31`.
20
+ *
21
+ * Only the parts used are compared, such as the month and year. */
22
+ max?: Date | string;
23
+ /** Messages to use instead of the defaults. */
24
+ messages?: Partial<Record<DateErrorCode, DateErrorMessage>>;
25
+ /** The earliest date allowed, as a `Date` or an ISO date such as `2020-01-01`.
26
+ *
27
+ * Only the parts used are compared, such as the month and year. */
28
+ min?: Date | string;
29
+ /** The parts used, in the order they are entered.
30
+ *
31
+ * Default: `day`, `month`, `year`. */
32
+ parts?: DatePart[];
33
+ }
34
+ export interface DateValidationSuccess {
35
+ /** Whether the date is valid. */
36
+ valid: true;
37
+ /** The date as an ISO 8601 string, such as 1990-08-21. */
38
+ value: string;
39
+ }
40
+ export interface DateValidationError {
41
+ /** The code for the first problem, in the order the parts are entered. */
42
+ error: DateErrorCode;
43
+ /** The parts with an error. Every part is invalid when the date is
44
+ * empty, impossible, has no ISO format or is outside `min` and `max`. */
45
+ invalid: DatePart[];
46
+ /** The message for the error. */
47
+ message: string;
48
+ /** Whether the date is valid. */
49
+ valid: false;
50
+ }
51
+ export type DateValidation = DateValidationSuccess | DateValidationError;
52
+ export declare const DATE_PARTS: DatePart[];
53
+ export declare const DATE_ERROR_MESSAGES: Record<DateErrorCode, string | ((label?: string, limits?: Limits) => string)>;
54
+ export declare const validateDateParts: (date: DateParts, { label, max, messages, min, parts }?: DateValidationOptions) => DateValidation;
55
+ /** Check a value, such as the DateInput value or the value a form
56
+ * submits, the same way as `validateDateParts`. Use the same `parts` as
57
+ * the DateInput. */
58
+ export declare const validateDate: (value: string, options?: DateValidationOptions) => DateValidation;
59
+ /** As this is a more specific function for a birth of date validation, we can make some assumptions.
60
+ * Such as, date needs to be today or in the past and error message should reflect date of birth.
61
+ * Check a value, such as the DateInput value or the value a form
62
+ * submits, the same way as `validateDateParts`. Use the same `parts` as
63
+ * the DateInput. */
64
+ export declare const validateDob: (value: string, options?: DateValidationOptions) => DateValidation;
65
+ export declare const getAutoComplete: (mode: DateInputMode, part: DatePart) => string | undefined;
66
+ //#endregion
@@ -0,0 +1,91 @@
1
+ import { formatLimit, getDateErrors, getInvalidParts, getMessage, getRangeError, parseDateValue, toISODate, toLimit, trimParts } from "./dateinput-internal.js";
2
+ //#region src/components/DateInput/dateinput-utils.ts
3
+ const DATE_PARTS = [
4
+ "day",
5
+ "month",
6
+ "year"
7
+ ];
8
+ const realDate = (label = "Date") => `${label} must be a real date`;
9
+ const DATE_ERROR_MESSAGES = {
10
+ "after-max": (label = "Date", limits) => `${label} must be on or before ${limits?.max}`,
11
+ "before-min": (label = "Date", limits) => `${label} must be on or after ${limits?.min}`,
12
+ "impossible-date": realDate,
13
+ "invalid-day": realDate,
14
+ "invalid-month": realDate,
15
+ "invalid-year": "Year must include 4 numbers",
16
+ "missing-date": "Enter a date",
17
+ "missing-day": (label = "Date") => `${label} must include a day`,
18
+ "missing-month": (label = "Date") => `${label} must include a month`,
19
+ "missing-year": (label = "Date") => `${label} must include a year`,
20
+ "unsupported-parts": realDate
21
+ };
22
+ const validateDateParts = (date, { label, max, messages = {}, min, parts = DATE_PARTS } = {}) => {
23
+ const minDate = min === void 0 ? void 0 : toLimit(min, "min");
24
+ const maxDate = max === void 0 ? void 0 : toLimit(max, "max");
25
+ const values = trimParts(date);
26
+ const errors = getDateErrors(values, parts);
27
+ let value = "";
28
+ if (errors.length === 0) {
29
+ value = toISODate(values, parts);
30
+ const rangeError = value ? getRangeError(value, minDate, maxDate) : "unsupported-parts";
31
+ if (!rangeError) return {
32
+ valid: true,
33
+ value
34
+ };
35
+ errors.push(rangeError);
36
+ }
37
+ const [error] = errors;
38
+ const limits = {
39
+ max: maxDate && value ? formatLimit(maxDate, value.length) : void 0,
40
+ min: minDate && value ? formatLimit(minDate, value.length) : void 0
41
+ };
42
+ return {
43
+ error,
44
+ invalid: getInvalidParts(errors, parts),
45
+ message: getMessage(error, label, messages, limits, DATE_ERROR_MESSAGES),
46
+ valid: false
47
+ };
48
+ };
49
+ /** Check a value, such as the DateInput value or the value a form
50
+ * submits, the same way as `validateDateParts`. Use the same `parts` as
51
+ * the DateInput. */
52
+ const validateDate = (value, options = {}) => {
53
+ const date = parseDateValue(value, options.parts);
54
+ if (date) return validateDateParts(date, options);
55
+ const { label, messages, parts = DATE_PARTS } = options;
56
+ return {
57
+ error: "impossible-date",
58
+ invalid: parts,
59
+ message: getMessage("impossible-date", label, messages, {}, DATE_ERROR_MESSAGES),
60
+ valid: false
61
+ };
62
+ };
63
+ /** As this is a more specific function for a birth of date validation, we can make some assumptions.
64
+ * Such as, date needs to be today or in the past and error message should reflect date of birth.
65
+ * Check a value, such as the DateInput value or the value a form
66
+ * submits, the same way as `validateDateParts`. Use the same `parts` as
67
+ * the DateInput. */
68
+ const validateDob = (value, options = {}) => {
69
+ const { messages, ...rest } = options;
70
+ return validateDate(value, {
71
+ label: "Date of birth",
72
+ max: /* @__PURE__ */ new Date(),
73
+ messages: {
74
+ "after-max": "Date of birth must be today or in the past",
75
+ "missing-date": "Enter your date of birth",
76
+ ...messages
77
+ },
78
+ parts: DATE_PARTS,
79
+ ...rest
80
+ });
81
+ };
82
+ const getAutoComplete = (mode, part) => {
83
+ if (!mode) return void 0;
84
+ if (mode === "dob") {
85
+ if (part === "day") return "bday-day";
86
+ if (part === "month") return "bday-month";
87
+ if (part === "year") return "bday-year";
88
+ }
89
+ };
90
+ //#endregion
91
+ export { DATE_ERROR_MESSAGES, DATE_PARTS, getAutoComplete, validateDate, validateDateParts, validateDob };
@@ -0,0 +1,3 @@
1
+ import { DATE_ERROR_MESSAGES, DATE_PARTS, DateErrorCode, DateErrorMessage, DateInputMode, DatePart, DateParts, DateValidation, DateValidationError, DateValidationOptions, DateValidationSuccess, getAutoComplete, validateDate, validateDateParts, validateDob } from "./dateinput-utils.js";
2
+ import { DateInput, DateInputChangeEvent, DateInputDay, DateInputMonth, DateInputPartProps, DateInputProps, DateInputYear } from "./DateInput.js";
3
+ export { DATE_ERROR_MESSAGES, DATE_PARTS, DateErrorCode, DateErrorMessage, DateInput, DateInput as default, DateInputChangeEvent, DateInputDay, DateInputMode, DateInputMonth, DateInputPartProps, DateInputProps, DateInputYear, DatePart, DateParts, DateValidation, DateValidationError, DateValidationOptions, DateValidationSuccess, getAutoComplete, validateDate, validateDateParts, validateDob };