@itwin/itwinui-react 2.0.4 → 2.1.1

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +15 -2
  2. package/cjs/core/ComboBox/ComboBox.d.ts +25 -8
  3. package/cjs/core/ComboBox/ComboBox.js +141 -44
  4. package/cjs/core/ComboBox/ComboBoxDropdown.d.ts +1 -1
  5. package/cjs/core/ComboBox/ComboBoxDropdown.js +2 -2
  6. package/cjs/core/ComboBox/ComboBoxEndIcon.js +1 -1
  7. package/cjs/core/ComboBox/ComboBoxInput.d.ts +2 -0
  8. package/cjs/core/ComboBox/ComboBoxInput.js +40 -23
  9. package/cjs/core/ComboBox/ComboBoxMenuItem.js +1 -1
  10. package/cjs/core/ComboBox/ComboBoxMultipleContainer.d.ts +4 -0
  11. package/cjs/core/ComboBox/ComboBoxMultipleContainer.js +17 -0
  12. package/cjs/core/ComboBox/helpers.d.ts +20 -5
  13. package/cjs/core/ComboBox/helpers.js +24 -6
  14. package/cjs/core/Select/Select.js +2 -7
  15. package/cjs/core/Select/SelectTagContainer.d.ts +16 -0
  16. package/cjs/core/Select/SelectTagContainer.js +27 -0
  17. package/cjs/core/Table/Table.js +4 -2
  18. package/cjs/core/Table/actionHandlers/index.d.ts +1 -1
  19. package/cjs/core/Table/actionHandlers/index.js +2 -2
  20. package/cjs/core/Table/actionHandlers/selectHandler.d.ts +2 -2
  21. package/cjs/core/Table/actionHandlers/selectHandler.js +20 -6
  22. package/cjs/core/ThemeProvider/ThemeProvider.d.ts +25 -6
  23. package/cjs/core/ThemeProvider/ThemeProvider.js +29 -12
  24. package/cjs/core/utils/components/Popover.d.ts +1 -1
  25. package/cjs/core/utils/hooks/index.d.ts +1 -0
  26. package/cjs/core/utils/hooks/index.js +1 -0
  27. package/cjs/core/utils/hooks/useIsThemeAlreadySet.d.ts +7 -0
  28. package/cjs/core/utils/hooks/useIsThemeAlreadySet.js +34 -0
  29. package/cjs/core/utils/hooks/useTheme.js +4 -9
  30. package/esm/core/ComboBox/ComboBox.d.ts +25 -8
  31. package/esm/core/ComboBox/ComboBox.js +141 -44
  32. package/esm/core/ComboBox/ComboBoxDropdown.d.ts +1 -1
  33. package/esm/core/ComboBox/ComboBoxDropdown.js +2 -2
  34. package/esm/core/ComboBox/ComboBoxEndIcon.js +1 -1
  35. package/esm/core/ComboBox/ComboBoxInput.d.ts +2 -0
  36. package/esm/core/ComboBox/ComboBoxInput.js +41 -24
  37. package/esm/core/ComboBox/ComboBoxMenuItem.js +1 -1
  38. package/esm/core/ComboBox/ComboBoxMultipleContainer.d.ts +4 -0
  39. package/esm/core/ComboBox/ComboBoxMultipleContainer.js +11 -0
  40. package/esm/core/ComboBox/helpers.d.ts +20 -5
  41. package/esm/core/ComboBox/helpers.js +24 -6
  42. package/esm/core/Select/Select.js +3 -8
  43. package/esm/core/Select/SelectTagContainer.d.ts +16 -0
  44. package/esm/core/Select/SelectTagContainer.js +21 -0
  45. package/esm/core/Table/Table.js +5 -3
  46. package/esm/core/Table/actionHandlers/index.d.ts +1 -1
  47. package/esm/core/Table/actionHandlers/index.js +1 -1
  48. package/esm/core/Table/actionHandlers/selectHandler.d.ts +2 -2
  49. package/esm/core/Table/actionHandlers/selectHandler.js +17 -3
  50. package/esm/core/ThemeProvider/ThemeProvider.d.ts +25 -6
  51. package/esm/core/ThemeProvider/ThemeProvider.js +30 -13
  52. package/esm/core/utils/components/Popover.d.ts +1 -1
  53. package/esm/core/utils/hooks/index.d.ts +1 -0
  54. package/esm/core/utils/hooks/index.js +1 -0
  55. package/esm/core/utils/hooks/useIsThemeAlreadySet.d.ts +7 -0
  56. package/esm/core/utils/hooks/useIsThemeAlreadySet.js +27 -0
  57. package/esm/core/utils/hooks/useTheme.js +4 -6
  58. package/package.json +10 -5
@@ -5,6 +5,7 @@
5
5
  import React from 'react';
6
6
  import cx from 'classnames';
7
7
  import { MenuExtraContent } from '../Menu';
8
+ import SelectTag from '../Select/SelectTag';
8
9
  import { Text } from '../Typography';
9
10
  import { useTheme, getRandomValue, mergeRefs, useLatestRef, useIsomorphicLayoutEffect, } from '../utils';
10
11
  import 'tippy.js/animations/shift-away.css';
