@coopdigital/react 1.1.3 → 1.2.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/DatePicker/DatePicker.js +14 -7
- package/dist/components/DatePicker/datepicker-utils.d.ts +1 -1
- package/dist/components/Searchbox/Searchbox.js +1 -0
- package/dist/components/ValidationSummary/ValidationSummary.d.ts +64 -0
- package/dist/components/ValidationSummary/ValidationSummary.js +77 -0
- package/dist/components/ValidationSummary/index.d.ts +4 -0
- package/dist/components/ValidationSummary/validationsummary-utils.d.ts +2 -0
- package/dist/components/ValidationSummary/validationsummary-utils.js +13 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/node_modules/merge-refs/dist/index.js +36 -0
- package/package.json +4 -3
- package/src/components/DatePicker/DatePicker.tsx +23 -8
- package/src/components/DatePicker/datepicker-utils.ts +1 -1
- package/src/components/Searchbox/Searchbox.tsx +1 -0
- package/src/components/ValidationSummary/ValidationSummary.tsx +180 -0
- package/src/components/ValidationSummary/index.ts +5 -0
- package/src/components/ValidationSummary/validationsummary-utils.ts +17 -0
- package/src/index.ts +1 -0
|
@@ -41,7 +41,7 @@ const Calendar = ({ captionLayout = "label", className, disabled, endMonth, form
|
|
|
41
41
|
}, navLayout: "around", showOutsideDays: showOutsideDays, startMonth: startMonth, weekStartsOn: 1, ...props }));
|
|
42
42
|
};
|
|
43
43
|
const DatePicker = ({ className, closeOnSelect = true, dateFormat = "dd/MM/yyyy", disabled, disabledDates, endDate, error = false, id, initialDate = now, initialSelected = false, mode = "single", name, onChange, onSelect, ref, required = false, size = "md", startDate, ...props }) => {
|
|
44
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
|
|
44
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0;
|
|
45
45
|
const uid = useId(id);
|
|
46
46
|
initialDate = getInitialDate(mode, initialDate);
|
|
47
47
|
const startDateProp = startDate;
|
|
@@ -57,8 +57,12 @@ const DatePicker = ({ className, closeOnSelect = true, dateFormat = "dd/MM/yyyy"
|
|
|
57
57
|
initialValues = (_a = getDateValues(mode, initialDate, dateFormat, now)) !== null && _a !== void 0 ? _a : {};
|
|
58
58
|
}
|
|
59
59
|
const [state, setState] = useReducer((stale, fresh) => ({ ...stale, ...fresh }), { ...initialState, [mode]: initialValues.state, open: false, view: initialValues.view });
|
|
60
|
+
const dispatchChanges = useCallback((value) => {
|
|
61
|
+
typeof onChange === "function" && onChange(value);
|
|
62
|
+
typeof onSelect === "function" && onSelect(value);
|
|
63
|
+
}, [onChange, onSelect]);
|
|
60
64
|
const updateValues = useCallback((selected) => {
|
|
61
|
-
var _a;
|
|
65
|
+
var _a, _b, _c;
|
|
62
66
|
const values = getDateValues(mode, selected, dateFormat, now);
|
|
63
67
|
if (values) {
|
|
64
68
|
setState({
|
|
@@ -71,11 +75,13 @@ const DatePicker = ({ className, closeOnSelect = true, dateFormat = "dd/MM/yyyy"
|
|
|
71
75
|
}),
|
|
72
76
|
});
|
|
73
77
|
if (values.validated && values.state) {
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
dispatchChanges(values.state.field);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
dispatchChanges((_c = (_b = values.state) === null || _b === void 0 ? void 0 : _b.input) !== null && _c !== void 0 ? _c : undefined);
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
|
-
}, [dateFormat, mode,
|
|
84
|
+
}, [dateFormat, mode, dispatchChanges, closeOnSelect]);
|
|
79
85
|
const updateFromInput = useCallback((e) => {
|
|
80
86
|
var _a;
|
|
81
87
|
if (shouldSkipParsing(e.currentTarget.value)) {
|
|
@@ -84,11 +90,12 @@ const DatePicker = ({ className, closeOnSelect = true, dateFormat = "dd/MM/yyyy"
|
|
|
84
90
|
[mode]: { input: e.currentTarget.value },
|
|
85
91
|
validated: false,
|
|
86
92
|
});
|
|
93
|
+
dispatchChanges(e.currentTarget.value);
|
|
87
94
|
}
|
|
88
95
|
else {
|
|
89
96
|
updateValues((_a = e.currentTarget) === null || _a === void 0 ? void 0 : _a.value);
|
|
90
97
|
}
|
|
91
|
-
}, [updateValues, mode]);
|
|
98
|
+
}, [updateValues, mode, dispatchChanges]);
|
|
92
99
|
const setOpen = useCallback((open) => setState({ open }), []);
|
|
93
100
|
const setView = useCallback((view) => setState({ view }), []);
|
|
94
101
|
const resetState = useCallback(() => setState({ ...initialState, [mode]: undefined }), [mode]);
|
|
@@ -134,7 +141,7 @@ const DatePicker = ({ className, closeOnSelect = true, dateFormat = "dd/MM/yyyy"
|
|
|
134
141
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
135
142
|
autoFocus: true, captionLayout: "dropdown",
|
|
136
143
|
//defaultMonth={defaultMonth}
|
|
137
|
-
disabled: disabledDates, endMonth: endDate, month: state.view, onMonthChange: setView, onSelect: updateValues, startMonth: startDate, ...calendarProps[mode] }), jsxs("div", { className: "coop-datepicker-actions", children: [jsxs(Button, { "aria-label": "Cancel", className: "bg-tint-grey", onClick: resetState, size: "sm", children: ["Clear ", jsx(CrossIcon, { stroke: "black", strokeWidth: 1 })] }), jsx(Popover.Close, { asChild: true, children: jsxs(Button, { "aria-label": "Accept", size: "sm", children: ["OK ", jsx(TickIcon, { stroke: "white", strokeWidth: 1 })] }) })] })] })] }), jsx("div", { "aria-live": "assertive", className: "sr-only coop-datepicker-status", role: "status", children: (_g = state.message) !== null && _g !== void 0 ? _g : footerMessage[mode] }), mode === "single" && jsx("input", { ...valueProps, value: (_j = (_h = state.single) === null || _h === void 0 ? void 0 : _h.field) !== null && _j !== void 0 ? _j : "" }), mode === "multiple" && jsx("input", { ...valueProps, value: (
|
|
144
|
+
disabled: disabledDates, endMonth: endDate, month: state.view, onMonthChange: setView, onSelect: updateValues, startMonth: startDate, ...calendarProps[mode] }), jsxs("div", { className: "coop-datepicker-actions", children: [jsxs(Button, { "aria-label": "Cancel", className: "bg-tint-grey", onClick: resetState, size: "sm", children: ["Clear ", jsx(CrossIcon, { stroke: "black", strokeWidth: 1 })] }), jsx(Popover.Close, { asChild: true, children: jsxs(Button, { "aria-label": "Accept", size: "sm", children: ["OK ", jsx(TickIcon, { stroke: "white", strokeWidth: 1 })] }) })] })] })] }), jsx("div", { "aria-live": "assertive", className: "sr-only coop-datepicker-status", role: "status", children: (_g = state.message) !== null && _g !== void 0 ? _g : footerMessage[mode] }), mode === "single" && (jsx("input", { ...valueProps, value: (_l = (_j = (_h = state.single) === null || _h === void 0 ? void 0 : _h.field) !== null && _j !== void 0 ? _j : (_k = state.single) === null || _k === void 0 ? void 0 : _k.input) !== null && _l !== void 0 ? _l : "" })), mode === "multiple" && (jsx("input", { ...valueProps, value: (_q = (_o = (_m = state.multiple) === null || _m === void 0 ? void 0 : _m.field) !== null && _o !== void 0 ? _o : (_p = state.multiple) === null || _p === void 0 ? void 0 : _p.input) !== null && _q !== void 0 ? _q : "" })), mode === "range" && (jsxs(Fragment, { children: [jsx("input", { ...baseValueProps, id: `${uid}_start`, name: `${name}_start`, value: (_v = (_t = (_s = (_r = state.range) === null || _r === void 0 ? void 0 : _r.field) === null || _s === void 0 ? void 0 : _s.from) !== null && _t !== void 0 ? _t : (_u = state.range) === null || _u === void 0 ? void 0 : _u.input) !== null && _v !== void 0 ? _v : "" }), jsx("input", { ...baseValueProps, id: `${uid}_end`, name: `${name}_end`, value: (_0 = (_y = (_x = (_w = state.range) === null || _w === void 0 ? void 0 : _w.field) === null || _x === void 0 ? void 0 : _x.to) !== null && _y !== void 0 ? _y : (_z = state.range) === null || _z === void 0 ? void 0 : _z.input) !== null && _0 !== void 0 ? _0 : "" })] }))] }));
|
|
138
145
|
};
|
|
139
146
|
DatePicker.config = componentConfig;
|
|
140
147
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { HTMLAttributes, JSX, ReactNode, Ref } from "react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
export interface ValidationSummaryProps extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
/** **(Optional)** Specify if the summary should be focused and the parent form scrolled into view if there are errors. */
|
|
5
|
+
autoScroll?: boolean;
|
|
6
|
+
/** **(Optional)** This should be a set of `ValidationSummary.Heading`, `ValidationSummary.Prompt` and `ValidationSummary.Errors` components. */
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
/** **(Optional)** Specify additional CSS classes to be applied to the component. */
|
|
9
|
+
className?: string;
|
|
10
|
+
/** **(Optional)** Specify a custom React ref for this component. */
|
|
11
|
+
ref?: Ref<HTMLDivElement>;
|
|
12
|
+
}
|
|
13
|
+
interface ValidationSummaryHeadingProps extends HTMLAttributes<HTMLDivElement> {
|
|
14
|
+
/** **(Optional)** Specify a custom element to use instead of `h2`. */
|
|
15
|
+
as?: "h2" | "h3" | "h4" | "h5" | "h6";
|
|
16
|
+
/** **(Optional)** Content inside the Heading.
|
|
17
|
+
*
|
|
18
|
+
* Defaults to `There's a problem`.
|
|
19
|
+
*/
|
|
20
|
+
children?: React.ReactNode;
|
|
21
|
+
/** **(Optional)** Specify additional CSS classes to be applied to the Heading. */
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
interface ValidationSummaryPromptProps extends HTMLAttributes<HTMLDivElement> {
|
|
25
|
+
/** **(Optional)** Content inside the Prompt.
|
|
26
|
+
*
|
|
27
|
+
* Defaults to `Check the form. You must:`.
|
|
28
|
+
*/
|
|
29
|
+
children?: React.ReactNode;
|
|
30
|
+
}
|
|
31
|
+
interface FieldError {
|
|
32
|
+
message?: string | number;
|
|
33
|
+
ref?: {
|
|
34
|
+
focus?: () => void;
|
|
35
|
+
id?: string;
|
|
36
|
+
};
|
|
37
|
+
type?: string | number;
|
|
38
|
+
}
|
|
39
|
+
interface ValidationSummaryErrorsProps extends HTMLAttributes<HTMLDivElement> {
|
|
40
|
+
/** **(Optional)** Content inside the Errors. */
|
|
41
|
+
errors?: Record<string, FieldError>;
|
|
42
|
+
}
|
|
43
|
+
export declare const ValidationSummary: {
|
|
44
|
+
({ autoScroll, children, className, ref, ...props }: ValidationSummaryProps): JSX.Element | null;
|
|
45
|
+
Heading: {
|
|
46
|
+
({ as, children, className, }: ValidationSummaryHeadingProps): JSX.Element;
|
|
47
|
+
config: {
|
|
48
|
+
name: string;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
Prompt: {
|
|
52
|
+
({ children, }: ValidationSummaryPromptProps): JSX.Element;
|
|
53
|
+
config: {
|
|
54
|
+
name: string;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
Errors: {
|
|
58
|
+
({ errors }: ValidationSummaryErrorsProps): JSX.Element | null;
|
|
59
|
+
config: {
|
|
60
|
+
name: string;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
export default ValidationSummary;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import mergeRefs from '../../node_modules/merge-refs/dist/index.js';
|
|
4
|
+
import React, { useRef, useEffect } from 'react';
|
|
5
|
+
import { useSlots } from '../../hooks/useSlots.js';
|
|
6
|
+
import { sanitizeFieldId, getFieldElement } from './validationsummary-utils.js';
|
|
7
|
+
|
|
8
|
+
/* eslint-disable jsx-a11y/anchor-is-valid */
|
|
9
|
+
const componentSlots = {
|
|
10
|
+
Children: null,
|
|
11
|
+
ValidationSummaryErrors: null,
|
|
12
|
+
ValidationSummaryHeading: null,
|
|
13
|
+
ValidationSummaryPrompt: null,
|
|
14
|
+
};
|
|
15
|
+
const ValidationSummary = ({ autoScroll = true, children, className, ref, ...props }) => {
|
|
16
|
+
var _a;
|
|
17
|
+
const slots = { ...useSlots(componentSlots, children) };
|
|
18
|
+
const errors = React.isValidElement(slots.ValidationSummaryErrors) &&
|
|
19
|
+
Object.keys((_a = slots.ValidationSummaryErrors.props.errors) !== null && _a !== void 0 ? _a : {});
|
|
20
|
+
const hasErrors = !!(Object.keys(errors).length > 0);
|
|
21
|
+
const internalRef = useRef(null);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
var _a, _b;
|
|
24
|
+
if (hasErrors && autoScroll) {
|
|
25
|
+
const form = (_a = internalRef.current) === null || _a === void 0 ? void 0 : _a.closest("form");
|
|
26
|
+
if (typeof (form === null || form === void 0 ? void 0 : form.scrollIntoView) === "function")
|
|
27
|
+
form.scrollIntoView({ behavior: "smooth" });
|
|
28
|
+
(_b = internalRef.current) === null || _b === void 0 ? void 0 : _b.focus();
|
|
29
|
+
}
|
|
30
|
+
}, [autoScroll, errors, hasErrors]);
|
|
31
|
+
if (!hasErrors) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const componentProps = {
|
|
35
|
+
className: clsx("coop-validation-summary ", className),
|
|
36
|
+
...props,
|
|
37
|
+
};
|
|
38
|
+
return (jsxs("div", { ...componentProps, "aria-live": "assertive", ref: mergeRefs(internalRef, ref), role: "alert", tabIndex: -1, children: [slots.ValidationSummaryHeading, slots.ValidationSummaryPrompt, slots.ValidationSummaryErrors, slots.Children] }));
|
|
39
|
+
};
|
|
40
|
+
const ValidationSummaryHeading = ({ as = "h2", children = "There's a problem", className, }) => {
|
|
41
|
+
return React.createElement(as, { className: clsx("coop-validation-summary--heading", className) }, children);
|
|
42
|
+
};
|
|
43
|
+
const ValidationSummaryPrompt = ({ children = "Check the form. You must:", }) => {
|
|
44
|
+
return jsx(Fragment, { children: children });
|
|
45
|
+
};
|
|
46
|
+
const focusable = ["INPUT", "TEXTAREA", "SELECT"];
|
|
47
|
+
const ValidationSummaryErrors = ({ errors }) => {
|
|
48
|
+
var _a;
|
|
49
|
+
const errorsArray = (_a = (errors && Object.entries(errors))) !== null && _a !== void 0 ? _a : [];
|
|
50
|
+
return errorsArray.length ? (jsx("div", { className: "coop-validation-summary--errors", children: jsx("ul", { children: errorsArray.map(([field, error]) => {
|
|
51
|
+
var _a;
|
|
52
|
+
const id = ((_a = error.ref) === null || _a === void 0 ? void 0 : _a.id) ? sanitizeFieldId(error.ref.id) : undefined;
|
|
53
|
+
return (jsx("li", { children: jsx("a", { href: "#", onClick: (e) => {
|
|
54
|
+
var _a;
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
const el = getFieldElement(field, id);
|
|
57
|
+
if (el) {
|
|
58
|
+
if (typeof el.scrollIntoView === "function")
|
|
59
|
+
el.scrollIntoView();
|
|
60
|
+
if (focusable.includes(el.nodeName)) {
|
|
61
|
+
el.focus();
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
(_a = el.querySelector("input")) === null || _a === void 0 ? void 0 : _a.focus();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}, children: error.message }) }, field));
|
|
68
|
+
}) }) })) : null;
|
|
69
|
+
};
|
|
70
|
+
ValidationSummaryHeading.config = { name: "ValidationSummaryHeading" };
|
|
71
|
+
ValidationSummaryPrompt.config = { name: "ValidationSummaryPrompt" };
|
|
72
|
+
ValidationSummaryErrors.config = { name: "ValidationSummaryErrors" };
|
|
73
|
+
ValidationSummary.Heading = ValidationSummaryHeading;
|
|
74
|
+
ValidationSummary.Prompt = ValidationSummaryPrompt;
|
|
75
|
+
ValidationSummary.Errors = ValidationSummaryErrors;
|
|
76
|
+
|
|
77
|
+
export { ValidationSummary, ValidationSummary as default };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const sanitizeFieldId = (id) => ((id === null || id === void 0 ? void 0 : id.startsWith("#")) ? id.slice(1) : id);
|
|
2
|
+
const getFieldElement = (field, id) => {
|
|
3
|
+
var _a, _b, _c;
|
|
4
|
+
if (id) {
|
|
5
|
+
const safeId = sanitizeFieldId(id);
|
|
6
|
+
return ((_a = document.querySelector(`#${safeId}`)) !== null && _a !== void 0 ? _a : document.querySelector(`[name=${safeId}]`));
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
return ((_c = (_b = document.querySelector(`[name=${field}_display]`)) !== null && _b !== void 0 ? _b : document.querySelector(`#${field}`)) !== null && _c !== void 0 ? _c : document.querySelector(`[name=${field}]`));
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export { getFieldElement, sanitizeFieldId };
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -22,3 +22,4 @@ export { Squircle } from './components/Squircle/Squircle.js';
|
|
|
22
22
|
export { Tag } from './components/Tag/Tag.js';
|
|
23
23
|
export { Textarea } from './components/Textarea/Textarea.js';
|
|
24
24
|
export { TextInput } from './components/TextInput/TextInput.js';
|
|
25
|
+
export { ValidationSummary } from './components/ValidationSummary/ValidationSummary.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A function that merges React refs into one.
|
|
3
|
+
* Supports both functions and ref objects created using createRef() and useRef().
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* ```tsx
|
|
7
|
+
* <div ref={mergeRefs(ref1, ref2, ref3)} />
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* @param {(React.Ref<T> | undefined)[]} inputRefs Array of refs
|
|
11
|
+
* @returns {React.Ref<T> | React.RefCallback<T>} Merged refs
|
|
12
|
+
*/
|
|
13
|
+
function mergeRefs() {
|
|
14
|
+
var inputRefs = [];
|
|
15
|
+
for (var _i = 0; _i < arguments.length; _i++) {
|
|
16
|
+
inputRefs[_i] = arguments[_i];
|
|
17
|
+
}
|
|
18
|
+
var filteredInputRefs = inputRefs.filter(Boolean);
|
|
19
|
+
if (filteredInputRefs.length <= 1) {
|
|
20
|
+
var firstRef = filteredInputRefs[0];
|
|
21
|
+
return firstRef || null;
|
|
22
|
+
}
|
|
23
|
+
return function mergedRefs(ref) {
|
|
24
|
+
for (var _i = 0, filteredInputRefs_1 = filteredInputRefs; _i < filteredInputRefs_1.length; _i++) {
|
|
25
|
+
var inputRef = filteredInputRefs_1[_i];
|
|
26
|
+
if (typeof inputRef === 'function') {
|
|
27
|
+
inputRef(ref);
|
|
28
|
+
}
|
|
29
|
+
else if (inputRef) {
|
|
30
|
+
inputRef.current = ref;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { mergeRefs as default };
|
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.2.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -84,10 +84,11 @@
|
|
|
84
84
|
"storybook": "$storybook"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@coopdigital/styles": "^1.
|
|
87
|
+
"@coopdigital/styles": "^1.2.0",
|
|
88
88
|
"@radix-ui/react-popover": "^1.1.15",
|
|
89
89
|
"clsx": "^2.1.1",
|
|
90
|
+
"merge-refs": "^2.0.0",
|
|
90
91
|
"react-day-picker": "^9.12.0"
|
|
91
92
|
},
|
|
92
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "ac480f6925495d9a458a410398dfdd6fcbf289d7"
|
|
93
94
|
}
|
|
@@ -16,6 +16,7 @@ import { DayPicker } from "react-day-picker"
|
|
|
16
16
|
import type { ComponentConfig, StandardSizes } from "../../types"
|
|
17
17
|
import type {
|
|
18
18
|
DateRange,
|
|
19
|
+
DateRangeStr,
|
|
19
20
|
DateType,
|
|
20
21
|
TDateMultiple,
|
|
21
22
|
TDateRange,
|
|
@@ -211,6 +212,14 @@ export const DatePicker = ({
|
|
|
211
212
|
{ ...initialState, [mode]: initialValues.state, open: false, view: initialValues.view }
|
|
212
213
|
)
|
|
213
214
|
|
|
215
|
+
const dispatchChanges = useCallback(
|
|
216
|
+
(value: string | DateRangeStr | undefined) => {
|
|
217
|
+
typeof onChange === "function" && onChange(value)
|
|
218
|
+
typeof onSelect === "function" && onSelect(value)
|
|
219
|
+
},
|
|
220
|
+
[onChange, onSelect]
|
|
221
|
+
)
|
|
222
|
+
|
|
214
223
|
const updateValues = useCallback(
|
|
215
224
|
(selected: DateType) => {
|
|
216
225
|
const values = getDateValues(mode, selected, dateFormat, now)
|
|
@@ -227,12 +236,13 @@ export const DatePicker = ({
|
|
|
227
236
|
})
|
|
228
237
|
|
|
229
238
|
if (values.validated && values.state) {
|
|
230
|
-
|
|
231
|
-
|
|
239
|
+
dispatchChanges(values.state.field)
|
|
240
|
+
} else {
|
|
241
|
+
dispatchChanges(values.state?.input ?? undefined)
|
|
232
242
|
}
|
|
233
243
|
}
|
|
234
244
|
},
|
|
235
|
-
[dateFormat, mode,
|
|
245
|
+
[dateFormat, mode, dispatchChanges, closeOnSelect]
|
|
236
246
|
)
|
|
237
247
|
|
|
238
248
|
const updateFromInput = useCallback(
|
|
@@ -243,11 +253,12 @@ export const DatePicker = ({
|
|
|
243
253
|
[mode]: { input: e.currentTarget.value },
|
|
244
254
|
validated: false,
|
|
245
255
|
})
|
|
256
|
+
dispatchChanges(e.currentTarget.value)
|
|
246
257
|
} else {
|
|
247
258
|
updateValues(e.currentTarget?.value)
|
|
248
259
|
}
|
|
249
260
|
},
|
|
250
|
-
[updateValues, mode]
|
|
261
|
+
[updateValues, mode, dispatchChanges]
|
|
251
262
|
)
|
|
252
263
|
|
|
253
264
|
const setOpen = useCallback((open: boolean) => setState({ open }), [])
|
|
@@ -360,21 +371,25 @@ export const DatePicker = ({
|
|
|
360
371
|
{state.message ?? footerMessage[mode]}
|
|
361
372
|
</div>
|
|
362
373
|
|
|
363
|
-
{mode === "single" &&
|
|
364
|
-
|
|
374
|
+
{mode === "single" && (
|
|
375
|
+
<input {...valueProps} value={state.single?.field ?? state.single?.input ?? ""} />
|
|
376
|
+
)}
|
|
377
|
+
{mode === "multiple" && (
|
|
378
|
+
<input {...valueProps} value={state.multiple?.field ?? state.multiple?.input ?? ""} />
|
|
379
|
+
)}
|
|
365
380
|
{mode === "range" && (
|
|
366
381
|
<>
|
|
367
382
|
<input
|
|
368
383
|
{...baseValueProps}
|
|
369
384
|
id={`${uid}_start`}
|
|
370
385
|
name={`${name}_start`}
|
|
371
|
-
value={state.range?.field?.from ?? ""}
|
|
386
|
+
value={state.range?.field?.from ?? state.range?.input ?? ""}
|
|
372
387
|
/>
|
|
373
388
|
<input
|
|
374
389
|
{...baseValueProps}
|
|
375
390
|
id={`${uid}_end`}
|
|
376
391
|
name={`${name}_end`}
|
|
377
|
-
value={state.range?.field?.to ?? ""}
|
|
392
|
+
value={state.range?.field?.to ?? state.range?.input ?? ""}
|
|
378
393
|
/>
|
|
379
394
|
</>
|
|
380
395
|
)}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/* eslint-disable jsx-a11y/anchor-is-valid */
|
|
2
|
+
"use client"
|
|
3
|
+
|
|
4
|
+
import type { HTMLAttributes, JSX, ReactNode, Ref } from "react"
|
|
5
|
+
|
|
6
|
+
import clsx from "clsx"
|
|
7
|
+
import mergeRefs from "merge-refs"
|
|
8
|
+
import React, { useEffect, useRef } from "react"
|
|
9
|
+
|
|
10
|
+
import { useSlots } from "../../hooks/useSlots"
|
|
11
|
+
import { getFieldElement, sanitizeFieldId } from "./validationsummary-utils"
|
|
12
|
+
|
|
13
|
+
export interface ValidationSummaryProps extends HTMLAttributes<HTMLDivElement> {
|
|
14
|
+
/** **(Optional)** Specify if the summary should be focused and the parent form scrolled into view if there are errors. */
|
|
15
|
+
autoScroll?: boolean
|
|
16
|
+
/** **(Optional)** This should be a set of `ValidationSummary.Heading`, `ValidationSummary.Prompt` and `ValidationSummary.Errors` components. */
|
|
17
|
+
children?: ReactNode
|
|
18
|
+
/** **(Optional)** Specify additional CSS classes to be applied to the component. */
|
|
19
|
+
className?: string
|
|
20
|
+
/** **(Optional)** Specify a custom React ref for this component. */
|
|
21
|
+
ref?: Ref<HTMLDivElement>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ValidationSummaryHeadingProps extends HTMLAttributes<HTMLDivElement> {
|
|
25
|
+
/** **(Optional)** Specify a custom element to use instead of `h2`. */
|
|
26
|
+
as?: "h2" | "h3" | "h4" | "h5" | "h6"
|
|
27
|
+
/** **(Optional)** Content inside the Heading.
|
|
28
|
+
*
|
|
29
|
+
* Defaults to `There's a problem`.
|
|
30
|
+
*/
|
|
31
|
+
children?: React.ReactNode
|
|
32
|
+
/** **(Optional)** Specify additional CSS classes to be applied to the Heading. */
|
|
33
|
+
className?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ValidationSummaryPromptProps extends HTMLAttributes<HTMLDivElement> {
|
|
37
|
+
/** **(Optional)** Content inside the Prompt.
|
|
38
|
+
*
|
|
39
|
+
* Defaults to `Check the form. You must:`.
|
|
40
|
+
*/
|
|
41
|
+
children?: React.ReactNode
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface FieldError {
|
|
45
|
+
message?: string | number
|
|
46
|
+
ref?: {
|
|
47
|
+
focus?: () => void
|
|
48
|
+
id?: string
|
|
49
|
+
}
|
|
50
|
+
type?: string | number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ValidationSummaryErrorsProps extends HTMLAttributes<HTMLDivElement> {
|
|
54
|
+
/** **(Optional)** Content inside the Errors. */
|
|
55
|
+
errors?: Record<string, FieldError>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const componentSlots = {
|
|
59
|
+
Children: null,
|
|
60
|
+
ValidationSummaryErrors: null,
|
|
61
|
+
ValidationSummaryHeading: null,
|
|
62
|
+
ValidationSummaryPrompt: null,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const ValidationSummary = ({
|
|
66
|
+
autoScroll = true,
|
|
67
|
+
children,
|
|
68
|
+
className,
|
|
69
|
+
ref,
|
|
70
|
+
...props
|
|
71
|
+
}: ValidationSummaryProps): JSX.Element | null => {
|
|
72
|
+
const slots = { ...useSlots(componentSlots, children) }
|
|
73
|
+
|
|
74
|
+
const errors =
|
|
75
|
+
React.isValidElement(slots.ValidationSummaryErrors) &&
|
|
76
|
+
Object.keys((slots.ValidationSummaryErrors.props as ValidationSummaryErrorsProps).errors ?? {})
|
|
77
|
+
|
|
78
|
+
const hasErrors = !!(Object.keys(errors).length > 0)
|
|
79
|
+
|
|
80
|
+
const internalRef = useRef<HTMLDivElement>(null)
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (hasErrors && autoScroll) {
|
|
84
|
+
const form = internalRef.current?.closest("form")
|
|
85
|
+
if (typeof form?.scrollIntoView === "function") form.scrollIntoView({ behavior: "smooth" })
|
|
86
|
+
internalRef.current?.focus()
|
|
87
|
+
}
|
|
88
|
+
}, [autoScroll, errors, hasErrors])
|
|
89
|
+
|
|
90
|
+
if (!hasErrors) {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const componentProps = {
|
|
95
|
+
className: clsx("coop-validation-summary ", className),
|
|
96
|
+
...props,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div
|
|
101
|
+
{...componentProps}
|
|
102
|
+
aria-live="assertive"
|
|
103
|
+
ref={mergeRefs(internalRef, ref)}
|
|
104
|
+
role="alert"
|
|
105
|
+
tabIndex={-1}
|
|
106
|
+
>
|
|
107
|
+
{slots.ValidationSummaryHeading}
|
|
108
|
+
{slots.ValidationSummaryPrompt}
|
|
109
|
+
{slots.ValidationSummaryErrors}
|
|
110
|
+
{slots.Children}
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const ValidationSummaryHeading = ({
|
|
116
|
+
as = "h2",
|
|
117
|
+
children = "There's a problem",
|
|
118
|
+
className,
|
|
119
|
+
}: ValidationSummaryHeadingProps): JSX.Element => {
|
|
120
|
+
return React.createElement(
|
|
121
|
+
as,
|
|
122
|
+
{ className: clsx("coop-validation-summary--heading", className) },
|
|
123
|
+
children
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const ValidationSummaryPrompt = ({
|
|
128
|
+
children = "Check the form. You must:",
|
|
129
|
+
}: ValidationSummaryPromptProps): JSX.Element => {
|
|
130
|
+
return <>{children}</>
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const focusable = ["INPUT", "TEXTAREA", "SELECT"]
|
|
134
|
+
|
|
135
|
+
const ValidationSummaryErrors = ({ errors }: ValidationSummaryErrorsProps): JSX.Element | null => {
|
|
136
|
+
const errorsArray = (errors && Object.entries(errors)) ?? []
|
|
137
|
+
|
|
138
|
+
return errorsArray.length ? (
|
|
139
|
+
<div className="coop-validation-summary--errors">
|
|
140
|
+
<ul>
|
|
141
|
+
{errorsArray.map(([field, error]) => {
|
|
142
|
+
const id: string | undefined = error.ref?.id ? sanitizeFieldId(error.ref.id) : undefined
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<li key={field}>
|
|
146
|
+
<a
|
|
147
|
+
href="#"
|
|
148
|
+
onClick={(e) => {
|
|
149
|
+
e.preventDefault()
|
|
150
|
+
const el = getFieldElement(field, id)
|
|
151
|
+
if (el) {
|
|
152
|
+
if (typeof el.scrollIntoView === "function") el.scrollIntoView()
|
|
153
|
+
|
|
154
|
+
if (focusable.includes(el.nodeName)) {
|
|
155
|
+
el.focus()
|
|
156
|
+
} else {
|
|
157
|
+
el.querySelector("input")?.focus()
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
{error.message}
|
|
163
|
+
</a>
|
|
164
|
+
</li>
|
|
165
|
+
)
|
|
166
|
+
})}
|
|
167
|
+
</ul>
|
|
168
|
+
</div>
|
|
169
|
+
) : null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
ValidationSummaryHeading.config = { name: "ValidationSummaryHeading" }
|
|
173
|
+
ValidationSummaryPrompt.config = { name: "ValidationSummaryPrompt" }
|
|
174
|
+
ValidationSummaryErrors.config = { name: "ValidationSummaryErrors" }
|
|
175
|
+
|
|
176
|
+
ValidationSummary.Heading = ValidationSummaryHeading
|
|
177
|
+
ValidationSummary.Prompt = ValidationSummaryPrompt
|
|
178
|
+
ValidationSummary.Errors = ValidationSummaryErrors
|
|
179
|
+
|
|
180
|
+
export default ValidationSummary
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const sanitizeFieldId = (id: string): string => (id?.startsWith("#") ? id.slice(1) : id)
|
|
2
|
+
|
|
3
|
+
export const getFieldElement = (field: string, id?: string) => {
|
|
4
|
+
if (id) {
|
|
5
|
+
const safeId = sanitizeFieldId(id)
|
|
6
|
+
return (
|
|
7
|
+
document.querySelector<HTMLInputElement>(`#${safeId}`) ??
|
|
8
|
+
document.querySelector<HTMLInputElement>(`[name=${safeId}]`)
|
|
9
|
+
)
|
|
10
|
+
} else {
|
|
11
|
+
return (
|
|
12
|
+
document.querySelector<HTMLInputElement>(`[name=${field}_display]`) ??
|
|
13
|
+
document.querySelector<HTMLInputElement>(`#${field}`) ??
|
|
14
|
+
document.querySelector<HTMLInputElement>(`[name=${field}]`)
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/index.ts
CHANGED