@canonical/react-components 4.4.1 → 4.5.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.
@@ -24,12 +24,21 @@ export type MultiSelectProps = {
24
24
  renderItem?: (item: MultiSelectItem) => ReactNode;
25
25
  dropdownHeader?: ReactNode;
26
26
  dropdownFooter?: ReactNode;
27
+ footerClassName?: string;
28
+ inputClassName?: string;
29
+ dropdownClassName?: string;
30
+ emptyState?: ReactNode;
31
+ emptyMessage?: string;
27
32
  showDropdownFooter?: boolean;
28
33
  variant?: "condensed" | "search";
29
34
  scrollOverflow?: boolean;
30
35
  isSortedAlphabetically?: boolean;
31
36
  hasSelectedItemsFirst?: boolean;
32
37
  id?: string;
38
+ onSearchChange?: (value: string) => void;
39
+ onOpen?: () => void;
40
+ onClose?: () => void;
41
+ searchButtonType?: "submit" | "button";
33
42
  };
34
43
  type GroupFn = (items: Parameters<typeof getGroupedItems>[0]) => ReturnType<typeof getGroupedItems>;
35
44
  type SortFn = typeof sortAlphabetically;
@@ -43,6 +52,10 @@ type MultiSelectDropdownProps = {
43
52
  onDeselectItem?: (item: MultiSelectItem) => void;
44
53
  onSelectItem?: (item: MultiSelectItem) => void;
45
54
  footer?: ReactNode;
55
+ footerClassName?: string;
56
+ dropdownClassName?: string;
57
+ emptyState?: ReactNode;
58
+ emptyMessage?: string;
46
59
  groupFn?: GroupFn;
47
60
  sortFn?: SortFn;
48
61
  hasSelectedItemsFirst?: boolean;
@@ -10,7 +10,7 @@ var _react = _interopRequireWildcard(require("react"));
10
10
  require("./MultiSelect.scss");
11
11
  var _index = require("../../index");
12
12
  var _FadeInDown = require("./FadeInDown");
13
- const _excluded = ["items", "selectedItems", "disabledItems", "header", "updateItems", "onSelectItem", "onDeselectItem", "isOpen", "footer", "sortFn", "groupFn", "hasSelectedItemsFirst"];
13
+ const _excluded = ["items", "selectedItems", "disabledItems", "header", "updateItems", "onSelectItem", "onDeselectItem", "isOpen", "footer", "dropdownClassName", "footerClassName", "emptyState", "emptyMessage", "sortFn", "groupFn", "hasSelectedItemsFirst"];
14
14
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
15
15
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
16
16
  function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
@@ -57,6 +57,10 @@ const MultiSelectDropdown = _ref2 => {
57
57
  onDeselectItem,
58
58
  isOpen,
59
59
  footer,
60
+ dropdownClassName,
61
+ footerClassName,
62
+ emptyState,
63
+ emptyMessage,
60
64
  sortFn = sortAlphabetically,
61
65
  groupFn = getGroupedItems,
62
66
  hasSelectedItemsFirst = true
@@ -72,6 +76,7 @@ const MultiSelectDropdown = _ref2 => {
72
76
  // eslint-disable-next-line react-hooks/exhaustive-deps
73
77
  }, [isOpen]);
74
78
  const hasGroup = (0, _react.useMemo)(() => items.some(item => item.group), [items]);
79
+ const hasItems = items.length > 0;
75
80
  const groupedItems = (0, _react.useMemo)(() => hasGroup ? groupFn(items) : [{
76
81
  group: "Ungrouped",
77
82
  items
@@ -98,9 +103,9 @@ const MultiSelectDropdown = _ref2 => {
98
103
  return /*#__PURE__*/_react.default.createElement(_FadeInDown.FadeInDown, {
99
104
  isVisible: isOpen
100
105
  }, /*#__PURE__*/_react.default.createElement("div", _extends({
101
- className: "multi-select__dropdown",
106
+ className: (0, _classnames.default)("multi-select__dropdown", dropdownClassName),
102
107
  role: "listbox"
103
- }, props), header ? header : null, groupedItems.map(_ref3 => {
108
+ }, props), header ? header : null, hasItems ? groupedItems.map(_ref3 => {
104
109
  let {
105
110
  group,
106
111
  items
@@ -124,8 +129,10 @@ const MultiSelectDropdown = _ref2 => {
124
129
  onChange: handleOnChange,
125
130
  key: item.value
126
131
  })))));
127
- }), footer ? /*#__PURE__*/_react.default.createElement("div", {
128
- className: "multi-select__footer"
132
+ }) : emptyState !== null && emptyState !== void 0 ? emptyState : emptyMessage ? /*#__PURE__*/_react.default.createElement("p", {
133
+ className: "multi-select__empty-state"
134
+ }, emptyMessage) : null, footer ? /*#__PURE__*/_react.default.createElement("div", {
135
+ className: (0, _classnames.default)("multi-select__footer", footerClassName)
129
136
  }, footer) : null));
130
137
  };
