@coopdigital/react 1.27.1 → 1.28.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 (65) hide show
  1. package/dist/components/Button/Button.js +1 -1
  2. package/dist/components/Card/Card.js +1 -1
  3. package/dist/components/Checkbox/Checkbox.d.ts +3 -3
  4. package/dist/components/Checkbox/Checkbox.js +5 -3
  5. package/dist/components/Combobox/Combobox.js +6 -3
  6. package/dist/components/DatePicker/DatePicker.js +5 -3
  7. package/dist/components/Dialog/Dialog.js +1 -1
  8. package/dist/components/Expandable/Expandable.js +1 -1
  9. package/dist/components/Field/Field.context.js +5 -6
  10. package/dist/components/Field/Field.d.ts +2 -2
  11. package/dist/components/Field/Field.js +40 -20
  12. package/dist/components/FieldMarkers/Error.js +10 -3
  13. package/dist/components/FieldMarkers/Label.js +1 -5
  14. package/dist/components/FieldMarkers/fieldmarkers-utils.js +13 -0
  15. package/dist/components/Fieldset/Fieldset.context.js +7 -0
  16. package/dist/components/Fieldset/Fieldset.d.ts +3 -1
  17. package/dist/components/Fieldset/Fieldset.js +34 -7
  18. package/dist/components/Pill/Pill.js +1 -1
  19. package/dist/components/Popover/Popover.js +1 -1
  20. package/dist/components/Radio/Radio.d.ts +1 -1
  21. package/dist/components/Radio/Radio.js +5 -3
  22. package/dist/components/Searchbox/Searchbox.js +2 -1
  23. package/dist/components/Select/Select.d.ts +3 -3
  24. package/dist/components/Select/Select.js +5 -3
  25. package/dist/components/Tag/Tag.js +1 -1
  26. package/dist/components/TextInput/TextInput.d.ts +3 -3
  27. package/dist/components/TextInput/TextInput.js +7 -3
  28. package/dist/components/Textarea/Textarea.d.ts +3 -3
  29. package/dist/components/Textarea/Textarea.js +5 -3
  30. package/dist/components/ValidationSummary/ValidationSummary.context.js +10 -0
  31. package/dist/components/ValidationSummary/ValidationSummary.d.ts +1 -8
  32. package/dist/components/ValidationSummary/ValidationSummary.js +36 -17
  33. package/dist/components/ValidationSummary/validationsummary-utils.d.ts +11 -0
  34. package/dist/components/ValidationSummary/validationsummary-utils.js +29 -6
  35. package/dist/hooks/useSlots.js +2 -26
  36. package/dist/types/index.d.ts +15 -0
  37. package/dist/utils/classes.d.ts +6 -0
  38. package/dist/utils/classes.js +13 -0
  39. package/dist/utils/index.d.ts +3 -7
  40. package/dist/utils/index.js +3 -17
  41. package/dist/utils/slots.d.ts +9 -0
  42. package/dist/utils/slots.js +37 -0
  43. package/package.json +3 -3
  44. package/src/components/Checkbox/Checkbox.tsx +8 -5
  45. package/src/components/Combobox/Combobox.context.ts +1 -0
  46. package/src/components/Combobox/Combobox.tsx +7 -3
  47. package/src/components/DatePicker/DatePicker.tsx +5 -3
  48. package/src/components/Field/Field.context.ts +14 -8
  49. package/src/components/Field/Field.tsx +39 -20
  50. package/src/components/FieldMarkers/Error.tsx +2 -1
  51. package/src/components/FieldMarkers/Label.tsx +0 -7
  52. package/src/components/FieldMarkers/fieldmarkers-utils.ts +26 -0
  53. package/src/components/Fieldset/Fieldset.context.ts +13 -0
  54. package/src/components/Fieldset/Fieldset.tsx +39 -5
  55. package/src/components/Radio/Radio.tsx +6 -3
  56. package/src/components/Select/Select.tsx +7 -4
  57. package/src/components/TextInput/TextInput.tsx +9 -8
  58. package/src/components/Textarea/Textarea.tsx +8 -7
  59. package/src/components/ValidationSummary/ValidationSummary.context.ts +18 -0
  60. package/src/components/ValidationSummary/ValidationSummary.tsx +38 -24
  61. package/src/components/ValidationSummary/validationsummary-utils.ts +81 -13
  62. package/src/hooks/useSlots.ts +1 -41
  63. package/src/utils/classes.ts +14 -0
  64. package/src/utils/index.ts +2 -24
  65. package/src/utils/slots.ts +63 -0
