@coopdigital/react 1.22.5 → 1.23.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/Combobox/Combobox.context.js +11 -0
- package/dist/components/Combobox/Combobox.d.ts +124 -0
- package/dist/components/Combobox/Combobox.js +166 -0
- package/dist/components/Combobox/combobox-utils.d.ts +20 -0
- package/dist/components/Combobox/combobox-utils.js +10 -0
- package/dist/components/Combobox/index.d.ts +2 -0
- package/dist/components/Combobox/index.js +5 -0
- package/dist/components/DatePicker/DatePicker.d.ts +3 -6
- package/dist/components/DatePicker/DatePicker.js +1 -1
- package/dist/components/DatePicker/datepicker-utils.d.ts +6 -0
- package/dist/components/DatePicker/index.d.ts +2 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -2
- package/package.json +9 -4
- package/src/components/Combobox/Combobox.context.ts +27 -0
- package/src/components/Combobox/Combobox.tsx +323 -0
- package/src/components/Combobox/combobox-utils.ts +39 -0
- package/src/components/Combobox/index.ts +5 -0
- package/src/components/DatePicker/DatePicker.tsx +4 -3
- package/src/components/DatePicker/datepicker-utils.ts +1 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
//#region src/components/Combobox/Combobox.context.ts
|
|
4
|
+
const ComboboxContext = createContext({});
|
|
5
|
+
const useComboboxContext = () => {
|
|
6
|
+
const context = useContext(ComboboxContext);
|
|
7
|
+
if (!context) throw new Error("Combobox components must be used inside <Combobox>");
|
|
8
|
+
return context;
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
11
|
+
export { ComboboxContext, useComboboxContext };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { ComboboxItem, ComboboxOption } from "./combobox-utils.js";
|
|
2
|
+
import { TextInputProps } from "../TextInput/TextInput.js";
|
|
3
|
+
import "../TextInput/index.js";
|
|
4
|
+
import { ButtonHTMLAttributes, HTMLAttributes, JSX, ReactNode, Ref } from "react";
|
|
5
|
+
import { UseComboboxHighlightedIndexChange, UseComboboxInputValueChange, UseComboboxIsOpenChange, UseComboboxSelectedItemChange, UseComboboxStateChange } from "downshift";
|
|
6
|
+
//#region src/components/Combobox/Combobox.d.ts
|
|
7
|
+
export interface ComboboxProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "onSelect" | "onToggle"> {
|
|
8
|
+
/** Specify whether custom values are accepted.
|
|
9
|
+
*
|
|
10
|
+
* Default: `false`
|
|
11
|
+
*/
|
|
12
|
+
allowCustom?: boolean;
|
|
13
|
+
/** Content inside the component.
|
|
14
|
+
*
|
|
15
|
+
* This should be a set of `<Combobox.Controls>` and `<Combobox.Options>`.
|
|
16
|
+
*/
|
|
17
|
+
children?: string | ReactNode;
|
|
18
|
+
/** Specify additional CSS classes. */
|
|
19
|
+
className?: string;
|
|
20
|
+
/** Specify the disabled state. */
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
/** Specify the component error state. */
|
|
23
|
+
error?: boolean;
|
|
24
|
+
/** Specify the component id. Will be auto-generated if not set. */
|
|
25
|
+
id?: string;
|
|
26
|
+
/** Custom function to filter the list of options.
|
|
27
|
+
*
|
|
28
|
+
* Takes a `ComboboxItem` array and an input string, and returns a filtered array.
|
|
29
|
+
*
|
|
30
|
+
* Default: Filters on item label or value: `item.label ?? item.value`.
|
|
31
|
+
*/
|
|
32
|
+
inputFilter?: (items: ComboboxItem[], value: string) => ComboboxItem[];
|
|
33
|
+
/** Custom function to mark item as disabled.
|
|
34
|
+
*
|
|
35
|
+
* Default: `disabled` prop on `.ComboboxOption`
|
|
36
|
+
*/
|
|
37
|
+
isItemDisabled?: (item: ComboboxItem) => boolean;
|
|
38
|
+
/** Custom function to render text in the input field after selecting a value.
|
|
39
|
+
*
|
|
40
|
+
* Default: Uses item label or value: `item.label ?? item.value`.
|
|
41
|
+
*/
|
|
42
|
+
itemToString?: (item: ComboboxItem | null) => string;
|
|
43
|
+
/** Callback fired when the input value changes.
|
|
44
|
+
*
|
|
45
|
+
* This is a `downshift` event.
|
|
46
|
+
*/
|
|
47
|
+
onChange?: (event: UseComboboxInputValueChange<ComboboxItem>) => void;
|
|
48
|
+
/** Callback fired when users highlight an option in the list using the mouse or keyboard.
|
|
49
|
+
*
|
|
50
|
+
* This is a `downshift` event.
|
|
51
|
+
*/
|
|
52
|
+
onHighlight?: (event: UseComboboxHighlightedIndexChange<ComboboxItem>) => void;
|
|
53
|
+
/** Callback fired when users select an option from the list.
|
|
54
|
+
*
|
|
55
|
+
* This is a `downshift` event.
|
|
56
|
+
*/
|
|
57
|
+
onSelect?: (event: UseComboboxSelectedItemChange<ComboboxItem>) => void;
|
|
58
|
+
/** Callback fired whenever the state changes.
|
|
59
|
+
*
|
|
60
|
+
* This is a `downshift` event.
|
|
61
|
+
*/
|
|
62
|
+
onStateChange?: (event: UseComboboxStateChange<ComboboxItem>) => void;
|
|
63
|
+
/** Callback fired when the menu opens or closes.
|
|
64
|
+
*
|
|
65
|
+
* This is a `downshift` event.
|
|
66
|
+
*/
|
|
67
|
+
onToggle?: (changes: UseComboboxIsOpenChange<ComboboxItem>) => void;
|
|
68
|
+
/** Specify whether the menu should appear immediately when clicking the input field.
|
|
69
|
+
*
|
|
70
|
+
* Default: `true`.
|
|
71
|
+
*/
|
|
72
|
+
openOnClick?: boolean;
|
|
73
|
+
/** Specify placeholder text. Do not use in place of a form label. */
|
|
74
|
+
placeholder?: string;
|
|
75
|
+
/** Specify a custom React ref for this component. */
|
|
76
|
+
ref?: Ref<HTMLDivElement>;
|
|
77
|
+
/** Specify the component size. */
|
|
78
|
+
size?: "sm" | "md" | "lg";
|
|
79
|
+
}
|
|
80
|
+
export interface ComboboxControlsProps extends HTMLAttributes<HTMLDivElement> {
|
|
81
|
+
/** Content inside the component.
|
|
82
|
+
*
|
|
83
|
+
* This should be a set of `<Combobox.Input>` and `<Combobox.Button>`.
|
|
84
|
+
*/
|
|
85
|
+
children?: React.ReactNode;
|
|
86
|
+
/** Specify additional CSS classes. */
|
|
87
|
+
className?: string;
|
|
88
|
+
}
|
|
89
|
+
export interface ComboboxOptionsProps extends HTMLAttributes<HTMLUListElement> {
|
|
90
|
+
/** Specify additional CSS classes. */
|
|
91
|
+
className?: string;
|
|
92
|
+
/** Specify text to display when there are no matching options. */
|
|
93
|
+
noResultsText?: string;
|
|
94
|
+
/** Specify array of options to choose from.
|
|
95
|
+
*
|
|
96
|
+
* Can be either a simple string array or a complex `ComboboxOption` array.
|
|
97
|
+
*/
|
|
98
|
+
options: ComboboxOption[];
|
|
99
|
+
}
|
|
100
|
+
export type ComboboxInputProps = TextInputProps;
|
|
101
|
+
export declare const Combobox: {
|
|
102
|
+
({ allowCustom, children, className, disabled, error, id, inputFilter, isItemDisabled, itemToString, onChange, onHighlight, onSelect, onStateChange, onToggle, openOnClick, ref, size, ...props }: ComboboxProps): JSX.Element | null;
|
|
103
|
+
slot: {
|
|
104
|
+
field: boolean;
|
|
105
|
+
name: string;
|
|
106
|
+
suffix: string;
|
|
107
|
+
};
|
|
108
|
+
Controls: {
|
|
109
|
+
({ children, className, ...props }: ComboboxControlsProps): JSX.Element;
|
|
110
|
+
slot: {
|
|
111
|
+
name: string;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
Input: ({ className, placeholder, ...props }: ComboboxInputProps) => JSX.Element;
|
|
115
|
+
Button: ({ children, className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) => JSX.Element;
|
|
116
|
+
Options: {
|
|
117
|
+
({ className, noResultsText, options, ...props }: ComboboxOptionsProps): JSX.Element | null;
|
|
118
|
+
slot: {
|
|
119
|
+
name: string;
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
//#endregion
|
|
124
|
+
export { Combobox as default };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { ChevronDownIcon } from "../icons/ChevronDownIcon.js";
|
|
3
|
+
import { getSlotProp } from "../../utils/index.js";
|
|
4
|
+
import { useSlots } from "../../hooks/useSlots.js";
|
|
5
|
+
import { useField } from "../Field/Field.context.js";
|
|
6
|
+
import { defaultFilter, defaultIsItemDisabled, defaultItemToString, normalizeOptions } from "./combobox-utils.js";
|
|
7
|
+
import { ComboboxContext, useComboboxContext } from "./Combobox.context.js";
|
|
8
|
+
import { TextInput } from "../TextInput/TextInput.js";
|
|
9
|
+
import clsx from "clsx";
|
|
10
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
import { useState } from "react";
|
|
12
|
+
import { useCombobox } from "downshift";
|
|
13
|
+
//#region src/components/Combobox/Combobox.tsx
|
|
14
|
+
const componentSlots = {
|
|
15
|
+
ComboboxControls: null,
|
|
16
|
+
ComboboxOptions: null
|
|
17
|
+
};
|
|
18
|
+
const Combobox = ({ allowCustom = false, children, className, disabled, error, id, inputFilter, isItemDisabled, itemToString, onChange, onHighlight, onSelect, onStateChange, onToggle, openOnClick = true, ref, size, ...props }) => {
|
|
19
|
+
const field = useField();
|
|
20
|
+
const uid = id ?? field?.id;
|
|
21
|
+
const slots = useSlots(componentSlots, children);
|
|
22
|
+
const userOptions = getSlotProp(slots.ComboboxOptions, "options") ?? [];
|
|
23
|
+
const items = normalizeOptions(userOptions);
|
|
24
|
+
const [inputItems, setInputItems] = useState(items);
|
|
25
|
+
const componentProps = {
|
|
26
|
+
className: clsx("coop-combobox", className),
|
|
27
|
+
"data-error": error ?? void 0,
|
|
28
|
+
"data-size": size ?? void 0,
|
|
29
|
+
id: uid,
|
|
30
|
+
...props
|
|
31
|
+
};
|
|
32
|
+
const inputFilterActual = inputFilter ?? defaultFilter;
|
|
33
|
+
const itemToStringActual = itemToString ?? defaultItemToString;
|
|
34
|
+
const comboboxProps = useCombobox({
|
|
35
|
+
id: uid,
|
|
36
|
+
inputId: uid + "_input",
|
|
37
|
+
isItemDisabled: isItemDisabled ?? defaultIsItemDisabled,
|
|
38
|
+
items: inputItems,
|
|
39
|
+
itemToString: itemToStringActual,
|
|
40
|
+
labelId: uid + "_label",
|
|
41
|
+
menuId: uid + "_menu",
|
|
42
|
+
onHighlightedIndexChange: onHighlight,
|
|
43
|
+
onInputValueChange: (event) => {
|
|
44
|
+
setInputItems(inputFilterActual(items, event.inputValue));
|
|
45
|
+
onChange?.(event);
|
|
46
|
+
},
|
|
47
|
+
onIsOpenChange: onToggle,
|
|
48
|
+
onSelectedItemChange: (event) => {
|
|
49
|
+
onSelect?.(event);
|
|
50
|
+
},
|
|
51
|
+
onStateChange,
|
|
52
|
+
stateReducer(_, actionAndChanges) {
|
|
53
|
+
const { InputBlur, InputClick, InputKeyDownEnter } = useCombobox.stateChangeTypes;
|
|
54
|
+
const { changes: state, type } = actionAndChanges;
|
|
55
|
+
const currentInput = state.inputValue ?? "";
|
|
56
|
+
const hasMatch = items.some((item) => itemToStringActual(item) === currentInput);
|
|
57
|
+
switch (type) {
|
|
58
|
+
case openOnClick === false && InputClick: return {
|
|
59
|
+
...state,
|
|
60
|
+
isOpen: false
|
|
61
|
+
};
|
|
62
|
+
case allowCustom === false && InputBlur: return hasMatch ? state : {
|
|
63
|
+
...state,
|
|
64
|
+
inputValue: ""
|
|
65
|
+
};
|
|
66
|
+
case allowCustom === false && InputKeyDownEnter: return hasMatch ? state : {
|
|
67
|
+
...state,
|
|
68
|
+
isOpen: true
|
|
69
|
+
};
|
|
70
|
+
default: return state;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return /* @__PURE__ */ jsx(ComboboxContext.Provider, {
|
|
75
|
+
value: {
|
|
76
|
+
disabled,
|
|
77
|
+
items: inputItems,
|
|
78
|
+
props: comboboxProps,
|
|
79
|
+
size
|
|
80
|
+
},
|
|
81
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
82
|
+
...componentProps,
|
|
83
|
+
ref,
|
|
84
|
+
children: [slots.ComboboxControls, slots.ComboboxOptions]
|
|
85
|
+
})
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
const ComboboxInput = ({ className, placeholder, ...props }) => {
|
|
89
|
+
const combobox = useComboboxContext();
|
|
90
|
+
const componentProps = {
|
|
91
|
+
className,
|
|
92
|
+
disabled: combobox.disabled,
|
|
93
|
+
placeholder,
|
|
94
|
+
size: combobox.size,
|
|
95
|
+
...props
|
|
96
|
+
};
|
|
97
|
+
return /* @__PURE__ */ jsx(TextInput, {
|
|
98
|
+
...componentProps,
|
|
99
|
+
...combobox.props?.getInputProps({})
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
const ComboboxButton = ({ children, className, ...props }) => {
|
|
103
|
+
const combobox = useComboboxContext();
|
|
104
|
+
const componentProps = {
|
|
105
|
+
"aria-label": props["aria-label"] ?? "Toggle menu",
|
|
106
|
+
className,
|
|
107
|
+
disabled: combobox.disabled,
|
|
108
|
+
...props
|
|
109
|
+
};
|
|
110
|
+
return /* @__PURE__ */ jsx("button", {
|
|
111
|
+
type: "button",
|
|
112
|
+
...componentProps,
|
|
113
|
+
...combobox.props?.getToggleButtonProps(),
|
|
114
|
+
children: children ?? /* @__PURE__ */ jsx(ChevronDownIcon, {
|
|
115
|
+
stroke: "black",
|
|
116
|
+
strokeWidth: 1,
|
|
117
|
+
width: "1.25rem"
|
|
118
|
+
})
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
const ComboboxControls = ({ children, className, ...props }) => {
|
|
122
|
+
const componentProps = {
|
|
123
|
+
className: clsx("coop-combobox-controls", className),
|
|
124
|
+
...props
|
|
125
|
+
};
|
|
126
|
+
return /* @__PURE__ */ jsx("div", {
|
|
127
|
+
...componentProps,
|
|
128
|
+
children
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
const ComboboxOptions = ({ className, noResultsText = "No results", options, ...props }) => {
|
|
132
|
+
const combobox = useComboboxContext();
|
|
133
|
+
const componentProps = {
|
|
134
|
+
className: clsx("coop-combobox-options", className),
|
|
135
|
+
...props
|
|
136
|
+
};
|
|
137
|
+
if (!options) return null;
|
|
138
|
+
return /* @__PURE__ */ jsxs("ul", {
|
|
139
|
+
...componentProps,
|
|
140
|
+
...combobox.props?.getMenuProps(),
|
|
141
|
+
children: [combobox.props?.isOpen && combobox.items?.length ? combobox.items.map((item, index) => {
|
|
142
|
+
const itemProps = { ...combobox.props?.getItemProps({
|
|
143
|
+
index,
|
|
144
|
+
item
|
|
145
|
+
}) };
|
|
146
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
147
|
+
...itemProps,
|
|
148
|
+
...item,
|
|
149
|
+
children: [item?.icon, item.children ?? item.label ?? item.value]
|
|
150
|
+
}, item.value);
|
|
151
|
+
}) : null, combobox.props?.isOpen && !combobox.items?.length && /* @__PURE__ */ jsx("li", { children: noResultsText })]
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
Combobox.slot = {
|
|
155
|
+
field: true,
|
|
156
|
+
name: "Combobox",
|
|
157
|
+
suffix: "_input"
|
|
158
|
+
};
|
|
159
|
+
ComboboxControls.slot = { name: "ComboboxControls" };
|
|
160
|
+
ComboboxOptions.slot = { name: "ComboboxOptions" };
|
|
161
|
+
Combobox.Controls = ComboboxControls;
|
|
162
|
+
Combobox.Input = ComboboxInput;
|
|
163
|
+
Combobox.Button = ComboboxButton;
|
|
164
|
+
Combobox.Options = ComboboxOptions;
|
|
165
|
+
//#endregion
|
|
166
|
+
export { Combobox, Combobox as default };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { HTMLAttributes } from "react";
|
|
2
|
+
//#region src/components/Combobox/combobox-utils.d.ts
|
|
3
|
+
export interface ComboboxItem extends HTMLAttributes<HTMLLIElement> {
|
|
4
|
+
/** Specify the disabled state.
|
|
5
|
+
*
|
|
6
|
+
* Options that are disabled still appear in the list but cannot be selected.
|
|
7
|
+
*/
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
/** Specify an icon to render before the item text. */
|
|
10
|
+
icon?: React.ReactNode;
|
|
11
|
+
/** Specify text to display to users if the underlying value needs to be different.
|
|
12
|
+
*
|
|
13
|
+
* If this value is provided, this is the string that gets filtered when the user types.
|
|
14
|
+
*/
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Specify the option value. */
|
|
17
|
+
value: string;
|
|
18
|
+
}
|
|
19
|
+
export type ComboboxOption = ComboboxItem | string;
|
|
20
|
+
//#endregion
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { matchSorter } from "match-sorter";
|
|
2
|
+
//#region src/components/Combobox/combobox-utils.ts
|
|
3
|
+
const defaultFilter = (items, inputValue) => {
|
|
4
|
+
return inputValue !== "" ? matchSorter(items, inputValue, { keys: [(item) => [item.label ?? item.value]] }) : items;
|
|
5
|
+
};
|
|
6
|
+
const defaultIsItemDisabled = (item) => item.disabled ?? false;
|
|
7
|
+
const defaultItemToString = (item) => item?.label ?? item?.value ?? "";
|
|
8
|
+
const normalizeOptions = (options) => options.filter((option) => option && typeof option !== "number").map((option) => typeof option === "string" ? { value: option } : option);
|
|
9
|
+
//#endregion
|
|
10
|
+
export { defaultFilter, defaultIsItemDisabled, defaultItemToString, normalizeOptions };
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { StandardSizes } from "../../types/index.js";
|
|
2
|
-
import { DateRange } from "./datepicker-utils.js";
|
|
2
|
+
import { DateRange, DateType, DateTypeStr } from "./datepicker-utils.js";
|
|
3
3
|
import { InputHTMLAttributes, JSX, Ref } from "react";
|
|
4
4
|
import { DayPickerProps, Mode } from "react-day-picker";
|
|
5
5
|
//#region src/components/DatePicker/DatePicker.d.ts
|
|
6
|
-
type OnDateSelectHandler = (selected:
|
|
7
|
-
from?: string;
|
|
8
|
-
to?: string;
|
|
9
|
-
} | undefined) => void;
|
|
6
|
+
type OnDateSelectHandler = (selected: DateTypeStr) => void;
|
|
10
7
|
export interface DatePickerProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onSelect" | "onChange" | "onChangeCapture" | "size" | "type"> {
|
|
11
8
|
/** Specify additional CSS classes. */
|
|
12
9
|
className?: string;
|
|
@@ -65,4 +62,4 @@ declare const DatePicker: {
|
|
|
65
62
|
};
|
|
66
63
|
};
|
|
67
64
|
//#endregion
|
|
68
|
-
export { DatePicker, DatePicker as default };
|
|
65
|
+
export { DatePicker, DatePicker as default, type DateType, type DateTypeStr };
|
|
@@ -4,8 +4,8 @@ import { CrossIcon } from "../icons/CrossIcon.js";
|
|
|
4
4
|
import { TickIcon } from "../icons/TickIcon.js";
|
|
5
5
|
import { Button } from "../Button/Button.js";
|
|
6
6
|
import { useField } from "../Field/Field.context.js";
|
|
7
|
-
import { Popover } from "../Popover/Popover.js";
|
|
8
7
|
import { TextInput } from "../TextInput/TextInput.js";
|
|
8
|
+
import { Popover } from "../Popover/Popover.js";
|
|
9
9
|
import { getDateValues, getDefaultMonth, getEndDate, getInitialDate, getStartDate, shouldClose, shouldSkipParsing } from "./datepicker-utils.js";
|
|
10
10
|
import clsx from "clsx";
|
|
11
11
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { Mode } from "react-day-picker";
|
|
2
2
|
//#region src/components/DatePicker/datepicker-utils.d.ts
|
|
3
|
+
export interface DateRangeStr {
|
|
4
|
+
from?: string;
|
|
5
|
+
to?: string;
|
|
6
|
+
}
|
|
3
7
|
export interface DateRange {
|
|
4
8
|
from: Date | undefined;
|
|
5
9
|
to?: Date | undefined;
|
|
6
10
|
}
|
|
11
|
+
export type DateType = Date | Date[] | DateRange | string | undefined;
|
|
12
|
+
export type DateTypeStr = string | string[] | DateRangeStr | undefined;
|
|
7
13
|
//#endregion
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
import { DateType, DateTypeStr } from "./datepicker-utils.js";
|
|
1
2
|
import { DatePicker, DatePickerProps } from "./DatePicker.js";
|
|
2
|
-
export { DatePicker, DatePicker as default, DatePickerProps };
|
|
3
|
+
export { DatePicker, DatePicker as default, DatePickerProps, type DateType, type DateTypeStr };
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,10 @@ import { Picture, PictureProps } from "./components/Picture/Picture.js";
|
|
|
8
8
|
import "./components/Picture/index.js";
|
|
9
9
|
import { Card, CardBadge, CardContent, CardHeading, CardIcon, CardImage, CardLink, CardPicture, CardProps } from "./components/Card/Card.js";
|
|
10
10
|
import { Checkbox, CheckboxProps } from "./components/Checkbox/Checkbox.js";
|
|
11
|
+
import { TextInput, TextInputProps } from "./components/TextInput/TextInput.js";
|
|
12
|
+
import { Combobox, ComboboxControlsProps, ComboboxInputProps, ComboboxOptionsProps, ComboboxProps } from "./components/Combobox/Combobox.js";
|
|
13
|
+
import "./components/Combobox/index.js";
|
|
14
|
+
import { DateType, DateTypeStr } from "./components/DatePicker/datepicker-utils.js";
|
|
11
15
|
import { DatePicker, DatePickerProps } from "./components/DatePicker/DatePicker.js";
|
|
12
16
|
import { Dialog, DialogBody, DialogHeader, DialogProps } from "./components/Dialog/Dialog.js";
|
|
13
17
|
import { Expandable, ExpandableContent, ExpandableProps, ExpandableSummary } from "./components/Expandable/Expandable.js";
|
|
@@ -17,7 +21,6 @@ import { Flourish, FlourishProps } from "./components/Flourish/Flourish.js";
|
|
|
17
21
|
import { Form, FormProps } from "./components/Form/Form.js";
|
|
18
22
|
import { LoadingSpinner, LoadingSpinnerProps } from "./components/LoadingSpinner/LoadingSpinner.js";
|
|
19
23
|
import { Logo, LogoProps } from "./components/Logo/Logo.js";
|
|
20
|
-
import { TextInput, TextInputProps } from "./components/TextInput/TextInput.js";
|
|
21
24
|
import { PasswordInput, PasswordInputProps } from "./components/PasswordInput/PasswordInput.js";
|
|
22
25
|
import { Pill, PillBadge, PillProps } from "./components/Pill/Pill.js";
|
|
23
26
|
import { Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverProps, PopoverTrigger } from "./components/Popover/Popover.js";
|
|
@@ -31,4 +34,4 @@ import { Squircle, SquircleProps, SquircleSvgProps } from "./components/Squircle
|
|
|
31
34
|
import { Tag, TagProps } from "./components/Tag/Tag.js";
|
|
32
35
|
import { Textarea, TextareaProps } from "./components/Textarea/Textarea.js";
|
|
33
36
|
import { ValidationSummary, ValidationSummaryErrors, ValidationSummaryHeading, ValidationSummaryPrompt, ValidationSummaryProps } from "./components/ValidationSummary/ValidationSummary.js";
|
|
34
|
-
export { AlertBanner, AlertBannerHeading, AlertBannerProps, Author, AuthorProps, Box, BoxProps, Button, ButtonProps, Card, CardBadge, CardContent, CardHeading, CardIcon, CardImage, CardLink, CardPicture, CardProps, Checkbox, CheckboxProps, DatePicker, DatePickerProps, Dialog, DialogBody, DialogHeader, DialogProps, Expandable, ExpandableContent, ExpandableProps, ExpandableSummary, Field, FieldError, FieldHint, FieldLabel, Fieldset, FieldsetError, FieldsetFields, FieldsetHint, FieldsetLegend, FieldsetProps, Flourish, FlourishProps, Form, FormProps, Image, ImageProps, LoadingSpinner, LoadingSpinnerProps, Logo, LogoProps, PasswordInput, PasswordInputProps, Picture, PictureProps, Pill, PillBadge, PillProps, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverProps, PopoverTrigger, Radio, RadioProps, RootSVG, Searchbox, SearchboxButton, SearchboxInput, SearchboxProps, Select, SelectOption, SelectOptionGroup, SelectProps, Signpost, SignpostProps, SkipNav, SkipNavProps, Squircle, SquircleProps, SquircleSvgProps, Tag, TagProps, TextInput, TextInputProps, Textarea, TextareaProps, ValidationSummary, ValidationSummaryErrors, ValidationSummaryHeading, ValidationSummaryPrompt, ValidationSummaryProps };
|
|
37
|
+
export { AlertBanner, AlertBannerHeading, AlertBannerProps, Author, AuthorProps, Box, BoxProps, Button, ButtonProps, Card, CardBadge, CardContent, CardHeading, CardIcon, CardImage, CardLink, CardPicture, CardProps, Checkbox, CheckboxProps, Combobox, ComboboxControlsProps, ComboboxInputProps, ComboboxOptionsProps, ComboboxProps, DatePicker, DatePickerProps, type DateType, type DateTypeStr, Dialog, DialogBody, DialogHeader, DialogProps, Expandable, ExpandableContent, ExpandableProps, ExpandableSummary, Field, FieldError, FieldHint, FieldLabel, Fieldset, FieldsetError, FieldsetFields, FieldsetHint, FieldsetLegend, FieldsetProps, Flourish, FlourishProps, Form, FormProps, Image, ImageProps, LoadingSpinner, LoadingSpinnerProps, Logo, LogoProps, PasswordInput, PasswordInputProps, Picture, PictureProps, Pill, PillBadge, PillProps, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverProps, PopoverTrigger, Radio, RadioProps, RootSVG, Searchbox, SearchboxButton, SearchboxInput, SearchboxProps, Select, SelectOption, SelectOptionGroup, SelectProps, Signpost, SignpostProps, SkipNav, SkipNavProps, Squircle, SquircleProps, SquircleSvgProps, Tag, TagProps, TextInput, TextInputProps, Textarea, TextareaProps, ValidationSummary, ValidationSummaryErrors, ValidationSummaryHeading, ValidationSummaryPrompt, ValidationSummaryProps };
|
package/dist/index.js
CHANGED
|
@@ -7,8 +7,9 @@ import { Button } from "./components/Button/Button.js";
|
|
|
7
7
|
import { Picture } from "./components/Picture/Picture.js";
|
|
8
8
|
import { Card, CardBadge, CardContent, CardHeading, CardIcon, CardImage, CardLink, CardPicture } from "./components/Card/Card.js";
|
|
9
9
|
import { Checkbox } from "./components/Checkbox/Checkbox.js";
|
|
10
|
-
import { Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger } from "./components/Popover/Popover.js";
|
|
11
10
|
import { TextInput } from "./components/TextInput/TextInput.js";
|
|
11
|
+
import { Combobox } from "./components/Combobox/Combobox.js";
|
|
12
|
+
import { Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger } from "./components/Popover/Popover.js";
|
|
12
13
|
import { DatePicker } from "./components/DatePicker/DatePicker.js";
|
|
13
14
|
import { Dialog, DialogBody, DialogHeader } from "./components/Dialog/Dialog.js";
|
|
14
15
|
import { Expandable, ExpandableContent, ExpandableSummary } from "./components/Expandable/Expandable.js";
|
|
@@ -29,4 +30,4 @@ import { Squircle } from "./components/Squircle/Squircle.js";
|
|
|
29
30
|
import { Tag } from "./components/Tag/Tag.js";
|
|
30
31
|
import { Textarea } from "./components/Textarea/Textarea.js";
|
|
31
32
|
import { ValidationSummary, ValidationSummaryErrors, ValidationSummaryHeading, ValidationSummaryPrompt } from "./components/ValidationSummary/ValidationSummary.js";
|
|
32
|
-
export { AlertBanner, AlertBannerHeading, Author, Box, Button, Card, CardBadge, CardContent, CardHeading, CardIcon, CardImage, CardLink, CardPicture, Checkbox, DatePicker, Dialog, DialogBody, DialogHeader, Expandable, ExpandableContent, ExpandableSummary, Field, FieldError, FieldHint, FieldLabel, Fieldset, FieldsetError, FieldsetFields, FieldsetHint, FieldsetLegend, Flourish, Form, Image, LoadingSpinner, Logo, PasswordInput, Picture, Pill, PillBadge, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Radio, RootSVG, Searchbox, SearchboxButton, SearchboxInput, Select, SelectOption, SelectOptionGroup, Signpost, SkipNav, Squircle, Tag, TextInput, Textarea, ValidationSummary, ValidationSummaryErrors, ValidationSummaryHeading, ValidationSummaryPrompt };
|
|
33
|
+
export { AlertBanner, AlertBannerHeading, Author, Box, Button, Card, CardBadge, CardContent, CardHeading, CardIcon, CardImage, CardLink, CardPicture, Checkbox, Combobox, DatePicker, Dialog, DialogBody, DialogHeader, Expandable, ExpandableContent, ExpandableSummary, Field, FieldError, FieldHint, FieldLabel, Fieldset, FieldsetError, FieldsetFields, FieldsetHint, FieldsetLegend, Flourish, Form, Image, LoadingSpinner, Logo, PasswordInput, Picture, Pill, PillBadge, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Radio, RootSVG, Searchbox, SearchboxButton, SearchboxInput, Select, SelectOption, SelectOptionGroup, Signpost, SkipNav, Squircle, Tag, TextInput, Textarea, ValidationSummary, ValidationSummaryErrors, ValidationSummaryHeading, ValidationSummaryPrompt };
|
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.23.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -41,12 +41,15 @@
|
|
|
41
41
|
"test:coverage": "vitest run --coverage",
|
|
42
42
|
"test:behaviour": "./test-utils/run-playwright-tests.sh -g behaviour",
|
|
43
43
|
"test:behaviour:ui": "npm run test:behaviour -- --ui",
|
|
44
|
-
"test:avt": "./test-utils/run-playwright-tests.sh -g avt",
|
|
44
|
+
"test:avt": "./test-utils/run-playwright-tests.sh -g avt --project=chromium",
|
|
45
45
|
"test:avt:ui": "npm run test:avt -- --ui",
|
|
46
46
|
"test:vrt": "ENVIRONMENT=docker ./test-utils/run-playwright-tests.sh -g vrt --workers 4 --timeout 5000 --fully-parallel",
|
|
47
47
|
"test:vrt:ui": "npm run test:vrt -- --ui",
|
|
48
48
|
"test:e2e": "./test-utils/run-e2e-tests.sh",
|
|
49
49
|
"test:e2e:ci": "npx playwright test --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL",
|
|
50
|
+
"test:avt:ci": "npm run test:e2e:ci -- -g avt --project=chromium",
|
|
51
|
+
"test:vrt:ci": "npm run test:e2e:ci -- -g vrt",
|
|
52
|
+
"test:behaviour:ci": "npm run test:e2e:ci -- -g behaviour",
|
|
50
53
|
"build": "tsx scripts build",
|
|
51
54
|
"build:docs": "tsx scripts build-docs",
|
|
52
55
|
"build:docs:all": "tsx scripts build-docs --all",
|
|
@@ -103,12 +106,14 @@
|
|
|
103
106
|
"storybook": "$storybook"
|
|
104
107
|
},
|
|
105
108
|
"dependencies": {
|
|
106
|
-
"@coopdigital/styles": "^1.
|
|
109
|
+
"@coopdigital/styles": "^1.17.0",
|
|
107
110
|
"@radix-ui/react-popover": "^1.1.23",
|
|
108
111
|
"clsx": "^2.1.1",
|
|
109
112
|
"date-fns": "^4.4.0",
|
|
113
|
+
"downshift": "^9.4.0",
|
|
114
|
+
"match-sorter": "^8.3.0",
|
|
110
115
|
"merge-refs": "^2.0.0",
|
|
111
116
|
"react-day-picker": "^10.0.1"
|
|
112
117
|
},
|
|
113
|
-
"gitHead": "
|
|
118
|
+
"gitHead": "c8458928ecfa27cda6cb6293952a31f68c00eed3"
|
|
114
119
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import { createContext, useContext } from "react"
|
|
3
|
+
|
|
4
|
+
import type { ComboboxItem } from "@/components/Combobox/combobox-utils"
|
|
5
|
+
|
|
6
|
+
import { StandardSizes } from "@/types"
|
|
7
|
+
|
|
8
|
+
interface ComboboxContextValue {
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
items?: ComboboxItem[]
|
|
11
|
+
props?: {
|
|
12
|
+
getInputProps: Function
|
|
13
|
+
getItemProps: Function
|
|
14
|
+
getMenuProps: Function
|
|
15
|
+
getToggleButtonProps: Function
|
|
16
|
+
isOpen: boolean
|
|
17
|
+
}
|
|
18
|
+
size?: StandardSizes
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const ComboboxContext = createContext<ComboboxContextValue>({})
|
|
22
|
+
|
|
23
|
+
export const useComboboxContext = () => {
|
|
24
|
+
const context = useContext(ComboboxContext)
|
|
25
|
+
if (!context) throw new Error("Combobox components must be used inside <Combobox>")
|
|
26
|
+
return context
|
|
27
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import type { ButtonHTMLAttributes, HTMLAttributes, JSX, ReactNode, Ref } from "react"
|
|
3
|
+
|
|
4
|
+
import clsx from "clsx"
|
|
5
|
+
import {
|
|
6
|
+
useCombobox,
|
|
7
|
+
UseComboboxHighlightedIndexChange,
|
|
8
|
+
UseComboboxInputValueChange,
|
|
9
|
+
UseComboboxIsOpenChange,
|
|
10
|
+
UseComboboxSelectedItemChange,
|
|
11
|
+
UseComboboxStateChange,
|
|
12
|
+
} from "downshift"
|
|
13
|
+
import { useState } from "react"
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
type ComboboxItem,
|
|
17
|
+
type ComboboxOption,
|
|
18
|
+
defaultFilter,
|
|
19
|
+
defaultIsItemDisabled,
|
|
20
|
+
defaultItemToString,
|
|
21
|
+
normalizeOptions,
|
|
22
|
+
} from "@/components/Combobox/combobox-utils"
|
|
23
|
+
import { ComboboxContext, useComboboxContext } from "@/components/Combobox/Combobox.context"
|
|
24
|
+
import { useField } from "@/components/Field/Field.context"
|
|
25
|
+
import { ChevronDownIcon } from "@/components/icons"
|
|
26
|
+
import { TextInput, TextInputProps } from "@/components/TextInput"
|
|
27
|
+
import { useSlots } from "@/hooks"
|
|
28
|
+
import { getSlotProp } from "@/utils"
|
|
29
|
+
|
|
30
|
+
export interface ComboboxProps extends Omit<
|
|
31
|
+
HTMLAttributes<HTMLDivElement>,
|
|
32
|
+
"onChange" | "onSelect" | "onToggle"
|
|
33
|
+
> {
|
|
34
|
+
/** Specify whether custom values are accepted.
|
|
35
|
+
*
|
|
36
|
+
* Default: `false`
|
|
37
|
+
*/
|
|
38
|
+
allowCustom?: boolean
|
|
39
|
+
/** Content inside the component.
|
|
40
|
+
*
|
|
41
|
+
* This should be a set of `<Combobox.Controls>` and `<Combobox.Options>`.
|
|
42
|
+
*/
|
|
43
|
+
children?: string | ReactNode
|
|
44
|
+
/** Specify additional CSS classes. */
|
|
45
|
+
className?: string
|
|
46
|
+
/** Specify the disabled state. */
|
|
47
|
+
disabled?: boolean
|
|
48
|
+
/** Specify the component error state. */
|
|
49
|
+
error?: boolean
|
|
50
|
+
/** Specify the component id. Will be auto-generated if not set. */
|
|
51
|
+
id?: string
|
|
52
|
+
/** Custom function to filter the list of options.
|
|
53
|
+
*
|
|
54
|
+
* Takes a `ComboboxItem` array and an input string, and returns a filtered array.
|
|
55
|
+
*
|
|
56
|
+
* Default: Filters on item label or value: `item.label ?? item.value`.
|
|
57
|
+
*/
|
|
58
|
+
inputFilter?: (items: ComboboxItem[], value: string) => ComboboxItem[]
|
|
59
|
+
/** Custom function to mark item as disabled.
|
|
60
|
+
*
|
|
61
|
+
* Default: `disabled` prop on `.ComboboxOption`
|
|
62
|
+
*/
|
|
63
|
+
isItemDisabled?: (item: ComboboxItem) => boolean
|
|
64
|
+
/** Custom function to render text in the input field after selecting a value.
|
|
65
|
+
*
|
|
66
|
+
* Default: Uses item label or value: `item.label ?? item.value`.
|
|
67
|
+
*/
|
|
68
|
+
itemToString?: (item: ComboboxItem | null) => string
|
|
69
|
+
/** Callback fired when the input value changes.
|
|
70
|
+
*
|
|
71
|
+
* This is a `downshift` event.
|
|
72
|
+
*/
|
|
73
|
+
onChange?: (event: UseComboboxInputValueChange<ComboboxItem>) => void
|
|
74
|
+
/** Callback fired when users highlight an option in the list using the mouse or keyboard.
|
|
75
|
+
*
|
|
76
|
+
* This is a `downshift` event.
|
|
77
|
+
*/
|
|
78
|
+
onHighlight?: (event: UseComboboxHighlightedIndexChange<ComboboxItem>) => void
|
|
79
|
+
/** Callback fired when users select an option from the list.
|
|
80
|
+
*
|
|
81
|
+
* This is a `downshift` event.
|
|
82
|
+
*/
|
|
83
|
+
onSelect?: (event: UseComboboxSelectedItemChange<ComboboxItem>) => void
|
|
84
|
+
/** Callback fired whenever the state changes.
|
|
85
|
+
*
|
|
86
|
+
* This is a `downshift` event.
|
|
87
|
+
*/
|
|
88
|
+
onStateChange?: (event: UseComboboxStateChange<ComboboxItem>) => void
|
|
89
|
+
/** Callback fired when the menu opens or closes.
|
|
90
|
+
*
|
|
91
|
+
* This is a `downshift` event.
|
|
92
|
+
*/
|
|
93
|
+
onToggle?: (changes: UseComboboxIsOpenChange<ComboboxItem>) => void
|
|
94
|
+
/** Specify whether the menu should appear immediately when clicking the input field.
|
|
95
|
+
*
|
|
96
|
+
* Default: `true`.
|
|
97
|
+
*/
|
|
98
|
+
openOnClick?: boolean
|
|
99
|
+
/** Specify placeholder text. Do not use in place of a form label. */
|
|
100
|
+
placeholder?: string
|
|
101
|
+
/** Specify a custom React ref for this component. */
|
|
102
|
+
ref?: Ref<HTMLDivElement>
|
|
103
|
+
/** Specify the component size. */
|
|
104
|
+
size?: "sm" | "md" | "lg"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface ComboboxControlsProps extends HTMLAttributes<HTMLDivElement> {
|
|
108
|
+
/** Content inside the component.
|
|
109
|
+
*
|
|
110
|
+
* This should be a set of `<Combobox.Input>` and `<Combobox.Button>`.
|
|
111
|
+
*/
|
|
112
|
+
children?: React.ReactNode
|
|
113
|
+
/** Specify additional CSS classes. */
|
|
114
|
+
className?: string
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface ComboboxOptionsProps extends HTMLAttributes<HTMLUListElement> {
|
|
118
|
+
/** Specify additional CSS classes. */
|
|
119
|
+
className?: string
|
|
120
|
+
/** Specify text to display when there are no matching options. */
|
|
121
|
+
noResultsText?: string
|
|
122
|
+
/** Specify array of options to choose from.
|
|
123
|
+
*
|
|
124
|
+
* Can be either a simple string array or a complex `ComboboxOption` array.
|
|
125
|
+
*/
|
|
126
|
+
options: ComboboxOption[]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type ComboboxInputProps = TextInputProps
|
|
130
|
+
|
|
131
|
+
const componentSlots = {
|
|
132
|
+
ComboboxControls: null,
|
|
133
|
+
ComboboxOptions: null,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const Combobox = ({
|
|
137
|
+
allowCustom = false,
|
|
138
|
+
children,
|
|
139
|
+
className,
|
|
140
|
+
disabled,
|
|
141
|
+
error,
|
|
142
|
+
id,
|
|
143
|
+
inputFilter,
|
|
144
|
+
isItemDisabled,
|
|
145
|
+
itemToString,
|
|
146
|
+
onChange,
|
|
147
|
+
onHighlight,
|
|
148
|
+
onSelect,
|
|
149
|
+
onStateChange,
|
|
150
|
+
onToggle,
|
|
151
|
+
openOnClick = true,
|
|
152
|
+
ref,
|
|
153
|
+
size,
|
|
154
|
+
...props
|
|
155
|
+
}: ComboboxProps): JSX.Element | null => {
|
|
156
|
+
const field = useField()
|
|
157
|
+
const uid = id ?? field?.id
|
|
158
|
+
|
|
159
|
+
const slots = useSlots(componentSlots, children)
|
|
160
|
+
|
|
161
|
+
const userOptions = getSlotProp<ComboboxOption[]>(slots.ComboboxOptions, "options") ?? []
|
|
162
|
+
const items = normalizeOptions(userOptions)
|
|
163
|
+
|
|
164
|
+
const [inputItems, setInputItems] = useState(items)
|
|
165
|
+
|
|
166
|
+
const componentProps = {
|
|
167
|
+
className: clsx("coop-combobox", className),
|
|
168
|
+
"data-error": error ?? undefined,
|
|
169
|
+
"data-size": size ?? undefined,
|
|
170
|
+
id: uid,
|
|
171
|
+
...props,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const inputFilterActual = inputFilter ?? defaultFilter
|
|
175
|
+
const itemToStringActual = itemToString ?? defaultItemToString
|
|
176
|
+
|
|
177
|
+
const comboboxProps = useCombobox<ComboboxItem>({
|
|
178
|
+
id: uid,
|
|
179
|
+
inputId: uid + "_input",
|
|
180
|
+
isItemDisabled: isItemDisabled ?? defaultIsItemDisabled,
|
|
181
|
+
items: inputItems,
|
|
182
|
+
itemToString: itemToStringActual,
|
|
183
|
+
labelId: uid + "_label",
|
|
184
|
+
menuId: uid + "_menu",
|
|
185
|
+
onHighlightedIndexChange: onHighlight,
|
|
186
|
+
onInputValueChange: (event) => {
|
|
187
|
+
setInputItems(inputFilterActual(items, event.inputValue))
|
|
188
|
+
onChange?.(event)
|
|
189
|
+
},
|
|
190
|
+
onIsOpenChange: onToggle,
|
|
191
|
+
onSelectedItemChange: (event) => {
|
|
192
|
+
onSelect?.(event) // This might need to broadcast just the value to work with RHF - need to check
|
|
193
|
+
},
|
|
194
|
+
onStateChange,
|
|
195
|
+
stateReducer(_, actionAndChanges) {
|
|
196
|
+
const { InputBlur, InputClick, InputKeyDownEnter } = useCombobox.stateChangeTypes
|
|
197
|
+
|
|
198
|
+
const { changes: state, type } = actionAndChanges
|
|
199
|
+
const currentInput = state.inputValue ?? ""
|
|
200
|
+
const hasMatch = items.some((item) => itemToStringActual(item) === currentInput)
|
|
201
|
+
|
|
202
|
+
switch (type) {
|
|
203
|
+
case openOnClick === false && InputClick:
|
|
204
|
+
return { ...state, isOpen: false }
|
|
205
|
+
case allowCustom === false && InputBlur:
|
|
206
|
+
return hasMatch ? state : { ...state, inputValue: "" }
|
|
207
|
+
case allowCustom === false && InputKeyDownEnter:
|
|
208
|
+
return hasMatch ? state : { ...state, isOpen: true }
|
|
209
|
+
default:
|
|
210
|
+
return state
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
// if (!slots.ComboboxControls || !slots.ComboboxOptions) {
|
|
216
|
+
// console.warn("Combobox must contain Combobox.Controls and Combobox.Options")
|
|
217
|
+
// return null
|
|
218
|
+
// }
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<ComboboxContext.Provider value={{ disabled, items: inputItems, props: comboboxProps, size }}>
|
|
222
|
+
<div {...componentProps} ref={ref}>
|
|
223
|
+
{slots.ComboboxControls}
|
|
224
|
+
{slots.ComboboxOptions}
|
|
225
|
+
</div>
|
|
226
|
+
</ComboboxContext.Provider>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const ComboboxInput = ({ className, placeholder, ...props }: ComboboxInputProps): JSX.Element => {
|
|
231
|
+
const combobox = useComboboxContext()
|
|
232
|
+
const componentProps = {
|
|
233
|
+
className,
|
|
234
|
+
disabled: combobox.disabled,
|
|
235
|
+
placeholder,
|
|
236
|
+
size: combobox.size,
|
|
237
|
+
...props,
|
|
238
|
+
}
|
|
239
|
+
return <TextInput {...componentProps} {...combobox.props?.getInputProps({})} />
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const ComboboxButton = ({
|
|
243
|
+
children,
|
|
244
|
+
className,
|
|
245
|
+
...props
|
|
246
|
+
}: ButtonHTMLAttributes<HTMLButtonElement>): JSX.Element => {
|
|
247
|
+
const combobox = useComboboxContext()
|
|
248
|
+
const componentProps = {
|
|
249
|
+
"aria-label": props["aria-label"] ?? "Toggle menu",
|
|
250
|
+
className,
|
|
251
|
+
disabled: combobox.disabled,
|
|
252
|
+
...props,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<button type="button" {...componentProps} {...combobox.props?.getToggleButtonProps()}>
|
|
257
|
+
{children ?? <ChevronDownIcon stroke="black" strokeWidth={1} width={"1.25rem"} />}
|
|
258
|
+
</button>
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const ComboboxControls = ({
|
|
263
|
+
children,
|
|
264
|
+
className,
|
|
265
|
+
...props
|
|
266
|
+
}: ComboboxControlsProps): JSX.Element => {
|
|
267
|
+
const componentProps = {
|
|
268
|
+
className: clsx("coop-combobox-controls", className),
|
|
269
|
+
...props,
|
|
270
|
+
}
|
|
271
|
+
return <div {...componentProps}>{children}</div>
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const ComboboxOptions = ({
|
|
275
|
+
className,
|
|
276
|
+
noResultsText = "No results",
|
|
277
|
+
options,
|
|
278
|
+
...props
|
|
279
|
+
}: ComboboxOptionsProps): JSX.Element | null => {
|
|
280
|
+
const combobox = useComboboxContext()
|
|
281
|
+
|
|
282
|
+
const componentProps = {
|
|
283
|
+
className: clsx("coop-combobox-options", className),
|
|
284
|
+
...props,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!options) {
|
|
288
|
+
return null
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<ul {...componentProps} {...combobox.props?.getMenuProps()}>
|
|
293
|
+
{combobox.props?.isOpen && combobox.items?.length
|
|
294
|
+
? combobox.items.map((item, index) => {
|
|
295
|
+
const itemProps = { ...combobox.props?.getItemProps({ index, item }) }
|
|
296
|
+
return (
|
|
297
|
+
<li key={item.value} {...itemProps} {...item}>
|
|
298
|
+
{item?.icon}
|
|
299
|
+
{item.children ?? item.label ?? item.value}
|
|
300
|
+
</li>
|
|
301
|
+
)
|
|
302
|
+
})
|
|
303
|
+
: null}
|
|
304
|
+
{combobox.props?.isOpen && !combobox.items?.length && <li>{noResultsText}</li>}
|
|
305
|
+
</ul>
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
Combobox.slot = {
|
|
310
|
+
field: true,
|
|
311
|
+
name: "Combobox",
|
|
312
|
+
suffix: "_input",
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
ComboboxControls.slot = { name: "ComboboxControls" }
|
|
316
|
+
ComboboxOptions.slot = { name: "ComboboxOptions" }
|
|
317
|
+
|
|
318
|
+
Combobox.Controls = ComboboxControls
|
|
319
|
+
Combobox.Input = ComboboxInput
|
|
320
|
+
Combobox.Button = ComboboxButton
|
|
321
|
+
Combobox.Options = ComboboxOptions
|
|
322
|
+
|
|
323
|
+
export default Combobox
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { matchSorter } from "match-sorter"
|
|
2
|
+
import { HTMLAttributes } from "react"
|
|
3
|
+
|
|
4
|
+
export interface ComboboxItem extends HTMLAttributes<HTMLLIElement> {
|
|
5
|
+
/** Specify the disabled state.
|
|
6
|
+
*
|
|
7
|
+
* Options that are disabled still appear in the list but cannot be selected.
|
|
8
|
+
*/
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
/** Specify an icon to render before the item text. */
|
|
11
|
+
icon?: React.ReactNode
|
|
12
|
+
/** Specify text to display to users if the underlying value needs to be different.
|
|
13
|
+
*
|
|
14
|
+
* If this value is provided, this is the string that gets filtered when the user types.
|
|
15
|
+
*/
|
|
16
|
+
label?: string
|
|
17
|
+
/** Specify the option value. */
|
|
18
|
+
value: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ComboboxOption = ComboboxItem | string
|
|
22
|
+
|
|
23
|
+
export const defaultFilter = (items: ComboboxItem[], inputValue: string): ComboboxItem[] => {
|
|
24
|
+
return inputValue !== ""
|
|
25
|
+
? matchSorter(items, inputValue, {
|
|
26
|
+
keys: [(item) => [item.label ?? item.value]],
|
|
27
|
+
})
|
|
28
|
+
: items
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const defaultIsItemDisabled = (item: ComboboxItem): boolean => item.disabled ?? false
|
|
32
|
+
|
|
33
|
+
export const defaultItemToString = (item: ComboboxItem | null): string =>
|
|
34
|
+
item?.label ?? item?.value ?? ""
|
|
35
|
+
|
|
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))
|
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
DateRange,
|
|
26
26
|
DateRangeStr,
|
|
27
27
|
DateType,
|
|
28
|
+
DateTypeStr,
|
|
28
29
|
TDateMultiple,
|
|
29
30
|
TDateRange,
|
|
30
31
|
TDateSingle,
|
|
@@ -40,9 +41,7 @@ import {
|
|
|
40
41
|
shouldSkipParsing,
|
|
41
42
|
} from "./datepicker-utils"
|
|
42
43
|
|
|
43
|
-
type OnDateSelectHandler = (
|
|
44
|
-
selected: string | string[] | { from?: string; to?: string } | undefined
|
|
45
|
-
) => void
|
|
44
|
+
type OnDateSelectHandler = (selected: DateTypeStr) => void
|
|
46
45
|
|
|
47
46
|
interface PickerState {
|
|
48
47
|
message?: string
|
|
@@ -402,3 +401,5 @@ DatePicker.slot = {
|
|
|
402
401
|
|
|
403
402
|
export default DatePicker
|
|
404
403
|
export { DatePicker }
|
|
404
|
+
|
|
405
|
+
export type { DateType, DateTypeStr }
|
|
@@ -19,6 +19,7 @@ interface DateReturn {
|
|
|
19
19
|
|
|
20
20
|
export type DateBase = string | undefined
|
|
21
21
|
export type DateType = Date | Date[] | DateRange | string | undefined
|
|
22
|
+
export type DateTypeStr = string | string[] | DateRangeStr | undefined
|
|
22
23
|
|
|
23
24
|
export interface TDateSingle {
|
|
24
25
|
event?: string
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./components/Box"
|
|
|
4
4
|
export * from "./components/Button/Button"
|
|
5
5
|
export * from "./components/Card/Card"
|
|
6
6
|
export * from "./components/Checkbox/Checkbox"
|
|
7
|
+
export * from "./components/Combobox"
|
|
7
8
|
export * from "./components/DatePicker/DatePicker"
|
|
8
9
|
export * from "./components/Dialog/Dialog"
|
|
9
10
|
export * from "./components/Expandable/Expandable"
|