@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
|
|
17
|
-
const
|
|
18
|
-
const
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
30
|
+
internalRef?.current?.focus({ preventScroll: true });
|
|
25
31
|
}
|
|
32
|
+
return () => form?.removeEventListener("submit", onSubmit);
|
|
26
33
|
}, [
|
|
27
34
|
autoScroll,
|
|
28
|
-
|
|
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.
|
|
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": "
|
|
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
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
|
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()
|