@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.
@@ -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
- startDate = useMemo(() => startDate !== null && startDate !== void 0 ? startDate : getStartDate(initialDate), [initialDate, startDate]);
48
- endDate = useMemo(() => endDate !== null && endDate !== void 0 ? endDate : getEndDate(initialDate), [initialDate, endDate]);
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
- typeof onChange === "function" && onChange(values.state.field);
73
- typeof onSelect === "function" && onSelect(values.state.field);
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, onChange, onSelect, closeOnSelect]);
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: (_l = (_k = state.multiple) === null || _k === void 0 ? void 0 : _k.field) !== null && _l !== void 0 ? _l : "" }), mode === "range" && (jsxs(Fragment, { children: [jsx("input", { ...baseValueProps, id: `${uid}_start`, name: `${name}_start`, value: (_p = (_o = (_m = state.range) === null || _m === void 0 ? void 0 : _m.field) === null || _o === void 0 ? void 0 : _o.from) !== null && _p !== void 0 ? _p : "" }), jsx("input", { ...baseValueProps, id: `${uid}_end`, name: `${name}_end`, value: (_s = (_r = (_q = state.range) === null || _q === void 0 ? void 0 : _q.field) === null || _r === void 0 ? void 0 : _r.to) !== null && _s !== void 0 ? _s : "" })] }))] }));
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
 
@@ -1,5 +1,5 @@
1
1
  import { Mode } from "react-day-picker";
2
- interface DateRangeStr {
2
+ export interface DateRangeStr {
3
3
  from?: string;
4
4
  to?: string;
5
5
  }
@@ -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,4 @@
1
+ import ValidationSummary from "./ValidationSummary";
2
+ export default ValidationSummary;
3
+ export { ValidationSummary };
4
+ export * from "./ValidationSummary";
@@ -0,0 +1,2 @@
1
+ export declare const sanitizeFieldId: (id: string) => string;
2
+ export declare const getFieldElement: (field: string, id?: string) => HTMLInputElement | null;
@@ -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
@@ -22,3 +22,4 @@ export * from "./components/Squircle";
22
22
  export * from "./components/Tag";
23
23
  export * from "./components/Textarea";
24
24
  export * from "./components/TextInput";
25
+ export * from "./components/ValidationSummary";
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.1.2",
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.1.2",
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": "773259f20340eb832f4ec5a5241df5140f34ab43"
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
- startDate = useMemo(() => startDate ?? getStartDate(initialDate), [initialDate, startDate])
190
- endDate = useMemo(() => endDate ?? getEndDate(initialDate), [initialDate, endDate])
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
- typeof onChange === "function" && onChange(values.state.field)
225
- typeof onSelect === "function" && onSelect(values.state.field)
239
+ dispatchChanges(values.state.field)
240
+ } else {
241
+ dispatchChanges(values.state?.input ?? undefined)
226
242
  }
227
243
  }
228
244
  },
229
- [dateFormat, mode, onChange, onSelect, closeOnSelect]
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" && <input {...valueProps} value={state.single?.field ?? ""} />}
358
- {mode === "multiple" && <input {...valueProps} value={state.multiple?.field ?? ""} />}
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
  )}
@@ -1,7 +1,7 @@
1
1
  import { addYears, format, isBefore, isValid, parse } from "date-fns"
2
2
  import { Mode } from "react-day-picker"
3
3
 
4
- interface DateRangeStr {
4
+ export interface DateRangeStr {
5
5
  from?: string
6
6
  to?: string
7
7
  }
@@ -55,8 +55,6 @@ export const Pill = ({
55
55
  ...props,
56
56
  }
57
57
 
58
- // https://github.com/facebook/react/issues/34775
59
- // eslint-disable-next-line react-hooks/refs
60
58
  return React.createElement(element, { ...componentProps, ref }, slots.PillBadge, slots.Children)
61
59
  }
62
60
 
@@ -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,5 @@
1
+ import ValidationSummary from "./ValidationSummary"
2
+
3
+ export default ValidationSummary
4
+ export { ValidationSummary }
5
+ export * from "./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
@@ -22,3 +22,4 @@ export * from "./components/Squircle"
22
22
  export * from "./components/Tag"
23
23
  export * from "./components/Textarea"
24
24
  export * from "./components/TextInput"
25
+ export * from "./components/ValidationSummary"