@coopdigital/react 1.25.1 → 1.26.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.
@@ -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
  }
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.0",
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": "3835a0b5cd0562f01905240d8df20d4b042323db"
120
120
  }
@@ -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()