@@ -15,6 +16,14 @@ import { ComboBoxInput } from './ComboBoxInput';
15
16
  import { ComboBoxInputContainer } from './ComboBoxInputContainer';
16
17
  import { ComboBoxMenu } from './ComboBoxMenu';
17
18
  import { ComboBoxMenuItem } from './ComboBoxMenuItem';
19
+ // Type guard for enabling multiple
20
+ const isMultipleEnabled = (variable, multiple) => {
21
+ return multiple && (Array.isArray(variable) || variable === undefined);
22
+ };
23
+ // Type guard for user onChange
24
+ const isSingleOnChange = (onChange, multiple) => {
25
+ return !multiple;
26
+ };
18
27
  /** Returns either `option.id` or derives a stable id using `idPrefix` and `option.label` (without whitespace) */
19
28
  const getOptionId = (option, idPrefix) => {
20
29
  var _a;
@@ -35,7 +44,7 @@ const getOptionId = (option, idPrefix) => {
35
44
  */
36
45
  export const ComboBox = (props) => {
37
46
  var _a, _b;
38
- const { options, value: valueProp, onChange, filterFunction, inputProps, dropdownMenuProps, emptyStateMessage = 'No options found', itemRenderer, enableVirtualization = false, onShow, onHide, ...rest } = props;
47
+ const { options, value: valueProp, onChange, filterFunction, inputProps, dropdownMenuProps, emptyStateMessage = 'No options found', itemRenderer, enableVirtualization = false, multiple = false, onShow, onHide, ...rest } = props;
39
48
  // Generate a stateful random id if not specified
40
49
  const [id] = React.useState(() => {
41
50
  var _a, _b;
@@ -46,8 +55,6 @@ export const ComboBox = (props) => {
46
55
  const inputRef = React.useRef(null);
47
56
  const menuRef = React.useRef(null);
48
57
  const toggleButtonRef = React.useRef(null);
49
- const mounted = React.useRef(false);
50
- const valuePropRef = useLatestRef(valueProp);
51
58
  const onChangeProp = useLatestRef(onChange);
52
59
  const optionsRef = useLatestRef(options);
53
60
  // Record to store all extra information (e.g. original indexes), where the key is the id of the option
@@ -65,12 +72,23 @@ export const ComboBox = (props) => {
65
72
  };
66
73
  });
67
74
  }
75
+ // Get indices of selected elements in options array when we have selected values.
76
+ const getSelectedIndexes = React.useCallback(() => {
77
+ if (isMultipleEnabled(valueProp, multiple)) {
78
+ const indexArray = [];
79
+ valueProp === null || valueProp === void 0 ? void 0 : valueProp.forEach((value) => {
80
+ indexArray.push(options.findIndex((option) => option.value === value));
81
+ });
82
+ return indexArray;
83
+ }
84
+ else {
85
+ return options.findIndex((option) => option.value === valueProp);
86
+ }
87
+ }, [multiple, options, valueProp]);
68
88
  // Reducer where all the component-wide state is stored
69
- const [{ isOpen, selectedIndex, focusedIndex }, dispatch] = React.useReducer(comboBoxReducer, {
89
+ const [{ isOpen, selected, focusedIndex }, dispatch] = React.useReducer(comboBoxReducer, {
70
90
  isOpen: false,
71
- selectedIndex: valueProp
72
- ? optionsRef.current.findIndex((option) => option.value === valueProp)
73
- : -1,
91
+ selected: getSelectedIndexes(),
74
92
  focusedIndex: -1,
75
93
  });
76
94
  useIsomorphicLayoutEffect(() => {
@@ -78,19 +96,24 @@ export const ComboBox = (props) => {
78
96
  // When the dropdown opens
79
97
  if (isOpen) {
80
98
  (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); // Focus the input
81
- setFilteredOptions(optionsRef.current); // Reset the filtered list
82
- dispatch(['focus']);
99
+ // Reset the filtered list (does not reset when multiple enabled)
100
+ if (!multiple) {
101
+ setFilteredOptions(optionsRef.current);
102
+ dispatch({ type: 'focus', value: undefined });
103
+ }
83
104
  }
84
105
  // When the dropdown closes
85
106
  else {
86
107
  // Reset the focused index
87
- dispatch(['focus']);
88
- // Reset the input value
89
- setInputValue(selectedIndex != undefined && selectedIndex >= 0
90
- ? (_b = optionsRef.current[selectedIndex]) === null || _b === void 0 ? void 0 : _b.label
91
- : '');
108
+ dispatch({ type: 'focus', value: undefined });
109
+ // Reset the input value if not multiple
110
+ if (!isMultipleEnabled(selected, multiple)) {
111
+ setInputValue(selected != undefined && selected >= 0
112
+ ? (_b = optionsRef.current[selected]) === null || _b === void 0 ? void 0 : _b.label
113
+ : '');
114
+ }
92
115
  }
93
- }, [isOpen, optionsRef, selectedIndex]);
116
+ }, [isOpen, multiple, optionsRef, selected]);
94
117
  // Set min-width of menu to be same as input
95
118
  const [minWidth, setMinWidth] = React.useState(0);
96
119
  React.useEffect(() => {
@@ -108,7 +131,7 @@ export const ComboBox = (props) => {
108
131
  else {
109
132
  setFilteredOptions(options);
110
133
  }
111
- dispatch(['focus']);
134
+ dispatch({ type: 'focus', value: undefined });
112
135
  // Only need to call on options update
113
136
  // eslint-disable-next-line react-hooks/exhaustive-deps
114
137
  }, [options]);
@@ -118,41 +141,101 @@ export const ComboBox = (props) => {
118
141
  var _a, _b;
119
142
  const { value } = event.currentTarget;
120
143
  setInputValue(value);
121
- dispatch(['open']); // reopen when typing
144
+ dispatch({ type: 'open' }); // reopen when typing
122
145
  setFilteredOptions((_a = filterFunction === null || filterFunction === void 0 ? void 0 : filterFunction(optionsRef.current, value)) !== null && _a !== void 0 ? _a : optionsRef.current.filter((option) => option.label.toLowerCase().includes(value.toLowerCase())));
123
146
  if (focusedIndex != -1) {
124
- dispatch(['focus', -1]);
147
+ dispatch({ type: 'focus', value: -1 });
125
148
  }
126
149
  (_b = inputProps === null || inputProps === void 0 ? void 0 : inputProps.onChange) === null || _b === void 0 ? void 0 : _b.call(inputProps, event);
127
150
  }, [filterFunction, focusedIndex, inputProps, optionsRef]);
128
- // When the value prop changes, update the selectedIndex
151
+ // When the value prop changes, update the selected index/indices
129
152
  React.useEffect(() => {
130
- dispatch([
131
- 'select',
132
- options.findIndex((option) => option.value === valueProp),
133
- ]);
134
- }, [options, valueProp]);
135
- // Call user-defined onChange when the value actually changes
136
- React.useEffect(() => {
137
- var _a, _b;
138
- // Prevent user-defined onChange to be called on mount
139
- if (!mounted.current) {
140
- mounted.current = true;
141
- return;
153
+ if (isMultipleEnabled(valueProp, multiple)) {
154
+ if (valueProp) {
155
+ // If user provided array of selected values
156
+ const indexes = valueProp.map((value) => {
157
+ return options.findIndex((option) => option.value === value);
158
+ });
159
+ dispatch({
160
+ type: 'multiselect',
161
+ value: indexes.filter((index) => index !== -1), // Add available options
162
+ });
163
+ }
164
+ else {
165
+ // if user provided one value or undefined
166
+ dispatch({
167
+ type: 'multiselect',
168
+ value: [], // Add empty list
169
+ });
170
+ }
171
+ }
172
+ else {
173
+ dispatch({
174
+ type: 'select',
175
+ value: options.findIndex((option) => option.value === valueProp),
176
+ });
177
+ }
178
+ }, [valueProp, options, multiple]);
179
+ const isMenuItemSelected = React.useCallback((index) => {
180
+ if (isMultipleEnabled(selected, multiple)) {
181
+ return !!selected.includes(index);
142
182
  }
143
- const currentValue = (_a = optionsRef.current[selectedIndex]) === null || _a === void 0 ? void 0 : _a.value;
144
- if (currentValue === valuePropRef.current || selectedIndex === -1) {
145
- return;
183
+ else {
184
+ return selected === index;
185
+ }
186
+ }, [multiple, selected]);
187
+ // Generates new array when item is added or removed
188
+ const selectedChangeHandler = React.useCallback((__originalIndex, action) => {
189
+ if (action === 'added') {
190
+ return [...selected, __originalIndex];
191
+ }
192
+ else {
193
+ return selected.filter((index) => index !== __originalIndex);
194
+ }
195
+ }, [selected]);
196
+ // Calls user defined onChange
197
+ const onChangeHandler = React.useCallback((__originalIndex, actionType, newArray) => {
198
+ var _a, _b, _c, _d;
199
+ if (isSingleOnChange(onChangeProp.current, multiple)) {
200
+ (_a = onChangeProp.current) === null || _a === void 0 ? void 0 : _a.call(onChangeProp, (_b = optionsRef.current[__originalIndex]) === null || _b === void 0 ? void 0 : _b.value);
201
+ }
202
+ else {
203
+ actionType &&
204
+ newArray &&
205
+ ((_c = onChangeProp.current) === null || _c === void 0 ? void 0 : _c.call(onChangeProp, newArray === null || newArray === void 0 ? void 0 : newArray.map((item) => { var _a; return (_a = optionsRef.current[item]) === null || _a === void 0 ? void 0 : _a.value; }), {
206
+ value: (_d = optionsRef.current[__originalIndex]) === null || _d === void 0 ? void 0 : _d.value,
207
+ type: actionType,
208
+ }));
209
+ }
210
+ }, [multiple, onChangeProp, optionsRef]);
211
+ const onClickHandler = React.useCallback((__originalIndex) => {
212
+ if (isMultipleEnabled(selected, multiple)) {
213
+ const actionType = isMenuItemSelected(__originalIndex)
214
+ ? 'removed'
215
+ : 'added';
216
+ const newArray = selectedChangeHandler(__originalIndex, actionType);
217
+ dispatch({ type: 'multiselect', value: newArray });
218
+ onChangeHandler(__originalIndex, actionType, newArray);
219
+ }
220
+ else {
221
+ dispatch({ type: 'select', value: __originalIndex });
222
+ dispatch({ type: 'close' });
223
+ onChangeHandler(__originalIndex);
146
224
  }
147
- (_b = onChangeProp.current) === null || _b === void 0 ? void 0 : _b.call(onChangeProp, currentValue);
148
- }, [onChangeProp, optionsRef, selectedIndex, valuePropRef]);
225
+ }, [
226
+ selectedChangeHandler,
227
+ isMenuItemSelected,
228
+ multiple,
229
+ onChangeHandler,
230
+ selected,
231
+ ]);
149
232
  const getMenuItem = React.useCallback((option, filteredIndex) => {
150
233
  const optionId = getOptionId(option, id);
151
234
  const { __originalIndex } = optionsExtraInfoRef.current[optionId];
152
235
  const customItem = itemRenderer
153
236
  ? itemRenderer(option, {
154
237
  isFocused: focusedIndex === __originalIndex,
155
- isSelected: selectedIndex === __originalIndex,
238
+ isSelected: selected === __originalIndex,
156
239
  index: __originalIndex,
157
240
  id: optionId,
158
241
  })
@@ -160,8 +243,7 @@ export const ComboBox = (props) => {
160
243
  return customItem ? (React.cloneElement(customItem, {
161
244
  onClick: (e) => {
162
245
  var _a, _b;
163
- dispatch(['select', __originalIndex]);
164
- dispatch(['close']);
246
+ onClickHandler(__originalIndex);
165
247
  (_b = (_a = customItem.props).onClick) === null || _b === void 0 ? void 0 : _b.call(_a, e);
166
248
  },
167
249
  // ComboBox.MenuItem handles scrollIntoView, data-iui-index and iui-focused through context
@@ -176,11 +258,18 @@ export const ComboBox = (props) => {
176
258
  el === null || el === void 0 ? void 0 : el.scrollIntoView({ block: 'nearest' });
177
259
  }
178
260
  }),
179
- })) : (React.createElement(ComboBoxMenuItem, { key: optionId, id: optionId, ...option, isSelected: selectedIndex === __originalIndex, onClick: () => {
180
- dispatch(['select', __originalIndex]);
181
- dispatch(['close']);
261
+ })) : (React.createElement(ComboBoxMenuItem, { key: optionId, id: optionId, ...option, isSelected: isMenuItemSelected(__originalIndex), onClick: () => {
262
+ onClickHandler(__originalIndex);
182
263
  }, index: __originalIndex, "data-iui-filtered-index": filteredIndex }, option.label));
183
- }, [enableVirtualization, focusedIndex, id, itemRenderer, selectedIndex]);
264
+ }, [
265
+ enableVirtualization,
266
+ focusedIndex,
267
+ id,
268
+ isMenuItemSelected,
269
+ itemRenderer,
270
+ onClickHandler,
271
+ selected,
272
+ ]);
184
273
  const emptyContent = React.useMemo(() => (React.createElement(React.Fragment, null, React.isValidElement(emptyStateMessage) ? (emptyStateMessage) : (React.createElement(MenuExtraContent, null,
185
274
  React.createElement(Text, { isMuted: true }, emptyStateMessage))))), [emptyStateMessage]);
186
275
  return (React.createElement(ComboBoxRefsContext.Provider, { value: { inputRef, menuRef, toggleButtonRef, optionsExtraInfoRef } },
@@ -190,12 +279,20 @@ export const ComboBox = (props) => {
190
279
  minWidth,
191
280
  isOpen,
192
281
  focusedIndex,
282
+ onClickHandler,
193
283
  enableVirtualization,
194
284
  filteredOptions,
195
285
  getMenuItem,
286
+ multiple,
196
287
  } },
197
288
  React.createElement(ComboBoxInputContainer, { disabled: inputProps === null || inputProps === void 0 ? void 0 : inputProps.disabled, ...rest },
198
- React.createElement(ComboBoxInput, { value: inputValue, ...inputProps, onChange: handleOnInput }),
289
+ React.createElement(React.Fragment, null,
290
+ React.createElement(ComboBoxInput, { value: inputValue, ...inputProps, onChange: handleOnInput, selectTags: isMultipleEnabled(selected, multiple)
291
+ ? selected.map((index) => {
292
+ const item = optionsRef.current[index];
293
+ return (React.createElement(SelectTag, { key: item.label, label: item.label }));
294
+ })
295
+ : undefined })),
199
296
  React.createElement(ComboBoxEndIcon, { disabled: inputProps === null || inputProps === void 0 ? void 0 : inputProps.disabled, isOpen: isOpen })),
200
297
  React.createElement(ComboBoxDropdown, { ...dropdownMenuProps, onShow: onShow, onHide: onHide },
201
298
  React.createElement(ComboBoxMenu, null, filteredOptions.length > 0 && !enableVirtualization
@@ -3,5 +3,5 @@ import { PopoverProps } from '../utils';
3
3
  declare type ComboBoxDropdownProps = PopoverProps & {
4
4
  children: JSX.Element;
5
5
  };
6
- export declare const ComboBoxDropdown: React.ForwardRefExoticComponent<Pick<ComboBoxDropdownProps, "disabled" | "theme" | "children" | "className" | "role" | "placement" | "trigger" | "visible" | "content" | "render" | "animateFill" | "appendTo" | "aria" | "delay" | "duration" | "followCursor" | "getReferenceClientRect" | "hideOnClick" | "ignoreAttributes" | "inlinePositioning" | "interactive" | "interactiveBorder" | "interactiveDebounce" | "moveTransition" | "offset" | "plugins" | "popperOptions" | "showOnCreate" | "sticky" | "touch" | "triggerTarget" | "onAfterUpdate" | "onBeforeUpdate" | "onCreate" | "onDestroy" | "onHidden" | "onHide" | "onMount" | "onShow" | "onShown" | "onTrigger" | "onUntrigger" | "onClickOutside" | "allowHTML" | "animation" | "arrow" | "inertia" | "maxWidth" | "zIndex" | "singleton" | "reference"> & React.RefAttributes<Element>>;
6
+ export declare const ComboBoxDropdown: React.ForwardRefExoticComponent<Pick<ComboBoxDropdownProps, "disabled" | "theme" | "children" | "className" | "role" | "offset" | "content" | "plugins" | "placement" | "trigger" | "visible" | "render" | "animateFill" | "appendTo" | "aria" | "delay" | "duration" | "followCursor" | "getReferenceClientRect" | "hideOnClick" | "ignoreAttributes" | "inlinePositioning" | "interactive" | "interactiveBorder" | "interactiveDebounce" | "moveTransition" | "popperOptions" | "showOnCreate" | "sticky" | "touch" | "triggerTarget" | "onAfterUpdate" | "onBeforeUpdate" | "onCreate" | "onDestroy" | "onHidden" | "onHide" | "onMount" | "onShow" | "onShown" | "onTrigger" | "onUntrigger" | "onClickOutside" | "allowHTML" | "animation" | "arrow" | "inertia" | "maxWidth" | "zIndex" | "singleton" | "reference"> & React.RefAttributes<Element>>;
7
7
  export {};
@@ -13,13 +13,13 @@ export const ComboBoxDropdown = React.forwardRef((props, forwardedRef) => {
13
13
  // sync internal isOpen state with user's visible prop
14
14
  React.useEffect(() => {
15
15
  if (props.visible != undefined) {
16
- dispatch([props.visible ? 'open' : 'close']);
16
+ dispatch({ type: props.visible ? 'open' : 'close' });
17
17
  }
18
18
  }, [dispatch, props.visible]);
19
19
  return (React.createElement(Popover, { placement: 'bottom-start', visible: isOpen, onClickOutside: React.useCallback((_, { target }) => {
20
20
  var _a;
21
21
  if (!((_a = toggleButtonRef.current) === null || _a === void 0 ? void 0 : _a.contains(target))) {
22
- dispatch(['close']);
22
+ dispatch({ type: 'close' });
23
23
  }
24
24
  }, [dispatch, toggleButtonRef]), animation: 'shift-away', duration: 200, reference: inputRef, ref: forwardedRef, content: children, ...rest }));
25
25
  });
@@ -18,7 +18,7 @@ export const ComboBoxEndIcon = React.forwardRef((props, forwardedRef) => {
18
18
  }, className), onClick: (e) => {
19
19
  onClickProp === null || onClickProp === void 0 ? void 0 : onClickProp(e);
20
20
  if (!e.isDefaultPrevented()) {
21
- dispatch([isOpen ? 'close' : 'open']);
21
+ dispatch({ type: isOpen ? 'close' : 'open' });
22
22
  }
23
23
  }, ...rest }, children !== null && children !== void 0 ? children : React.createElement(SvgCaretDownSmall, { "aria-hidden": true })));
24
24
  });
