@coopdigital/react 1.30.2 → 1.31.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/Author/Author.d.ts +3 -1
- package/dist/components/Author/Author.js +1 -1
- package/dist/components/Author/index.d.ts +2 -2
- package/dist/components/Author/index.js +2 -2
- package/dist/components/Combobox/Combobox.d.ts +1 -1
- package/dist/components/Combobox/Combobox.js +10 -5
- package/dist/components/Combobox/{combobox-utils.js → combobox-internal.js} +4 -2
- package/dist/components/DateInput/DateInput.context.js +7 -0
- package/dist/components/DateInput/DateInput.d.ts +104 -0
- package/dist/components/DateInput/DateInput.js +128 -0
- package/dist/components/DateInput/dateinput-internal.d.ts +6 -0
- package/dist/components/DateInput/dateinput-internal.js +82 -0
- package/dist/components/DateInput/dateinput-utils.d.ts +66 -0
- package/dist/components/DateInput/dateinput-utils.js +91 -0
- package/dist/components/DateInput/index.d.ts +3 -0
- package/dist/components/DateInput/index.js +6 -0
- package/dist/components/Field/Field.context.js +1 -1
- package/dist/components/Field/Field.js +1 -1
- package/dist/components/FieldMarkers/{fieldmarkers-utils.js → fieldmarkers-internal.js} +1 -1
- package/dist/components/Fieldset/Fieldset.js +1 -1
- package/dist/components/Form/Form.d.ts +7 -1
- package/dist/components/Form/Form.js +2 -1
- package/dist/components/Squircle/Squircle.d.ts +3 -1
- package/dist/components/Squircle/Squircle.js +1 -1
- package/dist/components/Squircle/index.d.ts +2 -2
- package/dist/components/Squircle/index.js +2 -2
- package/dist/components/ValidationSummary/ValidationSummary.d.ts +1 -1
- package/dist/components/ValidationSummary/ValidationSummary.js +1 -1
- package/dist/components/ValidationSummary/{validationsummary-utils.d.ts → validationsummary-internal.d.ts} +1 -1
- package/dist/components/ValidationSummary/{validationsummary-utils.js → validationsummary-internal.js} +3 -2
- package/dist/index.d.ts +6 -3
- package/dist/index.js +6 -4
- package/package.json +6 -5
- package/src/components/Author/Author.tsx +1 -1
- package/src/components/Combobox/Combobox.tsx +29 -20
- package/src/components/Combobox/combobox-internal.ts +60 -0
- package/src/components/Combobox/combobox-utils.ts +1 -41
- package/src/components/Combobox/index.ts +1 -0
- package/src/components/DateInput/DateInput.context.ts +25 -0
- package/src/components/DateInput/DateInput.tsx +252 -0
- package/src/components/DateInput/dateinput-internal.ts +176 -0
- package/src/components/DateInput/dateinput-utils.ts +194 -0
- package/src/components/DateInput/index.ts +6 -0
- package/src/components/Field/Field.context.ts +1 -1
- package/src/components/Field/Field.tsx +1 -1
- package/src/components/Fieldset/Fieldset.tsx +5 -1
- package/src/components/Form/Form.tsx +9 -1
- package/src/components/Squircle/Squircle.tsx +1 -1
- package/src/components/ValidationSummary/ValidationSummary.context.ts +1 -1
- package/src/components/ValidationSummary/ValidationSummary.tsx +1 -1
- package/src/components/ValidationSummary/{validationsummary-utils.ts → validationsummary-internal.ts} +9 -8
- package/src/index.ts +1 -0
- /package/src/components/FieldMarkers/{fieldmarkers-utils.ts → fieldmarkers-internal.ts} +0 -0
|
@@ -36,5 +36,7 @@ declare const Author: {
|
|
|
36
36
|
name: string;
|
|
37
37
|
};
|
|
38
38
|
};
|
|
39
|
+
declare const AuthorImage: ({ alt, src, ...props }: AuthorImageProps) => JSX.Element;
|
|
40
|
+
declare const AuthorMeta: ({ date, datePrefix, name, ...props }: AuthorMetaProps) => JSX.Element;
|
|
39
41
|
//#endregion
|
|
40
|
-
export { Author, Author as default };
|
|
42
|
+
export { Author, Author as default, AuthorImage, AuthorMeta };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Author, AuthorImageProps, AuthorMetaProps, AuthorProps } from "./Author.js";
|
|
2
|
-
export { Author, Author as default, AuthorImageProps, AuthorMetaProps, AuthorProps };
|
|
1
|
+
import { Author, AuthorImage, AuthorImageProps, AuthorMeta, AuthorMetaProps, AuthorProps } from "./Author.js";
|
|
2
|
+
export { Author, Author as default, AuthorImage, AuthorImageProps, AuthorMeta, AuthorMetaProps, AuthorProps };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Author } from "./Author.js";
|
|
1
|
+
import { Author, AuthorImage, AuthorMeta } from "./Author.js";
|
|
2
2
|
//#region src/components/Author/index.ts
|
|
3
3
|
var Author_default = Author;
|
|
4
4
|
//#endregion
|
|
5
|
-
export { Author, Author_default as default };
|
|
5
|
+
export { Author, AuthorImage, AuthorMeta, Author_default as default };
|
|
@@ -181,4 +181,4 @@ declare const ComboboxOptions: {
|
|
|
181
181
|
};
|
|
182
182
|
};
|
|
183
183
|
//#endregion
|
|
184
|
-
export { Combobox, Combobox as default, ComboboxButton, ComboboxControls, ComboboxInput,
|
|
184
|
+
export { Combobox, Combobox as default, ComboboxButton, ComboboxControls, ComboboxInput, ComboboxOptions };
|
|
@@ -4,7 +4,7 @@ import { ChevronDownIcon } from "../icons/ChevronDownIcon.js";
|
|
|
4
4
|
import { getSlotProp } from "../../utils/slots.js";
|
|
5
5
|
import { useSlots } from "../../hooks/useSlots.js";
|
|
6
6
|
import { getFieldError, useField } from "../Field/Field.context.js";
|
|
7
|
-
import { defaultFilter, defaultIsItemDisabled, defaultItemToString, isMatchingText, normalizeOptions, resolveOption } from "./combobox-
|
|
7
|
+
import { defaultFilter, defaultIsItemDisabled, defaultItemToString, defaultMaxResultsText, isMatchingText, normalizeOptions, resolveOption, shouldShowSelectionText } from "./combobox-internal.js";
|
|
8
8
|
import { ComboboxContext, useComboboxContext } from "./Combobox.context.js";
|
|
9
9
|
import { TextInput } from "../TextInput/TextInput.js";
|
|
10
10
|
import { useDebounce } from "../../hooks/useDebounce.js";
|
|
@@ -17,7 +17,6 @@ import mergeRefs from "merge-refs";
|
|
|
17
17
|
//#region src/components/Combobox/Combobox.tsx
|
|
18
18
|
const DEFAULT_MAX_RESULTS = 50;
|
|
19
19
|
const ANNOUNCEMENT_DELAY = 750;
|
|
20
|
-
const defaultMaxResultsText = (shown, total) => `Showing ${shown} of ${total} results.`;
|
|
21
20
|
const componentSlots = {
|
|
22
21
|
ComboboxControls: null,
|
|
23
22
|
ComboboxOptions: null
|
|
@@ -144,11 +143,12 @@ const Combobox = ({ allowCustom = false, children, className, defaultSelected, d
|
|
|
144
143
|
const { inputValue, reset, setInputValue } = downshift;
|
|
145
144
|
useEffect(() => {
|
|
146
145
|
const previous = previousSelection.current;
|
|
147
|
-
|
|
146
|
+
const current = {
|
|
148
147
|
text: selectionText,
|
|
149
148
|
value: selectionValue
|
|
150
149
|
};
|
|
151
|
-
|
|
150
|
+
previousSelection.current = current;
|
|
151
|
+
if (shouldShowSelectionText(previous, current, inputValue)) setInputValue(selectionText);
|
|
152
152
|
}, [
|
|
153
153
|
inputValue,
|
|
154
154
|
selectionText,
|
|
@@ -234,10 +234,15 @@ const ComboboxOptions = ({ className, maxResults: _maxResults, maxResultsText =
|
|
|
234
234
|
};
|
|
235
235
|
const message = getMessage();
|
|
236
236
|
const announcement = useDebounce(message, ANNOUNCEMENT_DELAY);
|
|
237
|
+
const onMouseLeave = (event) => {
|
|
238
|
+
props.onMouseLeave?.(event);
|
|
239
|
+
if (!isOpen) Object.assign(event, { preventDownshiftDefault: true });
|
|
240
|
+
};
|
|
237
241
|
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("ul", {
|
|
238
242
|
...getMenuProps({
|
|
239
243
|
...props,
|
|
240
|
-
className: clsx("coop-combobox-options", className)
|
|
244
|
+
className: clsx("coop-combobox-options", className),
|
|
245
|
+
onMouseLeave
|
|
241
246
|
}),
|
|
242
247
|
children: [isOpen && items.map((item, index) => {
|
|
243
248
|
const { children, disabled: _disabled, icon, label, onClick, onMouseDown, onMouseMove, value, ...attributes } = item;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { matchSorter } from "match-sorter";
|
|
2
|
-
//#region src/components/Combobox/combobox-
|
|
2
|
+
//#region src/components/Combobox/combobox-internal.ts
|
|
3
3
|
const defaultFilter = (items, inputValue) => inputValue !== "" ? matchSorter(items, inputValue, { keys: [(item) => [item.label ?? item.value]] }) : items;
|
|
4
4
|
const defaultIsItemDisabled = (item) => item.disabled ?? false;
|
|
5
5
|
const defaultItemToString = (item) => item?.label ?? item?.value ?? "";
|
|
6
|
+
const defaultMaxResultsText = (shown, total) => `Showing ${shown} of ${total} results.`;
|
|
6
7
|
const normalizeOptions = (options) => options.filter((option) => option && typeof option !== "number").map((option) => typeof option === "string" ? { value: option } : option);
|
|
7
8
|
const collator = new Intl.Collator(void 0, { sensitivity: "base" });
|
|
8
9
|
const isMatchingText = (optionText, text) => collator.compare(optionText.trim(), text.trim()) === 0;
|
|
@@ -11,5 +12,6 @@ const resolveOption = (option, items) => {
|
|
|
11
12
|
const item = typeof option === "string" ? { value: option } : option;
|
|
12
13
|
return items.find(({ value }) => value === item.value) ?? item;
|
|
13
14
|
};
|
|
15
|
+
const shouldShowSelectionText = (previous, current, inputValue) => current.value === previous.value && current.text !== previous.text && inputValue === previous.text;
|
|
14
16
|
//#endregion
|
|
15
|
-
export { defaultFilter, defaultIsItemDisabled, defaultItemToString, isMatchingText, normalizeOptions, resolveOption };
|
|
17
|
+
export { defaultFilter, defaultIsItemDisabled, defaultItemToString, defaultMaxResultsText, isMatchingText, normalizeOptions, resolveOption, shouldShowSelectionText };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
//#region src/components/DateInput/DateInput.context.ts
|
|
4
|
+
const DateInputContext = createContext(null);
|
|
5
|
+
const useDateInput = () => useContext(DateInputContext);
|
|
6
|
+
//#endregion
|
|
7
|
+
export { DateInputContext, useDateInput };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { StandardSizes } from "../../types/index.js";
|
|
2
|
+
import { TextInputProps } from "../TextInput/TextInput.js";
|
|
3
|
+
import "../TextInput/index.js";
|
|
4
|
+
import { DateInputMode, DateParts } from "./dateinput-utils.js";
|
|
5
|
+
import { HTMLAttributes, JSX, ReactNode, Ref } from "react";
|
|
6
|
+
//#region src/components/DateInput/DateInput.d.ts
|
|
7
|
+
export interface DateInputChangeEvent {
|
|
8
|
+
/** The parts as typed. */
|
|
9
|
+
parts: DateParts;
|
|
10
|
+
/** The parts as typed, joined from year to day, such as `2026-2-30`.
|
|
11
|
+
*
|
|
12
|
+
* Not checked: use `validateDate` before relying on it. */
|
|
13
|
+
value: string;
|
|
14
|
+
}
|
|
15
|
+
export interface DateInputProps extends Omit<HTMLAttributes<HTMLDivElement>, "defaultValue" | "onBlur" | "onChange"> {
|
|
16
|
+
/** Content inside the component.
|
|
17
|
+
*
|
|
18
|
+
* This should be a set of `<DateInput.Day>`, `<DateInput.Month>` and `<DateInput.Year>`. */
|
|
19
|
+
children?: ReactNode;
|
|
20
|
+
/** Specify additional CSS classes. */
|
|
21
|
+
className?: string;
|
|
22
|
+
/** Specify the initial value, when not controlled. One of:
|
|
23
|
+
*
|
|
24
|
+
* - an ISO date, such as `1990-08-21`
|
|
25
|
+
* - the parts, such as `{ year: "2026" }` */
|
|
26
|
+
defaultValue?: DateParts | string;
|
|
27
|
+
/** Specify the base for the part ids. Will be auto-generated if not set. */
|
|
28
|
+
id?: string;
|
|
29
|
+
/** Specify the DateInput mode. */
|
|
30
|
+
mode?: DateInputMode;
|
|
31
|
+
/** Specify the name.
|
|
32
|
+
*
|
|
33
|
+
* Each part is `{name}-day`, `{name}-month` and`{name}-year`.
|
|
34
|
+
*
|
|
35
|
+
* A hidden input `{name}` holds the value as typed. */
|
|
36
|
+
name?: string;
|
|
37
|
+
/** Called when focus leaves the whole DateInput, not each part. */
|
|
38
|
+
onBlur?: (event: DateInputChangeEvent) => void;
|
|
39
|
+
/** Called when any part changes. */
|
|
40
|
+
onChange?: (event: DateInputChangeEvent) => void;
|
|
41
|
+
/** Specify a custom React ref for this component. */
|
|
42
|
+
ref?: Ref<HTMLDivElement>;
|
|
43
|
+
/** Specify the component size. */
|
|
44
|
+
size?: StandardSizes;
|
|
45
|
+
/** Specify the value, when controlled. One of:
|
|
46
|
+
*
|
|
47
|
+
* - an ISO date, such as `1990-08-21`
|
|
48
|
+
* - the parts, such as `{ year: "2026" }`
|
|
49
|
+
*
|
|
50
|
+
* Update it in `onChange` with the `value` or `parts` of the event. */
|
|
51
|
+
value?: DateParts | string;
|
|
52
|
+
}
|
|
53
|
+
declare const DateInput: {
|
|
54
|
+
({ children, className, defaultValue, id, mode, name, onBlur, onChange, ref, size, value, ...props }: DateInputProps): JSX.Element;
|
|
55
|
+
slot: {
|
|
56
|
+
name: string;
|
|
57
|
+
};
|
|
58
|
+
Day: {
|
|
59
|
+
({ label, ...props }: DateInputPartProps): JSX.Element;
|
|
60
|
+
slot: {
|
|
61
|
+
name: string;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
Month: {
|
|
65
|
+
({ label, ...props }: DateInputPartProps): JSX.Element;
|
|
66
|
+
slot: {
|
|
67
|
+
name: string;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
Year: {
|
|
71
|
+
({ label, ...props }: DateInputPartProps): JSX.Element;
|
|
72
|
+
slot: {
|
|
73
|
+
name: string;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
export interface DateInputPartProps extends Omit<TextInputProps, "defaultValue" | "fit" | "prefix" | "prefixInline" | "suffix" | "suffixInline" | "type" | "value"> {
|
|
78
|
+
/** Specify the part error state.
|
|
79
|
+
*
|
|
80
|
+
* Inherits error state of a `group` Fieldset when not set. */
|
|
81
|
+
error?: boolean;
|
|
82
|
+
/** Specify the label. */
|
|
83
|
+
label?: ReactNode;
|
|
84
|
+
}
|
|
85
|
+
declare const DateInputDay: {
|
|
86
|
+
({ label, ...props }: DateInputPartProps): JSX.Element;
|
|
87
|
+
slot: {
|
|
88
|
+
name: string;
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
declare const DateInputMonth: {
|
|
92
|
+
({ label, ...props }: DateInputPartProps): JSX.Element;
|
|
93
|
+
slot: {
|
|
94
|
+
name: string;
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
declare const DateInputYear: {
|
|
98
|
+
({ label, ...props }: DateInputPartProps): JSX.Element;
|
|
99
|
+
slot: {
|
|
100
|
+
name: string;
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
//#endregion
|
|
104
|
+
export { DateInput, DateInput as default, DateInputDay, DateInputMonth, DateInputYear };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useId as useId$1 } from "../../hooks/useId.js";
|
|
3
|
+
import { forEachSlot } from "../../utils/slots.js";
|
|
4
|
+
import { TextInput } from "../TextInput/TextInput.js";
|
|
5
|
+
import { useFormReset } from "../../hooks/useFormReset.js";
|
|
6
|
+
import { Field } from "../Field/Field.js";
|
|
7
|
+
import { formatDateValue, toDateParts, toDateValue } from "./dateinput-internal.js";
|
|
8
|
+
import { getAutoComplete } from "./dateinput-utils.js";
|
|
9
|
+
import { DateInputContext, useDateInput } from "./DateInput.context.js";
|
|
10
|
+
import clsx from "clsx";
|
|
11
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
import { useRef, useState } from "react";
|
|
13
|
+
import mergeRefs from "merge-refs";
|
|
14
|
+
//#region src/components/DateInput/DateInput.tsx
|
|
15
|
+
const PART_SLOTS = {
|
|
16
|
+
DateInputDay: "day",
|
|
17
|
+
DateInputMonth: "month",
|
|
18
|
+
DateInputYear: "year"
|
|
19
|
+
};
|
|
20
|
+
const DateInput = ({ children, className, defaultValue, id, mode, name, onBlur, onChange, ref, size, value, ...props }) => {
|
|
21
|
+
const uid = useId$1(id);
|
|
22
|
+
const rendered = [];
|
|
23
|
+
forEachSlot(children, (slot) => {
|
|
24
|
+
const part = PART_SLOTS[slot.name];
|
|
25
|
+
if (part && !rendered.includes(part)) rendered.push(part);
|
|
26
|
+
});
|
|
27
|
+
const defaultParts = toDateParts(defaultValue, rendered);
|
|
28
|
+
const [typed, setTyped] = useState(() => value === void 0 ? defaultParts : toDateParts(value, rendered));
|
|
29
|
+
const controlled = value !== void 0;
|
|
30
|
+
const parts = controlled && toDateValue(value, rendered) !== toDateValue(typed, rendered) ? toDateParts(value, rendered) : typed;
|
|
31
|
+
const controlRef = useRef(null);
|
|
32
|
+
useFormReset(controlRef, () => {
|
|
33
|
+
if (!controlled) setTyped(defaultParts);
|
|
34
|
+
});
|
|
35
|
+
const getEvent = (next) => ({
|
|
36
|
+
parts: next,
|
|
37
|
+
value: formatDateValue(next, rendered)
|
|
38
|
+
});
|
|
39
|
+
const changePart = (part, partValue) => {
|
|
40
|
+
const next = {
|
|
41
|
+
...parts,
|
|
42
|
+
[part]: partValue
|
|
43
|
+
};
|
|
44
|
+
setTyped(next);
|
|
45
|
+
onChange?.(getEvent(next));
|
|
46
|
+
};
|
|
47
|
+
const handleBlur = (event) => {
|
|
48
|
+
if (!event.currentTarget.contains(event.relatedTarget)) onBlur?.(getEvent(parts));
|
|
49
|
+
};
|
|
50
|
+
const componentProps = {
|
|
51
|
+
...props,
|
|
52
|
+
className: clsx("coop-date-input", className),
|
|
53
|
+
onBlur: handleBlur
|
|
54
|
+
};
|
|
55
|
+
return /* @__PURE__ */ jsx(DateInputContext.Provider, {
|
|
56
|
+
value: {
|
|
57
|
+
changePart,
|
|
58
|
+
controlled,
|
|
59
|
+
controlPart: rendered[0],
|
|
60
|
+
controlRef,
|
|
61
|
+
defaultValue: defaultParts,
|
|
62
|
+
id: uid,
|
|
63
|
+
mode,
|
|
64
|
+
name,
|
|
65
|
+
parts,
|
|
66
|
+
size
|
|
67
|
+
},
|
|
68
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
69
|
+
...componentProps,
|
|
70
|
+
ref,
|
|
71
|
+
children: [name && /* @__PURE__ */ jsx("input", {
|
|
72
|
+
name,
|
|
73
|
+
type: "hidden",
|
|
74
|
+
value: formatDateValue(parts, rendered)
|
|
75
|
+
}), children]
|
|
76
|
+
})
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
const DateInputPart = ({ autoComplete, className, datePart: part, error, id, label, name, onChange, size, ...props }) => {
|
|
80
|
+
const dateInput = useDateInput();
|
|
81
|
+
const inputProps = {
|
|
82
|
+
...dateInput?.controlled ? { value: dateInput.parts[part] ?? "" } : { defaultValue: dateInput?.defaultValue?.[part] },
|
|
83
|
+
autoComplete: autoComplete ?? getAutoComplete(dateInput?.mode, part),
|
|
84
|
+
className: clsx("coop-date-input--input", className),
|
|
85
|
+
"data-part": part,
|
|
86
|
+
id: id ?? (dateInput ? `${dateInput.id}-${part}` : void 0),
|
|
87
|
+
inputMode: "numeric",
|
|
88
|
+
name: name ?? (dateInput?.name ? `${dateInput.name}-${part}` : void 0),
|
|
89
|
+
onChange: (event) => {
|
|
90
|
+
dateInput?.changePart(part, event.target.value);
|
|
91
|
+
onChange?.(event);
|
|
92
|
+
},
|
|
93
|
+
pattern: "[0-9]*",
|
|
94
|
+
size: size ?? dateInput?.size,
|
|
95
|
+
spellCheck: false,
|
|
96
|
+
...props,
|
|
97
|
+
ref: dateInput?.controlPart === part ? mergeRefs(dateInput.controlRef, props.ref) : props.ref
|
|
98
|
+
};
|
|
99
|
+
return /* @__PURE__ */ jsxs(Field, {
|
|
100
|
+
error,
|
|
101
|
+
hideErrorBar: true,
|
|
102
|
+
children: [/* @__PURE__ */ jsx(Field.Label, { children: label }), /* @__PURE__ */ jsx(TextInput, { ...inputProps })]
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
const DateInputDay = ({ label = "Day", ...props }) => /* @__PURE__ */ jsx(DateInputPart, {
|
|
106
|
+
...props,
|
|
107
|
+
datePart: "day",
|
|
108
|
+
label
|
|
109
|
+
});
|
|
110
|
+
const DateInputMonth = ({ label = "Month", ...props }) => /* @__PURE__ */ jsx(DateInputPart, {
|
|
111
|
+
...props,
|
|
112
|
+
datePart: "month",
|
|
113
|
+
label
|
|
114
|
+
});
|
|
115
|
+
const DateInputYear = ({ label = "Year", ...props }) => /* @__PURE__ */ jsx(DateInputPart, {
|
|
116
|
+
...props,
|
|
117
|
+
datePart: "year",
|
|
118
|
+
label
|
|
119
|
+
});
|
|
120
|
+
DateInput.slot = { name: "DateInput" };
|
|
121
|
+
DateInputDay.slot = { name: "DateInputDay" };
|
|
122
|
+
DateInputMonth.slot = { name: "DateInputMonth" };
|
|
123
|
+
DateInputYear.slot = { name: "DateInputYear" };
|
|
124
|
+
DateInput.Day = DateInputDay;
|
|
125
|
+
DateInput.Month = DateInputMonth;
|
|
126
|
+
DateInput.Year = DateInputYear;
|
|
127
|
+
//#endregion
|
|
128
|
+
export { DateInput, DateInput as default, DateInputDay, DateInputMonth, DateInputYear };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { format, formatISO, isExists, isValid, parseISO } from "date-fns";
|
|
2
|
+
//#region src/components/DateInput/dateinput-internal.ts
|
|
3
|
+
const VALUE_ORDER = [
|
|
4
|
+
"year",
|
|
5
|
+
"month",
|
|
6
|
+
"day"
|
|
7
|
+
];
|
|
8
|
+
const WHOLE_DATE_ERRORS = /* @__PURE__ */ new Set([
|
|
9
|
+
"after-max",
|
|
10
|
+
"before-min",
|
|
11
|
+
"impossible-date",
|
|
12
|
+
"missing-date",
|
|
13
|
+
"unsupported-parts"
|
|
14
|
+
]);
|
|
15
|
+
const valueParts = (parts) => VALUE_ORDER.filter((part) => parts.includes(part));
|
|
16
|
+
const formatDateValue = (date, parts = VALUE_ORDER) => {
|
|
17
|
+
const values = valueParts(parts).map((part) => date[part] ?? "");
|
|
18
|
+
return values.every((value) => value === "") ? "" : values.join("-");
|
|
19
|
+
};
|
|
20
|
+
const splitValue = (value, used) => {
|
|
21
|
+
const pieces = value.split("-");
|
|
22
|
+
return !used.includes("year") && value.startsWith("--") && pieces.length === used.length + 2 ? pieces.slice(2) : pieces;
|
|
23
|
+
};
|
|
24
|
+
const parseDateValue = (value, parts = VALUE_ORDER) => {
|
|
25
|
+
if (value === "") return {};
|
|
26
|
+
const used = valueParts(parts);
|
|
27
|
+
const pieces = splitValue(value, used);
|
|
28
|
+
if (pieces.length !== used.length) return void 0;
|
|
29
|
+
return Object.fromEntries(used.map((part, index) => [part, pieces[index]]));
|
|
30
|
+
};
|
|
31
|
+
const toDateValue = (input = "", parts) => typeof input === "string" ? input : formatDateValue(input, parts);
|
|
32
|
+
const toDateParts = (input = "", parts) => typeof input === "string" ? parseDateValue(input, parts) ?? {} : input;
|
|
33
|
+
const toNumber = (value) => /^\d+$/.test(value) ? Number(value) : null;
|
|
34
|
+
const isValidPart = (part, value) => {
|
|
35
|
+
if (part === "year") return /^\d{4}$/.test(value);
|
|
36
|
+
const number = toNumber(value);
|
|
37
|
+
return number !== null && number >= 1 && number <= (part === "day" ? 31 : 12);
|
|
38
|
+
};
|
|
39
|
+
const trimParts = (date) => ({
|
|
40
|
+
day: date.day?.trim() ?? "",
|
|
41
|
+
month: date.month?.trim() ?? "",
|
|
42
|
+
year: date.year?.trim() ?? ""
|
|
43
|
+
});
|
|
44
|
+
const getDateErrors = (values, parts) => {
|
|
45
|
+
if (parts.every((part) => values[part] === "")) return ["missing-date"];
|
|
46
|
+
const errors = [];
|
|
47
|
+
for (const part of parts) if (values[part] === "") errors.push(`missing-${part}`);
|
|
48
|
+
else if (!isValidPart(part, values[part])) errors.push(`invalid-${part}`);
|
|
49
|
+
if (errors.length > 0 || !parts.includes("day") || !parts.includes("month")) return errors;
|
|
50
|
+
const year = parts.includes("year") ? Number(values.year) : 2e3;
|
|
51
|
+
return isExists(year, Number(values.month) - 1, Number(values.day)) ? [] : ["impossible-date"];
|
|
52
|
+
};
|
|
53
|
+
const getInvalidParts = (errors, parts) => parts.filter((part) => errors.some((code) => WHOLE_DATE_ERRORS.has(code) || code.endsWith(part)));
|
|
54
|
+
const pad = (value) => String(Number(value)).padStart(2, "0");
|
|
55
|
+
const toISODate = ({ day, month, year }, parts) => {
|
|
56
|
+
const has = (part) => parts.includes(part);
|
|
57
|
+
if (has("day") && has("month") && has("year")) return `${year}-${pad(month)}-${pad(day)}`;
|
|
58
|
+
if (has("month") && has("year") && !has("day")) return `${year}-${pad(month)}`;
|
|
59
|
+
if (has("day") && has("month")) return `--${pad(month)}-${pad(day)}`;
|
|
60
|
+
if (parts.length === 1 && has("year")) return year;
|
|
61
|
+
return "";
|
|
62
|
+
};
|
|
63
|
+
const toLimit = (limit, name) => {
|
|
64
|
+
const date = typeof limit === "string" ? parseISO(limit) : limit;
|
|
65
|
+
if (!isValid(date)) throw new Error(`${name} must be a valid Date or an ISO date, such as 2020-01-31`);
|
|
66
|
+
return date;
|
|
67
|
+
};
|
|
68
|
+
const formatLimit = (limit, length) => format(limit, length === 4 ? "yyyy" : length === 7 ? "MMMM yyyy" : "d MMMM yyyy");
|
|
69
|
+
const getRangeError = (value, min, max) => {
|
|
70
|
+
if (value.startsWith("--")) return void 0;
|
|
71
|
+
const cut = (limit) => formatISO(limit, { representation: "date" }).slice(0, value.length);
|
|
72
|
+
if (min && value < cut(min)) return "before-min";
|
|
73
|
+
if (max && value > cut(max)) return "after-max";
|
|
74
|
+
};
|
|
75
|
+
const getMessage = (code, label, messages = {}, limits, defaults) => {
|
|
76
|
+
const custom = messages[code];
|
|
77
|
+
if (custom !== void 0) return typeof custom === "function" ? custom(label) : custom;
|
|
78
|
+
const fallback = defaults[code];
|
|
79
|
+
return typeof fallback === "function" ? fallback(label, limits) : fallback;
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|
|
82
|
+
export { WHOLE_DATE_ERRORS, formatDateValue, formatLimit, getDateErrors, getInvalidParts, getMessage, getRangeError, isValidPart, pad, parseDateValue, toDateParts, toDateValue, toISODate, toLimit, toNumber, trimParts };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Limits } from "./dateinput-internal.js";
|
|
2
|
+
//#region src/components/DateInput/dateinput-utils.d.ts
|
|
3
|
+
export type DatePart = "day" | "month" | "year";
|
|
4
|
+
export interface DateParts {
|
|
5
|
+
day?: string;
|
|
6
|
+
month?: string;
|
|
7
|
+
year?: string;
|
|
8
|
+
}
|
|
9
|
+
export type DateInputMode = "dob" | undefined;
|
|
10
|
+
export type DateErrorCode = "missing-date" | "missing-day" | "missing-month" | "missing-year" | "invalid-day" | "invalid-month" | "invalid-year" | "impossible-date" | "unsupported-parts" | "before-min" | "after-max";
|
|
11
|
+
/** A message, or a function that builds one from the label. */
|
|
12
|
+
export type DateErrorMessage = string | ((label?: string) => string);
|
|
13
|
+
export interface DateValidationOptions {
|
|
14
|
+
/** The name of the date, as it would start a sentence, such as "Date
|
|
15
|
+
* of birth".
|
|
16
|
+
*
|
|
17
|
+
* Default: `Date`. */
|
|
18
|
+
label?: string;
|
|
19
|
+
/** The latest date allowed, as a `Date` or an ISO date such as `2026-12-31`.
|
|
20
|
+
*
|
|
21
|
+
* Only the parts used are compared, such as the month and year. */
|
|
22
|
+
max?: Date | string;
|
|
23
|
+
/** Messages to use instead of the defaults. */
|
|
24
|
+
messages?: Partial<Record<DateErrorCode, DateErrorMessage>>;
|
|
25
|
+
/** The earliest date allowed, as a `Date` or an ISO date such as `2020-01-01`.
|
|
26
|
+
*
|
|
27
|
+
* Only the parts used are compared, such as the month and year. */
|
|
28
|
+
min?: Date | string;
|
|
29
|
+
/** The parts used, in the order they are entered.
|
|
30
|
+
*
|
|
31
|
+
* Default: `day`, `month`, `year`. */
|
|
32
|
+
parts?: DatePart[];
|
|
33
|
+
}
|
|
34
|
+
export interface DateValidationSuccess {
|
|
35
|
+
/** Whether the date is valid. */
|
|
36
|
+
valid: true;
|
|
37
|
+
/** The date as an ISO 8601 string, such as 1990-08-21. */
|
|
38
|
+
value: string;
|
|
39
|
+
}
|
|
40
|
+
export interface DateValidationError {
|
|
41
|
+
/** The code for the first problem, in the order the parts are entered. */
|
|
42
|
+
error: DateErrorCode;
|
|
43
|
+
/** The parts with an error. Every part is invalid when the date is
|
|
44
|
+
* empty, impossible, has no ISO format or is outside `min` and `max`. */
|
|
45
|
+
invalid: DatePart[];
|
|
46
|
+
/** The message for the error. */
|
|
47
|
+
message: string;
|
|
48
|
+
/** Whether the date is valid. */
|
|
49
|
+
valid: false;
|
|
50
|
+
}
|
|
51
|
+
export type DateValidation = DateValidationSuccess | DateValidationError;
|
|
52
|
+
export declare const DATE_PARTS: DatePart[];
|
|
53
|
+
export declare const DATE_ERROR_MESSAGES: Record<DateErrorCode, string | ((label?: string, limits?: Limits) => string)>;
|
|
54
|
+
export declare const validateDateParts: (date: DateParts, { label, max, messages, min, parts }?: DateValidationOptions) => DateValidation;
|
|
55
|
+
/** Check a value, such as the DateInput value or the value a form
|
|
56
|
+
* submits, the same way as `validateDateParts`. Use the same `parts` as
|
|
57
|
+
* the DateInput. */
|
|
58
|
+
export declare const validateDate: (value: string, options?: DateValidationOptions) => DateValidation;
|
|
59
|
+
/** As this is a more specific function for a birth of date validation, we can make some assumptions.
|
|
60
|
+
* Such as, date needs to be today or in the past and error message should reflect date of birth.
|
|
61
|
+
* Check a value, such as the DateInput value or the value a form
|
|
62
|
+
* submits, the same way as `validateDateParts`. Use the same `parts` as
|
|
63
|
+
* the DateInput. */
|
|
64
|
+
export declare const validateDob: (value: string, options?: DateValidationOptions) => DateValidation;
|
|
65
|
+
export declare const getAutoComplete: (mode: DateInputMode, part: DatePart) => string | undefined;
|
|
66
|
+
//#endregion
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { formatLimit, getDateErrors, getInvalidParts, getMessage, getRangeError, parseDateValue, toISODate, toLimit, trimParts } from "./dateinput-internal.js";
|
|
2
|
+
//#region src/components/DateInput/dateinput-utils.ts
|
|
3
|
+
const DATE_PARTS = [
|
|
4
|
+
"day",
|
|
5
|
+
"month",
|
|
6
|
+
"year"
|
|
7
|
+
];
|
|
8
|
+
const realDate = (label = "Date") => `${label} must be a real date`;
|
|
9
|
+
const DATE_ERROR_MESSAGES = {
|
|
10
|
+
"after-max": (label = "Date", limits) => `${label} must be on or before ${limits?.max}`,
|
|
11
|
+
"before-min": (label = "Date", limits) => `${label} must be on or after ${limits?.min}`,
|
|
12
|
+
"impossible-date": realDate,
|
|
13
|
+
"invalid-day": realDate,
|
|
14
|
+
"invalid-month": realDate,
|
|
15
|
+
"invalid-year": "Year must include 4 numbers",
|
|
16
|
+
"missing-date": "Enter a date",
|
|
17
|
+
"missing-day": (label = "Date") => `${label} must include a day`,
|
|
18
|
+
"missing-month": (label = "Date") => `${label} must include a month`,
|
|
19
|
+
"missing-year": (label = "Date") => `${label} must include a year`,
|
|
20
|
+
"unsupported-parts": realDate
|
|
21
|
+
};
|
|
22
|
+
const validateDateParts = (date, { label, max, messages = {}, min, parts = DATE_PARTS } = {}) => {
|
|
23
|
+
const minDate = min === void 0 ? void 0 : toLimit(min, "min");
|
|
24
|
+
const maxDate = max === void 0 ? void 0 : toLimit(max, "max");
|
|
25
|
+
const values = trimParts(date);
|
|
26
|
+
const errors = getDateErrors(values, parts);
|
|
27
|
+
let value = "";
|
|
28
|
+
if (errors.length === 0) {
|
|
29
|
+
value = toISODate(values, parts);
|
|
30
|
+
const rangeError = value ? getRangeError(value, minDate, maxDate) : "unsupported-parts";
|
|
31
|
+
if (!rangeError) return {
|
|
32
|
+
valid: true,
|
|
33
|
+
value
|
|
34
|
+
};
|
|
35
|
+
errors.push(rangeError);
|
|
36
|
+
}
|
|
37
|
+
const [error] = errors;
|
|
38
|
+
const limits = {
|
|
39
|
+
max: maxDate && value ? formatLimit(maxDate, value.length) : void 0,
|
|
40
|
+
min: minDate && value ? formatLimit(minDate, value.length) : void 0
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
error,
|
|
44
|
+
invalid: getInvalidParts(errors, parts),
|
|
45
|
+
message: getMessage(error, label, messages, limits, DATE_ERROR_MESSAGES),
|
|
46
|
+
valid: false
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
/** Check a value, such as the DateInput value or the value a form
|
|
50
|
+
* submits, the same way as `validateDateParts`. Use the same `parts` as
|
|
51
|
+
* the DateInput. */
|
|
52
|
+
const validateDate = (value, options = {}) => {
|
|
53
|
+
const date = parseDateValue(value, options.parts);
|
|
54
|
+
if (date) return validateDateParts(date, options);
|
|
55
|
+
const { label, messages, parts = DATE_PARTS } = options;
|
|
56
|
+
return {
|
|
57
|
+
error: "impossible-date",
|
|
58
|
+
invalid: parts,
|
|
59
|
+
message: getMessage("impossible-date", label, messages, {}, DATE_ERROR_MESSAGES),
|
|
60
|
+
valid: false
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
/** As this is a more specific function for a birth of date validation, we can make some assumptions.
|
|
64
|
+
* Such as, date needs to be today or in the past and error message should reflect date of birth.
|
|
65
|
+
* Check a value, such as the DateInput value or the value a form
|
|
66
|
+
* submits, the same way as `validateDateParts`. Use the same `parts` as
|
|
67
|
+
* the DateInput. */
|
|
68
|
+
const validateDob = (value, options = {}) => {
|
|
69
|
+
const { messages, ...rest } = options;
|
|
70
|
+
return validateDate(value, {
|
|
71
|
+
label: "Date of birth",
|
|
72
|
+
max: /* @__PURE__ */ new Date(),
|
|
73
|
+
messages: {
|
|
74
|
+
"after-max": "Date of birth must be today or in the past",
|
|
75
|
+
"missing-date": "Enter your date of birth",
|
|
76
|
+
...messages
|
|
77
|
+
},
|
|
78
|
+
parts: DATE_PARTS,
|
|
79
|
+
...rest
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
const getAutoComplete = (mode, part) => {
|
|
83
|
+
if (!mode) return void 0;
|
|
84
|
+
if (mode === "dob") {
|
|
85
|
+
if (part === "day") return "bday-day";
|
|
86
|
+
if (part === "month") return "bday-month";
|
|
87
|
+
if (part === "year") return "bday-year";
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
//#endregion
|
|
91
|
+
export { DATE_ERROR_MESSAGES, DATE_PARTS, getAutoComplete, validateDate, validateDateParts, validateDob };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { DATE_ERROR_MESSAGES, DATE_PARTS, DateErrorCode, DateErrorMessage, DateInputMode, DatePart, DateParts, DateValidation, DateValidationError, DateValidationOptions, DateValidationSuccess, getAutoComplete, validateDate, validateDateParts, validateDob } from "./dateinput-utils.js";
|
|
2
|
+
import { DateInput, DateInputChangeEvent, DateInputDay, DateInputMonth, DateInputPartProps, DateInputProps, DateInputYear } from "./DateInput.js";
|
|
3
|
+
export { DATE_ERROR_MESSAGES, DATE_PARTS, DateErrorCode, DateErrorMessage, DateInput, DateInput as default, DateInputChangeEvent, DateInputDay, DateInputMode, DateInputMonth, DateInputPartProps, DateInputProps, DateInputYear, DatePart, DateParts, DateValidation, DateValidationError, DateValidationOptions, DateValidationSuccess, getAutoComplete, validateDate, validateDateParts, validateDob };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { DATE_ERROR_MESSAGES, DATE_PARTS, getAutoComplete, validateDate, validateDateParts, validateDob } from "./dateinput-utils.js";
|
|
2
|
+
import { DateInput, DateInputDay, DateInputMonth, DateInputYear } from "./DateInput.js";
|
|
3
|
+
//#region src/components/DateInput/index.ts
|
|
4
|
+
var DateInput_default = DateInput;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { DATE_ERROR_MESSAGES, DATE_PARTS, DateInput, DateInputDay, DateInputMonth, DateInputYear, DateInput_default as default, getAutoComplete, validateDate, validateDateParts, validateDob };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { joinIds } from "../FieldMarkers/fieldmarkers-
|
|
2
|
+
import { joinIds } from "../FieldMarkers/fieldmarkers-internal.js";
|
|
3
3
|
import { createContext, useContext } from "react";
|
|
4
4
|
//#region src/components/Field/Field.context.ts
|
|
5
5
|
const FieldContext = createContext(null);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useId as useId$1 } from "../../hooks/useId.js";
|
|
3
3
|
import { forEachSlot, getSlotProp } from "../../utils/slots.js";
|
|
4
|
-
import { getMarkerIds } from "../FieldMarkers/fieldmarkers-
|
|
4
|
+
import { getMarkerIds } from "../FieldMarkers/fieldmarkers-internal.js";
|
|
5
5
|
import { FieldContext, useField } from "./Field.context.js";
|
|
6
6
|
import { Error } from "../FieldMarkers/Error.js";
|
|
7
7
|
import { Hint } from "../FieldMarkers/Hint.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region src/components/FieldMarkers/fieldmarkers-
|
|
1
|
+
//#region src/components/FieldMarkers/fieldmarkers-internal.ts
|
|
2
2
|
const joinIds = (...ids) => ids.filter(Boolean).join(" ") || void 0;
|
|
3
3
|
const getMarkerIds = (markers, baseId) => {
|
|
4
4
|
const hintId = markers.hint?.id ?? `${baseId}_hint`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useId } from "../../hooks/useId.js";
|
|
3
3
|
import { forEachSlot, getSlotProp } from "../../utils/slots.js";
|
|
4
|
-
import { getMarkerIds, joinIds } from "../FieldMarkers/fieldmarkers-
|
|
4
|
+
import { getMarkerIds, joinIds } from "../FieldMarkers/fieldmarkers-internal.js";
|
|
5
5
|
import { Error } from "../FieldMarkers/Error.js";
|
|
6
6
|
import { Hint } from "../FieldMarkers/Hint.js";
|
|
7
7
|
import { FieldsetContext, useFieldset } from "./Fieldset.context.js";
|