@coopdigital/react 1.1.2 → 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 +18 -9
- package/dist/components/DatePicker/datepicker-utils.d.ts +1 -1
- package/dist/components/Pill/Pill.js +0 -2
- package/dist/components/Searchbox/Searchbox.js +2 -1
- 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 +31 -10
- package/src/components/DatePicker/datepicker-utils.ts +1 -1
- package/src/components/Pill/Pill.tsx +0 -2
- package/src/components/Searchbox/Searchbox.tsx +2 -1
- 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,11 +41,13 @@ 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
|
-
|
|
48
|
-
|
|
47
|
+
const startDateProp = startDate;
|
|
48
|
+
const endDateProp = endDate;
|
|
49
|
+
startDate = useMemo(() => startDateProp !== null && startDateProp !== void 0 ? startDateProp : getStartDate(initialDate), [initialDate, startDateProp]);
|
|
50
|
+
endDate = useMemo(() => endDateProp !== null && endDateProp !== void 0 ? endDateProp : getEndDate(initialDate), [initialDate, endDateProp]);
|
|
49
51
|
const defaultMonth = useMemo(() => getDefaultMonth(initialDate), [initialDate]);
|
|
50
52
|
let initialValues = {
|
|
51
53
|
state: undefined,
|
|
@@ -55,8 +57,12 @@ const DatePicker = ({ className, closeOnSelect = true, dateFormat = "dd/MM/yyyy"
|
|
|
55
57
|
initialValues = (_a = getDateValues(mode, initialDate, dateFormat, now)) !== null && _a !== void 0 ? _a : {};
|
|
56
58
|
}
|
|
57
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]);
|
|
58
64
|
const updateValues = useCallback((selected) => {
|
|
59
|
-
var _a;
|
|
65
|
+
var _a, _b, _c;
|
|
60
66
|
const values = getDateValues(mode, selected, dateFormat, now);
|
|
61
67
|
if (values) {
|
|
62
68
|
setState({
|
|
@@ -69,11 +75,13 @@ const DatePicker = ({ className, closeOnSelect = true, dateFormat = "dd/MM/yyyy"
|
|
|
69
75
|
}),
|
|
70
76
|
});
|
|
71
77
|
if (values.validated && values.state) {
|
|
72
|
-
|
|
73
|
-
|
|
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);
|
|
74
82
|
}
|
|
75
83
|
}
|
|
76
|
-
}, [dateFormat, mode,
|
|
84
|
+
}, [dateFormat, mode, dispatchChanges, closeOnSelect]);
|
|
77
85
|
const updateFromInput = useCallback((e) => {
|
|
78
86
|
var _a;
|
|
79
87
|
if (shouldSkipParsing(e.currentTarget.value)) {
|
|
@@ -82,11 +90,12 @@ const DatePicker = ({ className, closeOnSelect = true, dateFormat = "dd/MM/yyyy"
|
|
|
82
90
|
[mode]: { input: e.currentTarget.value },
|
|
83
91
|
validated: false,
|
|
84
92
|
});
|
|
93
|
+
dispatchChanges(e.currentTarget.value);
|
|
85
94
|
}
|
|
86
95
|
else {
|
|
87
96
|
updateValues((_a = e.currentTarget) === null || _a === void 0 ? void 0 : _a.value);
|
|
88
97
|
}
|
|
89
|
-
}, [updateValues, mode]);
|
|
98
|
+
}, [updateValues, mode, dispatchChanges]);
|
|
90
99
|
const setOpen = useCallback((open) => setState({ open }), []);
|
|
91
100
|
const setView = useCallback((view) => setState({ view }), []);
|
|
92
101
|
const resetState = useCallback(() => setState({ ...initialState, [mode]: undefined }), [mode]);
|
|
@@ -132,7 +141,7 @@ const DatePicker = ({ className, closeOnSelect = true, dateFormat = "dd/MM/yyyy"
|
|
|
132
141
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
133
142
|
autoFocus: true, captionLayout: "dropdown",
|
|
134
143
|
//defaultMonth={defaultMonth}
|
|
135
|
-
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 : "" })] }))] }));
|
|
136
145
|
};
|
|
137
146
|
DatePicker.config = componentConfig;
|
|
138
147
|
|
|
@@ -17,8 +17,6 @@ const Pill = ({ as, children, className, href, ref, size = "md", ...props }) =>
|
|
|
17
17
|
href,
|
|
18
18
|
...props,
|
|
19
19
|
};
|
|
20
|
-
// https://github.com/facebook/react/issues/34775
|
|
21
|
-
// eslint-disable-next-line react-hooks/refs
|
|
22
20
|
return React.createElement(element, { ...componentProps, ref }, slots.PillBadge, slots.Children);
|
|
23
21
|
};
|
|
24
22
|
const PillBadge = ({ children, className }) => {
|
|
@@ -21,7 +21,7 @@ const defaultButtonText = React.createElement(SearchIcon, {
|
|
|
21
21
|
width: 24,
|
|
22
22
|
});
|
|
23
23
|
const Searchbox = ({ action, children, className, id, label, labelVisible = false, onSubmit, ref, size = "md", variant = "solid", ...props }) => {
|
|
24
|
-
const slots = useSlots(componentSlots, children);
|
|
24
|
+
const slots = { ...useSlots(componentSlots, children) };
|
|
25
25
|
const [isPending, setIsPending] = useState(false);
|
|
26
26
|
const uid = useId(id);
|
|
27
27
|
const handleSubmit = useCallback(async (event) => {
|
|
@@ -57,6 +57,7 @@ const Searchbox = ({ action, children, className, id, label, labelVisible = fals
|
|
|
57
57
|
autoCapitalize: "off",
|
|
58
58
|
id: uid,
|
|
59
59
|
size,
|
|
60
|
+
type: "search",
|
|
60
61
|
});
|
|
61
62
|
const labelProps = {
|
|
62
63
|
htmlFor: uid,
|
|
@@ -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,
|
|
@@ -186,8 +187,14 @@ export const DatePicker = ({
|
|
|
186
187
|
|
|
187
188
|
initialDate = getInitialDate(mode, initialDate)
|
|
188
189
|
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
const startDateProp = startDate
|
|
191
|
+
const endDateProp = endDate
|
|
192
|
+
|
|
193
|
+
startDate = useMemo(
|
|
194
|
+
() => startDateProp ?? getStartDate(initialDate),
|
|
195
|
+
[initialDate, startDateProp]
|
|
196
|
+
)
|
|
197
|
+
endDate = useMemo(() => endDateProp ?? getEndDate(initialDate), [initialDate, endDateProp])
|
|
191
198
|
|
|
192
199
|
const defaultMonth = useMemo(() => getDefaultMonth(initialDate), [initialDate])
|
|
193
200
|
|
|
@@ -205,6 +212,14 @@ export const DatePicker = ({
|
|
|
205
212
|
{ ...initialState, [mode]: initialValues.state, open: false, view: initialValues.view }
|
|
206
213
|
)
|
|
207
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
|
+
|
|
208
223
|
const updateValues = useCallback(
|
|
209
224
|
(selected: DateType) => {
|
|
210
225
|
const values = getDateValues(mode, selected, dateFormat, now)
|
|
@@ -221,12 +236,13 @@ export const DatePicker = ({
|
|
|
221
236
|
})
|
|
222
237
|
|
|
223
238
|
if (values.validated && values.state) {
|
|
224
|
-
|
|
225
|
-
|
|
239
|
+
dispatchChanges(values.state.field)
|
|
240
|
+
} else {
|
|
241
|
+
dispatchChanges(values.state?.input ?? undefined)
|
|
226
242
|
}
|
|
227
243
|
}
|
|
228
244
|
},
|
|
229
|
-
[dateFormat, mode,
|
|
245
|
+
[dateFormat, mode, dispatchChanges, closeOnSelect]
|
|
230
246
|
)
|
|
231
247
|
|
|
232
248
|
const updateFromInput = useCallback(
|
|
@@ -237,11 +253,12 @@ export const DatePicker = ({
|
|
|
237
253
|
[mode]: { input: e.currentTarget.value },
|
|
238
254
|
validated: false,
|
|
239
255
|
})
|
|
256
|
+
dispatchChanges(e.currentTarget.value)
|
|
240
257
|
} else {
|
|
241
258
|
updateValues(e.currentTarget?.value)
|
|
242
259
|
}
|
|
243
260
|
},
|
|
244
|
-
[updateValues, mode]
|
|
261
|
+
[updateValues, mode, dispatchChanges]
|
|
245
262
|
)
|
|
246
263
|
|
|
247
264
|
const setOpen = useCallback((open: boolean) => setState({ open }), [])
|
|
@@ -354,21 +371,25 @@ export const DatePicker = ({
|
|
|
354
371
|
{state.message ?? footerMessage[mode]}
|
|
355
372
|
</div>
|
|
356
373
|
|
|
357
|
-
{mode === "single" &&
|
|
358
|
-
|
|
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
|
+
)}
|
|
359
380
|
{mode === "range" && (
|
|
360
381
|
<>
|
|
361
382
|
<input
|
|
362
383
|
{...baseValueProps}
|
|
363
384
|
id={`${uid}_start`}
|
|
364
385
|
name={`${name}_start`}
|
|
365
|
-
value={state.range?.field?.from ?? ""}
|
|
386
|
+
value={state.range?.field?.from ?? state.range?.input ?? ""}
|
|
366
387
|
/>
|
|
367
388
|
<input
|
|
368
389
|
{...baseValueProps}
|
|
369
390
|
id={`${uid}_end`}
|
|
370
391
|
name={`${name}_end`}
|
|
371
|
-
value={state.range?.field?.to ?? ""}
|
|
392
|
+
value={state.range?.field?.to ?? state.range?.input ?? ""}
|
|
372
393
|
/>
|
|
373
394
|
</>
|
|
374
395
|
)}
|
|
@@ -65,7 +65,7 @@ export const Searchbox = ({
|
|
|
65
65
|
variant = "solid",
|
|
66
66
|
...props
|
|
67
67
|
}: SearchboxProps): JSX.Element => {
|
|
68
|
-
const slots = useSlots(componentSlots, children)
|
|
68
|
+
const slots = { ...useSlots(componentSlots, children) }
|
|
69
69
|
|
|
70
70
|
const [isPending, setIsPending] = useState(false)
|
|
71
71
|
const uid = useId(id)
|
|
@@ -116,6 +116,7 @@ export const Searchbox = ({
|
|
|
116
116
|
autoCapitalize: "off",
|
|
117
117
|
id: uid,
|
|
118
118
|
size,
|
|
119
|
+
type: "search",
|
|
119
120
|
}
|
|
120
121
|
)
|
|
121
122
|
|
|
@@ -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