@coopdigital/react 1.30.1 → 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 +29 -8
- 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/Textarea/Textarea.js +14 -4
- 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/components/icons/AccountCircleFilledIcon.d.ts +2 -1
- package/dist/components/icons/AccountCircleIcon.d.ts +2 -1
- package/dist/components/icons/AccountFilledIcon.d.ts +2 -1
- package/dist/components/icons/AccountIcon.d.ts +2 -1
- package/dist/components/icons/AddIcon.d.ts +2 -1
- package/dist/components/icons/AppsFilledIcon.d.ts +2 -1
- package/dist/components/icons/AppsIcon.d.ts +2 -1
- package/dist/components/icons/ArrowDownIcon.d.ts +2 -1
- package/dist/components/icons/ArrowLeftIcon.d.ts +2 -1
- package/dist/components/icons/ArrowRightIcon.d.ts +2 -1
- package/dist/components/icons/ArrowUpIcon.d.ts +2 -1
- package/dist/components/icons/BackspaceFilledIcon.d.ts +2 -1
- package/dist/components/icons/BackspaceIcon.d.ts +2 -1
- package/dist/components/icons/BasketFilledIcon.d.ts +2 -1
- package/dist/components/icons/BasketIcon.d.ts +2 -1
- package/dist/components/icons/BellFilledIcon.d.ts +2 -1
- package/dist/components/icons/BellIcon.d.ts +2 -1
- package/dist/components/icons/BroomIcon.d.ts +2 -1
- package/dist/components/icons/CalendarFilledIcon.d.ts +2 -1
- package/dist/components/icons/CalendarIcon.d.ts +2 -1
- package/dist/components/icons/CashFilledIcon.d.ts +2 -1
- package/dist/components/icons/CashIcon.d.ts +2 -1
- package/dist/components/icons/ChevronDownIcon.d.ts +2 -1
- package/dist/components/icons/ChevronLeftIcon.d.ts +2 -1
- package/dist/components/icons/ChevronRightIcon.d.ts +2 -1
- package/dist/components/icons/ChevronUpIcon.d.ts +2 -1
- package/dist/components/icons/CircleBurgerFilledIcon.d.ts +2 -1
- package/dist/components/icons/CircleBurgerIcon.d.ts +2 -1
- package/dist/components/icons/ClipboardFilledIcon.d.ts +2 -1
- package/dist/components/icons/ClipboardIcon.d.ts +2 -1
- package/dist/components/icons/ClockFilledIcon.d.ts +2 -1
- package/dist/components/icons/ClockIcon.d.ts +2 -1
- package/dist/components/icons/CloseAltFilledIcon.d.ts +2 -1
- package/dist/components/icons/CloseAltIcon.d.ts +2 -1
- package/dist/components/icons/CogIcon.d.ts +2 -1
- package/dist/components/icons/CommunityFilledIcon.d.ts +2 -1
- package/dist/components/icons/CommunityIcon.d.ts +2 -1
- package/dist/components/icons/ComputerIcon.d.ts +2 -1
- package/dist/components/icons/ContactFilledIcon.d.ts +2 -1
- package/dist/components/icons/ContactIcon.d.ts +2 -1
- package/dist/components/icons/ContactlessFilledIcon.d.ts +2 -1
- package/dist/components/icons/ContactlessIcon.d.ts +2 -1
- package/dist/components/icons/CoopLiveIcon.d.ts +2 -1
- package/dist/components/icons/CoopLocationFilledIcon.d.ts +2 -1
- package/dist/components/icons/CoopLogoIcon.d.ts +2 -1
- package/dist/components/icons/CoopMembershipCardFilledIcon.d.ts +2 -1
- package/dist/components/icons/CreditCardFilledIcon.d.ts +2 -1
- package/dist/components/icons/CreditCardIcon.d.ts +2 -1
- package/dist/components/icons/CrossIcon.d.ts +2 -1
- package/dist/components/icons/DocumentIcon.d.ts +2 -1
- package/dist/components/icons/DownloadIcon.d.ts +2 -1
- package/dist/components/icons/EnterStoreIcon.d.ts +2 -1
- package/dist/components/icons/ExitStoreIcon.d.ts +2 -1
- package/dist/components/icons/EyeFilledIcon.d.ts +2 -1
- package/dist/components/icons/EyeIcon.d.ts +2 -1
- package/dist/components/icons/EyeStrikeFilledIcon.d.ts +2 -1
- package/dist/components/icons/EyeStrikeIcon.d.ts +2 -1
- package/dist/components/icons/FeedbackFilledIcon.d.ts +2 -1
- package/dist/components/icons/FeedbackIcon.d.ts +2 -1
- package/dist/components/icons/FilterIcon.d.ts +2 -1
- package/dist/components/icons/GiftCardFilledIcon.d.ts +2 -1
- package/dist/components/icons/GiftCardIcon.d.ts +2 -1
- package/dist/components/icons/GiftFilledIcon.d.ts +2 -1
- package/dist/components/icons/GiftIcon.d.ts +2 -1
- package/dist/components/icons/HelpFilledIcon.d.ts +2 -1
- package/dist/components/icons/HelpIcon.d.ts +2 -1
- package/dist/components/icons/HomeFilledIcon.d.ts +2 -1
- package/dist/components/icons/HomeIcon.d.ts +2 -1
- package/dist/components/icons/HowDoIAssistantIcon.d.ts +2 -1
- package/dist/components/icons/InfoIcon.d.ts +2 -1
- package/dist/components/icons/KebabIcon.d.ts +2 -1
- package/dist/components/icons/LoadingIcon.d.ts +2 -1
- package/dist/components/icons/LocationFilledIcon.d.ts +2 -1
- package/dist/components/icons/LocationIcon.d.ts +2 -1
- package/dist/components/icons/LogoutIcon.d.ts +2 -1
- package/dist/components/icons/MailIcon.d.ts +2 -1
- package/dist/components/icons/MenuIcon.d.ts +2 -1
- package/dist/components/icons/MessageIcon.d.ts +2 -1
- package/dist/components/icons/MinusIcon.d.ts +2 -1
- package/dist/components/icons/NotesFilledIcon.d.ts +2 -1
- package/dist/components/icons/NotesIcon.d.ts +2 -1
- package/dist/components/icons/OpenNewIcon.d.ts +2 -1
- package/dist/components/icons/OwnBagIcon.d.ts +2 -1
- package/dist/components/icons/PadlockIcon.d.ts +2 -1
- package/dist/components/icons/PencilIcon.d.ts +2 -1
- package/dist/components/icons/PhoneIcon.d.ts +2 -1
- package/dist/components/icons/PowerIcon.d.ts +2 -1
- package/dist/components/icons/SaveFilledIcon.d.ts +2 -1
- package/dist/components/icons/SaveIcon.d.ts +2 -1
- package/dist/components/icons/ScooterIcon.d.ts +2 -1
- package/dist/components/icons/SearchFilledIcon.d.ts +2 -1
- package/dist/components/icons/SearchIcon.d.ts +2 -1
- package/dist/components/icons/SendIcon.d.ts +2 -1
- package/dist/components/icons/SpannerIcon.d.ts +2 -1
- package/dist/components/icons/StarFilledIcon.d.ts +2 -1
- package/dist/components/icons/StarIcon.d.ts +2 -1
- package/dist/components/icons/TickAltFilledIcon.d.ts +2 -1
- package/dist/components/icons/TickAltIcon.d.ts +2 -1
- package/dist/components/icons/TickIcon.d.ts +2 -1
- package/dist/components/icons/TruckIcon.d.ts +2 -1
- package/dist/components/icons/UserWipeIcon.d.ts +2 -1
- package/dist/components/icons/VoucherFilledIcon.d.ts +2 -1
- package/dist/components/icons/VoucherIcon.d.ts +2 -1
- package/dist/components/icons/WarningCircleFilledIcon.d.ts +2 -1
- package/dist/components/icons/WarningCircleIcon.d.ts +2 -1
- package/dist/components/icons/WarningFilledIcon.d.ts +2 -1
- package/dist/components/icons/WarningIcon.d.ts +2 -1
- package/dist/components/icons/WifiIcon.d.ts +2 -1
- package/dist/components/icons/WriteIcon.d.ts +2 -1
- package/dist/hooks/index.d.ts +2 -1
- package/dist/hooks/index.js +2 -1
- package/dist/hooks/useFormReset.d.ts +6 -0
- package/dist/hooks/useFormReset.js +19 -0
- package/dist/hooks/useSlots.d.ts +2 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +6 -4
- package/dist/types/index.js +4 -0
- package/dist/utils/slots.d.ts +1 -1
- package/dist/utils/slots.js +20 -12
- package/package.json +22 -18
- package/src/components/Author/Author.tsx +1 -1
- package/src/components/Combobox/Combobox.context.ts +3 -1
- package/src/components/Combobox/Combobox.tsx +57 -24
- 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/Textarea/Textarea.tsx +19 -4
- 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/hooks/index.ts +1 -0
- package/src/hooks/useFormReset.ts +28 -0
- package/src/index.ts +1 -0
- package/src/utils/slots.ts +37 -23
- /package/src/components/FieldMarkers/{fieldmarkers-utils.ts → fieldmarkers-internal.ts} +0 -0
package/dist/utils/slots.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { LazyComponent, SlotConfig } from "../types/index.js";
|
|
|
2
2
|
import React, { ReactElement, ReactNode } from "react";
|
|
3
3
|
//#region src/utils/slots.d.ts
|
|
4
4
|
export declare function getSlotProp<R = unknown>(slot: React.ReactNode, prop: string): R | undefined;
|
|
5
|
-
export declare const forEachSlot: (children: ReactNode, fn: (slot: SlotConfig, child: ReactElement) => void) => void;
|
|
6
5
|
export declare function isLazy<T extends React.ComponentType<any>>(type: unknown): type is LazyComponent<T>;
|
|
6
|
+
export declare const forEachSlot: (children: ReactNode, fn: (slot: SlotConfig, child: ReactElement) => void) => void;
|
|
7
7
|
export declare function resolveComponentName(child: React.ReactNode): string | null;
|
|
8
8
|
export declare function getSlotName(node: React.ReactNode): string | false;
|
|
9
9
|
//#endregion
|
package/dist/utils/slots.js
CHANGED
|
@@ -1,26 +1,34 @@
|
|
|
1
|
+
import { REACT_LAZY_TYPE } from "../types/index.js";
|
|
1
2
|
import { isValidElement } from "react";
|
|
2
3
|
//#region src/utils/slots.ts
|
|
3
4
|
function getSlotProp(slot, prop) {
|
|
4
5
|
if (isValidElement(slot) && prop in slot.props) return slot.props[prop];
|
|
5
6
|
}
|
|
7
|
+
function isLazy(type) {
|
|
8
|
+
return typeof type === "object" && type !== null && type.$$typeof === REACT_LAZY_TYPE;
|
|
9
|
+
}
|
|
10
|
+
const resolveType = (type) => {
|
|
11
|
+
if (!isLazy(type)) return type;
|
|
12
|
+
try {
|
|
13
|
+
return type._init(type._payload);
|
|
14
|
+
} catch {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const getSlotConfig = (type) => {
|
|
19
|
+
const resolved = resolveType(type);
|
|
20
|
+
return typeof resolved === "function" && "slot" in resolved ? resolved.slot : void 0;
|
|
21
|
+
};
|
|
6
22
|
const forEachSlot = (children, fn) => {
|
|
7
23
|
Array.from([children].flat()).forEach((child) => {
|
|
8
|
-
if (isValidElement(child)
|
|
24
|
+
if (!isValidElement(child)) return;
|
|
25
|
+
const slot = getSlotConfig(child.type);
|
|
26
|
+
if (slot) fn(slot, child);
|
|
9
27
|
});
|
|
10
28
|
};
|
|
11
|
-
function isLazy(type) {
|
|
12
|
-
return typeof type === "object" && type !== null && "_payload" in type && "_init" in type;
|
|
13
|
-
}
|
|
14
29
|
function resolveComponentName(child) {
|
|
15
30
|
if (child && typeof child === "object" && "type" in child) {
|
|
16
|
-
const type = child.type;
|
|
17
|
-
if (isLazy(type)) try {
|
|
18
|
-
const resolved = type._init(type._payload);
|
|
19
|
-
if ("slot" in resolved) return resolved.slot.name;
|
|
20
|
-
return resolved.displayName ?? resolved.name ?? null;
|
|
21
|
-
} catch {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
31
|
+
const type = resolveType(child.type);
|
|
24
32
|
if (typeof type === "function") {
|
|
25
33
|
if ("slot" in type) return type.slot.name;
|
|
26
34
|
return type.displayName ?? type.name ?? null;
|
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.31.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
13
|
},
|
|
14
14
|
"./components/*": {
|
|
15
|
-
"types": "./dist/components
|
|
16
|
-
"default": "./dist/components
|
|
15
|
+
"types": "./dist/components/*/index.d.ts",
|
|
16
|
+
"default": "./dist/components/*/index.js"
|
|
17
17
|
},
|
|
18
|
+
"./components/icons": null,
|
|
18
19
|
"./icons": {
|
|
19
20
|
"types": "./dist/components/icons/index.d.ts",
|
|
20
21
|
"default": "./dist/components/icons/index.js"
|
|
@@ -25,7 +26,8 @@
|
|
|
25
26
|
"dist",
|
|
26
27
|
"src/**/*.{tsx,ts}",
|
|
27
28
|
"!**/__{screenshots,tests}__",
|
|
28
|
-
"!**/*.{stories,test}.*"
|
|
29
|
+
"!**/*.{stories,test}.*",
|
|
30
|
+
"!**/recipes/**"
|
|
29
31
|
],
|
|
30
32
|
"keywords": [
|
|
31
33
|
"co-op",
|
|
@@ -52,8 +54,8 @@
|
|
|
52
54
|
"test:behaviour:ci": "npm run test:e2e:ci -- -g behaviour",
|
|
53
55
|
"build": "tsx scripts build",
|
|
54
56
|
"build:docs": "tsx scripts build-docs",
|
|
55
|
-
"build:docs:all": "tsx scripts build-docs --all",
|
|
56
57
|
"build:icons": "tsx scripts build-icons",
|
|
58
|
+
"build:pattern-images": "tsx scripts build-pattern-images",
|
|
57
59
|
"build:manifest": "tsx scripts build-manifest",
|
|
58
60
|
"build:storybook": "storybook build --disable-telemetry",
|
|
59
61
|
"build:storybook:test": "storybook build --test --disable-telemetry --output-dir storybook-test",
|
|
@@ -67,23 +69,25 @@
|
|
|
67
69
|
"license": "MIT",
|
|
68
70
|
"description": "React components for the Experience Library design system",
|
|
69
71
|
"devDependencies": {
|
|
70
|
-
"@axe-core/playwright": "^4.
|
|
71
|
-
"@playwright/test": "^1.64.
|
|
72
|
-
"@storybook/addon-a11y": "^10.6.
|
|
73
|
-
"@storybook/addon-docs": "^10.6.
|
|
74
|
-
"@storybook/addon-links": "^10.6.
|
|
75
|
-
"@storybook/addon-onboarding": "^10.6.
|
|
76
|
-
"@storybook/react-vite": "^10.6.
|
|
72
|
+
"@axe-core/playwright": "^4.14.0",
|
|
73
|
+
"@playwright/test": "^1.64.1",
|
|
74
|
+
"@storybook/addon-a11y": "^10.6.2",
|
|
75
|
+
"@storybook/addon-docs": "^10.6.2",
|
|
76
|
+
"@storybook/addon-links": "^10.6.2",
|
|
77
|
+
"@storybook/addon-onboarding": "^10.6.2",
|
|
78
|
+
"@storybook/react-vite": "^10.6.2",
|
|
77
79
|
"@testing-library/jest-dom": "^7.0.1",
|
|
78
80
|
"@testing-library/react": "^16.3.3",
|
|
79
81
|
"@types/react": "^19.3.0",
|
|
80
82
|
"@types/react-dom": "^19.3.0",
|
|
81
83
|
"@whitespace/storybook-addon-html": "^9.0.0",
|
|
82
|
-
"react": "^19.3.
|
|
83
|
-
"react-dom": "^19.3.
|
|
84
|
+
"react": "^19.3.1",
|
|
85
|
+
"react-dom": "^19.3.1",
|
|
86
|
+
"react-element-to-jsx-string": "^18.0.0",
|
|
87
|
+
"react-hook-form": "^7.90.0",
|
|
84
88
|
"resize-observer-polyfill": "^1.5.1",
|
|
85
89
|
"serve": "^14.2.6",
|
|
86
|
-
"storybook": "^10.6.
|
|
90
|
+
"storybook": "^10.6.2",
|
|
87
91
|
"storybook-addon-tag-badges": "^3.1.0"
|
|
88
92
|
},
|
|
89
93
|
"peerDependencies": {
|
|
@@ -107,8 +111,8 @@
|
|
|
107
111
|
"storybook": "$storybook"
|
|
108
112
|
},
|
|
109
113
|
"dependencies": {
|
|
110
|
-
"@coopdigital/styles": "^1.
|
|
111
|
-
"@radix-ui/react-popover": "^1.
|
|
114
|
+
"@coopdigital/styles": "^1.22.0",
|
|
115
|
+
"@radix-ui/react-popover": "^1.3.1",
|
|
112
116
|
"clsx": "^2.1.1",
|
|
113
117
|
"date-fns": "^4.4.0",
|
|
114
118
|
"downshift": "^9.4.0",
|
|
@@ -116,5 +120,5 @@
|
|
|
116
120
|
"merge-refs": "^2.0.0",
|
|
117
121
|
"react-day-picker": "^10.0.2"
|
|
118
122
|
},
|
|
119
|
-
"gitHead": "
|
|
123
|
+
"gitHead": "6a649143b3381563d36754d873c1410eb307fdc4"
|
|
120
124
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import type { UseComboboxReturnValue } from "downshift"
|
|
3
3
|
|
|
4
|
-
import { createContext, useContext } from "react"
|
|
4
|
+
import { createContext, type RefObject, useContext } from "react"
|
|
5
5
|
|
|
6
6
|
import type { ComboboxItem } from "@/components/Combobox/combobox-utils"
|
|
7
7
|
|
|
@@ -11,6 +11,8 @@ interface ComboboxContextValue {
|
|
|
11
11
|
disabled: boolean
|
|
12
12
|
downshift: UseComboboxReturnValue<ComboboxItem>
|
|
13
13
|
error: boolean
|
|
14
|
+
// The visible input, which the form reset is looked up from
|
|
15
|
+
inputRef: RefObject<HTMLInputElement | null>
|
|
14
16
|
// The options currently shown, after filtering and the maxResults cap
|
|
15
17
|
items: ComboboxItem[]
|
|
16
18
|
// The number of options that match the input, before the cap
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client"
|
|
2
|
-
import type { ButtonHTMLAttributes, HTMLAttributes, JSX, ReactNode, Ref } from "react"
|
|
2
|
+
import type { ButtonHTMLAttributes, HTMLAttributes, JSX, MouseEvent, ReactNode, Ref } from "react"
|
|
3
3
|
|
|
4
4
|
import clsx from "clsx"
|
|
5
5
|
import {
|
|
@@ -10,28 +10,30 @@ import {
|
|
|
10
10
|
type UseComboboxState,
|
|
11
11
|
type UseComboboxStateChange,
|
|
12
12
|
} from "downshift"
|
|
13
|
+
import mergeRefs from "merge-refs"
|
|
13
14
|
import { useEffect, useMemo, useRef, useState } from "react"
|
|
14
15
|
|
|
16
|
+
import type { ComboboxItem, ComboboxOption } from "@/components/Combobox/combobox-utils"
|
|
17
|
+
|
|
15
18
|
import {
|
|
16
|
-
type ComboboxItem,
|
|
17
|
-
type ComboboxOption,
|
|
18
19
|
defaultFilter,
|
|
19
20
|
defaultIsItemDisabled,
|
|
20
21
|
defaultItemToString,
|
|
22
|
+
defaultMaxResultsText,
|
|
21
23
|
isMatchingText,
|
|
22
24
|
normalizeOptions,
|
|
23
25
|
resolveOption,
|
|
24
|
-
|
|
26
|
+
type SelectionSnapshot,
|
|
27
|
+
shouldShowSelectionText,
|
|
28
|
+
} from "@/components/Combobox/combobox-internal"
|
|
25
29
|
import { ComboboxContext, useComboboxContext } from "@/components/Combobox/Combobox.context"
|
|
26
30
|
import { getFieldError, useField } from "@/components/Field/Field.context"
|
|
27
31
|
import { ChevronDownIcon } from "@/components/icons"
|
|
28
32
|
import { TextInput, type TextInputProps } from "@/components/TextInput"
|
|
29
|
-
import { useDebounce, useId, useSlots } from "@/hooks"
|
|
33
|
+
import { useDebounce, useFormReset, useId, useSlots } from "@/hooks"
|
|
30
34
|
import { StandardSizes } from "@/types"
|
|
31
35
|
import { getSlotProp } from "@/utils"
|
|
32
36
|
|
|
33
|
-
export type { ComboboxItem, ComboboxOption } from "@/components/Combobox/combobox-utils"
|
|
34
|
-
|
|
35
37
|
export interface ComboboxSelectEvent {
|
|
36
38
|
/** Whether the selection is a custom value, entered by users when `allowCustom` is set,
|
|
37
39
|
* rather than one of the options.
|
|
@@ -189,9 +191,6 @@ type ComboboxState = UseComboboxState<ComboboxItem>
|
|
|
189
191
|
const DEFAULT_MAX_RESULTS = 50
|
|
190
192
|
const ANNOUNCEMENT_DELAY = 750
|
|
191
193
|
|
|
192
|
-
const defaultMaxResultsText = (shown: number, total: number): string =>
|
|
193
|
-
`Showing ${shown} of ${total} results.`
|
|
194
|
-
|
|
195
194
|
const componentSlots = {
|
|
196
195
|
ComboboxControls: null,
|
|
197
196
|
ComboboxOptions: null,
|
|
@@ -247,7 +246,9 @@ const Combobox = ({
|
|
|
247
246
|
)
|
|
248
247
|
|
|
249
248
|
const [initialSelectedItem] = useState(() => resolveOption(defaultSelected, items))
|
|
250
|
-
const
|
|
249
|
+
const defaultSelectedItem = resolveOption(defaultSelected, items)
|
|
250
|
+
const controlled = selected !== undefined
|
|
251
|
+
const selectedItem = controlled ? resolveOption(selected, items) : undefined
|
|
251
252
|
|
|
252
253
|
// Work out the selection from the text in the input, so the two always
|
|
253
254
|
// agree. Returns null when the text does not match and custom values are
|
|
@@ -304,6 +305,13 @@ const Combobox = ({
|
|
|
304
305
|
}
|
|
305
306
|
|
|
306
307
|
const downshift = useCombobox<ComboboxItem>({
|
|
308
|
+
// reset() goes back to the current default selection and its text, as a
|
|
309
|
+
// native field goes back to its current default value when its form is
|
|
310
|
+
// reset. A controlled selection is left to the consumer.
|
|
311
|
+
...(!controlled && {
|
|
312
|
+
defaultInputValue: toString(defaultSelectedItem),
|
|
313
|
+
defaultSelectedItem,
|
|
314
|
+
}),
|
|
307
315
|
getItemId: (index) => visibleItems[index]?.id ?? `${uid}_item_${index}`,
|
|
308
316
|
id: uid,
|
|
309
317
|
initialSelectedItem,
|
|
@@ -343,34 +351,42 @@ const Combobox = ({
|
|
|
343
351
|
},
|
|
344
352
|
})
|
|
345
353
|
|
|
346
|
-
//
|
|
347
|
-
// so show the option text once it is known. Text that users are typing is
|
|
348
|
-
// left alone.
|
|
354
|
+
// Keep the input text in step with the selection
|
|
349
355
|
const selection = resolveOption(downshift.selectedItem, items)
|
|
350
356
|
const selectionText = toString(selection)
|
|
351
357
|
const selectionValue = selection?.value
|
|
352
|
-
const previousSelection = useRef({
|
|
353
|
-
|
|
358
|
+
const previousSelection = useRef<SelectionSnapshot>({
|
|
359
|
+
text: selectionText,
|
|
360
|
+
value: selectionValue,
|
|
361
|
+
})
|
|
362
|
+
const { inputValue, reset, setInputValue } = downshift
|
|
354
363
|
|
|
355
364
|
useEffect(() => {
|
|
356
365
|
const previous = previousSelection.current
|
|
357
|
-
|
|
366
|
+
const current = { text: selectionText, value: selectionValue }
|
|
367
|
+
previousSelection.current = current
|
|
358
368
|
|
|
359
|
-
if (
|
|
360
|
-
selectionValue === previous.value &&
|
|
361
|
-
selectionText !== previous.text &&
|
|
362
|
-
inputValue === previous.text
|
|
363
|
-
) {
|
|
369
|
+
if (shouldShowSelectionText(previous, current, inputValue)) {
|
|
364
370
|
setInputValue(selectionText)
|
|
365
371
|
}
|
|
366
372
|
}, [inputValue, selectionText, selectionValue, setInputValue])
|
|
367
373
|
|
|
374
|
+
// When the form is reset, go back to the default selection
|
|
375
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
376
|
+
|
|
377
|
+
useFormReset(inputRef, () => {
|
|
378
|
+
if (controlled) return
|
|
379
|
+
reset()
|
|
380
|
+
setFilterText("")
|
|
381
|
+
})
|
|
382
|
+
|
|
368
383
|
return (
|
|
369
384
|
<ComboboxContext.Provider
|
|
370
385
|
value={{
|
|
371
386
|
disabled,
|
|
372
387
|
downshift,
|
|
373
388
|
error: hasError,
|
|
389
|
+
inputRef,
|
|
374
390
|
items: visibleItems,
|
|
375
391
|
matchCount: matchingItems.length,
|
|
376
392
|
size,
|
|
@@ -407,7 +423,11 @@ const ComboboxInput = ({ error, name, size, ...props }: ComboboxInputProps): JSX
|
|
|
407
423
|
|
|
408
424
|
// Pass the props through downshift, so it combines its handlers and ref
|
|
409
425
|
// with those given here
|
|
410
|
-
const inputProps = combobox.downshift.getInputProps({
|
|
426
|
+
const inputProps = combobox.downshift.getInputProps({
|
|
427
|
+
...props,
|
|
428
|
+
disabled,
|
|
429
|
+
ref: mergeRefs(combobox.inputRef, props.ref),
|
|
430
|
+
})
|
|
411
431
|
|
|
412
432
|
// The visible input shows the option text, which can be a label, so it has
|
|
413
433
|
// no name. The form submits the selected value from a hidden input
|
|
@@ -469,9 +489,22 @@ const ComboboxOptions = ({
|
|
|
469
489
|
const message = getMessage()
|
|
470
490
|
const announcement = useDebounce(message, ANNOUNCEMENT_DELAY)
|
|
471
491
|
|
|
492
|
+
// downshift bug: a mouse leave after the menu closes is handled with the
|
|
493
|
+
// props from before a new controlled selection, so it is ignored
|
|
494
|
+
const onMouseLeave = (event: MouseEvent<HTMLUListElement>) => {
|
|
495
|
+
props.onMouseLeave?.(event)
|
|
496
|
+
if (!isOpen) Object.assign(event, { preventDownshiftDefault: true })
|
|
497
|
+
}
|
|
498
|
+
|
|
472
499
|
return (
|
|
473
500
|
<>
|
|
474
|
-
<ul
|
|
501
|
+
<ul
|
|
502
|
+
{...getMenuProps({
|
|
503
|
+
...props,
|
|
504
|
+
className: clsx("coop-combobox-options", className),
|
|
505
|
+
onMouseLeave,
|
|
506
|
+
})}
|
|
507
|
+
>
|
|
475
508
|
{isOpen &&
|
|
476
509
|
items.map((item, index) => {
|
|
477
510
|
// Keep the option fields off the element, and pass its handlers
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { matchSorter } from "match-sorter"
|
|
2
|
+
|
|
3
|
+
import type { ComboboxItem, ComboboxOption } from "./combobox-utils"
|
|
4
|
+
|
|
5
|
+
export const defaultFilter = (items: ComboboxItem[], inputValue: string): ComboboxItem[] =>
|
|
6
|
+
inputValue !== ""
|
|
7
|
+
? matchSorter(items, inputValue, {
|
|
8
|
+
keys: [(item) => [item.label ?? item.value]],
|
|
9
|
+
})
|
|
10
|
+
: items
|
|
11
|
+
|
|
12
|
+
export const defaultIsItemDisabled = (item: ComboboxItem): boolean => item.disabled ?? false
|
|
13
|
+
|
|
14
|
+
export const defaultItemToString = (item: ComboboxItem | null): string =>
|
|
15
|
+
item?.label ?? item?.value ?? ""
|
|
16
|
+
|
|
17
|
+
export const defaultMaxResultsText = (shown: number, total: number): string =>
|
|
18
|
+
`Showing ${shown} of ${total} results.`
|
|
19
|
+
|
|
20
|
+
// Convert options given as strings or objects into a list of items
|
|
21
|
+
export const normalizeOptions = (options: ComboboxOption[]): ComboboxItem[] =>
|
|
22
|
+
options
|
|
23
|
+
.filter((option) => option && typeof option !== "number")
|
|
24
|
+
.map((option) => (typeof option === "string" ? { value: option } : option))
|
|
25
|
+
|
|
26
|
+
// Created once, as localeCompare with options creates a new collator on
|
|
27
|
+
// every call, which is slow when checking a long list of options
|
|
28
|
+
const collator = new Intl.Collator(undefined, { sensitivity: "base" })
|
|
29
|
+
|
|
30
|
+
// Check whether typed text matches an option, ignoring case, accents and
|
|
31
|
+
// surrounding whitespace
|
|
32
|
+
export const isMatchingText = (optionText: string, text: string): boolean =>
|
|
33
|
+
collator.compare(optionText.trim(), text.trim()) === 0
|
|
34
|
+
|
|
35
|
+
// Find the item for an option given as a value or an object. An option that
|
|
36
|
+
// is not in the list, such as a custom value, is kept as it is.
|
|
37
|
+
export const resolveOption = (
|
|
38
|
+
option: ComboboxOption | null | undefined,
|
|
39
|
+
items: ComboboxItem[]
|
|
40
|
+
): ComboboxItem | null => {
|
|
41
|
+
if (option === null || option === undefined) return null
|
|
42
|
+
|
|
43
|
+
const item = typeof option === "string" ? { value: option } : option
|
|
44
|
+
return items.find(({ value }) => value === item.value) ?? item
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SelectionSnapshot {
|
|
48
|
+
text: string
|
|
49
|
+
value: string | undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Whether the input should change to the text of the selection. Options can
|
|
53
|
+
// arrive after the selection, such as when they are fetched, so show the
|
|
54
|
+
// option text once it is known, unless users are typing.
|
|
55
|
+
export const shouldShowSelectionText = (
|
|
56
|
+
previous: SelectionSnapshot,
|
|
57
|
+
current: SelectionSnapshot,
|
|
58
|
+
inputValue: string
|
|
59
|
+
): boolean =>
|
|
60
|
+
current.value === previous.value && current.text !== previous.text && inputValue === previous.text
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { HTMLAttributes } from "react"
|
|
1
|
+
import type { HTMLAttributes } from "react"
|
|
3
2
|
|
|
4
3
|
export interface ComboboxItem extends HTMLAttributes<HTMLLIElement> {
|
|
5
4
|
/** Specify the disabled state.
|
|
@@ -19,42 +18,3 @@ export interface ComboboxItem extends HTMLAttributes<HTMLLIElement> {
|
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
export type ComboboxOption = ComboboxItem | string
|
|
22
|
-
|
|
23
|
-
export const defaultFilter = (items: ComboboxItem[], inputValue: string): ComboboxItem[] =>
|
|
24
|
-
inputValue !== ""
|
|
25
|
-
? matchSorter(items, inputValue, {
|
|
26
|
-
keys: [(item) => [item.label ?? item.value]],
|
|
27
|
-
})
|
|
28
|
-
: items
|
|
29
|
-
|
|
30
|
-
export const defaultIsItemDisabled = (item: ComboboxItem): boolean => item.disabled ?? false
|
|
31
|
-
|
|
32
|
-
export const defaultItemToString = (item: ComboboxItem | null): string =>
|
|
33
|
-
item?.label ?? item?.value ?? ""
|
|
34
|
-
|
|
35
|
-
// Convert options given as strings or objects into a list of items
|
|
36
|
-
export const normalizeOptions = (options: ComboboxOption[]): ComboboxItem[] =>
|
|
37
|
-
options
|
|
38
|
-
.filter((option) => option && typeof option !== "number")
|
|
39
|
-
.map((option) => (typeof option === "string" ? { value: option } : option))
|
|
40
|
-
|
|
41
|
-
// Created once, as localeCompare with options creates a new collator on
|
|
42
|
-
// every call, which is slow when checking a long list of options
|
|
43
|
-
const collator = new Intl.Collator(undefined, { sensitivity: "base" })
|
|
44
|
-
|
|
45
|
-
// Check whether typed text matches an option, ignoring case, accents and
|
|
46
|
-
// surrounding whitespace
|
|
47
|
-
export const isMatchingText = (optionText: string, text: string): boolean =>
|
|
48
|
-
collator.compare(optionText.trim(), text.trim()) === 0
|
|
49
|
-
|
|
50
|
-
// Find the item for an option given as a value or an object. An option that
|
|
51
|
-
// is not in the list, such as a custom value, is kept as it is.
|
|
52
|
-
export const resolveOption = (
|
|
53
|
-
option: ComboboxOption | null | undefined,
|
|
54
|
-
items: ComboboxItem[]
|
|
55
|
-
): ComboboxItem | null => {
|
|
56
|
-
if (option === null || option === undefined) return null
|
|
57
|
-
|
|
58
|
-
const item = typeof option === "string" ? { value: option } : option
|
|
59
|
-
return items.find(({ value }) => value === item.value) ?? item
|
|
60
|
-
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import { createContext, type RefObject, useContext } from "react"
|
|
3
|
+
|
|
4
|
+
import type { StandardSizes } from "@/types"
|
|
5
|
+
|
|
6
|
+
import type { DateInputMode, DatePart, DateParts } from "./dateinput-utils"
|
|
7
|
+
|
|
8
|
+
export interface DateInputContextValue {
|
|
9
|
+
changePart: (part: DatePart, value: string) => void
|
|
10
|
+
controlled: boolean
|
|
11
|
+
// The first rendered part attaches controlRef, which the form reset is
|
|
12
|
+
// looked up from
|
|
13
|
+
controlPart?: DatePart
|
|
14
|
+
controlRef: RefObject<HTMLInputElement | null>
|
|
15
|
+
defaultValue?: DateParts
|
|
16
|
+
id: string
|
|
17
|
+
mode?: DateInputMode
|
|
18
|
+
name?: string
|
|
19
|
+
parts: DateParts
|
|
20
|
+
size?: StandardSizes
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const DateInputContext = createContext<DateInputContextValue | null>(null)
|
|
24
|
+
|
|
25
|
+
export const useDateInput = () => useContext(DateInputContext)
|