131
138
 
@@ -158,6 +165,10 @@ MultiSelectDropdown.propTypes = {
158
165
  onDeselectItem: _propTypes.default.func,
159
166
  onSelectItem: _propTypes.default.func,
160
167
  footer: _propTypes.default.node,
168
+ footerClassName: _propTypes.default.string,
169
+ dropdownClassName: _propTypes.default.string,
170
+ emptyState: _propTypes.default.node,
171
+ emptyMessage: _propTypes.default.string,
161
172
  groupFn: _propTypes.default.func,
162
173
  sortFn: _propTypes.default.any,
163
174
  hasSelectedItemsFirst: _propTypes.default.bool
@@ -177,21 +188,48 @@ const MultiSelect = _ref4 => {
177
188
  disabledItems = [],
178
189
  dropdownHeader,
179
190
  dropdownFooter,
191
+ footerClassName,
192
+ dropdownClassName,
193
+ inputClassName,
194
+ emptyState,
195
+ emptyMessage,
196
+ searchButtonType = "submit",
180
197
  showDropdownFooter = true,
181
198
  variant = "search",
182
199
  scrollOverflow = false,
183
200
  isSortedAlphabetically = true,
184
201
  hasSelectedItemsFirst = true,
185
202
  id,
203
+ onSearchChange,
204
+ onOpen,
205
+ onClose,
186
206
  help,
187
207
  helpClassName
188
208
  } = _ref4;
189
209
  const buttonRef = (0, _react.useRef)(null);
190
210
  const [isDropdownOpen, setIsDropdownOpen] = (0, _react.useState)(false);
191
211
  const [filter, setFilter] = (0, _react.useState)("");
212
+ const handleSetDropdownOpen = newState => {
213
+ if (newState && !isDropdownOpen) {
214
+ onOpen === null || onOpen === void 0 || onOpen();
215
+ } else if (!newState && isDropdownOpen) {
216
+ onClose === null || onClose === void 0 || onClose();
217
+ }
218
+ setIsDropdownOpen(newState);
219
+ };
192
220
  const [internalSelectedItems, setInternalSelectedItems] = (0, _react.useState)([]);
193
221
  const selectedItems = externalSelectedItems || internalSelectedItems;
194
222
  const helpId = (0, _react.useId)();
223
+ const updateFilter = value => {
224
+ setFilter(value);
225
+ onSearchChange === null || onSearchChange === void 0 || onSearchChange(value);
226
+ };
227
+ const resetSearch = () => {
228
+ if (!filter.length) {
229
+ return;
230
+ }
231
+ updateFilter("");
232
+ };
195
233
  const updateItems = newItems => {
196
234
  const uniqueItems = Array.from(new Set(newItems));
197
235
  setInternalSelectedItems(uniqueItems);
@@ -222,12 +260,12 @@ const MultiSelect = _ref4 => {
222
260
  className: "multi-select",
223
261
  onToggleMenu: isOpen => {
224
262
  if (!isOpen) {
225
- setFilter("");
263
+ resetSearch();
226
264
  }
227
265
  // Handle syncing the state when toggling the menu from within the
228
266
  // contextual menu component e.g. when clicking outside.
229
267
  if (isOpen !== isDropdownOpen) {
230
- setIsDropdownOpen(isOpen);
268
+ handleSetDropdownOpen(isOpen);
231
269
  }
232
270
  },
233
271
  position: "left",
@@ -242,16 +280,18 @@ const MultiSelect = _ref4 => {
242
280
  disabled: disabled,
243
281
  autoComplete: "off",
244
282
  onChange: value => {
245
- setFilter(value);
283
+ updateFilter(value);
246
284
  // reopen if dropdown has been closed via ESC
247
- setIsDropdownOpen(true);
285
+ handleSetDropdownOpen(true);
248
286
  },
249
- onFocus: () => setIsDropdownOpen(true),
287
+ onClear: resetSearch,
288
+ onFocus: () => handleSetDropdownOpen(true),
250
289
  placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : "Search",
251
290
  required: required,
252
291
  type: "text",
253
292
  value: filter,
254
- className: "multi-select__input"
293
+ searchButtonType: searchButtonType,
294
+ className: (0, _classnames.default)("multi-select__input", inputClassName)
255
295
  }) : /*#__PURE__*/_react.default.createElement("button", {
256
296
  role: "combobox",
257
297
  type: "button",
@@ -260,7 +300,7 @@ const MultiSelect = _ref4 => {
260
300
  "aria-expanded": isDropdownOpen,
261
301
  className: "multi-select__select-button",
262
302
  onClick: () => {
263
- setIsDropdownOpen(!isDropdownOpen);
303
+ handleSetDropdownOpen(!isDropdownOpen);
264
304
  },
265
305
  onMouseDown: event => {
266
306
  // If the dropdown is open when this button is clicked the
@@ -295,6 +335,10 @@ const MultiSelect = _ref4 => {
295
335
  onSelectItem: onSelectItem,
296
336
  onDeselectItem: onDeselectItem,
297
337
  footer: footer,
338
+ footerClassName: footerClassName,
339
+ dropdownClassName: dropdownClassName,
340
+ emptyState: emptyState,
341
+ emptyMessage: emptyMessage,
298
342
  sortFn: isSortedAlphabetically ? sortAlphabetically : () => 0,
299
343
  hasSelectedItemsFirst: hasSelectedItemsFirst
300
344
  })), help && /*#__PURE__*/_react.default.createElement("p", {
@@ -333,10 +377,12 @@ MultiSelect.propTypes = {
333
377
  renderItem: _propTypes.default.func,
334
378
  dropdownHeader: _propTypes.default.node,
335
379
  dropdownFooter: _propTypes.default.node,
380
+ footerClassName: _propTypes.default.string,
381
+ inputClassName: _propTypes.default.string,
382
+ dropdownClassName: _propTypes.default.string,
383
+ emptyState: _propTypes.default.node,
384
+ emptyMessage: _propTypes.default.string,
336
385
  showDropdownFooter: _propTypes.default.bool,
337
386
  variant: _propTypes.default.oneOf(["condensed", "search"]),
338
- scrollOverflow: _propTypes.default.bool,
339
- isSortedAlphabetically: _propTypes.default.bool,
340
- hasSelectedItemsFirst: _propTypes.default.bool,
341
- id: _propTypes.default.string
387
+ scrollOverflow: _propTypes.default.bool
342
388
  };
@@ -93,6 +93,12 @@ $dropdown-max-height: 20rem;
93
93
  }
94
94
  }
95
95
 
96
+ .multi-select__empty-state {
97
+ color: $colors--theme--text-muted;
98
+ margin: 0;
99
+ padding: $spv--small $sph--large;
100
+ }
101
+
96
102
  .multi-select__dropdown-button {
97
103
  border: 0;
98
104
  margin-bottom: 0;
@@ -9,3 +9,6 @@ export declare const WithDisabledItems: Story;
9
9
  export declare const WithoutSorting: Story;
10
10
  export declare const Disabled: Story;
11
11
  export declare const HelpText: Story;
12
+ export declare const FormikCallbacksAndEmptyState: Story;
13
+ export declare const EmptyMessage: Story;
14
+ export declare const EmptyStateNode: Story;
@@ -3,8 +3,10 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = exports.WithoutSorting = exports.WithDisabledItems = exports.SearchExample = exports.HelpText = exports.Disabled = exports.CondensedExample = void 0;
6
+ exports.default = exports.WithoutSorting = exports.WithDisabledItems = exports.SearchExample = exports.HelpText = exports.FormikCallbacksAndEmptyState = exports.EmptyStateNode = exports.EmptyMessage = exports.Disabled = exports.CondensedExample = void 0;
7
7
  var _react = _interopRequireWildcard(require("react"));
8
+ var _formik = require("formik");
9
+ var _index = require("../../index");
8
10
  var _MultiSelect = require("./MultiSelect");
9
11
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
10
12
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
@@ -27,6 +29,23 @@ const meta = {
27
29
  parameters: {}
28
30
  };
29
31
  var _default = exports.default = meta;
32
+ const groupedItems = [{
33
+ label: "Almond",
34
+ value: "almond",
35
+ group: "Nuts"
36
+ }, {
37
+ label: "Cashew",
38
+ value: "cashew",
39
+ group: "Nuts"
40
+ }, {
41
+ label: "Mango",
42
+ value: "mango",
43
+ group: "Fruit"
44
+ }, {
45
+ label: "Peach",
46
+ value: "peach",
47
+ group: "Fruit"
48
+ }];
30
49
  const CondensedExample = exports.CondensedExample = {
31
50
  args: {
32
51
  items: [...Array.from({
@@ -119,4 +138,75 @@ const HelpText = exports.HelpText = {
119
138
  args: _objectSpread(_objectSpread({}, CondensedExample.args), {}, {
120
139
  help: /*#__PURE__*/_react.default.createElement("span", null, "This is a help text, that should appear underneath the component.")
121
140
  })
141
+ };
142
+ const FormikCallbacksAndEmptyStateTemplate = () => {
143
+ const [selectedItems, setSelectedItems] = (0, _react.useState)([]);
144
+ const [events, setEvents] = (0, _react.useState)([]);
145
+ const addEvent = eventName => {
146
+ setEvents(previousEvents => [eventName, ...previousEvents].slice(0, 6));
147
+ };
148
+ return /*#__PURE__*/_react.default.createElement("div", {
149
+ style: {
150
+ maxWidth: "28rem"
151
+ }
152
+ }, /*#__PURE__*/_react.default.createElement(_formik.Formik, {
153
+ initialValues: {
154
+ ingredients: ""
155
+ },
156
+ onSubmit: () => {}
157
+ }, /*#__PURE__*/_react.default.createElement(_index.FormikField, {
158
+ name: "ingredients",
159
+ component: _MultiSelect.MultiSelect,
160
+ label: "Ingredients",
161
+ variant: "search",
162
+ placeholder: "Search ingredients",
163
+ items: groupedItems,
164
+ selectedItems: selectedItems,
165
+ onItemsUpdate: setSelectedItems,
166
+ onSearchChange: value => {
167
+ addEvent("onSearchChange(\"".concat(value, "\")"));
168
+ },
169
+ onOpen: () => addEvent("onOpen()"),
170
+ onClose: () => addEvent("onClose()"),
171
+ emptyMessage: "No ingredients found"
172
+ })), /*#__PURE__*/_react.default.createElement("p", {
173
+ style: {
174
+ marginBottom: "0.5rem"
175
+ }
176
+ }, "Callback log:"), /*#__PURE__*/_react.default.createElement("ul", {
177
+ style: {
178
+ margin: 0,
179
+ paddingLeft: "1.25rem"
180
+ }
181
+ }, events.map((event, index) => /*#__PURE__*/_react.default.createElement("li", {
182
+ key: "".concat(event, "-").concat(index)
183
+ }, event))));
184
+ };
185
+ const FormikCallbacksAndEmptyState = exports.FormikCallbacksAndEmptyState = {
186
+ render: FormikCallbacksAndEmptyStateTemplate
187
+ };
188
+ const EmptyMessage = exports.EmptyMessage = {
189
+ args: {
190
+ variant: "search",
191
+ items: groupedItems,
192
+ emptyMessage: "No matching ingredients.",
193
+ placeholder: "Try typing kiwi"
194
+ }
195
+ };
196
+ const EmptyStateNode = exports.EmptyStateNode = {
197
+ args: {
198
+ variant: "search",
199
+ items: groupedItems,
200
+ placeholder: "Try typing kiwi",
201
+ emptyState: /*#__PURE__*/_react.default.createElement("div", {
202
+ className: "u-align--center",
203
+ style: {
204
+ padding: "0.75rem 1rem"
205
+ }
206
+ }, /*#__PURE__*/_react.default.createElement("strong", null, "No ingredient matches."), /*#__PURE__*/_react.default.createElement("p", {
207
+ style: {
208
+ margin: "0.25rem 0 0"
209
+ }
210
+ }, "Use a broader term."))
211
+ }
122
212
  };
@@ -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 button should be a button or submit type.
42
+ */
43
+ searchButtonType?: "submit" | "button";
40
44
  /**
41
45
  * Whether the search input should lose focus when searching.
42
46
  */
@@ -7,7 +7,7 @@ exports.default = exports.Label = void 0;
7
7
  var _classnames = _interopRequireDefault(require("classnames"));
8
8
  var _react = _interopRequireWildcard(require("react"));
9
9
  var _Icon = _interopRequireDefault(require("../Icon"));
10
- const _excluded = ["name", "id", "autocomplete", "className", "disabled", "externallyControlled", "onChange", "onSearch", "onClear", "placeholder", "shouldBlurOnSearch", "shouldRefocusAfterReset", "value"];
10
+ const _excluded = ["name", "id", "autocomplete", "className", "disabled", "externallyControlled", "onChange", "onSearch", "onClear", "searchButtonType", "placeholder", "shouldBlurOnSearch", "shouldRefocusAfterReset", "value"];
11
11
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
12
12
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
13
13
  function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
@@ -34,6 +34,7 @@ const SearchBox = /*#__PURE__*/_react.default.forwardRef((_ref, forwardedRef) =>
34
34
  onChange,
35
35
  onSearch,
36
36
  onClear,
37
+ searchButtonType = "submit",
37
38
  placeholder = "Search",
38
39
  shouldBlurOnSearch = true,
39
40
  shouldRefocusAfterReset,
@@ -102,7 +103,8 @@ const SearchBox = /*#__PURE__*/_react.default.forwardRef((_ref, forwardedRef) =>
102
103
  }, Label.Clear)), /*#__PURE__*/_react.default.createElement("button", {
103
104
  className: "p-search-box__button",
104
105
  disabled: disabled,
105
- onClick: triggerSearch
106
+ onClick: triggerSearch,
107
+ type: searchButtonType
106
108
  }, /*#__PURE__*/_react.default.createElement(_Icon.default, {
107
109
  name: "search"
108
110
  }, Label.Search)));
@@ -24,12 +24,21 @@ export type MultiSelectProps = {
24
24
  renderItem?: (item: MultiSelectItem) => ReactNode;
25
25
  dropdownHeader?: ReactNode;
26
26
  dropdownFooter?: ReactNode;
27
+ footerClassName?: string;
28
+ inputClassName?: string;
29
+ dropdownClassName?: string;
30
+ emptyState?: ReactNode;
31
+ emptyMessage?: string;
27
32
  showDropdownFooter?: boolean;
28
33
  variant?: "condensed" | "search";
29
34
  scrollOverflow?: boolean;
30
35
  isSortedAlphabetically?: boolean;
31
36
  hasSelectedItemsFirst?: boolean;
32
37
  id?: string;
38
+ onSearchChange?: (value: string) => void;
39
+ onOpen?: () => void;
40
+ onClose?: () => void;
41
+ searchButtonType?: "submit" | "button";
33
42
  };
34
43
  type GroupFn = (items: Parameters<typeof getGroupedItems>[0]) => ReturnType<typeof getGroupedItems>;
35
44
  type SortFn = typeof sortAlphabetically;
@@ -43,6 +52,10 @@ type MultiSelectDropdownProps = {
43
52
  onDeselectItem?: (item: MultiSelectItem) => void;
44
53
  onSelectItem?: (item: MultiSelectItem) => void;
45
54
  footer?: ReactNode;
55
+ footerClassName?: string;
56
+ dropdownClassName?: string;
57
+ emptyState?: ReactNode;
58
+ emptyMessage?: string;
46
59
  groupFn?: GroupFn;
47
60
  sortFn?: SortFn;
48
61
  hasSelectedItemsFirst?: boolean;
@@ -1,5 +1,5 @@
1
1
  import _pt from "prop-types";
2
- var _excluded = ["items", "selectedItems", "disabledItems", "header", "updateItems", "onSelectItem", "onDeselectItem", "isOpen", "footer", "sortFn", "groupFn", "hasSelectedItemsFirst"];
2
+ var _excluded = ["items", "selectedItems", "disabledItems", "header", "updateItems", "onSelectItem", "onDeselectItem", "isOpen", "footer", "dropdownClassName", "footerClassName", "emptyState", "emptyMessage", "sortFn", "groupFn", "hasSelectedItemsFirst"];
3
3
  function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
4
4
  function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }
5
5
  function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
@@ -49,6 +49,10 @@ export var MultiSelectDropdown = _ref2 => {
49
49
  onDeselectItem,
50
50
  isOpen,
51
51
  footer,
52
+ dropdownClassName,
53
+ footerClassName,
54
+ emptyState,
55
+ emptyMessage,
52
56
  sortFn = sortAlphabetically,
53
57
  groupFn = getGroupedItems,
54
58
  hasSelectedItemsFirst = true
@@ -64,6 +68,7 @@ export var MultiSelectDropdown = _ref2 => {
64
68
  // eslint-disable-next-line react-hooks/exhaustive-deps
65
69
  }, [isOpen]);
66
70
  var hasGroup = useMemo(() => items.some(item => item.group), [items]);
71
+ var hasItems = items.length > 0;
67
72
  var groupedItems = useMemo(() => hasGroup ? groupFn(items) : [{
68
73
  group: "Ungrouped",
69
74
  items
@@ -90,9 +95,9 @@ export var MultiSelectDropdown = _ref2 => {
90
95
  return /*#__PURE__*/React.createElement(FadeInDown, {
91
96
  isVisible: isOpen
92
97
  }, /*#__PURE__*/React.createElement("div", _extends({
93
- className: "multi-select__dropdown",
98
+ className: classNames("multi-select__dropdown", dropdownClassName),
94
99
  role: "listbox"
95
- }, props), header ? header : null, groupedItems.map(_ref3 => {
100
+ }, props), header ? header : null, hasItems ? groupedItems.map(_ref3 => {
96
101
  var {
97
102
  group,
98
103
  items
@@ -116,8 +121,10 @@ export var MultiSelectDropdown = _ref2 => {
116
121
  onChange: handleOnChange,
117
122
  key: item.value
118
123
  })))));
119
- }), footer ? /*#__PURE__*/React.createElement("div", {
120
- className: "multi-select__footer"
124
+ }) : emptyState !== null && emptyState !== void 0 ? emptyState : emptyMessage ? /*#__PURE__*/React.createElement("p", {
125
+ className: "multi-select__empty-state"
126
+ }, emptyMessage) : null, footer ? /*#__PURE__*/React.createElement("div", {
127
+ className: classNames("multi-select__footer", footerClassName)
121
128
  }, footer) : null));
122
129
  };
123
130
 
@@ -149,6 +156,10 @@ MultiSelectDropdown.propTypes = {
149
156
  onDeselectItem: _pt.func,
150
157
  onSelectItem: _pt.func,
151
158
  footer: _pt.node,
159
+ footerClassName: _pt.string,
160
+ dropdownClassName: _pt.string,
161
+ emptyState: _pt.node,
162
+ emptyMessage: _pt.string,
152
163
  groupFn: _pt.func,
153
164
  sortFn: _pt.any,
154
165
  hasSelectedItemsFirst: _pt.bool
@@ -168,21 +179,48 @@ export var MultiSelect = _ref4 => {
168
179
  disabledItems = [],
169
180
  dropdownHeader,
170
181
  dropdownFooter,
182
+ footerClassName,
183
+ dropdownClassName,
184
+ inputClassName,
185
+ emptyState,
186
+ emptyMessage,
187
+ searchButtonType = "submit",
171
188
  showDropdownFooter = true,
172
189
  variant = "search",
173
190
  scrollOverflow = false,
174
191
  isSortedAlphabetically = true,
175
192
  hasSelectedItemsFirst = true,
176
193
  id,
194
+ onSearchChange,
195
+ onOpen,
196
+ onClose,
177
197
  help,
178
198
  helpClassName
179
199
  } = _ref4;
180
200
  var buttonRef = useRef(null);
181
201
  var [isDropdownOpen, setIsDropdownOpen] = useState(false);
182
202
  var [filter, setFilter] = useState("");
203
+ var handleSetDropdownOpen = newState => {
204
+ if (newState && !isDropdownOpen) {
205
+ onOpen === null || onOpen === void 0 || onOpen();
206
+ } else if (!newState && isDropdownOpen) {
207
+ onClose === null || onClose === void 0 || onClose();
208
+ }
209
+ setIsDropdownOpen(newState);
210
+ };
183
211
  var [internalSelectedItems, setInternalSelectedItems] = useState([]);
184
212
  var selectedItems = externalSelectedItems || internalSelectedItems;
185
213
  var helpId = useId();
214
+ var updateFilter = value => {
215
+ setFilter(value);
216
+ onSearchChange === null || onSearchChange === void 0 || onSearchChange(value);
217
+ };
218
+ var resetSearch = () => {
219
+ if (!filter.length) {
220
+ return;
221
+ }
222
+ updateFilter("");
223
+ };
186
224
  var updateItems = newItems => {
187
225
  var uniqueItems = Array.from(new Set(newItems));
188
226
  setInternalSelectedItems(uniqueItems);
@@ -213,12 +251,12 @@ export var MultiSelect = _ref4 => {
213
251
  className: "multi-select",
214
252
  onToggleMenu: isOpen => {
215
253
  if (!isOpen) {
216
- setFilter("");
254
+ resetSearch();
217
255
  }
218
256
  // Handle syncing the state when toggling the menu from within the
219
257
  // contextual menu component e.g. when clicking outside.
220
258
  if (isOpen !== isDropdownOpen) {
221
- setIsDropdownOpen(isOpen);
259
+ handleSetDropdownOpen(isOpen);
222
260
  }
223
261
  },
224
262
  position: "left",
@@ -233,16 +271,18 @@ export var MultiSelect = _ref4 => {
233
271
  disabled: disabled,
234
272
  autoComplete: "off",
235
273
  onChange: value => {
236
- setFilter(value);
274
+ updateFilter(value);
237
275
  // reopen if dropdown has been closed via ESC
238
- setIsDropdownOpen(true);
276
+ handleSetDropdownOpen(true);
239
277
  },
240
- onFocus: () => setIsDropdownOpen(true),
278
+ onClear: resetSearch,
279
+ onFocus: () => handleSetDropdownOpen(true),
241
280
  placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : "Search",
242
281
  required: required,
243
282
  type: "text",
244
283
  value: filter,
245
- className: "multi-select__input"
284
+ searchButtonType: searchButtonType,
285
+ className: classNames("multi-select__input", inputClassName)
246
286
  }) : /*#__PURE__*/React.createElement("button", {
247
287
  role: "combobox",
248
288
  type: "button",
@@ -251,7 +291,7 @@ export var MultiSelect = _ref4 => {
251
291
  "aria-expanded": isDropdownOpen,
252
292
  className: "multi-select__select-button",
253
293
  onClick: () => {
254
- setIsDropdownOpen(!isDropdownOpen);
294
+ handleSetDropdownOpen(!isDropdownOpen);
255
295
  },
256
296
  onMouseDown: event => {
257
297
  // If the dropdown is open when this button is clicked the
@@ -286,6 +326,10 @@ export var MultiSelect = _ref4 => {
286
326
  onSelectItem: onSelectItem,
287
327
  onDeselectItem: onDeselectItem,
288
328
  footer: footer,
329
+ footerClassName: footerClassName,
330
+ dropdownClassName: dropdownClassName,
331
+ emptyState: emptyState,
332
+ emptyMessage: emptyMessage,
289
333
  sortFn: isSortedAlphabetically ? sortAlphabetically : () => 0,
290
334
  hasSelectedItemsFirst: hasSelectedItemsFirst
291
335
  })), help && /*#__PURE__*/React.createElement("p", {
@@ -323,10 +367,12 @@ MultiSelect.propTypes = {
323
367
  renderItem: _pt.func,
324
368
  dropdownHeader: _pt.node,
325
369
  dropdownFooter: _pt.node,
370
+ footerClassName: _pt.string,
371
+ inputClassName: _pt.string,
372
+ dropdownClassName: _pt.string,
373
+ emptyState: _pt.node,
374
+ emptyMessage: _pt.string,
326
375
  showDropdownFooter: _pt.bool,
327
376
  variant: _pt.oneOf(["condensed", "search"]),
328
- scrollOverflow: _pt.bool,
329
- isSortedAlphabetically: _pt.bool,
330
- hasSelectedItemsFirst: _pt.bool,
331
- id: _pt.string
377
+ scrollOverflow: _pt.bool
332
378
  };
@@ -93,6 +93,12 @@ $dropdown-max-height: 20rem;
93
93
  }
94
94
  }
95
95
 
96
+ .multi-select__empty-state {
97
+ color: $colors--theme--text-muted;
98
+ margin: 0;
99
+ padding: $spv--small $sph--large;
100
+ }
101
+
96
102
  .multi-select__dropdown-button {
97
103
  border: 0;
98
104
  margin-bottom: 0;
@@ -9,3 +9,6 @@ export declare const WithDisabledItems: Story;
9
9
  export declare const WithoutSorting: Story;
10
10
  export declare const Disabled: Story;
11
11
  export declare const HelpText: Story;
12
+ export declare const FormikCallbacksAndEmptyState: Story;
13
+ export declare const EmptyMessage: Story;
14
+ export declare const EmptyStateNode: Story;
@@ -6,6 +6,8 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
6
6
  function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
7
7
  import React from "react";
8
8
  import { useState } from "react";
9
+ import { Formik } from "formik";
10
+ import { FormikField } from "../../index";
9
11
  import { MultiSelect } from "./MultiSelect";
10
12
  var Template = props => {
11
13
  var [selectedItems, setSelectedItems] = useState(props.selectedItems || []);
@@ -21,6 +23,23 @@ var meta = {
21
23
  parameters: {}
22
24
  };
23
25
  export default meta;
26
+ var groupedItems = [{
27
+ label: "Almond",
28
+ value: "almond",
29
+ group: "Nuts"
30
+ }, {
31
+ label: "Cashew",
32
+ value: "cashew",
33
+ group: "Nuts"
34
+ }, {
35
+ label: "Mango",
36
+ value: "mango",
37
+ group: "Fruit"
38
+ }, {
39
+ label: "Peach",
40
+ value: "peach",
41
+ group: "Fruit"
42
+ }];
24
43
  export var CondensedExample = {
25
44
  args: {
26
45
  items: [...Array.from({
@@ -113,4 +132,75 @@ export var HelpText = {
113
132
  args: _objectSpread(_objectSpread({}, CondensedExample.args), {}, {
114
133
  help: /*#__PURE__*/React.createElement("span", null, "This is a help text, that should appear underneath the component.")
115
134
  })
135
+ };
136
+ var FormikCallbacksAndEmptyStateTemplate = () => {
137
+ var [selectedItems, setSelectedItems] = useState([]);
138
+ var [events, setEvents] = useState([]);
139
+ var addEvent = eventName => {
140
+ setEvents(previousEvents => [eventName, ...previousEvents].slice(0, 6));
141
+ };
142
+ return /*#__PURE__*/React.createElement("div", {
143
+ style: {
144
+ maxWidth: "28rem"
145
+ }
146
+ }, /*#__PURE__*/React.createElement(Formik, {
147
+ initialValues: {
148
+ ingredients: ""
149
+ },
150
+ onSubmit: () => {}
151
+ }, /*#__PURE__*/React.createElement(FormikField, {
152
+ name: "ingredients",
153
+ component: MultiSelect,
154
+ label: "Ingredients",
155
+ variant: "search",
156
+ placeholder: "Search ingredients",
157
+ items: groupedItems,
158
+ selectedItems: selectedItems,
159
+ onItemsUpdate: setSelectedItems,
160
+ onSearchChange: value => {
161
+ addEvent("onSearchChange(\"".concat(value, "\")"));
162
+ },
163
+ onOpen: () => addEvent("onOpen()"),
164
+ onClose: () => addEvent("onClose()"),
165
+ emptyMessage: "No ingredients found"
166
+ })), /*#__PURE__*/React.createElement("p", {
167
+ style: {
168
+ marginBottom: "0.5rem"
169
+ }
170
+ }, "Callback log:"), /*#__PURE__*/React.createElement("ul", {
171
+ style: {
172
+ margin: 0,
173
+ paddingLeft: "1.25rem"
174
+ }
175
+ }, events.map((event, index) => /*#__PURE__*/React.createElement("li", {
176
+ key: "".concat(event, "-").concat(index)
177
+ }, event))));
178
+ };
179
+ export var FormikCallbacksAndEmptyState = {
180
+ render: FormikCallbacksAndEmptyStateTemplate
181
+ };
182
+ export var EmptyMessage = {
183
+ args: {
184
+ variant: "search",
185
+ items: groupedItems,
186
+ emptyMessage: "No matching ingredients.",
187
+ placeholder: "Try typing kiwi"
188
+ }
189
+ };
190
+ export var EmptyStateNode = {
191
+ args: {
192
+ variant: "search",
193
+ items: groupedItems,
194
+ placeholder: "Try typing kiwi",
195
+ emptyState: /*#__PURE__*/React.createElement("div", {
196
+ className: "u-align--center",
197
+ style: {
198
+ padding: "0.75rem 1rem"
199
+ }
200
+ }, /*#__PURE__*/React.createElement("strong", null, "No ingredient matches."), /*#__PURE__*/React.createElement("p", {
201
+ style: {
202
+ margin: "0.25rem 0 0"
203
+ }
204
+ }, "Use a broader term."))
205
+ }
116
206
  };
@@ -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 button should be a button or submit type.
42
+ */
43
+ searchButtonType?: "submit" | "button";
40
44
  /**
41
45
  * Whether the search input should lose focus when searching.
42
46
  */
@@ -1,4 +1,4 @@
1
- var _excluded = ["name", "id", "autocomplete", "className", "disabled", "externallyControlled", "onChange", "onSearch", "onClear", "placeholder", "shouldBlurOnSearch", "shouldRefocusAfterReset", "value"];
1
+ var _excluded = ["name", "id", "autocomplete", "className", "disabled", "externallyControlled", "onChange", "onSearch", "onClear", "searchButtonType", "placeholder", "shouldBlurOnSearch", "shouldRefocusAfterReset", "value"];
2
2
  function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
3
3
  function _objectWithoutProperties(e, t) { if (null == e) return {}; var o, r, i = _objectWithoutPropertiesLoose(e, t); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]); } return i; }
4
4
  function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
@@ -26,6 +26,7 @@ var SearchBox = /*#__PURE__*/React.forwardRef((_ref, forwardedRef) => {
26
26
  onChange: _onChange,
27
27
  onSearch,
28
28
  onClear,
29
+ searchButtonType = "submit",
29
30
  placeholder = "Search",
30
31
  shouldBlurOnSearch = true,
31
32
  shouldRefocusAfterReset,
@@ -94,7 +95,8 @@ var SearchBox = /*#__PURE__*/React.forwardRef((_ref, forwardedRef) => {
94
95
  }, Label.Clear)), /*#__PURE__*/React.createElement("button", {
95
96
  className: "p-search-box__button",
96
97
  disabled: disabled,
97
- onClick: triggerSearch
98
+ onClick: triggerSearch,
99
+ type: searchButtonType
98
100
  }, /*#__PURE__*/React.createElement(Icon, {
99
101
  name: "search"
100
102
  }, Label.Search)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonical/react-components",
3
- "version": "4.4.1",
3
+ "version": "4.5.1",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "author": {