@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
@@ -73,4 +73,4 @@ Squircle.Text = SquircleText
73
73
  Squircle.slot = { name: "Squircle" }
74
74
 
75
75
  export default Squircle
76
- export { Squircle }
76
+ export { Squircle, SquircleSvg, SquircleText }
@@ -3,14 +3,19 @@
3
3
  import type { ChangeEvent, JSX, Ref, TextareaHTMLAttributes } from "react"
4
4
 
5
5
  import clsx from "clsx"
6
- import { useState } from "react"
6
+ import mergeRefs from "merge-refs"
7
+ import { useRef, useState } from "react"
7
8
 
8
9
  import { getDescribedBy, getFieldError, useField } from "@/components/Field/Field.context"
9
10
  import { useDebounce } from "@/hooks/useDebounce"
11
+ import { useFormReset } from "@/hooks/useFormReset"
10
12
  import { StandardSizes } from "@/types"
11
13
 
12
14
  const DEBOUNCE_DELAY = 750
13
15
 
16
+ const lengthOf = (text: TextareaHTMLAttributes<HTMLTextAreaElement>["value"]): number =>
17
+ String(text ?? "").length
18
+
14
19
  const charCountMessage = (remaining: number) => {
15
20
  return `You have ${Math.abs(remaining).toLocaleString()} ${Math.abs(remaining) === 1 ? "character" : "characters"} ${remaining < 0 ? "too many" : "remaining"}`
16
21
  }
@@ -94,19 +99,29 @@ const Textarea = ({
94
99
  ...props,
95
100
  }
96
101
 
97
- const [remaining, setRemaining] = useState(maxLength)
102
+ const internalRef = useRef<HTMLTextAreaElement>(null)
103
+ const controlled = props.value !== undefined
104
+ // The length typed, when not controlled, starting from the default value
105
+ const [typedLength, setTypedLength] = useState(() => lengthOf(props.defaultValue))
106
+ const length = controlled ? lengthOf(props.value) : typedLength
107
+ const remaining = maxLength == null ? undefined : maxLength - length
98
108
  const debouncedRemaining = useDebounce(remaining, DEBOUNCE_DELAY)
109
+
110
+ // The browser resets the textarea to its default value, so reset the count
111
+ useFormReset(internalRef, () => {
112
+ if (!controlled) setTypedLength(lengthOf(props.defaultValue))
113
+ })
99
114
  const showCounter =
100
115
  !disabled && counter && maxLength && remaining != null && debouncedRemaining != null
101
116
 
102
117
  const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
103
118
  userOnChange?.(e)
104
- if (maxLength && e.target) setRemaining(maxLength - e.target.value.length)
119
+ if (!controlled && e.target) setTypedLength(e.target.value.length)
105
120
  }
106
121
  // const formItemProps = { "aria-disabled": disabled ? true : undefined }
