@canonical/react-components 1.0.0 → 1.2.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,17 +1,13 @@
1
1
  import React from "react";
2
- import type { HTMLProps } from "react";
2
+ import type { HTMLProps, ReactNode } from "react";
3
3
  import type { ButtonProps } from "../Button";
4
4
  import type { ContextualMenuDropdownProps } from "./ContextualMenuDropdown";
5
5
  import type { MenuLink, Position } from "./ContextualMenuDropdown";
6
- import { ClassName, PropsWithSpread, SubComponentProps } from "../../types";
6
+ import { ClassName, ExclusiveProps, PropsWithSpread, SubComponentProps } from "../../types";
7
7
  export declare enum Label {
8
8
  Toggle = "Toggle menu"
9
9
  }
10
- /**
11
- * The props for the ContextualMenu component.
12
- * @template L - The type of the link props.
13
- */
14
- export type Props<L> = PropsWithSpread<{
10
+ export type BaseProps<L> = PropsWithSpread<{
15
11
  /**
16
12
  * Whether the menu should adjust its horizontal position to fit on the screen.
17
13
  */
@@ -44,10 +40,6 @@ export type Props<L> = PropsWithSpread<{
44
40
  * Additional props to pass to the dropdown.
45
41
  */
46
42
  dropdownProps?: SubComponentProps<ContextualMenuDropdownProps>;
47
- /**
48
- * Whether the toggle should display a chevron icon.
49
- */
50
- hasToggleIcon?: boolean;
51
43
  /**
52
44
  * A list of links to display in the menu (if the children prop is not supplied.)
53
45
  */
@@ -68,6 +60,20 @@ export type Props<L> = PropsWithSpread<{
68
60
  * Whether the dropdown should scroll if it is too long to fit on the screen.
69
61
  */
70
62
  scrollOverflow?: boolean;
63
+ /**
64
+ * Whether the menu should be visible.
65
+ */
66
+ visible?: boolean;
67
+ }, HTMLProps<HTMLSpanElement>>;
68
+ /**
69
+ * The props for the ContextualMenu component.
70
+ * @template L - The type of the link props.
71
+ */
72
+ export type Props<L> = BaseProps<L> & ExclusiveProps<{
73
+ /**
74
+ * Whether the toggle should display a chevron icon.
75
+ */
76
+ hasToggleIcon?: boolean;
71
77
  /**
72
78
  * The appearance of the toggle button.
73
79
  */
@@ -92,15 +98,13 @@ export type Props<L> = PropsWithSpread<{
92
98
  * Additional props to pass to the toggle button.
93
99
  */
94
100
  toggleProps?: SubComponentProps<ButtonProps>;
95
- /**
96
- * Whether the menu should be visible.
97
- */
98
- visible?: boolean;
99
- }, HTMLProps<HTMLSpanElement>>;
101
+ }, {
102
+ toggle: ReactNode;
103
+ }>;
100
104
  /**
101
105
  * This is a [React](https://reactjs.org/) component for the Vanilla [Contextual menu](https://docs.vanillaframework.io/patterns//contextual-menu/).
102
106
  *
103
107
  * A contextual menu can be used in conjunction with any page element to provide a contextual menu.
104
108
  */
105
- declare const ContextualMenu: <L>({ autoAdjust, children, className, closeOnEsc, closeOnOutsideClick, constrainPanelWidth, dropdownClassName, dropdownProps, hasToggleIcon, links, onToggleMenu, position, positionNode, scrollOverflow, toggleAppearance, toggleClassName, toggleDisabled, toggleLabel, toggleLabelFirst, toggleProps, visible, ...wrapperProps }: Props<L>) => JSX.Element;
109
+ declare const ContextualMenu: <L>({ autoAdjust, children, className, closeOnEsc, closeOnOutsideClick, constrainPanelWidth, dropdownClassName, dropdownProps, hasToggleIcon, links, onToggleMenu, position, positionNode, scrollOverflow, toggle, toggleAppearance, toggleClassName, toggleDisabled, toggleLabel, toggleLabelFirst, toggleProps, visible, ...wrapperProps }: Props<L>) => JSX.Element;
106
110
  export default ContextualMenu;
@@ -69,6 +69,7 @@ const ContextualMenu = _ref => {
69
69
  position = "right",
70
70
  positionNode,
71
71
  scrollOverflow,
72
+ toggle,
72
73
  toggleAppearance,
73
74
  toggleClassName,
74
75
  toggleDisabled,
@@ -82,7 +83,6 @@ const ContextualMenu = _ref => {
82
83
  const wrapper = (0, _react.useRef)(null);
83
84
  const [positionCoords, setPositionCoords] = (0, _react.useState)();
84
85
  const [adjustedPosition, setAdjustedPosition] = (0, _react.useState)(position);
85
- const hasToggle = hasToggleIcon || toggleLabel;
86
86
  (0, _react.useEffect)(() => {
87
87
  setAdjustedPosition(position);
88
88
  }, [position, autoAdjust]);
@@ -106,14 +106,15 @@ const ContextualMenu = _ref => {
106
106
  isOpen: visible,
107
107
  onOpen: () => {
108
108
  // Call the toggle callback, if supplied.
109
- onToggleMenu && onToggleMenu(true);
109
+ onToggleMenu === null || onToggleMenu === void 0 || onToggleMenu(true);
110
110
  // When the menu opens then update the coordinates of the parent.
111
111
  updatePositionCoords();
112
112
  },
113
113
  onClose: () => {
114
114
  // Call the toggle callback, if supplied.
115
- onToggleMenu && onToggleMenu(false);
116
- }
115
+ onToggleMenu === null || onToggleMenu === void 0 || onToggleMenu(false);
116
+ },
117
+ programmaticallyOpen: true
117
118
  });
118
119
  const previousVisible = (0, _hooks.usePrevious)(visible);
119
120
  const labelNode = toggleLabel && typeof toggleLabel === "string" ? /*#__PURE__*/_react.default.createElement("span", null, toggleLabel) : /*#__PURE__*/_react.default.isValidElement(toggleLabel) ? toggleLabel : null;
@@ -166,32 +167,38 @@ const ContextualMenu = _ref => {
166
167
  }, [positionNode, updatePositionCoords]);
167
168
  (0, _hooks.useListener)(window, onResize, "resize", true, isOpen);
168
169
  (0, _hooks.useListener)(window, onScroll, "scroll", false, isOpen, true);
170
+ let toggleNode = null;
171
+ if (toggle) {
172
+ toggleNode = toggle;
173
+ } else if (hasToggleIcon || toggleLabel) {
174
+ toggleNode = /*#__PURE__*/_react.default.createElement(_Button.default, _extends({
175
+ appearance: toggleAppearance,
176
+ "aria-controls": id,
177
+ "aria-expanded": isOpen ? "true" : "false",
178
+ "aria-label": toggleLabel ? null : Label.Toggle,
179
+ "aria-pressed": isOpen ? "true" : "false",
180
+ "aria-haspopup": "true",
181
+ className: (0, _classnames.default)("p-contextual-menu__toggle", toggleClassName),
182
+ disabled: toggleDisabled,
183
+ hasIcon: hasToggleIcon,
184
+ onClick: evt => {
185
+ if (!isOpen) {
186
+ openPortal(evt);
187
+ } else {
188
+ closePortal(evt);
189
+ }
190
+ },
191
+ type: "button"
192
+ }, toggleProps), toggleLabelFirst ? labelNode : null, hasToggleIcon ? /*#__PURE__*/_react.default.createElement("i", {
193
+ className: (0, _classnames.default)("p-icon--chevron-down p-contextual-menu__indicator", {
194
+ "is-light": ["negative", "positive"].includes(toggleAppearance)
195
+ })
196
+ }) : null, toggleLabelFirst ? null : labelNode);
197
+ }
169
198
  return /*#__PURE__*/_react.default.createElement("span", _extends({
170
199
  className: contextualMenuClassName,
171
200
  ref: wrapperRef
172
- }, wrapperProps), hasToggle ? /*#__PURE__*/_react.default.createElement(_Button.default, _extends({
173
- appearance: toggleAppearance,
174
- "aria-controls": id,
175
- "aria-expanded": isOpen ? "true" : "false",
176
- "aria-label": toggleLabel ? null : Label.Toggle,
177
- "aria-pressed": isOpen ? "true" : "false",
178
- "aria-haspopup": "true",
179
- className: (0, _classnames.default)("p-contextual-menu__toggle", toggleClassName),
180
- disabled: toggleDisabled,
181
- hasIcon: hasToggleIcon,
182
- onClick: evt => {
183
- if (!isOpen) {
184
- openPortal(evt);
185
- } else {
186
- closePortal(evt);
187
- }
188
- },
189
- type: "button"
190
- }, toggleProps), toggleLabelFirst ? labelNode : null, hasToggleIcon ? /*#__PURE__*/_react.default.createElement("i", {
191
- className: (0, _classnames.default)("p-icon--chevron-down p-contextual-menu__indicator", {
192
- "is-light": ["negative", "positive"].includes(toggleAppearance)
193
- })
194
- }) : null, toggleLabelFirst ? null : labelNode) : null, isOpen && /*#__PURE__*/_react.default.createElement(Portal, null, /*#__PURE__*/_react.default.createElement(_ContextualMenuDropdown.default, _extends({
201
+ }, wrapperProps), toggleNode, isOpen && /*#__PURE__*/_react.default.createElement(Portal, null, /*#__PURE__*/_react.default.createElement(_ContextualMenuDropdown.default, _extends({
195
202
  adjustedPosition: adjustedPosition,
196
203
  autoAdjust: autoAdjust,
197
204
  handleClose: closePortal,
@@ -153,7 +153,7 @@ const ContextualMenuDropdown = _ref => {
153
153
  contextualMenuClassName,
154
154
  ...props
155
155
  } = _ref;
156
- const dropdown = (0, _react.useRef)();
156
+ const dropdown = (0, _react.useRef)(null);
157
157
  const [verticalPosition, setVerticalPosition] = (0, _react.useState)("bottom");
158
158
  const [positionStyle, setPositionStyle] = (0, _react.useState)(getPositionStyle(adjustedPosition, verticalPosition, positionCoords, constrainPanelWidth));
159
159
  const [maxHeight, setMaxHeight] = (0, _react.useState)();
@@ -162,6 +162,7 @@ const ContextualMenuDropdown = _ref => {
162
162
  setPositionStyle(getPositionStyle(adjustedPosition, verticalPosition, positionCoords, constrainPanelWidth));
163
163
  }, [adjustedPosition, positionCoords, verticalPosition, constrainPanelWidth]);
164
164
  const updateVerticalPosition = (0, _react.useCallback)(() => {
165
+ var _dropdown$current$get;
165
166
  if (!positionNode) {
166
167
  return null;
167
168
  }
@@ -170,18 +171,18 @@ const ContextualMenuDropdown = _ref => {
170
171
  return null;
171
172
  }
172
173
  const scrollableParentRect = scrollableParent.getBoundingClientRect();
173
- const rect = positionNode.getBoundingClientRect();
174
+ const toggleRect = positionNode.getBoundingClientRect();
174
175
 
175
176
  // Calculate the rect in relation to the scrollableParent
176
- const relativeRect = {
177
- top: rect.top - scrollableParentRect.top,
178
- bottom: rect.bottom - scrollableParentRect.top,
179
- height: rect.height
177
+ const relativeToScrollParentRect = {
178
+ top: toggleRect.top - scrollableParentRect.top,
179
+ bottom: toggleRect.bottom - scrollableParentRect.top
180
180
  };
181
- const spaceBelow = scrollableParentRect.height - relativeRect.bottom;
182
- const spaceAbove = relativeRect.top;
183
- const dropdownHeight = relativeRect.height;
184
- setVerticalPosition(spaceBelow >= dropdownHeight || spaceBelow > spaceAbove ? "bottom" : "top");
181
+ const scrollParentSpaceBelow = scrollableParentRect.height - relativeToScrollParentRect.bottom;
182
+ const scrollParentSpaceAbove = relativeToScrollParentRect.top;
183
+ const dropdownHeight = (_dropdown$current$get = dropdown.current.getBoundingClientRect().height) !== null && _dropdown$current$get !== void 0 ? _dropdown$current$get : 0;
184
+ const windowSpaceBelow = window.innerHeight - toggleRect.bottom;
185
+ setVerticalPosition(scrollParentSpaceBelow >= dropdownHeight && windowSpaceBelow >= dropdownHeight || windowSpaceBelow > scrollParentSpaceAbove ? "bottom" : "top");
185
186
  }, [positionNode]);
186
187
 
187
188
  // Update the position when the window fitment info changes.
@@ -51,7 +51,7 @@ export type Props = {
51
51
  /**
52
52
  * Optional class(es) to pass to the label component.
53
53
  */
54
- labelClassName?: string;
54
+ labelClassName?: string | null;
55
55
  /**
56
56
  * Whether the label should show before the input.
57
57
  */
@@ -12,7 +12,10 @@ export type MultiSelectProps = {
12
12
  selectedItems?: MultiSelectItem[];
13
13
  help?: string;
14
14
  label?: string | null;
15
+ listSelected?: boolean;
16
+ onDeselectItem?: (item: MultiSelectItem) => void;
15
17
  onItemsUpdate?: (items: MultiSelectItem[]) => void;
18
+ onSelectItem?: (item: MultiSelectItem) => void;
16
19
  placeholder?: string;
17
20
  required?: boolean;
18
21
  items: MultiSelectItem[];
@@ -20,6 +23,7 @@ export type MultiSelectProps = {
20
23
  renderItem?: (item: MultiSelectItem) => ReactNode;
21
24
  dropdownHeader?: ReactNode;
22
25
  dropdownFooter?: ReactNode;
26
+ showDropdownFooter?: boolean;
23
27
  variant?: "condensed" | "search";
24
28
  };
25
29
  type GroupFn = (items: Parameters<typeof getGroupedItems>[0]) => ReturnType<typeof getGroupedItems>;
@@ -31,10 +35,11 @@ type MultiSelectDropdownProps = {
31
35
  disabledItems: MultiSelectItem[];
32
36
  header?: ReactNode;
33
37
  updateItems: (newItems: MultiSelectItem[]) => void;
38
+ onDeselectItem?: (item: MultiSelectItem) => void;
39
+ onSelectItem?: (item: MultiSelectItem) => void;
34
40
  footer?: ReactNode;
35
41
  groupFn?: GroupFn;
36
42
  sortFn?: SortFn;
37
- shouldPinSelectedItems?: boolean;
38
43
  } & React.HTMLAttributes<HTMLDivElement>;
39
44
  declare const sortAlphabetically: (a: MultiSelectItem, b: MultiSelectItem) => number;
40
45
  declare const getGroupedItems: (items: MultiSelectItem[]) => {
@@ -50,6 +50,8 @@ const MultiSelectDropdown = _ref2 => {
50
50
  disabledItems,
51
51
  header,
52
52
  updateItems,
53
+ onSelectItem,
54
+ onDeselectItem,
53
55
  isOpen,
54
56
  footer,
55
57
  sortFn = sortAlphabetically,
@@ -82,6 +84,11 @@ const MultiSelectDropdown = _ref2 => {
82
84
  var _selectedItems$filter;
83
85
  const newSelectedItems = checked ? [...selectedItems, foundItem] : (_selectedItems$filter = selectedItems.filter(item => "".concat(item.value) !== value)) !== null && _selectedItems$filter !== void 0 ? _selectedItems$filter : [];
84
86
  updateItems(newSelectedItems);
87
+ if (checked) {
88
+ onSelectItem === null || onSelectItem === void 0 || onSelectItem(foundItem);
89
+ } else {
90
+ onDeselectItem === null || onDeselectItem === void 0 || onDeselectItem(foundItem);
91
+ }
85
92
  }
86
93
  };
87
94
  return /*#__PURE__*/_react.default.createElement(_FadeInDown.FadeInDown, {
@@ -144,40 +151,33 @@ MultiSelectDropdown.propTypes = {
144
151
  })).isRequired,
145
152
  header: _propTypes.default.node,
146
153
  updateItems: _propTypes.default.func.isRequired,
154
+ onDeselectItem: _propTypes.default.func,
155
+ onSelectItem: _propTypes.default.func,
147
156
  footer: _propTypes.default.node,
148
157
  groupFn: _propTypes.default.func,
149
- sortFn: _propTypes.default.any,
150
- shouldPinSelectedItems: _propTypes.default.bool
158
+ sortFn: _propTypes.default.any
151
159
  };
152
160
  const MultiSelect = _ref4 => {
153
161
  let {
154
162
  disabled,
155
163
  selectedItems: externalSelectedItems = [],
156
164
  label,
165
+ listSelected = true,
157
166
  onItemsUpdate,
167
+ onSelectItem,
168
+ onDeselectItem,
158
169
  placeholder,
159
170
  required = false,
160
171
  items = [],
161
172
  disabledItems = [],
162
173
  dropdownHeader,
163
174
  dropdownFooter,
175
+ showDropdownFooter = true,
164
176
  variant = "search"
165
177
  } = _ref4;
166
- const wrapperRef = (0, _index.useClickOutside)(() => {
167
- setIsDropdownOpen(false);
168
- setFilter("");
169
- });
170
- (0, _index.useOnEscapePressed)(() => {
171
- setIsDropdownOpen(false);
172
- setFilter("");
173
- });
178
+ const buttonRef = (0, _react.useRef)();
174
179
  const [isDropdownOpen, setIsDropdownOpen] = (0, _react.useState)(false);
175
180
  const [filter, setFilter] = (0, _react.useState)("");
176
- (0, _react.useEffect)(() => {
177
- if (!isDropdownOpen) {
178
- setFilter("");
179
- }
180
- }, [isDropdownOpen]);
181
181
  const [internalSelectedItems, setInternalSelectedItems] = (0, _react.useState)([]);
182
182
  const selectedItems = externalSelectedItems || internalSelectedItems;
183
183
  const updateItems = newItems => {
@@ -188,51 +188,9 @@ const MultiSelect = _ref4 => {
188
188
  const dropdownId = (0, _react.useId)();
189
189
  const inputId = (0, _react.useId)();
190
190
  const selectedItemsLabel = selectedItems.filter(selectedItem => items.some(item => item.value === selectedItem.value)).map(el => el.label).join(", ");
191
- return /*#__PURE__*/_react.default.createElement("div", {
192
- ref: wrapperRef
193
- }, /*#__PURE__*/_react.default.createElement("div", {
194
- className: "multi-select"
195
- }, variant === "search" ? /*#__PURE__*/_react.default.createElement(_index.SearchBox, {
196
- externallyControlled: true,
197
- "aria-controls": dropdownId,
198
- "aria-expanded": isDropdownOpen,
199
- id: inputId,
200
- role: "combobox",
201
- "aria-label": label || placeholder || "Search",
202
- disabled: disabled,
203
- autoComplete: "off",
204
- onChange: value => {
205
- setFilter(value);
206
- // reopen if dropdown has been closed via ESC
207
- setIsDropdownOpen(true);
208
- },
209
- onFocus: () => setIsDropdownOpen(true),
210
- placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : "Search",
211
- required: required,
212
- type: "text",
213
- value: filter,
214
- className: "multi-select__input"
215
- }) : /*#__PURE__*/_react.default.createElement("button", {
216
- role: "combobox",
217
- type: "button",
218
- "aria-label": label || placeholder || "Select items",
219
- "aria-controls": dropdownId,
220
- "aria-expanded": isDropdownOpen,
221
- className: "multi-select__select-button",
222
- onClick: () => {
223
- setIsDropdownOpen(isOpen => !isOpen);
224
- }
225
- }, /*#__PURE__*/_react.default.createElement("span", {
226
- className: "multi-select__condensed-text"
227
- }, selectedItems.length > 0 ? selectedItemsLabel : placeholder !== null && placeholder !== void 0 ? placeholder : "Select items")), /*#__PURE__*/_react.default.createElement(MultiSelectDropdown, {
228
- id: dropdownId,
229
- isOpen: isDropdownOpen,
230
- items: filter.length > 0 ? items.filter(item => item.label.toLowerCase().includes(filter.toLowerCase())) : items,
231
- selectedItems: selectedItems,
232
- disabledItems: disabledItems,
233
- header: dropdownHeader,
234
- updateItems: updateItems,
235
- footer: dropdownFooter ? dropdownFooter : /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_index.Button, {
191
+ let footer = null;
192
+ if (showDropdownFooter) {
193
+ footer = dropdownFooter ? dropdownFooter : /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_index.Button, {
236
194
  appearance: "link",
237
195
  onClick: () => {
238
196
  const enabledItems = items.filter(item => !disabledItems.some(disabledItem => disabledItem.value === item.value));
@@ -246,8 +204,82 @@ const MultiSelect = _ref4 => {
246
204
  updateItems(disabledSelectedItems);
247
205
  },
248
206
  type: "button"
249
- }, "Clear"))
250
- })));
207
+ }, "Clear"));
208
+ }
209
+ return /*#__PURE__*/_react.default.createElement(_index.ContextualMenu, {
210
+ className: "multi-select",
211
+ onToggleMenu: isOpen => {
212
+ if (!isOpen) {
213
+ setFilter("");
214
+ }
215
+ // Handle syncing the state when toggling the menu from within the
216
+ // contextual menu component e.g. when clicking outside.
217
+ if (isOpen !== isDropdownOpen) {
218
+ setIsDropdownOpen(isOpen);
219
+ }
220
+ },
221
+ position: "left",
222
+ constrainPanelWidth: true,
223
+ toggle: variant === "search" ? /*#__PURE__*/_react.default.createElement(_index.SearchBox, {
224
+ externallyControlled: true,
225
+ "aria-controls": dropdownId,
226
+ "aria-expanded": isDropdownOpen,
227
+ id: inputId,
228
+ role: "combobox",
229
+ "aria-label": label || placeholder || "Search",
230
+ disabled: disabled,
231
+ autoComplete: "off",
232
+ onChange: value => {
233
+ setFilter(value);
234
+ // reopen if dropdown has been closed via ESC
235
+ setIsDropdownOpen(true);
236
+ },
237
+ onFocus: () => setIsDropdownOpen(true),
238
+ placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : "Search",
239
+ required: required,
240
+ type: "text",
241
+ value: filter,
242
+ className: "multi-select__input"
243
+ }) : /*#__PURE__*/_react.default.createElement("button", {
244
+ role: "combobox",
245
+ type: "button",
246
+ "aria-label": label || placeholder || "Select items",
247
+ "aria-controls": dropdownId,
248
+ "aria-expanded": isDropdownOpen,
249
+ className: "multi-select__select-button",
250
+ onClick: () => {
251
+ setIsDropdownOpen(!isDropdownOpen);
252
+ },
253
+ onMouseDown: event => {
254
+ // If the dropdown is open when this button is clicked the
255
+ // click-outside event will fire which will close the dropdown, but
256
+ // then the button click event will fire which will immediately
257
+ // reopen the dropdown.
258
+ // To prevent this we can stop the propagation to the click event
259
+ // while `isDropdownOpen` is still set to `true` (by the time we
260
+ // get to the `onClick` event `isDropdownOpen` will already be `false`,
261
+ // hence having to do this on mouse down).
262
+ if (isDropdownOpen) {
263
+ event.stopPropagation();
264
+ }
265
+ },
266
+ ref: buttonRef
267
+ }, /*#__PURE__*/_react.default.createElement("span", {
268
+ className: "multi-select__condensed-text"
269
+ }, listSelected && selectedItems.length > 0 ? selectedItemsLabel : placeholder !== null && placeholder !== void 0 ? placeholder : "Select items")),
270
+ visible: isDropdownOpen
271
+ }, /*#__PURE__*/_react.default.createElement(MultiSelectDropdown, {
272
+ id: dropdownId,
273
+ isOpen: isDropdownOpen,
274
+ items: filter.length > 0 ? items.filter(item => item.label.toLowerCase().includes(filter.toLowerCase())) : items,
275
+ selectedItems: selectedItems,
276
+ disabledItems: disabledItems,
277
+ header: dropdownHeader,
278
+ updateItems: updateItems,
279
+ onSelectItem: onSelectItem,
280
+ onDeselectItem: onDeselectItem,
281
+ footer: footer
282
+ }));
251
283
  };
252
284
  exports.MultiSelect = MultiSelect;
253
285
  MultiSelect.propTypes = {
@@ -260,7 +292,10 @@ MultiSelect.propTypes = {
260
292
  })),
261
293
  help: _propTypes.default.string,
262
294
  label: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.oneOf([null])]),
295
+ listSelected: _propTypes.default.bool,
296
+ onDeselectItem: _propTypes.default.func,
263
297
  onItemsUpdate: _propTypes.default.func,
298
+ onSelectItem: _propTypes.default.func,
264
299
  placeholder: _propTypes.default.string,
265
300
  required: _propTypes.default.bool,
266
301
  items: _propTypes.default.arrayOf(_propTypes.default.shape({
@@ -276,5 +311,6 @@ MultiSelect.propTypes = {
276
311
  renderItem: _propTypes.default.func,
277
312
  dropdownHeader: _propTypes.default.node,
278
313
  dropdownFooter: _propTypes.default.node,
314
+ showDropdownFooter: _propTypes.default.bool,
279
315
  variant: _propTypes.default.oneOf(["condensed", "search"])
280
316
  };
@@ -10,7 +10,9 @@ $dropdown-max-height: 20rem;
10
10
  // XXX: This is a workaround for https://github.com/canonical/vanilla-framework/issues/5030
11
11
  @include vf-b-forms;
12
12
 
13
+ margin-bottom: $input-margin-bottom;
13
14
  position: relative;
15
+ width: 100%;
14
16
  }
15
17
 
16
18
  .multi-select .p-form-validation__message {
@@ -26,6 +28,7 @@ $dropdown-max-height: 20rem;
26
28
 
27
29
  .multi-select__input {
28
30
  cursor: pointer;
31
+ margin-bottom: 0;
29
32
  position: relative;
30
33
 
31
34
  &.items-selected {
@@ -40,19 +43,6 @@ $dropdown-max-height: 20rem;
40
43
  }
41
44
  }
42
45
 
43
- .multi-select__dropdown {
44
- background-color: $colors--theme--background-default;
45
- box-shadow: $box-shadow;
46
- color: $colors--theme--text-default;
47
- left: 0;
48
- max-height: $dropdown-max-height;
49
- overflow: auto;
50
- padding-top: $spv--small;
51
- position: absolute;
52
- right: 0;
53
- top: calc(100% - #{$input-margin-bottom});
54
- }
55
-
56
46
  .multi-select__dropdown--side-by-side {
57
47
  display: flex;
58
48
  flex-wrap: wrap;
@@ -132,6 +122,10 @@ $dropdown-max-height: 20rem;
132
122
  position: relative;
133
123
  z-index: 0;
134
124
 
125
+ .multi-select & {
126
+ margin-bottom: 0;
127
+ }
128
+
135
129
  &::after {
136
130
  content: "";
137
131
  margin-left: $sph--large;
@@ -28,7 +28,7 @@ export type Props = PropsWithSpread<{
28
28
  /**
29
29
  * A function that is called when the user clicks the search icon or presses enter
30
30
  */
31
- onSearch?: () => void;
31
+ onSearch?: (inputValue: string) => void;
32
32
  /**
33
33
  * A function that is called when the user clicks the reset icon
34
34
  */
@@ -37,6 +37,10 @@ export type Props = PropsWithSpread<{
37
37
  * A search input placeholder message.
38
38
  */
39
39
  placeholder?: string;
40
+ /**
41
+ * Whether the search input should lose focus when searching.
42
+ */
43
+ shouldBlurOnSearch?: boolean;
40
44
  /**
41
45
  * Whether the search input should receive focus after pressing the reset button
42
46
  */
@@ -31,6 +31,7 @@ const SearchBox = /*#__PURE__*/_react.default.forwardRef((_ref, forwardedRef) =>
31
31
  onSearch,
32
32
  onClear,
33
33
  placeholder = "Search",
34
+ shouldBlurOnSearch = true,
34
35
  shouldRefocusAfterReset,
35
36
  value,
36
37
  ...props
@@ -45,11 +46,13 @@ const SearchBox = /*#__PURE__*/_react.default.forwardRef((_ref, forwardedRef) =>
45
46
  }
46
47
  };
47
48
  const triggerSearch = () => {
48
- onSearch && onSearch();
49
+ onSearch === null || onSearch === void 0 || onSearch(externallyControlled ? value : internalRef.current.value);
49
50
  };
50
51
  const onKeyDown = e => {
51
52
  if (e.key === "Enter" && internalRef.current.checkValidity()) {
52
- internalRef.current.blur();
53
+ if (shouldBlurOnSearch) {
54
+ internalRef.current.blur();
55
+ }
53
56
  triggerSearch();
54
57
  }
55
58
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonical/react-components",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "author": {