@coopdigital/react 1.29.2 → 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.
@@ -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, UseComboboxSelectedItemChange, UseComboboxStateChange } from "downshift";
6
+ import { UseComboboxHighlightedIndexChange, UseComboboxInputValueChange, UseComboboxIsOpenChange, UseComboboxStateChange } from "downshift";
7
7
  //#region src/components/Combobox/Combobox.d.ts
8
- export interface ComboboxProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "onSelect" | "onToggle"> {
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?: string | ReactNode;
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` prop on `.ComboboxOption`
51
+ * Default: the `disabled` property of the option.
37
52
  */
38
53
  isItemDisabled?: (item: ComboboxItem) => boolean;
39
- /** Custom function to render text in the input field after selecting a value.
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 users select an option from the list.
69
+ /** Callback fired when the selection changes, including when it is cleared.
55
70
  *
56
- * This is a `downshift` event.
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: UseComboboxSelectedItemChange<ComboboxItem>) => void;
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?: (changes: UseComboboxIsOpenChange<ComboboxItem>) => void;
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?: React.ReactNode;
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
- * Can be either a simple string array or a complex `ComboboxOption` array.
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
- export type ComboboxInputProps = TextInputProps;
102
- export declare const Combobox: {
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: ({ className, placeholder, ...props }: ComboboxInputProps) => JSX.Element;
116
- Button: ({ children, className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) => JSX.Element;
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 | null;
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 userOptions = getSlotProp(slots.ComboboxOptions, "options") ?? [];
24
- const items = normalizeOptions(userOptions);
25
- const [inputItems, setInputItems] = useState(items);
26
- const componentProps = {
27
- className: clsx("coop-combobox", className),
28
- "data-field": "",
29
- "data-size": size ?? void 0,
30
- id: uid,
31
- ...props
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 inputFilterActual = inputFilter ?? defaultFilter;
34
- const itemToStringActual = itemToString ?? defaultItemToString;
35
- const comboboxProps = useCombobox({
94
+ const downshift = useCombobox({
95
+ getItemId: (index) => visibleItems[index]?.id ?? `${uid}_item_${index}`,
36
96
  id: uid,
37
- inputId: uid + "_input",
38
- isItemDisabled: isItemDisabled ?? defaultIsItemDisabled,
39
- items: inputItems,
40
- itemToString: itemToStringActual,
41
- labelId: uid + "_label",
42
- menuId: uid + "_menu",
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
- setInputItems(inputFilterActual(items, event.inputValue));
107
+ if (event.type === useCombobox.stateChangeTypes.InputChange) setFilterText(event.inputValue);
46
108
  onChange?.(event);
47
109
  },
48
- onIsOpenChange: onToggle,
49
- onSelectedItemChange: (event) => {
50
- onSelect?.(event);
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
- stateReducer(_, actionAndChanges) {
54
- const { InputBlur, InputClick, InputKeyDownEnter } = useCombobox.stateChangeTypes;
55
- const { changes: state, type } = actionAndChanges;
56
- const currentInput = state.inputValue ?? "";
57
- const hasMatch = items.some((item) => itemToStringActual(item) === currentInput);
58
- switch (type) {
59
- case openOnClick === false && InputClick: return {
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: inputItems,
80
- props: comboboxProps,
155
+ items: visibleItems,
156
+ matchCount: matchingItems.length,
81
157
  size
82
158
  },
83
159
  children: /* @__PURE__ */ jsxs("div", {
84
- ...componentProps,
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 ComboboxInput = ({ className, placeholder, ...props }) => {
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 componentProps = {
93
- className,
94
- disabled: combobox.disabled,
95
- error: combobox.error,
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 = ({ children, className, ...props }) => {
193
+ const ComboboxButton = ({ "aria-label": ariaLabel = "Toggle menu", children, ...props }) => {
106
194
  const combobox = useComboboxContext();
107
- const componentProps = {
108
- "aria-label": props["aria-label"] ?? "Toggle menu",
109
- className,
110
- disabled: combobox.disabled,
111
- ...props
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
- ...componentProps,
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 ComboboxControls = ({ children, className, ...props }) => {
125
- const componentProps = {
126
- className: clsx("coop-combobox-controls", className),
127
- ...props
128
- };
129
- return /* @__PURE__ */ jsx("div", {
130
- ...componentProps,
131
- children
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
- if (!options) return null;
141
- return /* @__PURE__ */ jsxs("ul", {
142
- ...componentProps,
143
- ...combobox.props?.getMenuProps(),
144
- children: [combobox.props?.isOpen && combobox.items?.length ? combobox.items.map((item, index) => {
145
- const itemProps = { ...combobox.props?.getItemProps({
146
- index,
147
- item
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
- ...itemProps,
151
- ...item,
152
- children: [item?.icon, item.children ?? item.label ?? item.value]
153
- }, item.value);
154
- }) : null, combobox.props?.isOpen && !combobox.items?.length && /* @__PURE__ */ jsx("li", { children: noResultsText })]
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 { Combobox, ComboboxControlsProps, ComboboxInputProps, ComboboxOptionsProps, ComboboxProps } from "./Combobox.js";
2
- export { Combobox, Combobox as default, ComboboxControlsProps, ComboboxInputProps, ComboboxOptionsProps, ComboboxProps };
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 };