@coopdigital/react 1.27.2 → 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.
- package/dist/components/Button/Button.js +1 -1
- package/dist/components/Card/Card.js +1 -1
- package/dist/components/Checkbox/Checkbox.d.ts +3 -3
- package/dist/components/Checkbox/Checkbox.js +5 -3
- package/dist/components/Combobox/Combobox.js +6 -3
- package/dist/components/DatePicker/DatePicker.js +5 -3
- package/dist/components/Dialog/Dialog.js +1 -1
- package/dist/components/Expandable/Expandable.js +1 -1
- package/dist/components/Field/Field.context.js +5 -6
- package/dist/components/Field/Field.d.ts +2 -2
- package/dist/components/Field/Field.js +40 -20
- package/dist/components/FieldMarkers/Error.js +10 -3
- package/dist/components/FieldMarkers/Label.js +1 -5
- package/dist/components/FieldMarkers/fieldmarkers-utils.js +13 -0
- package/dist/components/Fieldset/Fieldset.context.js +7 -0
- package/dist/components/Fieldset/Fieldset.d.ts +3 -1
- package/dist/components/Fieldset/Fieldset.js +34 -7
- package/dist/components/Pill/Pill.js +1 -1
- package/dist/components/Popover/Popover.js +1 -1
- package/dist/components/Radio/Radio.d.ts +1 -1
- package/dist/components/Radio/Radio.js +5 -3
- package/dist/components/Searchbox/Searchbox.js +2 -1
- package/dist/components/Select/Select.d.ts +3 -3
- package/dist/components/Select/Select.js +5 -3
- package/dist/components/Tag/Tag.js +1 -1
- package/dist/components/TextInput/TextInput.d.ts +3 -3
- package/dist/components/TextInput/TextInput.js +7 -3
- package/dist/components/Textarea/Textarea.d.ts +3 -3
- package/dist/components/Textarea/Textarea.js +5 -3
- package/dist/components/ValidationSummary/ValidationSummary.context.js +10 -0
- package/dist/components/ValidationSummary/ValidationSummary.d.ts +1 -8
- package/dist/components/ValidationSummary/ValidationSummary.js +36 -17
- package/dist/components/ValidationSummary/validationsummary-utils.d.ts +11 -0
- package/dist/components/ValidationSummary/validationsummary-utils.js +29 -6
- package/dist/hooks/useSlots.js +2 -26
- package/dist/types/index.d.ts +15 -0
- package/dist/utils/classes.d.ts +6 -0
- package/dist/utils/classes.js +13 -0
- package/dist/utils/index.d.ts +3 -7
- package/dist/utils/index.js +3 -17
- package/dist/utils/slots.d.ts +9 -0
- package/dist/utils/slots.js +37 -0
- package/package.json +3 -3
- package/src/components/Checkbox/Checkbox.tsx +8 -5
- package/src/components/Combobox/Combobox.context.ts +1 -0
- package/src/components/Combobox/Combobox.tsx +7 -3
- package/src/components/DatePicker/DatePicker.tsx +5 -3
- package/src/components/Field/Field.context.ts +14 -8
- package/src/components/Field/Field.tsx +39 -20
- package/src/components/FieldMarkers/Error.tsx +2 -1
- package/src/components/FieldMarkers/Label.tsx +0 -7
- package/src/components/FieldMarkers/fieldmarkers-utils.ts +26 -0
- package/src/components/Fieldset/Fieldset.context.ts +13 -0
- package/src/components/Fieldset/Fieldset.tsx +39 -5
- package/src/components/Radio/Radio.tsx +6 -3
- package/src/components/Select/Select.tsx +7 -4
- package/src/components/TextInput/TextInput.tsx +9 -8
- package/src/components/Textarea/Textarea.tsx +8 -7
- package/src/components/ValidationSummary/ValidationSummary.context.ts +18 -0
- package/src/components/ValidationSummary/ValidationSummary.tsx +38 -24
- package/src/components/ValidationSummary/validationsummary-utils.ts +81 -13
- package/src/hooks/useSlots.ts +1 -41
- package/src/utils/classes.ts +14 -0
- package/src/utils/index.ts +2 -24
- 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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
}
|
package/src/hooks/useSlots.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react"
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,24 +1,2 @@
|
|
|
1
|
-
|
|
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
|
+
}
|