@@ -1,5 +1,7 @@
1
1
  import React from 'react';
2
2
  export declare const ComboBoxInput: React.ForwardRefExoticComponent<{
3
+ selectTags?: JSX.Element[] | undefined;
4
+ } & {
3
5
  setFocus?: boolean | undefined;
4
6
  size?: "small" | "large" | undefined;
5
7
  } & Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> & React.RefAttributes<HTMLInputElement>>;
@@ -4,11 +4,12 @@
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import React from 'react';
6
6
  import { Input } from '../Input';
7
- import { useSafeContext, useMergedRefs } from '../utils';
7
+ import { useSafeContext, useMergedRefs, useContainerWidth } from '../utils';
8
+ import { ComboBoxMultipleContainer } from './ComboBoxMultipleContainer';
8
9
  import { ComboBoxStateContext, ComboBoxActionContext, ComboBoxRefsContext, } from './helpers';
9
10
  export const ComboBoxInput = React.forwardRef((props, forwardedRef) => {
10
- const { onKeyDown: onKeyDownProp, onFocus: onFocusProp, ...rest } = props;
11
- const { isOpen, id, focusedIndex, enableVirtualization } = useSafeContext(ComboBoxStateContext);
11
+ const { onKeyDown: onKeyDownProp, onFocus: onFocusProp, selectTags, ...rest } = props;
12
+ const { isOpen, id, focusedIndex, enableVirtualization, multiple, onClickHandler, } = useSafeContext(ComboBoxStateContext);
12
13
  const dispatch = useSafeContext(ComboBoxActionContext);
13
14
  const { inputRef, menuRef, optionsExtraInfoRef } = useSafeContext(ComboBoxRefsContext);
14
15
  const refs = useMergedRefs(inputRef, forwardedRef);
@@ -28,17 +29,17 @@ export const ComboBoxInput = React.forwardRef((props, forwardedRef) => {
28
29
  case 'ArrowDown': {
29
30
  event.preventDefault();
30
31
  if (!isOpen) {
31
- return dispatch(['open']);
32
+ return dispatch({ type: 'open' });
32
33
  }
33
34
  if (length === 0) {
34
35
  return;
35
36
  }
36
37
  if (focusedIndexRef.current === -1) {
37
38
  const currentElement = (_b = menuRef.current) === null || _b === void 0 ? void 0 : _b.querySelector('[data-iui-index]');
38
- return dispatch([
39
- 'focus',
40
- Number((_c = currentElement === null || currentElement === void 0 ? void 0 : currentElement.getAttribute('data-iui-index')) !== null && _c !== void 0 ? _c : 0),
41
- ]);
39
+ return dispatch({
40
+ type: 'focus',
41
+ value: Number((_c = currentElement === null || currentElement === void 0 ? void 0 : currentElement.getAttribute('data-iui-index')) !== null && _c !== void 0 ? _c : 0),
42
+ });
42
43
  }
43
44
  // If virtualization is enabled, dont let round scrolling
44
45
  if (enableVirtualization &&
@@ -51,7 +52,7 @@ export const ComboBoxInput = React.forwardRef((props, forwardedRef) => {
51
52
  const nextElement = (_g = currentElement === null || currentElement === void 0 ? void 0 : currentElement.nextElementSibling) !== null && _g !== void 0 ? _g : (_h = menuRef.current) === null || _h === void 0 ? void 0 : _h.querySelector('[data-iui-index]');
52
53
  nextIndex = Number(nextElement === null || nextElement === void 0 ? void 0 : nextElement.getAttribute('data-iui-index'));
53
54
  if ((nextElement === null || nextElement === void 0 ? void 0 : nextElement.ariaDisabled) !== 'true') {
54
- return dispatch(['focus', nextIndex]);
55
+ return dispatch({ type: 'focus', value: nextIndex });
55
56
  }
56
57
  } while (nextIndex !== focusedIndexRef.current);
57
58
  break;
@@ -59,7 +60,7 @@ export const ComboBoxInput = React.forwardRef((props, forwardedRef) => {
59
60
  case 'ArrowUp': {
60
61
  event.preventDefault();
61
62
  if (!isOpen) {
62
- return dispatch(['open']);
63
+ return dispatch({ type: 'open' });
63
64
  }
64
65
  if (length === 0) {
65
66
  return;
@@ -70,10 +71,10 @@ export const ComboBoxInput = React.forwardRef((props, forwardedRef) => {
70
71
  return;
71
72
  }
72
73
  if (focusedIndexRef.current === -1) {
73
- return dispatch([
74
- 'focus',
75
- (_m = (_l = Object.values(optionsExtraInfoRef.current)) === null || _l === void 0 ? void 0 : _l[length - 1].__originalIndex) !== null && _m !== void 0 ? _m : -1,
76
- ]);
74
+ return dispatch({
75
+ type: 'focus',
76
+ value: (_m = (_l = Object.values(optionsExtraInfoRef.current)) === null || _l === void 0 ? void 0 : _l[length - 1].__originalIndex) !== null && _m !== void 0 ? _m : -1,
77
+ });
77
78
  }
78
79
  let prevIndex = focusedIndexRef.current;
79
80
  do {
@@ -81,7 +82,7 @@ export const ComboBoxInput = React.forwardRef((props, forwardedRef) => {
81
82
  const prevElement = (_p = currentElement === null || currentElement === void 0 ? void 0 : currentElement.previousElementSibling) !== null && _p !== void 0 ? _p : (_q = menuRef.current) === null || _q === void 0 ? void 0 : _q.querySelector('[data-iui-index]:last-of-type');
82
83
  prevIndex = Number(prevElement === null || prevElement === void 0 ? void 0 : prevElement.getAttribute('data-iui-index'));
83
84
  if ((prevElement === null || prevElement === void 0 ? void 0 : prevElement.ariaDisabled) !== 'true') {
84
- return dispatch(['focus', prevIndex]);
85
+ return dispatch({ type: 'focus', value: prevIndex });
85
86
  }
86
87
  } while (prevIndex !== focusedIndexRef.current);
87
88
  break;
@@ -89,21 +90,32 @@ export const ComboBoxInput = React.forwardRef((props, forwardedRef) => {
89
90
  case 'Enter': {
90
91
  event.preventDefault();
91
92
  if (isOpen) {
92
- dispatch(['select', focusedIndexRef.current]);
93
- dispatch(['close']);
93
+ if (multiple) {
94
+ // Keep menu open when multiselect is enabled and user selects an item
95
+ if (focusedIndexRef.current > -1) {
96
+ onClickHandler === null || onClickHandler === void 0 ? void 0 : onClickHandler(focusedIndexRef.current);
97
+ }
98
+ else {
99
+ dispatch({ type: 'close' });
100
+ }
101
+ }
102
+ else {
103
+ onClickHandler === null || onClickHandler === void 0 ? void 0 : onClickHandler(focusedIndexRef.current);
104
+ dispatch({ type: 'close' });
105
+ }
94
106
  }
95
107
  else {
96
- dispatch(['open']);
108
+ dispatch({ type: 'open' });
97
109
  }
98
110
  break;
99
111
  }
100
112
  case 'Escape': {
101
113
  event.preventDefault();
102
- dispatch(['close']);
114
+ dispatch({ type: 'close' });
103
115
  break;
104
116
  }
105
117
  case 'Tab':
106
- dispatch(['close']);
118
+ dispatch({ type: 'close' });
107
119
  break;
108
120
  }
109
121
  }, [
@@ -111,15 +123,20 @@ export const ComboBoxInput = React.forwardRef((props, forwardedRef) => {
111
123
  enableVirtualization,
112
124
  isOpen,
113
125
  menuRef,
126
+ multiple,
127
+ onClickHandler,
114
128
  onKeyDownProp,
115
129
  optionsExtraInfoRef,
116
130
  ]);
117
131
  const handleFocus = React.useCallback((event) => {
118
- dispatch(['open']);
132
+ dispatch({ type: 'open' });
119
133
  onFocusProp === null || onFocusProp === void 0 ? void 0 : onFocusProp(event);
120
134
  }, [dispatch, onFocusProp]);
121
- return (React.createElement(Input, { ref: refs, onKeyDown: handleKeyDown, onFocus: handleFocus, "aria-activedescendant": isOpen && focusedIndex != undefined && focusedIndex > -1
122
- ? getIdFromIndex(focusedIndex)
123
- : undefined, role: 'combobox', "aria-controls": isOpen ? `${id}-list` : undefined, "aria-autocomplete": 'list', spellCheck: false, autoCapitalize: 'none', autoCorrect: 'off', ...rest }));
135
+ const [tagContainerWidthRef, tagContainerWidth] = useContainerWidth();
136
+ return (React.createElement(React.Fragment, null,
137
+ React.createElement(Input, { ref: refs, onKeyDown: handleKeyDown, onFocus: handleFocus, "aria-activedescendant": isOpen && focusedIndex != undefined && focusedIndex > -1
138
+ ? getIdFromIndex(focusedIndex)
139
+ : undefined, role: 'combobox', "aria-controls": isOpen ? `${id}-list` : undefined, "aria-autocomplete": 'list', spellCheck: false, autoCapitalize: 'none', autoCorrect: 'off', style: multiple ? { paddingLeft: tagContainerWidth + 18 } : {}, ...rest }),
140
+ multiple && selectTags && (React.createElement(ComboBoxMultipleContainer, { ref: tagContainerWidthRef, selectedItems: selectTags }))));
124
141
  });
125
142
  ComboBoxInput.displayName = 'ComboBoxInput';
@@ -7,7 +7,7 @@ import React from 'react';
7
7
  import { useSafeContext, useMergedRefs } from '../utils';
8
8
  import { ComboBoxStateContext } from './helpers';
9
9
  export const ComboBoxMenuItem = React.memo(React.forwardRef((props, forwardedRef) => {
10
- const { children, isSelected, disabled, value, onClick, sublabel, size = !!sublabel ? 'large' : 'default', icon, badge, className, role = 'menuitem', index, ...rest } = props;
10
+ const { children, isSelected, disabled, value, onClick, sublabel, size = !!sublabel ? 'large' : 'default', icon, badge, className, role = 'option', index, ...rest } = props;
11
11
  const { focusedIndex, enableVirtualization } = useSafeContext(ComboBoxStateContext);
12
12
  const focusRef = (el) => {
13
13
  if (!enableVirtualization && focusedIndex === index) {
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ export declare const ComboBoxMultipleContainer: React.ForwardRefExoticComponent<{
3
+ selectedItems?: React.ReactNode[] | undefined;
4
+ } & Omit<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>>, "children"> & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,11 @@
1
+ /*---------------------------------------------------------------------------------------------
2
+ * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
3
+ * See LICENSE.md in the project root for license terms and full copyright notice.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ import React from 'react';
6
+ import SelectTagContainer from '../Select/SelectTagContainer';
7
+ export const ComboBoxMultipleContainer = React.forwardRef((props, ref) => {
8
+ const { selectedItems = [], ...rest } = props;
9
+ return React.createElement(SelectTagContainer, { ref: ref, tags: selectedItems, ...rest });
10
+ });
11
+ ComboBoxMultipleContainer.displayName = 'ComboBoxMultipleContainer';
@@ -1,13 +1,26 @@
1
1
  import React from 'react';
2
2
  import { SelectOption } from '../Select/Select';
3
- declare type ComboBoxAction = 'open' | 'close' | 'select' | 'focus';
3
+ declare type ComboBoxAction = {
4
+ type: 'multiselect';
5
+ value: number[];
6
+ } | {
7
+ type: 'open';
8
+ } | {
9
+ type: 'close';
10
+ } | {
11
+ type: 'select';
12
+ value: number;
13
+ } | {
14
+ type: 'focus';
15
+ value: number | undefined;
16
+ };
4
17
  export declare const comboBoxReducer: (state: {
5
18
  isOpen: boolean;
6
- selectedIndex: number;
19
+ selected: number | number[];
7
20
  focusedIndex: number;
8
- }, [type, value]: [ComboBoxAction] | [ComboBoxAction, number | undefined]) => {
21
+ }, action: ComboBoxAction) => {
9
22
  isOpen: boolean;
10
- selectedIndex: number;
23
+ selected: number | number[];
11
24
  focusedIndex: number;
12
25
  };
13
26
  export declare const ComboBoxRefsContext: React.Context<{
@@ -24,9 +37,11 @@ declare type ComboBoxStateContextProps<T = unknown> = {
24
37
  minWidth: number;
25
38
  enableVirtualization: boolean;
26
39
  filteredOptions: SelectOption<T>[];
40
+ onClickHandler?: (prop: number) => void;
27
41
  getMenuItem: (option: SelectOption<T>, filteredIndex?: number) => JSX.Element;
28
42
  focusedIndex?: number;
43
+ multiple?: boolean;
29
44
  };
30
45
  export declare const ComboBoxStateContext: React.Context<ComboBoxStateContextProps<unknown> | undefined>;
31
- export declare const ComboBoxActionContext: React.Context<((x: [ComboBoxAction] | [ComboBoxAction, number]) => void) | undefined>;
46
+ export declare const ComboBoxActionContext: React.Context<((x: ComboBoxAction) => void) | undefined>;
32
47
  export {};
@@ -3,9 +3,9 @@
3
3
  * See LICENSE.md in the project root for license terms and full copyright notice.
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import React from 'react';
6
- export const comboBoxReducer = (state, [type, value]) => {
7
- var _a;
8
- switch (type) {
6
+ export const comboBoxReducer = (state, action) => {
7
+ var _a, _b, _c, _d, _e;
8
+ switch (action.type) {
9
9
  case 'open': {
10
10
  return { ...state, isOpen: true };
11
11
  }
@@ -13,14 +13,32 @@ export const comboBoxReducer = (state, [type, value]) => {
13
13
  return { ...state, isOpen: false };
14
14
  }
15
15
  case 'select': {
16
+ if (Array.isArray(state.selected)) {
17
+ return { ...state };
18
+ }
16
19
  return {
17
20
  ...state,
18
- selectedIndex: value !== null && value !== void 0 ? value : state.selectedIndex,
19
- focusedIndex: value !== null && value !== void 0 ? value : state.focusedIndex,
21
+ selected: (_a = action.value) !== null && _a !== void 0 ? _a : state.selected,
22
+ focusedIndex: (_b = action.value) !== null && _b !== void 0 ? _b : state.focusedIndex,
20
23
  };
21
24
  }
25
+ case 'multiselect': {
26
+ if (!Array.isArray(state.selected)) {
27
+ return { ...state };
28
+ }
29
+ return { ...state, selected: action.value };
30
+ }
22
31
  case 'focus': {
23
- return { ...state, focusedIndex: (_a = value !== null && value !== void 0 ? value : state.selectedIndex) !== null && _a !== void 0 ? _a : -1 };
32
+ if (Array.isArray(state.selected)) {
33
+ return {
34
+ ...state,
35
+ focusedIndex: (_c = action.value) !== null && _c !== void 0 ? _c : -1,
36
+ };
37
+ }
38
+ return {
39
+ ...state,
40
+ focusedIndex: (_e = (_d = action.value) !== null && _d !== void 0 ? _d : state.selected) !== null && _e !== void 0 ? _e : -1,
41
+ };
24
42
  }
25
43
  default: {
26
44
  return state;
@@ -6,9 +6,10 @@ import React from 'react';
6
6
  import cx from 'classnames';
7
7
  import { DropdownMenu } from '../DropdownMenu';
8
8
  import { MenuItem } from '../Menu/MenuItem';
9
- import { useTheme, useOverflow, SvgCaretDownSmall, } from '../utils';
9
+ import { useTheme, SvgCaretDownSmall, } from '../utils';
10
10
  import '@itwin/itwinui-css/css/select.css';
11
11
  import SelectTag from './SelectTag';
12
+ import SelectTagContainer from './SelectTagContainer';
12
13
  const isMultipleEnabled = (variable, multiple) => {
13
14
  return multiple;
14
15
  };
@@ -197,17 +198,11 @@ const MultipleSelectButton = ({ selectedItems, selectedItemsRenderer, tagRendere
197
198
  }
198
199
  return selectedItems.map((item) => tagRenderer(item));
199
200
  }, [selectedItems, tagRenderer]);
200
- const [containerRef, visibleCount] = useOverflow(selectedItemsElements);
201
201
  return (React.createElement(React.Fragment, null,
202
202
  selectedItems &&
203
203
  selectedItemsRenderer &&
204
204
  selectedItemsRenderer(selectedItems),
205
205
  selectedItems && !selectedItemsRenderer && (React.createElement("span", { className: 'iui-content' },
206
- React.createElement("div", { className: 'iui-select-tag-container', ref: containerRef },
207
- React.createElement(React.Fragment, null,
208
- visibleCount < selectedItemsElements.length
209
- ? selectedItemsElements.slice(0, visibleCount - 1)
210
- : selectedItemsElements,
211
- visibleCount < selectedItemsElements.length && (React.createElement(SelectTag, { label: `+${selectedItemsElements.length - visibleCount + 1} item(s)` }))))))));
206
+ React.createElement(SelectTagContainer, { tags: selectedItemsElements })))));
212
207
  };
213
208
  export default Select;