@coopdigital/react 1.25.1 → 1.26.1

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.
@@ -12,7 +12,7 @@ const Author = ({ author = "Co-op team", className, date, datePrefix = "", image
12
12
  };
13
13
  const componentProps = {
14
14
  className: clsx("coop-author", className),
15
- "data-size": size?.length ? size : void 0,
15
+ "data-size": size ?? void 0,
16
16
  ...props
17
17
  };
18
18
  return /* @__PURE__ */ jsxs("div", {
@@ -1,3 +1,4 @@
1
+ import { StandardSizes } from "../../types/index.js";
1
2
  import React, { ButtonHTMLAttributes, ForwardRefExoticComponent, JSX, Ref } from "react";
2
3
  //#region src/components/Button/Button.d.ts
3
4
  export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
@@ -25,7 +26,7 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
25
26
  /** Specify a custom React ref for this component. */
26
27
  ref?: Ref<HTMLElement>;
27
28
  /** Specify the component size. */
28
- size?: "sm" | "md" | "lg";
29
+ size?: StandardSizes;
29
30
  /** Specify the component variant. */
30
31
  variant?: "solid" | "ghost" | "text";
31
32
  }
@@ -1,3 +1,4 @@
1
+ import { StandardSizes } from "../../types/index.js";
1
2
  import { ComboboxItem, ComboboxOption } from "./combobox-utils.js";
2
3
  import { TextInputProps } from "../TextInput/TextInput.js";
3
4
  import "../TextInput/index.js";
@@ -75,7 +76,7 @@ export interface ComboboxProps extends Omit<HTMLAttributes<HTMLDivElement>, "onC
75
76
  /** Specify a custom React ref for this component. */
76
77
  ref?: Ref<HTMLDivElement>;
77
78
  /** Specify the component size. */
78
- size?: "sm" | "md" | "lg";
79
+ size?: StandardSizes;
79
80
  }
80
81
  export interface ComboboxControlsProps extends HTMLAttributes<HTMLDivElement> {
81
82
  /** Content inside the component.
@@ -13,7 +13,7 @@ const Pill = ({ as, children, className, href, ref, size, ...props }) => {
13
13
  const slots = useSlots(componentSlots, children);
14
14
  const componentProps = {
15
15
  className: clsx("coop-pill", !hasUserBg(className) && "bg-tint-grey", className),
16
- "data-size": size?.length ? size : void 0,
16
+ "data-size": size ?? void 0,
17
17
  href,
18
18
  ...props
19
19
  };
@@ -6,11 +6,11 @@ export interface SectionProps extends HTMLAttributes<HTMLDivElement> {
6
6
  * Default: `<section>`
7
7
  */
8
8
  as?: React.FC<any> | string;
9
- /** **(Optional)** Content inside the component. */
9
+ /** Content inside the component. */
10
10
  children?: ReactNode;
11
- /** **(Optional)** Specify additional CSS classes. */
11
+ /** Specify additional CSS classes. */
12
12
  className?: string;
13
- /** **(Optional)** Specify a custom React ref for this component. */
13
+ /** Specify a custom React ref for this component. */
14
14
  ref?: Ref<HTMLDivElement>;
15
15
  }
16
16
  export declare const Section: ({ as, children, className, ref, ...props }: SectionProps) => JSX.Element;
@@ -6,7 +6,7 @@ const Tag = ({ as, children, className, href, ref, size, ...props }) => {
6
6
  const element = as ?? (href ? "a" : "span");
7
7
  const componentProps = {
8
8
  className: clsx("coop-tag", !hasUserBg(className) && "bg-tint-grey", className),
9
- "data-size": size?.length ? size : void 0,
9
+ "data-size": size ?? void 0,
10
10
  href,
11
11
  ...props
12
12
  };
@@ -9,7 +9,7 @@ const TextInput = ({ className, disabled, error = false, fit = false, id, name,
9
9
  className: clsx("coop-text-input", className),
10
10
  "data-error": error || void 0,
11
11
  "data-fit": fit || void 0,
12
- "data-size": size?.length ? size : void 0,
12
+ "data-size": size ?? void 0,
13
13
  disabled,
14
14
  id: id ?? field?.id,
15
15
  name,
@@ -1,4 +1,4 @@
1
- import React, { HTMLAttributes, JSX, ReactNode, Ref } from "react";
1
+ import React, { HTMLAttributes, JSX, ReactNode, Ref, RefObject } from "react";
2
2
  //#region src/components/ValidationSummary/ValidationSummary.d.ts
3
3
  export interface ValidationSummaryProps extends HTMLAttributes<HTMLDivElement> {
4
4
  /** Specify if the summary should be focused and the parent form scrolled into view if there are errors.
@@ -17,6 +17,11 @@ export interface ValidationSummaryProps extends HTMLAttributes<HTMLDivElement> {
17
17
  * Default: `false`
18
18
  */
19
19
  forceMount?: boolean;
20
+ /** Specify a React ref for the form this summary is associated with.
21
+ *
22
+ * You don't need to set this if you render the summary inside a form.
23
+ */
24
+ formRef?: RefObject<HTMLFormElement | null>;
20
25
  /** Specify a custom React ref for this component. */
21
26
  ref?: Ref<HTMLDivElement>;
22
27
  }
@@ -54,7 +59,7 @@ interface ValidationSummaryErrorsProps extends HTMLAttributes<HTMLDivElement> {
54
59
  errors?: Record<string, FieldError>;
55
60
  }
56
61
  declare const ValidationSummary: {
57
- ({ autoScroll, children, className, forceMount, ref, ...props }: ValidationSummaryProps): JSX.Element | null;
62
+ ({ autoScroll, children, className, forceMount, formRef, ref, ...props }: ValidationSummaryProps): JSX.Element | null;
58
63
  slot: {
59
64
  name: string;
60
65
  };
@@ -1,9 +1,10 @@
1
1
  "use client";
2
+ import { getSlotProp } from "../../utils/index.js";
2
3
  import { useSlots } from "../../hooks/useSlots.js";
3
4
  import { getFieldElement, sanitizeFieldId } from "./validationsummary-utils.js";
4
5
  import clsx from "clsx";
5
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
- import React, { useEffect, useRef } from "react";
7
+ import React, { useCallback, useEffect, useRef, useState } from "react";
7
8
  import mergeRefs from "merge-refs";
8
9
  //#region src/components/ValidationSummary/ValidationSummary.tsx
9
10
  const componentSlots = {
@@ -12,27 +13,39 @@ const componentSlots = {
12
13
  ValidationSummaryHeading: null,
13
14
  ValidationSummaryPrompt: null
14
15
  };
15
- const ValidationSummary = ({ autoScroll = true, children, className, forceMount = false, ref, ...props }) => {
16
- const slots = { ...useSlots(componentSlots, children) };
17
- const errors = React.isValidElement(slots.ValidationSummaryErrors) && Object.keys(slots.ValidationSummaryErrors.props.errors ?? {});
18
- const hasErrors = Object.keys(errors).length > 0;
16
+ const ValidationSummary = ({ autoScroll = true, children, className, forceMount = false, formRef, ref, ...props }) => {
17
+ const [stamp, setStamp] = useState(0);
18
+ const slots = useSlots(componentSlots, children);
19
+ const errors = Object.keys(getSlotProp(slots.ValidationSummaryErrors, "errors") ?? {});
20
+ const hasErrors = errors && errors.length > 0;
19
21
  const internalRef = useRef(null);
22
+ const onSubmit = useCallback(() => {
23
+ setStamp(Date.now());
24
+ }, []);
20
25
  useEffect(() => {
21
- if (hasErrors && autoScroll) {
22
- const form = internalRef.current?.closest("form");
26
+ const form = formRef?.current ?? internalRef.current?.closest("form");
27
+ form?.addEventListener("submit", onSubmit);
28
+ if (form && hasErrors && autoScroll) {
23
29
  if (typeof form?.scrollIntoView === "function") form.scrollIntoView({ behavior: "smooth" });
24
- internalRef.current?.focus();
30
+ internalRef?.current?.focus({ preventScroll: true });
25
31
  }
32
+ return () => form?.removeEventListener("submit", onSubmit);
26
33
  }, [
27
34
  autoScroll,
28
- errors,
29
- hasErrors
35
+ formRef,
36
+ hasErrors,
37
+ stamp,
38
+ onSubmit
30
39
  ]);
31
- if (!hasErrors && !forceMount) return null;
32
40
  const componentProps = {
33
41
  className: clsx("coop-validation-summary", className),
34
42
  ...props
35
43
  };
44
+ if (!hasErrors && !forceMount) return /* @__PURE__ */ jsx("div", {
45
+ hidden: true,
46
+ inert: true,
47
+ ref: mergeRefs(internalRef, ref)
48
+ });
36
49
  return /* @__PURE__ */ jsxs("div", {
37
50
  ...componentProps,
38
51
  "aria-live": "assertive",
@@ -70,7 +83,6 @@ const ValidationSummaryErrors = ({ errors }) => {
70
83
  e.preventDefault();
71
84
  const el = getFieldElement(field, id);
72
85
  if (el) {
73
- if (typeof el.scrollIntoView === "function") el.scrollIntoView();
74
86
  if (focusable.includes(el.nodeName)) el.focus();
75
87
  else el.querySelector("input")?.focus();
76
88
  }
@@ -6,11 +6,11 @@ export interface WrapperProps extends HTMLAttributes<HTMLDivElement> {
6
6
  * Default: `<div>`
7
7
  */
8
8
  as?: React.FC<any> | string;
9
- /** **(Optional)** Content inside the component. */
9
+ /** Content inside the component. */
10
10
  children?: ReactNode;
11
- /** **(Optional)** Specify additional CSS classes. */
11
+ /** Specify additional CSS classes. */
12
12
  className?: string;
13
- /** **(Optional)** Specify a custom React ref for this component. */
13
+ /** Specify a custom React ref for this component. */
14
14
  ref?: Ref<HTMLDivElement>;
15
15
  }
16
16
  export declare const Wrapper: ({ as, children, className, ref, ...props }: WrapperProps) => JSX.Element;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@coopdigital/react",
3
3
  "type": "module",
4
- "version": "1.25.1",
4
+ "version": "1.26.1",
5
5
  "private": false,
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -116,5 +116,5 @@
116
116
  "merge-refs": "^2.0.0",
117
117
  "react-day-picker": "^10.0.1"
118
118
  },
119
- "gitHead": "cd6a31d17eff8e2b96dfba2eee5363c0d96aa80c"
119
+ "gitHead": "d973a1436fe2245a6af205621b69762c3d7f7d09"
120
120
  }
@@ -43,7 +43,7 @@ const Author = ({
43
43
 
44
44
  const componentProps = {
45
45
  className: clsx("coop-author", className),
46
- "data-size": size?.length ? size : undefined,
46
+ "data-size": size ?? undefined,
47
47
  ...props,
48
48
  }
49
49
  return (
@@ -6,6 +6,7 @@ import clsx from "clsx"
6
6
  import React, { useCallback, useState } from "react"
7
7
 
8
8
  import { LoadingSpinner } from "@/components/LoadingSpinner"
9
+ import { StandardSizes } from "@/types"
9
10
  import { hasUserBg } from "@/utils"
10
11
 
11
12
  export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
@@ -34,7 +35,7 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
34
35
  /** Specify a custom React ref for this component. */
35
36
  ref?: Ref<HTMLElement>
36
37
  /** Specify the component size. */
37
- size?: "sm" | "md" | "lg"
38
+ size?: StandardSizes
38
39
  /** Specify the component variant. */
39
40
  variant?: "solid" | "ghost" | "text"
40
41
  }
@@ -25,6 +25,7 @@ import { 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"
28
+ import { StandardSizes } from "@/types"
28
29
  import { getSlotProp } from "@/utils"
29
30
 
30
31
  export interface ComboboxProps extends Omit<
@@ -101,7 +102,7 @@ export interface ComboboxProps extends Omit<
101
102
  /** Specify a custom React ref for this component. */
102
103
  ref?: Ref<HTMLDivElement>
103
104
  /** Specify the component size. */
104
- size?: "sm" | "md" | "lg"
105
+ size?: StandardSizes
105
106
  }
106
107
 
107
108
  export interface ComboboxControlsProps extends HTMLAttributes<HTMLDivElement> {
@@ -47,7 +47,7 @@ const Pill = ({ as, children, className, href, ref, size, ...props }: PillProps)
47
47
 
48
48
  const componentProps = {
49
49
  className: clsx("coop-pill", !hasUserBg(className) && "bg-tint-grey", className),
50
- "data-size": size?.length ? size : undefined,
50
+ "data-size": size ?? undefined,
51
51
  href,
52
52
  ...props,
53
53
  }
@@ -10,11 +10,11 @@ export interface SectionProps extends HTMLAttributes<HTMLDivElement> {
10
10
  */
11
11
  // oxlint-disable-next-line typescript/no-explicit-any
12
12
  as?: React.FC<any> | string
13
- /** **(Optional)** Content inside the component. */
13
+ /** Content inside the component. */
14
14
  children?: ReactNode
15
- /** **(Optional)** Specify additional CSS classes. */
15
+ /** Specify additional CSS classes. */
16
16
  className?: string
17
- /** **(Optional)** Specify a custom React ref for this component. */
17
+ /** Specify a custom React ref for this component. */
18
18
  ref?: Ref<HTMLDivElement>
19
19
  }
20
20
 
@@ -29,7 +29,7 @@ const Tag = ({ as, children, className, href, ref, size, ...props }: TagProps):
29
29
 
30
30
  const componentProps = {
31
31
  className: clsx("coop-tag", !hasUserBg(className) && "bg-tint-grey", className),
32
- "data-size": size?.length ? size : undefined,
32
+ "data-size": size ?? undefined,
33
33
  href,
34
34
  ...props,
35
35
  }
@@ -80,7 +80,7 @@ const TextInput = ({
80
80
  className: clsx("coop-text-input", className),
81
81
  "data-error": error || undefined,
82
82
  "data-fit": fit || undefined,
83
- "data-size": size?.length ? size : undefined,
83
+ "data-size": size ?? undefined,
84
84
  disabled,
85
85
  id: id ?? field?.id,
86
86
  name,
@@ -1,17 +1,18 @@
1
1
  "use client"
2
2
  /* oxlint-disable jsx-a11y/anchor-is-valid */
3
3
 
4
- import type { HTMLAttributes, JSX, ReactNode, Ref } from "react"
4
+ import type { HTMLAttributes, JSX, ReactNode, Ref, RefObject } from "react"
5
5
 
6
6
  import clsx from "clsx"
7
7
  import mergeRefs from "merge-refs"
8
- import React, { useEffect, useRef } from "react"
8
+ import React, { useCallback, useEffect, useRef, useState } from "react"
9
9
 
10
10
  import {
11
11
  getFieldElement,
12
12
  sanitizeFieldId,
13
13
  } from "@/components/ValidationSummary/validationsummary-utils"
14
14
  import { useSlots } from "@/hooks/useSlots"
15
+ import { getSlotProp } from "@/utils"
15
16
 
16
17
  export interface ValidationSummaryProps extends HTMLAttributes<HTMLDivElement> {
17
18
  /** Specify if the summary should be focused and the parent form scrolled into view if there are errors.
@@ -30,6 +31,11 @@ export interface ValidationSummaryProps extends HTMLAttributes<HTMLDivElement> {
30
31
  * Default: `false`
31
32
  */
32
33
  forceMount?: boolean
34
+ /** Specify a React ref for the form this summary is associated with.
35
+ *
36
+ * You don't need to set this if you render the summary inside a form.
37
+ */
38
+ formRef?: RefObject<HTMLFormElement | null>
33
39
  /** Specify a custom React ref for this component. */
34
40
  ref?: Ref<HTMLDivElement>
35
41
  }
@@ -83,36 +89,43 @@ const ValidationSummary = ({
83
89
  children,
84
90
  className,
85
91
  forceMount = false,
92
+ formRef,
86
93
  ref,
87
94
  ...props
88
95
  }: ValidationSummaryProps): JSX.Element | null => {
89
- const slots = { ...useSlots(componentSlots, children) }
90
-
91
- const errors =
92
- React.isValidElement(slots.ValidationSummaryErrors) &&
93
- Object.keys((slots.ValidationSummaryErrors.props as ValidationSummaryErrorsProps).errors ?? {})
96
+ const [stamp, setStamp] = useState(0)
97
+ const slots = useSlots(componentSlots, children)
94
98
 
95
- const hasErrors = Object.keys(errors).length > 0
99
+ const errors = Object.keys(getSlotProp<FieldError>(slots.ValidationSummaryErrors, "errors") ?? {})
96
100
 
101
+ const hasErrors = errors && errors.length > 0
97
102
  const internalRef = useRef<HTMLDivElement>(null)
98
103
 
104
+ const onSubmit = useCallback(() => {
105
+ setStamp(Date.now())
106
+ }, [])
107
+
99
108
  useEffect(() => {
100
- if (hasErrors && autoScroll) {
101
- const form = internalRef.current?.closest("form")
102
- if (typeof form?.scrollIntoView === "function") form.scrollIntoView({ behavior: "smooth" })
103
- internalRef.current?.focus()
109
+ const form = formRef?.current ?? internalRef.current?.closest("form")
110
+ form?.addEventListener("submit", onSubmit)
111
+ if (form && hasErrors && autoScroll) {
112
+ if (typeof form?.scrollIntoView === "function") {
113
+ form.scrollIntoView({ behavior: "smooth" })
114
+ }
115
+ internalRef?.current?.focus({ preventScroll: true })
104
116
  }
105
- }, [autoScroll, errors, hasErrors])
106
-
107
- if (!hasErrors && !forceMount) {
108
- return null
109
- }
117
+ return () => form?.removeEventListener("submit", onSubmit)
118
+ }, [autoScroll, formRef, hasErrors, stamp, onSubmit])
110
119
 
111
120
  const componentProps = {
112
121
  className: clsx("coop-validation-summary", className),
113
122
  ...props,
114
123
  }
115
124
 
125
+ if (!hasErrors && !forceMount) {
126
+ return <div hidden inert ref={mergeRefs(internalRef, ref)} />
127
+ }
128
+
116
129
  return (
117
130
  <div
118
131
  {...componentProps}
@@ -154,7 +167,7 @@ const ValidationSummaryErrors = ({ errors }: ValidationSummaryErrorsProps): JSX.
154
167
  <div className="coop-validation-summary--errors">
155
168
  <ul>
156
169
  {errorsArray.map(([field, error]) => {
157
- const id: string | undefined = error.ref?.id ? sanitizeFieldId(error.ref.id) : undefined
170
+ const id = error.ref?.id ? sanitizeFieldId(error.ref.id) : undefined
158
171
 
159
172
  return (
160
173
  <li key={field}>
@@ -164,7 +177,7 @@ const ValidationSummaryErrors = ({ errors }: ValidationSummaryErrorsProps): JSX.
164
177
  e.preventDefault()
165
178
  const el = getFieldElement(field, id)
166
179
  if (el) {
167
- if (typeof el.scrollIntoView === "function") el.scrollIntoView()
180
+ // if (typeof el.scrollIntoView === "function") el.scrollIntoView()
168
181
 
169
182
  if (focusable.includes(el.nodeName)) {
170
183
  el.focus()
@@ -10,11 +10,11 @@ export interface WrapperProps extends HTMLAttributes<HTMLDivElement> {
10
10
  */
11
11
  // oxlint-disable-next-line typescript/no-explicit-any
12
12
  as?: React.FC<any> | string
13
- /** **(Optional)** Content inside the component. */
13
+ /** Content inside the component. */
14
14
  children?: ReactNode
15
- /** **(Optional)** Specify additional CSS classes. */
15
+ /** Specify additional CSS classes. */
16
16
  className?: string
17
- /** **(Optional)** Specify a custom React ref for this component. */
17
+ /** Specify a custom React ref for this component. */
18
18
  ref?: Ref<HTMLDivElement>
19
19
  }
20
20