@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
@@ -0,0 +1,10 @@
1
+ "use client";
2
+ import { createContext, useContext } from "react";
3
+ //#region src/components/ValidationSummary/ValidationSummary.context.ts
4
+ const ValidationSummaryContext = createContext({
5
+ fieldOrder: [],
6
+ getForm: () => null
7
+ });
8
+ const useValidationSummary = () => useContext(ValidationSummaryContext);
9
+ //#endregion
10
+ export { ValidationSummaryContext, useValidationSummary };
@@ -1,3 +1,4 @@
1
+ import { FieldError } from "./validationsummary-utils.js";
1
2
  import React, { HTMLAttributes, JSX, ReactNode, Ref, RefObject } from "react";
2
3
  //#region src/components/ValidationSummary/ValidationSummary.d.ts
3
4
  export interface ValidationSummaryProps extends HTMLAttributes<HTMLDivElement> {
@@ -46,14 +47,6 @@ interface ValidationSummaryPromptProps extends HTMLAttributes<HTMLDivElement> {
46
47
  */
47
48
  children?: React.ReactNode;
48
49
  }
49
- interface FieldError {
50
- message?: string | number;
51
- ref?: {
52
- focus?: () => void;
53
- id?: string;
54
- };
55
- type?: string | number;
56
- }
57
50
  interface ValidationSummaryErrorsProps extends HTMLAttributes<HTMLDivElement> {
58
51
  /** Specify the validation error array. Refer to the FieldError type for the shape.. */
59
52
  errors?: Record<string, FieldError>;
@@ -1,10 +1,11 @@
1
1
  "use client";
2
- import { getSlotProp } from "../../utils/index.js";
2
+ import { getSlotProp } from "../../utils/slots.js";
3
3
  import { useSlots } from "../../hooks/useSlots.js";
4
- import { getFieldElement, sanitizeFieldId } from "./validationsummary-utils.js";
4
+ import { getFieldElement, getFieldOrder, getForm, sanitizeFieldId, sortErrorsByFieldOrder } from "./validationsummary-utils.js";
5
+ import { ValidationSummaryContext, useValidationSummary } from "./ValidationSummary.context.js";
5
6
  import clsx from "clsx";
6
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
- import React, { useCallback, useEffect, useRef, useState } from "react";
8
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
8
9
  import mergeRefs from "merge-refs";
9
10
  //#region src/components/ValidationSummary/ValidationSummary.tsx
10
11
  const componentSlots = {
@@ -19,11 +20,18 @@ const ValidationSummary = ({ autoScroll = true, children, className, forceMount
19
20
  const errors = Object.keys(getSlotProp(slots.ValidationSummaryErrors, "errors") ?? {});
20
21
  const hasErrors = errors && errors.length > 0;
21
22
  const internalRef = useRef(null);
23
+ const [fieldOrder, setFieldOrder] = useState([]);
22
24
  const onSubmit = useCallback(() => {
25
+ const form = getForm(formRef, internalRef);
26
+ if (form) setFieldOrder(getFieldOrder(form));
23
27
  setStamp(Date.now());
24
- }, []);
28
+ }, [formRef]);
29
+ const context = useMemo(() => ({
30
+ fieldOrder,
31
+ getForm: () => getForm(formRef, internalRef)
32
+ }), [fieldOrder, formRef]);
25
33
  useEffect(() => {
26
- const form = formRef?.current ?? internalRef.current?.closest("form");
34
+ const form = getForm(formRef, internalRef);
27
35
  form?.addEventListener("submit", onSubmit);
28
36
  if (form && hasErrors && autoScroll) {
29
37
  if (typeof form?.scrollIntoView === "function") form.scrollIntoView({ behavior: "smooth" });
@@ -46,18 +54,21 @@ const ValidationSummary = ({ autoScroll = true, children, className, forceMount
46
54
  inert: true,
47
55
  ref: mergeRefs(internalRef, ref)
48
56
  });
49
- return /* @__PURE__ */ jsxs("div", {
57
+ return /* @__PURE__ */ jsx("div", {
50
58
  ...componentProps,
51
59
  "aria-live": "assertive",
52
60
  ref: mergeRefs(internalRef, ref),
53
61
  role: "alert",
54
62
  tabIndex: -1,
55
- children: [
56
- slots.ValidationSummaryHeading,
57
- slots.ValidationSummaryPrompt,
58
- slots.ValidationSummaryErrors,
59
- slots.Children
60
- ]
63
+ children: /* @__PURE__ */ jsxs(ValidationSummaryContext.Provider, {
64
+ value: context,
65
+ children: [
66
+ slots.ValidationSummaryHeading,
67
+ slots.ValidationSummaryPrompt,
68
+ slots.ValidationSummaryErrors,
69
+ slots.Children
70
+ ]
71
+ })
61
72
  });
62
73
  };
63
74
  const ValidationSummaryHeading = ({ as = "h2", children, className }) => {
@@ -71,20 +82,28 @@ const focusable = [
71
82
  "TEXTAREA",
72
83
  "SELECT"
73
84
  ];
85
+ const focusOptions = {
86
+ focusVisible: true,
87
+ preventScroll: true
88
+ };
74
89
  const ValidationSummaryErrors = ({ errors }) => {
75
90
  const errorsArray = (errors && Object.entries(errors)) ?? [];
91
+ const { fieldOrder, getForm } = useValidationSummary();
92
+ const sortedErrors = sortErrorsByFieldOrder(errorsArray, fieldOrder);
76
93
  return errorsArray.length ? /* @__PURE__ */ jsx("div", {
77
94
  className: "coop-validation-summary--errors",
78
- children: /* @__PURE__ */ jsx("ul", { children: errorsArray.map(([field, error]) => {
95
+ children: /* @__PURE__ */ jsx("ul", { children: sortedErrors.map(([field, error]) => {
79
96
  const id = error.ref?.id ? sanitizeFieldId(error.ref.id) : void 0;
80
97
  return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("a", {
81
98
  href: "#",
82
99
  onClick: (e) => {
83
100
  e.preventDefault();
84
- const el = getFieldElement(field, id);
85
- if (el) {
86
- if (focusable.includes(el.nodeName)) el.focus();
87
- else el.querySelector("input")?.focus();
101
+ const form = getForm();
102
+ const el = form && getFieldElement(form, field, id);
103
+ if (el instanceof HTMLElement) {
104
+ el.scrollIntoView({ block: "center" });
105
+ if (focusable.includes(el.nodeName)) el.focus(focusOptions);
106
+ else el.querySelector("input")?.focus(focusOptions);
88
107
  }
89
108
  },
90
109
  children: error.message
@@ -0,0 +1,11 @@
1
+ import { RefObject } from "react";
2
+ //#region src/components/ValidationSummary/validationsummary-utils.d.ts
3
+ export interface FieldError {
4
+ message?: string | number;
5
+ ref?: {
6
+ focus?: () => void;
7
+ id?: string;
8
+ };
9
+ type?: string | number;
10
+ }
11
+ //#endregion
@@ -1,10 +1,33 @@
1
1
  //#region src/components/ValidationSummary/validationsummary-utils.ts
2
2
  const sanitizeFieldId = (id) => id?.startsWith("#") ? id.slice(1) : id;
3
- const getFieldElement = (field, id) => {
4
- if (id) {
5
- const safeId = sanitizeFieldId(id);
6
- return document.querySelector(`#${safeId}`) ?? document.querySelector(`[name=${safeId}]`);
7
- } else return document.querySelector(`[name=${field}_display]`) ?? document.querySelector(`#${field}`) ?? document.querySelector(`[name=${field}]`);
3
+ const getFieldKeys = (field, id) => [
4
+ ...id ? [sanitizeFieldId(id)] : [],
5
+ `${field}_display`,
6
+ field
7
+ ];
8
+ const getFieldElement = (form, field, id) => {
9
+ for (const key of getFieldKeys(field, id)) {
10
+ const item = form.elements.namedItem(key);
11
+ const el = item instanceof RadioNodeList ? item[0] : item;
12
+ if (el instanceof Element) return el;
13
+ }
14
+ return null;
15
+ };
16
+ const getForm = (formRef, internalRef) => {
17
+ return formRef?.current ?? internalRef?.current?.closest("form") ?? null;
18
+ };
19
+ const isFormControl = (el) => el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement;
20
+ const getFieldOrder = (form) => [...form.elements].filter(isFormControl).map(({ id, name }) => ({
21
+ id,
22
+ name
23
+ }));
24
+ const getFieldPosition = (fieldOrder, [field, error]) => getFieldKeys(field, error.ref?.id).map((key) => fieldOrder.findIndex((f) => f.id === key || f.name === key)).find((index) => index !== -1) ?? fieldOrder.length;
25
+ const sortErrorsByFieldOrder = (errorsArray, fieldOrder) => {
26
+ if (!fieldOrder.length) return errorsArray;
27
+ return errorsArray.map((entry) => ({
28
+ entry,
29
+ position: getFieldPosition(fieldOrder, entry)
30
+ })).sort((a, b) => a.position - b.position).map(({ entry }) => entry);
8
31
  };
9
32
  //#endregion
10
- export { getFieldElement, sanitizeFieldId };
33
+ export { getFieldElement, getFieldOrder, getFieldPosition, getForm, sanitizeFieldId, sortErrorsByFieldOrder };
@@ -1,33 +1,9 @@
1
+ import { getSlotName } from "../utils/slots.js";
1
2
  import React from "react";
2
3
  //#region src/hooks/useSlots.ts
3
4
  function isKey(x, k) {
4
5
  return k in x;
5
6
  }
6
- function isLazy(type) {
7
- return typeof type === "object" && type !== null && "_payload" in type && "_init" in type;
8
- }
9
- function resolveComponentName(child) {
10
- if (child && typeof child === "object" && "type" in child) {
11
- const type = child.type;
12
- if (isLazy(type)) try {
13
- const resolved = type._init(type._payload);
14
- if ("slot" in resolved) return resolved.slot.name;
15
- return resolved.displayName ?? resolved.name ?? null;
16
- } catch {
17
- return null;
18
- }
19
- if (typeof type === "function") {
20
- if ("slot" in type) return type.slot.name;
21
- return type.displayName ?? type.name ?? null;
22
- }
23
- }
24
- return null;
25
- }
26
- function getSlotName(node) {
27
- const slotName = resolveComponentName(node);
28
- if (slotName) return slotName.replaceAll(".", "");
29
- return false;
30
- }
31
7
  function useSlots(componentSlots, children, options) {
32
8
  return React.Children.toArray(children).reduce((slots, child) => {
33
9
  const slotName = getSlotName(child);
@@ -39,4 +15,4 @@ function useSlots(componentSlots, children, options) {
39
15
  }, { ...componentSlots });
40
16
  }
41
17
  //#endregion
42
- export { getSlotName, isKey, isLazy, resolveComponentName, useSlots };
18
+ export { isKey, useSlots };
@@ -1,4 +1,19 @@
1
1
  //#region src/types/index.d.ts
2
2
  export type StandardSizes = "sm" | "md" | "lg";
3
3
  export type StandardSizesWithNone = "none" | "sm" | "md" | "lg";
4
+ export interface SlotConfig {
5
+ field?: boolean;
6
+ inline?: boolean;
7
+ name: string;
8
+ suffix?: string;
9
+ }
10
+ export interface LazyPayload<T> {
11
+ default: T;
12
+ }
13
+ export interface LazyComponent<T extends React.ComponentType<any>> {
14
+ _init: (payload: unknown) => T;
15
+ _payload: Promise<LazyPayload<T>> | LazyPayload<T>;
16
+ $$typeof: typeof REACT_LAZY_TYPE;
17
+ }
18
+ export declare const REACT_LAZY_TYPE: any;
4
19
  //#endregion
@@ -0,0 +1,6 @@
1
+ //#region src/utils/classes.d.ts
2
+ export declare const bgPropToClass: (color: string, userClasses?: string) => string;
3
+ export declare const hasUserBg: (userClasses?: string) => boolean;
4
+ export declare const hasUserBorder: (userClasses?: string) => boolean;
5
+ export declare const bgClassToColor: (userClasses: string) => string | null;
6
+ //#endregion
@@ -0,0 +1,13 @@
1
+ //#region src/utils/classes.ts
2
+ const bgPropToClass = (color, userClasses) => userClasses?.includes("bg-") ? "" : `bg-${color}`;
3
+ const hasUserBg = (userClasses) => {
4
+ return typeof userClasses === "string" && userClasses?.includes("bg-") ? true : false;
5
+ };
6
+ const hasUserBorder = (userClasses) => {
7
+ return typeof userClasses === "string" && userClasses?.includes("border-") ? true : false;
8
+ };
9
+ const bgClassToColor = (userClasses) => {
10
+ return /bg-([^\s]+)/.exec(userClasses)?.[1] ?? null;
11
+ };
12
+ //#endregion
13
+ export { bgClassToColor, bgPropToClass, hasUserBg, hasUserBorder };
@@ -1,7 +1,3 @@
1
- //#region src/utils/index.d.ts
2
- export declare const bgPropToClass: (color: string, userClasses?: string) => string;
3
- export declare const hasUserBg: (userClasses?: string) => boolean;
4
- export declare const hasUserBorder: (userClasses?: string) => boolean;
5
- export declare const bgClassToColor: (userClasses: string) => string | null;
6
- export declare function getSlotProp<R = unknown>(slot: React.ReactNode, prop: string): R | undefined;
7
- //#endregion
1
+ import { bgClassToColor, bgPropToClass, hasUserBg, hasUserBorder } from "./classes.js";
2
+ import { forEachSlot, getSlotName, getSlotProp, isLazy, resolveComponentName } from "./slots.js";
3
+ export { bgClassToColor, bgPropToClass, forEachSlot, getSlotName, getSlotProp, hasUserBg, hasUserBorder, isLazy, resolveComponentName };
@@ -1,17 +1,3 @@
1
- import { isValidElement } from "react";
2
- //#region src/utils/index.ts
3
- const bgPropToClass = (color, userClasses) => userClasses?.includes("bg-") ? "" : `bg-${color}`;
4
- const hasUserBg = (userClasses) => {
5
- return typeof userClasses === "string" && userClasses?.includes("bg-") ? true : false;
6
- };
7
- const hasUserBorder = (userClasses) => {
8
- return typeof userClasses === "string" && userClasses?.includes("border-") ? true : false;
9
- };
10
- const bgClassToColor = (userClasses) => {
11
- return /bg-([^\s]+)/.exec(userClasses)?.[1] ?? null;
12
- };
13
- function getSlotProp(slot, prop) {
14
- if (isValidElement(slot) && prop in slot.props) return slot.props[prop];
15
- }
16
- //#endregion
17
- export { bgClassToColor, bgPropToClass, getSlotProp, hasUserBg, hasUserBorder };
1
+ import { bgClassToColor, bgPropToClass, hasUserBg, hasUserBorder } from "./classes.js";
2
+ import { forEachSlot, getSlotName, getSlotProp, isLazy, resolveComponentName } from "./slots.js";
3
+ export { bgClassToColor, bgPropToClass, forEachSlot, getSlotName, getSlotProp, hasUserBg, hasUserBorder, isLazy, resolveComponentName };
@@ -0,0 +1,9 @@
1
+ import { LazyComponent, SlotConfig } from "../types/index.js";
2
+ import React, { ReactElement, ReactNode } from "react";
3
+ //#region src/utils/slots.d.ts
4
+ export declare function getSlotProp<R = unknown>(slot: React.ReactNode, prop: string): R | undefined;
5
+ export declare const forEachSlot: (children: ReactNode, fn: (slot: SlotConfig, child: ReactElement) => void) => void;
6
+ export declare function isLazy<T extends React.ComponentType<any>>(type: unknown): type is LazyComponent<T>;
7
+ export declare function resolveComponentName(child: React.ReactNode): string | null;
8
+ export declare function getSlotName(node: React.ReactNode): string | false;
9
+ //#endregion
@@ -0,0 +1,37 @@
1
+ import { isValidElement } from "react";
2
+ //#region src/utils/slots.ts
3
+ function getSlotProp(slot, prop) {
4
+ if (isValidElement(slot) && prop in slot.props) return slot.props[prop];
5
+ }
6
+ const forEachSlot = (children, fn) => {
7
+ Array.from([children].flat()).forEach((child) => {
8
+ if (isValidElement(child) && typeof child.type === "function" && "slot" in child.type) fn(child.type.slot, child);
9
+ });
10
+ };
11
+ function isLazy(type) {
12
+ return typeof type === "object" && type !== null && "_payload" in type && "_init" in type;
13
+ }
14
+ function resolveComponentName(child) {
15
+ if (child && typeof child === "object" && "type" in child) {
16
+ const type = child.type;
17
+ if (isLazy(type)) try {
18
+ const resolved = type._init(type._payload);
19
+ if ("slot" in resolved) return resolved.slot.name;
20
+ return resolved.displayName ?? resolved.name ?? null;
21
+ } catch {
22
+ return null;
23
+ }
24
+ if (typeof type === "function") {
25
+ if ("slot" in type) return type.slot.name;
26
+ return type.displayName ?? type.name ?? null;
27
+ }
28
+ }
29
+ return null;
30
+ }
31
+ function getSlotName(node) {
32
+ const slotName = resolveComponentName(node);
33
+ if (slotName) return slotName.replaceAll(".", "");
34
+ return false;
35
+ }
36
+ //#endregion
37
+ export { forEachSlot, getSlotName, getSlotProp, isLazy, resolveComponentName };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@coopdigital/react",
3
3
  "type": "module",
4
- "version": "1.27.1",
4
+ "version": "1.28.0",
5
5
  "private": false,
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -107,7 +107,7 @@
107
107
  "storybook": "$storybook"
108
108
  },
109
109
  "dependencies": {
110
- "@coopdigital/styles": "^1.18.0",
110
+ "@coopdigital/styles": "^1.19.0",
111
111
  "@radix-ui/react-popover": "^1.1.23",
112
112
  "clsx": "^2.1.1",
113
113
  "date-fns": "^4.4.0",
@@ -116,5 +116,5 @@
116
116
  "merge-refs": "^2.0.0",
117
117
  "react-day-picker": "^10.0.2"
118
118
  },
119
- "gitHead": "9e3d7cf5e1e32c946117d5be84ed2a11d022f2f1"
119
+ "gitHead": "06d636d392fe951b4dc6b7bf30be44afc74414b1"
120
120
  }
@@ -3,7 +3,7 @@ import type { InputHTMLAttributes, JSX, Ref } from "react"
3
3
 
4
4
  import clsx from "clsx"
5
5
 
6
- import { useField } from "@/components/Field/Field.context"
6
+ import { getDescribedBy, getFieldError, useField } from "@/components/Field/Field.context"
7
7
  import { StandardSizes } from "@/types"
8
8
 
9
9
  export interface CheckboxProps extends Omit<
@@ -18,8 +18,8 @@ export interface CheckboxProps extends Omit<
18
18
  error?: boolean
19
19
  /** Specify the component id. Will be auto-generated if not set. */
20
20
  id?: string
21
- /** Specify the component name. */
22
- name: string
21
+ /** Specify the component name. Without a name, the value is not submitted with a form. */
22
+ name?: string
23
23
  /** Specify a custom React ref for this component. */
24
24
  ref?: Ref<HTMLInputElement>
25
25
  /** Specify the component size. */
@@ -27,9 +27,10 @@ export interface CheckboxProps extends Omit<
27
27
  }
28
28
 
29
29
  const Checkbox = ({
30
+ "aria-describedby": ariaDescribedBy,
30
31
  className,
31
32
  disabled,
32
- error = false,
33
+ error,
33
34
  id,
34
35
  name,
35
36
  ref,
@@ -37,10 +38,12 @@ const Checkbox = ({
37
38
  ...props
38
39
  }: CheckboxProps): JSX.Element => {
39
40
  const field = useField()
41
+ const hasError = getFieldError(error, field)
40
42
 
41
43
  const componentProps = {
44
+ "aria-describedby": getDescribedBy(ariaDescribedBy, field),
45
+ "aria-invalid": hasError || undefined,
42
46
  className: clsx("coop-checkbox", className),
43
- "data-error": error || undefined,
44
47
  "data-size": size ?? undefined,
45
48
  disabled,
46
49
  id: id ?? field?.id,
@@ -7,6 +7,7 @@ import { StandardSizes } from "@/types"
7
7
 
8
8
  interface ComboboxContextValue {
9
9
  disabled?: boolean
10
+ error?: boolean
10
11
  items?: ComboboxItem[]
11
12
  props?: {
12
13
  getInputProps: Function
@@ -21,7 +21,7 @@ import {
21
21
  normalizeOptions,
22
22
  } from "@/components/Combobox/combobox-utils"
23
23
  import { ComboboxContext, useComboboxContext } from "@/components/Combobox/Combobox.context"
24
- import { useField } from "@/components/Field/Field.context"
24
+ import { getFieldError, useField } from "@/components/Field/Field.context"
25
25
  import { ChevronDownIcon } from "@/components/icons"
26
26
  import { TextInput, TextInputProps } from "@/components/TextInput"
27
27
  import { useSlots } from "@/hooks"
@@ -156,6 +156,7 @@ export const Combobox = ({
156
156
  }: ComboboxProps): JSX.Element | null => {
157
157
  const field = useField()
158
158
  const uid = id ?? field?.id
159
+ const hasError = getFieldError(error, field)
159
160
 
160
161
  const slots = useSlots(componentSlots, children)
161
162
 
@@ -166,7 +167,7 @@ export const Combobox = ({
166
167
 
167
168
  const componentProps = {
168
169
  className: clsx("coop-combobox", className),
169
- "data-error": error ?? undefined,
170
+ "data-field": "",
170
171
  "data-size": size ?? undefined,
171
172
  id: uid,
172
173
  ...props,
@@ -219,7 +220,9 @@ export const Combobox = ({
219
220
  // }
220
221
 
221
222
  return (
222
- <ComboboxContext.Provider value={{ disabled, items: inputItems, props: comboboxProps, size }}>
223
+ <ComboboxContext.Provider
224
+ value={{ disabled, error: hasError, items: inputItems, props: comboboxProps, size }}
225
+ >
223
226
  <div {...componentProps} ref={ref}>
224
227
  {slots.ComboboxControls}
225
228
  {slots.ComboboxOptions}
@@ -233,6 +236,7 @@ const ComboboxInput = ({ className, placeholder, ...props }: ComboboxInputProps)
233
236
  const componentProps = {
234
237
  className,
235
238
  disabled: combobox.disabled,
239
+ error: combobox.error,
236
240
  placeholder,
237
241
  size: combobox.size,
238
242
  ...props,
@@ -16,7 +16,7 @@ import { DayPicker } from "react-day-picker"
16
16
  import type { StandardSizes } from "@/types"
17
17
 
18
18
  import { Button } from "@/components/Button"
19
- import { useField } from "@/components/Field/Field.context"
19
+ import { getFieldError, useField } from "@/components/Field/Field.context"
20
20
  import { CalendarIcon, CrossIcon, TickIcon } from "@/components/icons"
21
21
  import { Popover } from "@/components/Popover"
22
22
  import { TextInput } from "@/components/TextInput"
@@ -163,7 +163,7 @@ const DatePicker = ({
163
163
  disabled,
164
164
  disabledDates,
165
165
  endDate,
166
- error = false,
166
+ error,
167
167
  id,
168
168
  initialDate = now,
169
169
  initialSelected = false,
@@ -178,6 +178,7 @@ const DatePicker = ({
178
178
  ...props
179
179
  }: DatePickerProps): JSX.Element => {
180
180
  const field = useField()
181
+ const hasError = getFieldError(error, field)
181
182
 
182
183
  initialDate = getInitialDate(mode, initialDate)
183
184
 
@@ -262,7 +263,7 @@ const DatePicker = ({
262
263
  const componentProps = {
263
264
  className: clsx("coop-datepicker", className),
264
265
  "data-disabled": disabled ?? undefined,
265
- "data-error": error || undefined,
266
+ "data-field": "",
266
267
  "data-mode": mode,
267
268
  ...props,
268
269
  }
@@ -315,6 +316,7 @@ const DatePicker = ({
315
316
  autoComplete="off"
316
317
  className="coop-datepicker-input"
317
318
  disabled={disabled}
319
+ error={hasError}
318
320
  id={valueProps.id + "_display"}
319
321
  //maxLength={dateFormat.length}
320
322
  name={name + "_display"}
@@ -1,6 +1,8 @@
1
1
  "use client"
2
2
  import { createContext, useContext } from "react"
3
3
 
4
+ import { joinIds, type MarkerIds } from "@/components/FieldMarkers/fieldmarkers-utils"
5
+
4
6
  export interface FieldConfig {
5
7
  disabled: boolean
6
8
  id: string
@@ -8,8 +10,9 @@ export interface FieldConfig {
8
10
  suffix?: string
9
11
  }
10
12
 
11
- export interface FieldContextValue {
13
+ export interface FieldContextValue extends Partial<MarkerIds> {
12
14
  disabled?: boolean
15
+ error?: boolean
13
16
  id?: string
14
17
  inline?: boolean
15
18
  suffix?: string
@@ -17,12 +20,15 @@ export interface FieldContextValue {
17
20
 
18
21
  export const FieldContext = createContext<FieldContextValue | null>(null)
19
22
 
20
- export const useField = () => {
21
- const context = useContext(FieldContext)
23
+ export const useField = () => useContext(FieldContext)
22
24
 
23
- if (!context) {
24
- return null
25
- }
25
+ // Combine the hint and error ids from the Field with any aria-describedby set on the control
26
+ export const getDescribedBy = (
27
+ describedBy: string | undefined,
28
+ field: FieldContextValue | null
29
+ ): string | undefined => joinIds(field?.describedBy, describedBy)
26
30
 
27
- return context
28
- }
31
+ export const getFieldError = (
32
+ error: boolean | undefined,
33
+ field: FieldContextValue | null
34
+ ): boolean => error ?? field?.error ?? false