@coopdigital/react 1.29.1 → 1.30.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 +1 -1
- package/dist/components/Combobox/Combobox.d.ts +79 -20
- package/dist/components/Combobox/Combobox.js +195 -102
- package/dist/components/Combobox/combobox-utils.js +9 -4
- package/dist/components/Combobox/index.d.ts +3 -2
- package/dist/components/Combobox/index.js +2 -2
- package/dist/components/ValidationSummary/validationsummary-utils.js +10 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/package.json +3 -3
- package/src/components/Combobox/Combobox.context.ts +11 -12
- package/src/components/Combobox/Combobox.tsx +324 -121
- package/src/components/Combobox/combobox-utils.ts +24 -3
- package/src/components/ValidationSummary/validationsummary-utils.ts +21 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { createContext, useContext } from "react";
|
|
3
3
|
//#region src/components/Combobox/Combobox.context.ts
|
|
4
|
-
const ComboboxContext = createContext(
|
|
4
|
+
const ComboboxContext = createContext(null);
|
|
5
5
|
const useComboboxContext = () => {
|
|
6
6
|
const context = useContext(ComboboxContext);
|
|
7
7
|
if (!context) throw new Error("Combobox components must be used inside <Combobox>");
|
|
@@ -3,9 +3,19 @@ import { ComboboxItem, ComboboxOption } from "./combobox-utils.js";
|
|
|
3
3
|
import { TextInputProps } from "../TextInput/TextInput.js";
|
|
4
4
|
import "../TextInput/index.js";
|
|
5
5
|
import { ButtonHTMLAttributes, HTMLAttributes, JSX, ReactNode, Ref } from "react";
|
|
6
|
-
import { UseComboboxHighlightedIndexChange, UseComboboxInputValueChange, UseComboboxIsOpenChange,
|
|
6
|
+
import { UseComboboxHighlightedIndexChange, UseComboboxInputValueChange, UseComboboxIsOpenChange, UseComboboxStateChange } from "downshift";
|
|
7
7
|
//#region src/components/Combobox/Combobox.d.ts
|
|
8
|
-
export interface
|
|
8
|
+
export interface ComboboxSelectEvent {
|
|
9
|
+
/** Whether the selection is a custom value, entered by users when `allowCustom` is set,
|
|
10
|
+
* rather than one of the options.
|
|
11
|
+
*/
|
|
12
|
+
custom: boolean;
|
|
13
|
+
/** The selected item, or `null` when the selection is cleared. For a custom value, this is a new item with that value. */
|
|
14
|
+
selectedItem: ComboboxItem | null;
|
|
15
|
+
/** The value of the selected option, or `null` when the selection is cleared. */
|
|
16
|
+
value: string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface ComboboxProps extends Omit<HTMLAttributes<HTMLDivElement>, "defaultValue" | "onChange" | "onSelect" | "onToggle"> {
|
|
9
19
|
/** Specify whether custom values are accepted.
|
|
10
20
|
*
|
|
11
21
|
* Default: `false`
|
|
@@ -15,9 +25,14 @@ export interface ComboboxProps extends Omit<HTMLAttributes<HTMLDivElement>, "onC
|
|
|
15
25
|
*
|
|
16
26
|
* This should be a set of `<Combobox.Controls>` and `<Combobox.Options>`.
|
|
17
27
|
*/
|
|
18
|
-
children?:
|
|
28
|
+
children?: ReactNode;
|
|
19
29
|
/** Specify additional CSS classes. */
|
|
20
30
|
className?: string;
|
|
31
|
+
/** Specify the initial selected option, when the selection is not controlled with `selected`.
|
|
32
|
+
*
|
|
33
|
+
* Accepts a `ComboboxOption`, matched to the options by its value.
|
|
34
|
+
*/
|
|
35
|
+
defaultSelected?: ComboboxOption | null;
|
|
21
36
|
/** Specify the disabled state. */
|
|
22
37
|
disabled?: boolean;
|
|
23
38
|
/** Specify the component error state. */
|
|
@@ -33,10 +48,10 @@ export interface ComboboxProps extends Omit<HTMLAttributes<HTMLDivElement>, "onC
|
|
|
33
48
|
inputFilter?: (items: ComboboxItem[], value: string) => ComboboxItem[];
|
|
34
49
|
/** Custom function to mark item as disabled.
|
|
35
50
|
*
|
|
36
|
-
* Default: `disabled`
|
|
51
|
+
* Default: the `disabled` property of the option.
|
|
37
52
|
*/
|
|
38
53
|
isItemDisabled?: (item: ComboboxItem) => boolean;
|
|
39
|
-
/** Custom function
|
|
54
|
+
/** Custom function that gives the text shown in the input for an item. Typed text is matched against this text.
|
|
40
55
|
*
|
|
41
56
|
* Default: Uses item label or value: `item.label ?? item.value`.
|
|
42
57
|
*/
|
|
@@ -51,11 +66,11 @@ export interface ComboboxProps extends Omit<HTMLAttributes<HTMLDivElement>, "onC
|
|
|
51
66
|
* This is a `downshift` event.
|
|
52
67
|
*/
|
|
53
68
|
onHighlight?: (event: UseComboboxHighlightedIndexChange<ComboboxItem>) => void;
|
|
54
|
-
/** Callback fired when
|
|
69
|
+
/** Callback fired when the selection changes, including when it is cleared.
|
|
55
70
|
*
|
|
56
|
-
*
|
|
71
|
+
* The event contains the full option object in `selectedItem`, and its value in `value`. Both are `null` when the selection is cleared.
|
|
57
72
|
*/
|
|
58
|
-
onSelect?: (event:
|
|
73
|
+
onSelect?: (event: ComboboxSelectEvent) => void;
|
|
59
74
|
/** Callback fired whenever the state changes.
|
|
60
75
|
*
|
|
61
76
|
* This is a `downshift` event.
|
|
@@ -65,16 +80,19 @@ export interface ComboboxProps extends Omit<HTMLAttributes<HTMLDivElement>, "onC
|
|
|
65
80
|
*
|
|
66
81
|
* This is a `downshift` event.
|
|
67
82
|
*/
|
|
68
|
-
onToggle?: (
|
|
83
|
+
onToggle?: (event: UseComboboxIsOpenChange<ComboboxItem>) => void;
|
|
69
84
|
/** Specify whether the menu should appear immediately when clicking the input field.
|
|
70
85
|
*
|
|
71
86
|
* Default: `true`.
|
|
72
87
|
*/
|
|
73
88
|
openOnClick?: boolean;
|
|
74
|
-
/** Specify placeholder text. Do not use in place of a form label. */
|
|
75
|
-
placeholder?: string;
|
|
76
89
|
/** Specify a custom React ref for this component. */
|
|
77
90
|
ref?: Ref<HTMLDivElement>;
|
|
91
|
+
/** Specify the selected option, when the selection is controlled. Update it from `onSelect`.
|
|
92
|
+
*
|
|
93
|
+
* Accepts a `ComboboxOption`, matched to the options by its value, or `null` for no selection.
|
|
94
|
+
*/
|
|
95
|
+
selected?: ComboboxOption | null;
|
|
78
96
|
/** Specify the component size. */
|
|
79
97
|
size?: StandardSizes;
|
|
80
98
|
}
|
|
@@ -83,24 +101,51 @@ export interface ComboboxControlsProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
83
101
|
*
|
|
84
102
|
* This should be a set of `<Combobox.Input>` and `<Combobox.Button>`.
|
|
85
103
|
*/
|
|
86
|
-
children?:
|
|
104
|
+
children?: ReactNode;
|
|
87
105
|
/** Specify additional CSS classes. */
|
|
88
106
|
className?: string;
|
|
107
|
+
/** Specify a custom React ref for this component. */
|
|
108
|
+
ref?: Ref<HTMLDivElement>;
|
|
109
|
+
}
|
|
110
|
+
export type ComboboxInputProps = Omit<TextInputProps, "defaultValue" | "prefix" | "prefixInline" | "suffix" | "suffixInline" | "value">;
|
|
111
|
+
export interface ComboboxButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
112
|
+
/** Specify the accessible label for the button.
|
|
113
|
+
*
|
|
114
|
+
* Default: `Toggle menu`
|
|
115
|
+
*/
|
|
116
|
+
"aria-label"?: string;
|
|
117
|
+
/** Content inside the component.
|
|
118
|
+
*
|
|
119
|
+
* Default: `ChevronIconDown`. */
|
|
120
|
+
children?: ReactNode;
|
|
121
|
+
/** Specify a custom React ref for this component. */
|
|
122
|
+
ref?: Ref<HTMLButtonElement>;
|
|
89
123
|
}
|
|
90
124
|
export interface ComboboxOptionsProps extends HTMLAttributes<HTMLUListElement> {
|
|
91
125
|
/** Specify additional CSS classes. */
|
|
92
126
|
className?: string;
|
|
127
|
+
/** Specify the maximum number of options to show in the list. Set to `false` to show all matching options.
|
|
128
|
+
*
|
|
129
|
+
* Default: `50`
|
|
130
|
+
*/
|
|
131
|
+
maxResults?: false | number;
|
|
132
|
+
/** Specify the message shown when more options match than `maxResults`.
|
|
133
|
+
*
|
|
134
|
+
* Default: `Showing X of Y results.`
|
|
135
|
+
*/
|
|
136
|
+
maxResultsText?: (shown: number, total: number) => string;
|
|
93
137
|
/** Specify text to display when there are no matching options. */
|
|
94
138
|
noResultsText?: string;
|
|
95
139
|
/** Specify array of options to choose from.
|
|
96
140
|
*
|
|
97
|
-
*
|
|
141
|
+
* Accepts a `ComboboxOption` array: strings, or `ComboboxItem` objects with a `value` and an optional `label`.
|
|
98
142
|
*/
|
|
99
143
|
options: ComboboxOption[];
|
|
144
|
+
/** Specify a custom React ref for this component. */
|
|
145
|
+
ref?: Ref<HTMLUListElement>;
|
|
100
146
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
({ allowCustom, children, className, disabled, error, id, inputFilter, isItemDisabled, itemToString, onChange, onHighlight, onSelect, onStateChange, onToggle, openOnClick, ref, size, ...props }: ComboboxProps): JSX.Element | null;
|
|
147
|
+
declare const Combobox: {
|
|
148
|
+
({ allowCustom, children, className, defaultSelected, disabled, error, id, inputFilter, isItemDisabled, itemToString, onChange, onHighlight, onSelect, onStateChange, onToggle, openOnClick, ref, selected, size, ...props }: ComboboxProps): JSX.Element;
|
|
104
149
|
slot: {
|
|
105
150
|
field: boolean;
|
|
106
151
|
name: string;
|
|
@@ -112,14 +157,28 @@ export declare const Combobox: {
|
|
|
112
157
|
name: string;
|
|
113
158
|
};
|
|
114
159
|
};
|
|
115
|
-
Input: ({
|
|
116
|
-
Button: ({
|
|
160
|
+
Input: ({ error, name, size, ...props }: ComboboxInputProps) => JSX.Element;
|
|
161
|
+
Button: ({ "aria-label": ariaLabel, children, ...props }: ComboboxButtonProps) => JSX.Element;
|
|
117
162
|
Options: {
|
|
118
|
-
({ className, noResultsText, options, ...props }: ComboboxOptionsProps): JSX.Element
|
|
163
|
+
({ className, maxResults: _maxResults, maxResultsText, noResultsText, options: _options, ...props }: ComboboxOptionsProps): JSX.Element;
|
|
119
164
|
slot: {
|
|
120
165
|
name: string;
|
|
121
166
|
};
|
|
122
167
|
};
|
|
123
168
|
};
|
|
169
|
+
declare const ComboboxControls: {
|
|
170
|
+
({ children, className, ...props }: ComboboxControlsProps): JSX.Element;
|
|
171
|
+
slot: {
|
|
172
|
+
name: string;
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
declare const ComboboxInput: ({ error, name, size, ...props }: ComboboxInputProps) => JSX.Element;
|
|
176
|
+
declare const ComboboxButton: ({ "aria-label": ariaLabel, children, ...props }: ComboboxButtonProps) => JSX.Element;
|
|
177
|
+
declare const ComboboxOptions: {
|
|
178
|
+
({ className, maxResults: _maxResults, maxResultsText, noResultsText, options: _options, ...props }: ComboboxOptionsProps): JSX.Element;
|
|
179
|
+
slot: {
|
|
180
|
+
name: string;
|
|
181
|
+
};
|
|
182
|
+
};
|
|
124
183
|
//#endregion
|
|
125
|
-
export { Combobox as default };
|
|
184
|
+
export { Combobox, Combobox as default, ComboboxButton, ComboboxControls, ComboboxInput, type ComboboxItem, type ComboboxOption, ComboboxOptions };
|
|
@@ -1,119 +1,205 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { useId as useId$1 } from "../../hooks/useId.js";
|
|
2
3
|
import { ChevronDownIcon } from "../icons/ChevronDownIcon.js";
|
|
3
4
|
import { getSlotProp } from "../../utils/slots.js";
|
|
4
5
|
import { useSlots } from "../../hooks/useSlots.js";
|
|
5
6
|
import { getFieldError, useField } from "../Field/Field.context.js";
|
|
6
|
-
import { defaultFilter, defaultIsItemDisabled, defaultItemToString, normalizeOptions } from "./combobox-utils.js";
|
|
7
|
+
import { defaultFilter, defaultIsItemDisabled, defaultItemToString, isMatchingText, normalizeOptions, resolveOption } from "./combobox-utils.js";
|
|
7
8
|
import { ComboboxContext, useComboboxContext } from "./Combobox.context.js";
|
|
8
9
|
import { TextInput } from "../TextInput/TextInput.js";
|
|
10
|
+
import { useDebounce } from "../../hooks/useDebounce.js";
|
|
9
11
|
import clsx from "clsx";
|
|
10
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
-
import { useState } from "react";
|
|
12
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
12
14
|
import { useCombobox } from "downshift";
|
|
13
15
|
//#region src/components/Combobox/Combobox.tsx
|
|
16
|
+
const DEFAULT_MAX_RESULTS = 50;
|
|
17
|
+
const ANNOUNCEMENT_DELAY = 750;
|
|
18
|
+
const defaultMaxResultsText = (shown, total) => `Showing ${shown} of ${total} results.`;
|
|
14
19
|
const componentSlots = {
|
|
15
20
|
ComboboxControls: null,
|
|
16
21
|
ComboboxOptions: null
|
|
17
22
|
};
|
|
18
|
-
const Combobox = ({ allowCustom = false, children, className, disabled, error, id, inputFilter, isItemDisabled, itemToString, onChange, onHighlight, onSelect, onStateChange, onToggle, openOnClick = true, ref, size, ...props }) => {
|
|
23
|
+
const Combobox = ({ allowCustom = false, children, className, defaultSelected, disabled = false, error, id, inputFilter, isItemDisabled, itemToString, onChange, onHighlight, onSelect, onStateChange, onToggle, openOnClick = true, ref, selected, size, ...props }) => {
|
|
19
24
|
const field = useField();
|
|
20
|
-
const uid = id ?? field?.id;
|
|
25
|
+
const uid = useId$1(id ?? field?.id);
|
|
21
26
|
const hasError = getFieldError(error, field);
|
|
22
27
|
const slots = useSlots(componentSlots, children);
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const options = getSlotProp(slots.ComboboxOptions, "options");
|
|
29
|
+
const maxResults = getSlotProp(slots.ComboboxOptions, "maxResults") ?? DEFAULT_MAX_RESULTS;
|
|
30
|
+
const items = useMemo(() => normalizeOptions(options ?? []), [options]);
|
|
31
|
+
const filter = inputFilter ?? defaultFilter;
|
|
32
|
+
const toString = itemToString ?? defaultItemToString;
|
|
33
|
+
const isDisabled = isItemDisabled ?? defaultIsItemDisabled;
|
|
34
|
+
const [filterText, setFilterText] = useState("");
|
|
35
|
+
const matchingItems = useMemo(() => filter(items, filterText), [
|
|
36
|
+
filter,
|
|
37
|
+
items,
|
|
38
|
+
filterText
|
|
39
|
+
]);
|
|
40
|
+
const visibleItems = useMemo(() => maxResults === false ? matchingItems : matchingItems.slice(0, maxResults), [matchingItems, maxResults]);
|
|
41
|
+
const [initialSelectedItem] = useState(() => resolveOption(defaultSelected, items));
|
|
42
|
+
const selectedItem = selected === void 0 ? void 0 : resolveOption(selected, items);
|
|
43
|
+
const commit = (changes, closeMenu) => {
|
|
44
|
+
const text = (changes.inputValue ?? "").trim();
|
|
45
|
+
if (text === "") return {
|
|
46
|
+
...changes,
|
|
47
|
+
inputValue: "",
|
|
48
|
+
selectedItem: null
|
|
49
|
+
};
|
|
50
|
+
const picked = changes.selectedItem;
|
|
51
|
+
const match = picked && isMatchingText(toString(picked), text) ? picked : items.find((item) => !isDisabled(item) && isMatchingText(toString(item), text));
|
|
52
|
+
const closed = closeMenu ? { isOpen: false } : {};
|
|
53
|
+
if (match) return {
|
|
54
|
+
...changes,
|
|
55
|
+
...closed,
|
|
56
|
+
inputValue: toString(match),
|
|
57
|
+
selectedItem: match
|
|
58
|
+
};
|
|
59
|
+
if (allowCustom) return {
|
|
60
|
+
...changes,
|
|
61
|
+
...closed,
|
|
62
|
+
inputValue: text,
|
|
63
|
+
selectedItem: { value: text }
|
|
64
|
+
};
|
|
65
|
+
return null;
|
|
66
|
+
};
|
|
67
|
+
const getChanges = (state, type, changes) => {
|
|
68
|
+
const { InputBlur, InputChange, InputClick, InputKeyDownEnter, InputKeyDownEscape } = useCombobox.stateChangeTypes;
|
|
69
|
+
switch (type) {
|
|
70
|
+
case InputChange: return changes.inputValue === "" ? {
|
|
71
|
+
...changes,
|
|
72
|
+
selectedItem: null
|
|
73
|
+
} : changes;
|
|
74
|
+
case InputClick: return openOnClick ? changes : {
|
|
75
|
+
...changes,
|
|
76
|
+
isOpen: false
|
|
77
|
+
};
|
|
78
|
+
case InputKeyDownEscape: return state.isOpen ? {
|
|
79
|
+
...changes,
|
|
80
|
+
inputValue: toString(state.selectedItem)
|
|
81
|
+
} : changes;
|
|
82
|
+
case InputBlur: return commit(changes, false) ?? {
|
|
83
|
+
...changes,
|
|
84
|
+
inputValue: "",
|
|
85
|
+
selectedItem: null
|
|
86
|
+
};
|
|
87
|
+
case InputKeyDownEnter: return commit(changes, true) ?? {
|
|
88
|
+
...changes,
|
|
89
|
+
isOpen: true
|
|
90
|
+
};
|
|
91
|
+
default: return changes;
|
|
92
|
+
}
|
|
32
93
|
};
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const comboboxProps = useCombobox({
|
|
94
|
+
const downshift = useCombobox({
|
|
95
|
+
getItemId: (index) => visibleItems[index]?.id ?? `${uid}_item_${index}`,
|
|
36
96
|
id: uid,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
97
|
+
initialSelectedItem,
|
|
98
|
+
inputId: `${uid}_input`,
|
|
99
|
+
isItemDisabled: isDisabled,
|
|
100
|
+
items: visibleItems,
|
|
101
|
+
itemToKey: (item) => item?.value,
|
|
102
|
+
itemToString: toString,
|
|
103
|
+
labelId: `${uid}_label`,
|
|
104
|
+
menuId: `${uid}_menu`,
|
|
43
105
|
onHighlightedIndexChange: onHighlight,
|
|
44
106
|
onInputValueChange: (event) => {
|
|
45
|
-
|
|
107
|
+
if (event.type === useCombobox.stateChangeTypes.InputChange) setFilterText(event.inputValue);
|
|
46
108
|
onChange?.(event);
|
|
47
109
|
},
|
|
48
|
-
onIsOpenChange:
|
|
49
|
-
|
|
50
|
-
|
|
110
|
+
onIsOpenChange: (event) => {
|
|
111
|
+
if (!event.isOpen) setFilterText("");
|
|
112
|
+
onToggle?.(event);
|
|
51
113
|
},
|
|
114
|
+
onSelectedItemChange: ({ selectedItem }) => onSelect?.({
|
|
115
|
+
custom: !!selectedItem && !items.some(({ value }) => value === selectedItem.value),
|
|
116
|
+
selectedItem,
|
|
117
|
+
value: selectedItem?.value ?? null
|
|
118
|
+
}),
|
|
52
119
|
onStateChange,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
...state,
|
|
61
|
-
isOpen: false
|
|
62
|
-
};
|
|
63
|
-
case allowCustom === false && InputBlur: return hasMatch ? state : {
|
|
64
|
-
...state,
|
|
65
|
-
inputValue: ""
|
|
66
|
-
};
|
|
67
|
-
case allowCustom === false && InputKeyDownEnter: return hasMatch ? state : {
|
|
68
|
-
...state,
|
|
69
|
-
isOpen: true
|
|
70
|
-
};
|
|
71
|
-
default: return state;
|
|
72
|
-
}
|
|
120
|
+
selectedItem,
|
|
121
|
+
stateReducer(state, { changes, type }) {
|
|
122
|
+
const next = getChanges(state, type, changes);
|
|
123
|
+
return next.selectedItem && next.selectedItem.value === state.selectedItem?.value ? {
|
|
124
|
+
...next,
|
|
125
|
+
selectedItem: state.selectedItem
|
|
126
|
+
} : next;
|
|
73
127
|
}
|
|
74
128
|
});
|
|
129
|
+
const selection = resolveOption(downshift.selectedItem, items);
|
|
130
|
+
const selectionText = toString(selection);
|
|
131
|
+
const selectionValue = selection?.value;
|
|
132
|
+
const previousSelection = useRef({
|
|
133
|
+
text: selectionText,
|
|
134
|
+
value: selectionValue
|
|
135
|
+
});
|
|
136
|
+
const { inputValue, setInputValue } = downshift;
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
const previous = previousSelection.current;
|
|
139
|
+
previousSelection.current = {
|
|
140
|
+
text: selectionText,
|
|
141
|
+
value: selectionValue
|
|
142
|
+
};
|
|
143
|
+
if (selectionValue === previous.value && selectionText !== previous.text && inputValue === previous.text) setInputValue(selectionText);
|
|
144
|
+
}, [
|
|
145
|
+
inputValue,
|
|
146
|
+
selectionText,
|
|
147
|
+
selectionValue,
|
|
148
|
+
setInputValue
|
|
149
|
+
]);
|
|
75
150
|
return /* @__PURE__ */ jsx(ComboboxContext.Provider, {
|
|
76
151
|
value: {
|
|
77
152
|
disabled,
|
|
153
|
+
downshift,
|
|
78
154
|
error: hasError,
|
|
79
|
-
items:
|
|
80
|
-
|
|
155
|
+
items: visibleItems,
|
|
156
|
+
matchCount: matchingItems.length,
|
|
81
157
|
size
|
|
82
158
|
},
|
|
83
159
|
children: /* @__PURE__ */ jsxs("div", {
|
|
84
|
-
...
|
|
160
|
+
...props,
|
|
161
|
+
className: clsx("coop-combobox", className),
|
|
162
|
+
"data-field": "",
|
|
163
|
+
"data-size": size ?? void 0,
|
|
164
|
+
id: uid,
|
|
85
165
|
ref,
|
|
86
166
|
children: [slots.ComboboxControls, slots.ComboboxOptions]
|
|
87
167
|
})
|
|
88
168
|
});
|
|
89
169
|
};
|
|
90
|
-
const
|
|
170
|
+
const ComboboxControls = ({ children, className, ...props }) => /* @__PURE__ */ jsx("div", {
|
|
171
|
+
...props,
|
|
172
|
+
className: clsx("coop-combobox-controls", className),
|
|
173
|
+
children
|
|
174
|
+
});
|
|
175
|
+
const ComboboxInput = ({ error, name, size, ...props }) => {
|
|
91
176
|
const combobox = useComboboxContext();
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
placeholder,
|
|
97
|
-
size: combobox.size,
|
|
98
|
-
...props
|
|
99
|
-
};
|
|
100
|
-
return /* @__PURE__ */ jsx(TextInput, {
|
|
101
|
-
...componentProps,
|
|
102
|
-
...combobox.props?.getInputProps({})
|
|
177
|
+
const disabled = combobox.disabled || props.disabled;
|
|
178
|
+
const inputProps = combobox.downshift.getInputProps({
|
|
179
|
+
...props,
|
|
180
|
+
disabled
|
|
103
181
|
});
|
|
182
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [name && /* @__PURE__ */ jsx("input", {
|
|
183
|
+
disabled,
|
|
184
|
+
name,
|
|
185
|
+
type: "hidden",
|
|
186
|
+
value: combobox.downshift.selectedItem?.value ?? ""
|
|
187
|
+
}), /* @__PURE__ */ jsx(TextInput, {
|
|
188
|
+
...inputProps,
|
|
189
|
+
error: error ?? combobox.error,
|
|
190
|
+
size: size ?? combobox.size
|
|
191
|
+
})] });
|
|
104
192
|
};
|
|
105
|
-
const ComboboxButton = ({
|
|
193
|
+
const ComboboxButton = ({ "aria-label": ariaLabel = "Toggle menu", children, ...props }) => {
|
|
106
194
|
const combobox = useComboboxContext();
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
disabled: combobox.disabled
|
|
111
|
-
|
|
112
|
-
};
|
|
195
|
+
const buttonProps = combobox.downshift.getToggleButtonProps({
|
|
196
|
+
...props,
|
|
197
|
+
"aria-label": ariaLabel,
|
|
198
|
+
disabled: combobox.disabled || props.disabled
|
|
199
|
+
});
|
|
113
200
|
return /* @__PURE__ */ jsx("button", {
|
|
114
201
|
type: "button",
|
|
115
|
-
...
|
|
116
|
-
...combobox.props?.getToggleButtonProps(),
|
|
202
|
+
...buttonProps,
|
|
117
203
|
children: children ?? /* @__PURE__ */ jsx(ChevronDownIcon, {
|
|
118
204
|
stroke: "black",
|
|
119
205
|
strokeWidth: 1,
|
|
@@ -121,38 +207,45 @@ const ComboboxButton = ({ children, className, ...props }) => {
|
|
|
121
207
|
})
|
|
122
208
|
});
|
|
123
209
|
};
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
133
|
-
};
|
|
134
|
-
const ComboboxOptions = ({ className, noResultsText = "No results", options, ...props }) => {
|
|
135
|
-
const combobox = useComboboxContext();
|
|
136
|
-
const componentProps = {
|
|
137
|
-
className: clsx("coop-combobox-options", className),
|
|
138
|
-
...props
|
|
210
|
+
const ComboboxOptions = ({ className, maxResults: _maxResults, maxResultsText = defaultMaxResultsText, noResultsText = "No results", options: _options, ...props }) => {
|
|
211
|
+
const { downshift, items, matchCount } = useComboboxContext();
|
|
212
|
+
const { getItemProps, getMenuProps, isOpen } = downshift;
|
|
213
|
+
const getMessage = () => {
|
|
214
|
+
if (!isOpen) return "";
|
|
215
|
+
if (items.length === 0) return noResultsText;
|
|
216
|
+
if (matchCount > items.length) return maxResultsText(items.length, matchCount);
|
|
217
|
+
return "";
|
|
139
218
|
};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
...
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
219
|
+
const message = getMessage();
|
|
220
|
+
const announcement = useDebounce(message, ANNOUNCEMENT_DELAY);
|
|
221
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("ul", {
|
|
222
|
+
...getMenuProps({
|
|
223
|
+
...props,
|
|
224
|
+
className: clsx("coop-combobox-options", className)
|
|
225
|
+
}),
|
|
226
|
+
children: [isOpen && items.map((item, index) => {
|
|
227
|
+
const { children, disabled: _disabled, icon, label, onClick, onMouseDown, onMouseMove, value, ...attributes } = item;
|
|
149
228
|
return /* @__PURE__ */ jsxs("li", {
|
|
150
|
-
...
|
|
151
|
-
...
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
229
|
+
...attributes,
|
|
230
|
+
...getItemProps({
|
|
231
|
+
index,
|
|
232
|
+
item,
|
|
233
|
+
onClick,
|
|
234
|
+
onMouseDown,
|
|
235
|
+
onMouseMove
|
|
236
|
+
}),
|
|
237
|
+
children: [icon, children ?? label ?? value]
|
|
238
|
+
}, value);
|
|
239
|
+
}), message && /* @__PURE__ */ jsx("li", {
|
|
240
|
+
"aria-hidden": true,
|
|
241
|
+
className: "coop-combobox-message",
|
|
242
|
+
children: message
|
|
243
|
+
})]
|
|
244
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
245
|
+
"aria-live": "polite",
|
|
246
|
+
className: "sr-only",
|
|
247
|
+
children: announcement
|
|
248
|
+
})] });
|
|
156
249
|
};
|
|
157
250
|
Combobox.slot = {
|
|
158
251
|
field: true,
|
|
@@ -166,4 +259,4 @@ Combobox.Input = ComboboxInput;
|
|
|
166
259
|
Combobox.Button = ComboboxButton;
|
|
167
260
|
Combobox.Options = ComboboxOptions;
|
|
168
261
|
//#endregion
|
|
169
|
-
export { Combobox, Combobox as default };
|
|
262
|
+
export { Combobox, Combobox as default, ComboboxButton, ComboboxControls, ComboboxInput, ComboboxOptions };
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { matchSorter } from "match-sorter";
|
|
2
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
|
-
};
|
|
3
|
+
const defaultFilter = (items, inputValue) => inputValue !== "" ? matchSorter(items, inputValue, { keys: [(item) => [item.label ?? item.value]] }) : items;
|
|
6
4
|
const defaultIsItemDisabled = (item) => item.disabled ?? false;
|
|
7
5
|
const defaultItemToString = (item) => item?.label ?? item?.value ?? "";
|
|
8
6
|
const normalizeOptions = (options) => options.filter((option) => option && typeof option !== "number").map((option) => typeof option === "string" ? { value: option } : option);
|
|
7
|
+
const collator = new Intl.Collator(void 0, { sensitivity: "base" });
|
|
8
|
+
const isMatchingText = (optionText, text) => collator.compare(optionText.trim(), text.trim()) === 0;
|
|
9
|
+
const resolveOption = (option, items) => {
|
|
10
|
+
if (option === null || option === void 0) return null;
|
|
11
|
+
const item = typeof option === "string" ? { value: option } : option;
|
|
12
|
+
return items.find(({ value }) => value === item.value) ?? item;
|
|
13
|
+
};
|
|
9
14
|
//#endregion
|
|
10
|
-
export { defaultFilter, defaultIsItemDisabled, defaultItemToString, normalizeOptions };
|
|
15
|
+
export { defaultFilter, defaultIsItemDisabled, defaultItemToString, isMatchingText, normalizeOptions, resolveOption };
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { ComboboxItem, ComboboxOption } from "./combobox-utils.js";
|
|
2
|
+
import { Combobox, ComboboxButton, ComboboxButtonProps, ComboboxControls, ComboboxControlsProps, ComboboxInput, ComboboxInputProps, ComboboxOptions, ComboboxOptionsProps, ComboboxProps, ComboboxSelectEvent } from "./Combobox.js";
|
|
3
|
+
export { Combobox, Combobox as default, ComboboxButton, ComboboxButtonProps, ComboboxControls, ComboboxControlsProps, ComboboxInput, ComboboxInputProps, type ComboboxItem, type ComboboxOption, ComboboxOptions, ComboboxOptionsProps, ComboboxProps, ComboboxSelectEvent };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Combobox } from "./Combobox.js";
|
|
1
|
+
import { Combobox, ComboboxButton, ComboboxControls, ComboboxInput, ComboboxOptions } from "./Combobox.js";
|
|
2
2
|
//#region src/components/Combobox/index.ts
|
|
3
3
|
var Combobox_default = Combobox;
|
|
4
4
|
//#endregion
|
|
5
|
-
export { Combobox, Combobox_default as default };
|
|
5
|
+
export { Combobox, ComboboxButton, ComboboxControls, ComboboxInput, ComboboxOptions, Combobox_default as default };
|
|
@@ -5,11 +5,20 @@ const getFieldKeys = (field, id) => [
|
|
|
5
5
|
`${field}_display`,
|
|
6
6
|
field
|
|
7
7
|
];
|
|
8
|
+
const VISIBLE_CONTROL = "input:not([type=\"hidden\"]), select, textarea";
|
|
9
|
+
const isHiddenInput = (el) => el instanceof HTMLInputElement && el.type === "hidden";
|
|
10
|
+
const getVisibleControl = (form, el) => {
|
|
11
|
+
for (let node = el.parentElement; node && node !== form; node = node.parentElement) {
|
|
12
|
+
const control = node.querySelector(VISIBLE_CONTROL);
|
|
13
|
+
if (control) return control;
|
|
14
|
+
}
|
|
15
|
+
return el;
|
|
16
|
+
};
|
|
8
17
|
const getFieldElement = (form, field, id) => {
|
|
9
18
|
for (const key of getFieldKeys(field, id)) {
|
|
10
19
|
const item = form.elements.namedItem(key);
|
|
11
20
|
const el = item instanceof RadioNodeList ? item[0] : item;
|
|
12
|
-
if (el instanceof Element) return el;
|
|
21
|
+
if (el instanceof Element) return isHiddenInput(el) ? getVisibleControl(form, el) : el;
|
|
13
22
|
}
|
|
14
23
|
return null;
|
|
15
24
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -8,8 +8,9 @@ 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 { ComboboxItem, ComboboxOption } from "./components/Combobox/combobox-utils.js";
|
|
11
12
|
import { TextInput, TextInputProps } from "./components/TextInput/TextInput.js";
|
|
12
|
-
import { Combobox, ComboboxControlsProps, ComboboxInputProps, ComboboxOptionsProps, ComboboxProps } from "./components/Combobox/Combobox.js";
|
|
13
|
+
import { Combobox, ComboboxButton, ComboboxButtonProps, ComboboxControls, ComboboxControlsProps, ComboboxInput, ComboboxInputProps, ComboboxOptions, ComboboxOptionsProps, ComboboxProps, ComboboxSelectEvent } from "./components/Combobox/Combobox.js";
|
|
13
14
|
import "./components/Combobox/index.js";
|
|
14
15
|
import { DateType, DateTypeStr } from "./components/DatePicker/datepicker-utils.js";
|
|
15
16
|
import { DatePicker, DatePickerProps } from "./components/DatePicker/DatePicker.js";
|
|
@@ -38,4 +39,4 @@ import { Textarea, TextareaProps } from "./components/Textarea/Textarea.js";
|
|
|
38
39
|
import { ValidationSummary, ValidationSummaryErrors, ValidationSummaryHeading, ValidationSummaryPrompt, ValidationSummaryProps } from "./components/ValidationSummary/ValidationSummary.js";
|
|
39
40
|
import { Wrapper, WrapperProps } from "./components/Wrapper/Wrapper.js";
|
|
40
41
|
import "./components/Wrapper/index.js";
|
|
41
|
-
export { AlertBanner, AlertBannerHeading, AlertBannerProps, Author, AuthorImageProps, AuthorMetaProps, 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, Section, SectionProps, Select, SelectOption, SelectOptionGroup, SelectProps, Signpost, SignpostProps, SkipNav, SkipNavProps, Squircle, SquircleProps, SquircleSvgProps, Tag, TagProps, TextInput, TextInputProps, Textarea, TextareaProps, ValidationSummary, ValidationSummaryErrors, ValidationSummaryHeading, ValidationSummaryPrompt, ValidationSummaryProps, Wrapper, WrapperProps };
|
|
42
|
+
export { AlertBanner, AlertBannerHeading, AlertBannerProps, Author, AuthorImageProps, AuthorMetaProps, AuthorProps, Box, BoxProps, Button, ButtonProps, Card, CardBadge, CardContent, CardHeading, CardIcon, CardImage, CardLink, CardPicture, CardProps, Checkbox, CheckboxProps, Combobox, ComboboxButton, ComboboxButtonProps, ComboboxControls, ComboboxControlsProps, ComboboxInput, ComboboxInputProps, type ComboboxItem, type ComboboxOption, ComboboxOptions, ComboboxOptionsProps, ComboboxProps, ComboboxSelectEvent, 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, Section, SectionProps, Select, SelectOption, SelectOptionGroup, SelectProps, Signpost, SignpostProps, SkipNav, SkipNavProps, Squircle, SquircleProps, SquircleSvgProps, Tag, TagProps, TextInput, TextInputProps, Textarea, TextareaProps, ValidationSummary, ValidationSummaryErrors, ValidationSummaryHeading, ValidationSummaryPrompt, ValidationSummaryProps, Wrapper, WrapperProps };
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,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
10
|
import { TextInput } from "./components/TextInput/TextInput.js";
|
|
11
|
-
import { Combobox } from "./components/Combobox/Combobox.js";
|
|
11
|
+
import { Combobox, ComboboxButton, ComboboxControls, ComboboxInput, ComboboxOptions } from "./components/Combobox/Combobox.js";
|
|
12
12
|
import { Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger } from "./components/Popover/Popover.js";
|
|
13
13
|
import { DatePicker } from "./components/DatePicker/DatePicker.js";
|
|
14
14
|
import { Dialog, DialogBody, DialogHeader } from "./components/Dialog/Dialog.js";
|
|
@@ -32,4 +32,4 @@ import { Tag } from "./components/Tag/Tag.js";
|
|
|
32
32
|
import { Textarea } from "./components/Textarea/Textarea.js";
|
|
33
33
|
import { ValidationSummary, ValidationSummaryErrors, ValidationSummaryHeading, ValidationSummaryPrompt } from "./components/ValidationSummary/ValidationSummary.js";
|
|
34
34
|
import { Wrapper } from "./components/Wrapper/Wrapper.js";
|
|
35
|
-
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, Section, Select, SelectOption, SelectOptionGroup, Signpost, SkipNav, Squircle, Tag, TextInput, Textarea, ValidationSummary, ValidationSummaryErrors, ValidationSummaryHeading, ValidationSummaryPrompt, Wrapper };
|
|
35
|
+
export { AlertBanner, AlertBannerHeading, Author, Box, Button, Card, CardBadge, CardContent, CardHeading, CardIcon, CardImage, CardLink, CardPicture, Checkbox, Combobox, ComboboxButton, ComboboxControls, ComboboxInput, ComboboxOptions, 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, Section, Select, SelectOption, SelectOptionGroup, Signpost, SkipNav, Squircle, Tag, TextInput, Textarea, ValidationSummary, ValidationSummaryErrors, ValidationSummaryHeading, ValidationSummaryPrompt, Wrapper };
|