107
122
  return (
108
123
  <div className="coop-textarea-wrapper">
109
- <textarea {...componentProps} onChange={handleChange} ref={ref} />
124
+ <textarea {...componentProps} onChange={handleChange} ref={mergeRefs(internalRef, ref)} />
110
125
  {showCounter && (
111
126
  <>
112
127
  <small
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
  import { createContext, useContext } from "react"
3
3
 
4
- import type { FieldPosition } from "./validationsummary-utils"
4
+ import type { FieldPosition } from "./validationsummary-internal"
5
5
 
6
6
  interface ValidationSummaryContextValue {
7
7
  /** The order of the fields in the form, read when the form is submitted. Empty until the first submit. */
@@ -15,7 +15,7 @@ import {
15
15
  getForm,
16
16
  sanitizeFieldId,
17
17
  sortErrorsByFieldOrder,
18
- } from "@/components/ValidationSummary/validationsummary-utils"
18
+ } from "@/components/ValidationSummary/validationsummary-internal"
19
19
  import {
20
20
  useValidationSummary,
21
21
  ValidationSummaryContext,
@@ -27,27 +27,28 @@ const getFieldKeys = (field: string, id?: string): string[] => [
27
27
  ]
28
28
 
29
29
  const VISIBLE_CONTROL = 'input:not([type="hidden"]), select, textarea'
30
+ const INVALID_CONTROL = `:is(${VISIBLE_CONTROL})[aria-invalid="true"]`
30
31
 
31
32
  const isHiddenInput = (el: Element): el is HTMLInputElement =>
32
33
  el instanceof HTMLInputElement && el.type === "hidden"
33
34
 
34
- // Components such as Combobox submit their value from a hidden input, which
35
- // cannot be focused. Find the visible control in the closest wrapper that
36
- // has one. The search stops before the form, so a hidden input placed
37
- // directly in the form is not matched to an unrelated field.
35
+ // Components such as Combobox and DateInput submit their value from a hidden
36
+ // input, which cannot be focused. Find the visible control in the closest
37
+ // wrapper that has one, preferring a control marked as invalid, such as the
38
+ // year of a DateInput. The search stops before the form, so a hidden input
39
+ // placed directly in the form is not matched to an unrelated field.
38
40
  const getVisibleControl = (form: HTMLFormElement, el: Element): Element => {
39
41
  for (let node = el.parentElement; node && node !== form; node = node.parentElement) {
40
42
  const control = node.querySelector(VISIBLE_CONTROL)
41
- if (control) return control
43
+ if (control) return node.querySelector(INVALID_CONTROL) ?? control
42
44
  }
43
45
 
44
46
  return el
45
47
  }
46
48
 
47
49
  // Find the field for a validation error. Try each key from getFieldKeys
48
- // and return immediately if there is a match. For a RadioNodeList,
49
- // return the first element only. A hidden input is swapped for its visible
50
- // control.
50
+ // and return if there is a match. For a RadioNodeList, return the first
51
+ // element only. A hidden input is swapped for its visible control.
51
52
  export const getFieldElement = (
52
53
  form: HTMLFormElement,
53
54
  field: string,
@@ -1,3 +1,4 @@
1
1
  export { useDebounce } from "./useDebounce"
2
+ export { useFormReset } from "./useFormReset"
2
3
  export { useId } from "./useId"
3
4
  export { useSlots } from "./useSlots"
@@ -0,0 +1,28 @@
1
+ import { type RefObject, useEffect, useRef } from "react"
2
+
3
+ type FormControl = HTMLButtonElement | HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
4
+
5
+ // Call onReset when the form that owns the control is reset, so a component
6
+ // that keeps its own state can go back to its default value, as native fields
7
+ // do. React 19 form actions reset the form after they run. The control's form
8
+ // property also covers a control linked to its form with the form attribute.
9
+ // A reset that has been prevented is ignored, as the fields are not reset then.
10
+ export function useFormReset(ref: RefObject<FormControl | null>, onReset: () => void): void {
11
+ // Keep the latest callback, so the listener sees the latest props without
12
+ // being added again
13
+ const onResetRef = useRef(onReset)
14
+ useEffect(() => {
15
+ onResetRef.current = onReset
16
+ })
17
+
18
+ useEffect(() => {
19
+ const form = ref.current?.form
20
+ if (!form) return
21
+
22
+ const handleReset = (event: Event) => {
23
+ if (!event.defaultPrevented) onResetRef.current()
24
+ }
25
+ form.addEventListener("reset", handleReset)
26
+ return () => form.removeEventListener("reset", handleReset)
27
+ }, [ref])
28
+ }
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ export * from "./components/Button/Button"
5
5
  export * from "./components/Card/Card"
6
6
  export * from "./components/Checkbox/Checkbox"
7
7
  export * from "./components/Combobox"
8
+ export * from "./components/DateInput"
8
9
  export * from "./components/DatePicker/DatePicker"
9
10
  export * from "./components/Dialog/Dialog"
10
11
  export * from "./components/Expandable/Expandable"
@@ -1,6 +1,6 @@
1
1
  import React, { isValidElement, type ReactElement, type ReactNode } from "react"
2
2
 
3
- import type { LazyComponent, SlotConfig } from "@/types"
3
+ import { type LazyComponent, REACT_LAZY_TYPE, type SlotConfig } from "@/types"
4
4
 
5
5
  // oxlint-disable-next-line typescript/no-unnecessary-type-parameters
6
6
  export function getSlotProp<R = unknown>(slot: React.ReactNode, prop: string): R | undefined {
@@ -10,39 +10,53 @@ export function getSlotProp<R = unknown>(slot: React.ReactNode, prop: string): R
10
10
  return undefined
11
11
  }
12
12
 
13
+ // oxlint-disable-next-line typescript/no-explicit-any
14
+ export function isLazy<T extends React.ComponentType<any>>(
15
+ type: unknown
16
+ ): type is LazyComponent<T> {
17
+ return (
18
+ typeof type === "object" &&
19
+ type !== null &&
20
+ (type as { $$typeof?: unknown }).$$typeof === REACT_LAZY_TYPE
21
+ )
22
+ }
23
+
24
+ // The component for an element type. Lazy types, such as client components
25
+ // rendered from a server component, are resolved once loaded. Undefined while
26
+ // still loading, as _init throws a Promise, or if loading failed.
27
+ const resolveType = (type: unknown): unknown => {
28
+ if (!isLazy(type)) return type
29
+
30
+ try {
31
+ return type._init(type._payload)
32
+ } catch {
33
+ return undefined
34
+ }
35
+ }
36
+
37
+ const getSlotConfig = (type: unknown): SlotConfig | undefined => {
38
+ const resolved = resolveType(type)
39
+ return typeof resolved === "function" && "slot" in resolved
40
+ ? (resolved.slot as SlotConfig)
41
+ : undefined
42
+ }
43
+
13
44
  // Call fn for each direct child that is a slot component
14
45
  export const forEachSlot = (
15
46
  children: ReactNode,
16
47
  fn: (slot: SlotConfig, child: ReactElement) => void
17
48
  ): void => {
18
49
  Array.from([children].flat()).forEach((child) => {
19
- if (isValidElement(child) && typeof child.type === "function" && "slot" in child.type) {
20
- fn((child.type as { slot: SlotConfig }).slot, child)
21
- }
22
- })
23
- }
50
+ if (!isValidElement(child)) return
24
51
 
25
- // oxlint-disable-next-line typescript/no-explicit-any
26
- export function isLazy<T extends React.ComponentType<any>>(
27
- type: unknown
28
- ): type is LazyComponent<T> {
29
- return typeof type === "object" && type !== null && "_payload" in type && "_init" in type
52
+ const slot = getSlotConfig(child.type)
53
+ if (slot) fn(slot, child)
54
+ })
30
55
  }
31
56
 
32
57
  export function resolveComponentName(child: React.ReactNode): string | null {
33
58
  if (child && typeof child === "object" && "type" in child) {
34
- const type = child.type
35
-
36
- if (isLazy(type)) {
37
- try {
38
- const resolved = type._init(type._payload)
39
- if ("slot" in resolved) return (resolved.slot as SlotConfig).name
40
- return resolved.displayName ?? resolved.name ?? null
41
- } catch {
42
- // _init throws a Promise if the payload is still pending
43
- return null
44
- }
45
- }
59
+ const type = resolveType(child.type)
46
60
 
47
61
  if (typeof type === "function") {
48
62
  if ("slot" in type) return (type.slot as SlotConfig).name