@@ -1,17 +1,85 @@
1
+ import { RefObject } from "react"
2
+
3
+ type FormControl = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
4
+
5
+ export interface FieldPosition {
6
+ id: string
7
+ name: string
8
+ }
9
+
10
+ export interface FieldError {
11
+ message?: string | number
12
+ ref?: {
13
+ focus?: () => void
14
+ id?: string
15
+ }
16
+ type?: string | number
17
+ }
18
+
1
19
  export const sanitizeFieldId = (id: string): string => (id?.startsWith("#") ? id.slice(1) : id)
2
20
 
3
- export const getFieldElement = (field: string, id?: string) => {
4
- if (id) {
5
- const safeId = sanitizeFieldId(id)
6
- return (
7
- document.querySelector<HTMLInputElement>(`#${safeId}`) ??
8
- document.querySelector<HTMLInputElement>(`[name=${safeId}]`)
9
- )
10
- } else {
11
- return (
12
- document.querySelector<HTMLInputElement>(`[name=${field}_display]`) ??
13
- document.querySelector<HTMLInputElement>(`#${field}`) ??
14
- document.querySelector<HTMLInputElement>(`[name=${field}]`)
15
- )
21
+ // The keys to look a field up by, in order: the error ref id, then the field
22
+ // name, preferring the _display suffix. Each key can match an id or name.
23
+ const getFieldKeys = (field: string, id?: string): string[] => [
24
+ ...(id ? [sanitizeFieldId(id)] : []),
25
+ `${field}_display`,
26
+ field,
27
+ ]
28
+
29
+ // Find the field for a validation error. Try each key from getFieldKeys
30
+ // and return immediately if there is a match. For a RadioNodeList,
31
+ // return the first element only.
32
+ export const getFieldElement = (
33
+ form: HTMLFormElement,
34
+ field: string,
35
+ id?: string
36
+ ): Element | null => {
37
+ for (const key of getFieldKeys(field, id)) {
38
+ const item = form.elements.namedItem(key)
39
+ const el = item instanceof RadioNodeList ? item[0] : item
40
+ if (el instanceof Element) return el
16
41
  }
42
+
43
+ return null
44
+ }
45
+
46
+ // Find the associated form. If a formRef is provided use that,
47
+ // otherwise find the closest form ancestor.
48
+ export const getForm = (
49
+ formRef?: RefObject<HTMLFormElement | null>,
50
+ internalRef?: RefObject<HTMLDivElement | null>
51
+ ): HTMLFormElement | null => {
52
+ return formRef?.current ?? internalRef?.current?.closest("form") ?? null
53
+ }
54
+
55
+ const isFormControl = (el: Element): el is FormControl =>
56
+ el instanceof HTMLInputElement ||
57
+ el instanceof HTMLSelectElement ||
58
+ el instanceof HTMLTextAreaElement
59
+
60
+ // List form fields in document order
61
+ export const getFieldOrder = (form: HTMLFormElement): FieldPosition[] =>
62
+ [...form.elements].filter(isFormControl).map(({ id, name }) => ({ id, name }))
63
+
64
+ // Find a field position in the form, with same match logic as
65
+ // getFieldElement. Fields that are not found are just appended.
66
+ export const getFieldPosition = (
67
+ fieldOrder: FieldPosition[],
68
+ [field, error]: [string, FieldError]
69
+ ) =>
70
+ getFieldKeys(field, error.ref?.id)
71
+ .map((key) => fieldOrder.findIndex((f) => f.id === key || f.name === key))
72
+ .find((index) => index !== -1) ?? fieldOrder.length
73
+
74
+ // Decorate-sort-undecorate errors array by position of associated field
75
+ export const sortErrorsByFieldOrder = (
76
+ errorsArray: [string, FieldError][],
77
+ fieldOrder: FieldPosition[]
78
+ ) => {
79
+ if (!fieldOrder.length) return errorsArray
80
+
81
+ return errorsArray
82
+ .map((entry) => ({ entry, position: getFieldPosition(fieldOrder, entry) }))
83
+ .sort((a, b) => a.position - b.position)
84
+ .map(({ entry }) => entry)
17
85
  }
@@ -1,6 +1,6 @@
1
1
  import React from "react"
2
2
 
3
- import { LazyComponent, SlotConfig } from "@/types"
3
+ import { getSlotName } from "@/utils/slots"
4
4
 
5
5
  type Slots<T> = Record<keyof T, React.ReactNode>
6
6
 
@@ -8,46 +8,6 @@ export function isKey<T extends object>(x: T, k: PropertyKey): k is keyof T {
8
8
  return k in x
9
9
  }
10
10
 
11
- // oxlint-disable-next-line typescript/no-explicit-any
12
- export function isLazy<T extends React.ComponentType<any>>(
13
- type: unknown
14
- ): type is LazyComponent<T> {
15
- return typeof type === "object" && type !== null && "_payload" in type && "_init" in type
16
- }
17
-
18
- export function resolveComponentName(child: React.ReactNode): string | null {
19
- if (child && typeof child === "object" && "type" in child) {
20
- const type = child.type
21
-
22
- if (isLazy(type)) {
23
- try {
24
- const resolved = type._init(type._payload)
25
- if ("slot" in resolved) return (resolved.slot as SlotConfig).name
26
- return resolved.displayName ?? resolved.name ?? null
27
- } catch {
28
- // _init throws a Promise if the payload is still pending
29
- return null
30
- }
31
- }
32
-
33
- if (typeof type === "function") {
34
- if ("slot" in type) return (type.slot as SlotConfig).name
35
- return (type as React.FC).displayName ?? (type as React.FC).name ?? null
36
- }
37
- }
38
- return null
39
- }
40
-
41
- export function getSlotName(node: React.ReactNode): string | false {
42
- const slotName = resolveComponentName(node)
43
-
44
- if (slotName) {
45
- return slotName.replaceAll(".", "")
46
- // return root && !safeName.startsWith(root) ? root + safeName : safeName
47
- }
48
- return false
49
- }
50
-
51
11
  export function useSlots<T>(
52
12
  componentSlots: Slots<T>,
53
13
  children: React.ReactNode,
@@ -0,0 +1,14 @@
1
+ export const bgPropToClass = (color: string, userClasses?: string) =>
2
+ userClasses?.includes("bg-") ? "" : `bg-${color}`
3
+
4
+ export const hasUserBg = (userClasses?: string) => {
5
+ return typeof userClasses === "string" && userClasses?.includes("bg-") ? true : false
6
+ }
7
+
8
+ export const hasUserBorder = (userClasses?: string) => {
9
+ return typeof userClasses === "string" && userClasses?.includes("border-") ? true : false
10
+ }
11
+
12
+ export const bgClassToColor = (userClasses: string) => {
13
+ return /bg-([^\s]+)/.exec(userClasses)?.[1] ?? null
14
+ }
@@ -1,24 +1,2 @@
1
- import { isValidElement } from "react"
2
-
3
- export const bgPropToClass = (color: string, userClasses?: string) =>
4
- userClasses?.includes("bg-") ? "" : `bg-${color}`
5
-
6
- export const hasUserBg = (userClasses?: string) => {
7
- return typeof userClasses === "string" && userClasses?.includes("bg-") ? true : false
8
- }
9
-
10
- export const hasUserBorder = (userClasses?: string) => {
11
- return typeof userClasses === "string" && userClasses?.includes("border-") ? true : false
12
- }
13
-
14
- export const bgClassToColor = (userClasses: string) => {
15
- return /bg-([^\s]+)/.exec(userClasses)?.[1] ?? null
16
- }
17
-
18
- // oxlint-disable-next-line typescript/no-unnecessary-type-parameters
19
- export function getSlotProp<R = unknown>(slot: React.ReactNode, prop: string): R | undefined {
20
- if (isValidElement(slot) && prop in (slot.props as object)) {
21
- return (slot.props as Record<string, unknown>)[prop] as R
22
- }
23
- return undefined
24
- }
1
+ export * from "./classes"
2
+ export * from "./slots"
@@ -0,0 +1,63 @@
1
+ import React, { isValidElement, type ReactElement, type ReactNode } from "react"
2
+
3
+ import type { LazyComponent, SlotConfig } from "@/types"
4
+
5
+ // oxlint-disable-next-line typescript/no-unnecessary-type-parameters
6
+ export function getSlotProp<R = unknown>(slot: React.ReactNode, prop: string): R | undefined {
7
+ if (isValidElement(slot) && prop in (slot.props as object)) {
8
+ return (slot.props as Record<string, unknown>)[prop] as R
9
+ }
10
+ return undefined
11
+ }
12
+
13
+ // Call fn for each direct child that is a slot component
14
+ export const forEachSlot = (
15
+ children: ReactNode,
16
+ fn: (slot: SlotConfig, child: ReactElement) => void
17
+ ): void => {
18
+ 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
+ }
24
+
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
30
+ }
31
+
32
+ export function resolveComponentName(child: React.ReactNode): string | null {
33
+ 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
+ }
46
+
47
+ if (typeof type === "function") {
48
+ if ("slot" in type) return (type.slot as SlotConfig).name
49
+ return (type as React.FC).displayName ?? (type as React.FC).name ?? null
50
+ }
51
+ }
52
+ return null
53
+ }
54
+
55
+ export function getSlotName(node: React.ReactNode): string | false {
56
+ const slotName = resolveComponentName(node)
57
+
58
+ if (slotName) {
59
+ return slotName.replaceAll(".", "")
60
+ // return root && !safeName.startsWith(root) ? root + safeName : safeName
61
+ }
62
+ return false
63
+ }