@canonical/react-components 4.4.0 → 4.5.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.
@@ -24,12 +24,17 @@ export type MultiSelectProps = {
24
24
  renderItem?: (item: MultiSelectItem) => ReactNode;
25
25
  dropdownHeader?: ReactNode;
26
26
  dropdownFooter?: ReactNode;
27
+ emptyState?: ReactNode;
28
+ emptyMessage?: string;
27
29
  showDropdownFooter?: boolean;
28
30
  variant?: "condensed" | "search";
29
31
  scrollOverflow?: boolean;
30
32
  isSortedAlphabetically?: boolean;
31
33
  hasSelectedItemsFirst?: boolean;
32
34
  id?: string;
35
+ onSearchChange?: (value: string) => void;
36
+ onOpen?: () => void;
37
+ onClose?: () => void;
33
38
  };
34
39
  type GroupFn = (items: Parameters<typeof getGroupedItems>[0]) => ReturnType<typeof getGroupedItems>;
35
40
  type SortFn = typeof sortAlphabetically;
@@ -43,6 +48,8 @@ type MultiSelectDropdownProps = {
43
48
  onDeselectItem?: (item: MultiSelectItem) => void;
44
49
  onSelectItem?: (item: MultiSelectItem) => void;
45
50
  footer?: ReactNode;
51
+ emptyState?: ReactNode;
52
+ emptyMessage?: string;
46
53
  groupFn?: GroupFn;
47
54
  sortFn?: SortFn;
48
55
  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", "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,8 @@ const MultiSelectDropdown = _ref2 => {
57
57
  onDeselectItem,
58
58
  isOpen,
59
59
  footer,
60
+ emptyState,
61
+ emptyMessage,
60
62
  sortFn = sortAlphabetically,
61
63
  groupFn = getGroupedItems,
62
64
  hasSelectedItemsFirst = true
@@ -72,6 +74,7 @@ const MultiSelectDropdown = _ref2 => {
72
74
  // eslint-disable-next-line react-hooks/exhaustive-deps
73
75
  }, [isOpen]);
74
76
  const hasGroup = (0, _react.useMemo)(() => items.some(item => item.group), [items]);
77
+ const hasItems = items.length > 0;
75
78
  const groupedItems = (0, _react.useMemo)(() => hasGroup ? groupFn(items) : [{
76
79
  group: "Ungrouped",
77
80
  items
@@ -100,7 +103,7 @@ const MultiSelectDropdown = _ref2 => {
100
103
  }, /*#__PURE__*/_react.default.createElement("div", _extends({
101
104
  className: "multi-select__dropdown",
102
105
  role: "listbox"
103
- }, props), header ? header : null, groupedItems.map(_ref3 => {
106
+ }, props), header ? header : null, hasItems ? groupedItems.map(_ref3 => {
104
107
  let {
105
108
  group,
106
109
  items
@@ -124,7 +127,9 @@ const MultiSelectDropdown = _ref2 => {
124
127
  onChange: handleOnChange,
125
128
  key: item.value
126
129
  })))));
127
- }), footer ? /*#__PURE__*/_react.default.createElement("div", {
130
+ }) : emptyState !== null && emptyState !== void 0 ? emptyState : emptyMessage ? /*#__PURE__*/_react.default.createElement("p", {
131
+ className: "multi-select__empty-state"
132
+ }, emptyMessage) : null, footer ? /*#__PURE__*/_react.default.createElement("div", {
128
133
  className: "multi-select__footer"
129
134
  }, footer) : null));
130
135
  };
@@ -158,6 +163,8 @@ MultiSelectDropdown.propTypes = {
158
163
  onDeselectItem: _propTypes.default.func,
159
164
  onSelectItem: _propTypes.default.func,
160
165
  footer: _propTypes.default.node,
166
+ emptyState: _propTypes.default.node,
167
+ emptyMessage: _propTypes.default.string,
161
168
  groupFn: _propTypes.default.func,
162
169
  sortFn: _propTypes.default.any,
163
170
  hasSelectedItemsFirst: _propTypes.default.bool
@@ -177,21 +184,44 @@ const MultiSelect = _ref4 => {
177
184
  disabledItems = [],
178
185
  dropdownHeader,
179
186
  dropdownFooter,
187
+ emptyState,
188
+ emptyMessage,
180
189
  showDropdownFooter = true,
181
190
  variant = "search",
182
191
  scrollOverflow = false,
183
192
  isSortedAlphabetically = true,
184
193
  hasSelectedItemsFirst = true,
185
194
  id,
195
+ onSearchChange,
196
+ onOpen,
197
+ onClose,
186
198
  help,
187
199
  helpClassName
188
200
  } = _ref4;
189
201
  const buttonRef = (0, _react.useRef)(null);
190
202
  const [isDropdownOpen, setIsDropdownOpen] = (0, _react.useState)(false);
191
203
  const [filter, setFilter] = (0, _react.useState)("");
204
+ const handleSetDropdownOpen = newState => {
205
+ if (newState && !isDropdownOpen) {
206
+ onOpen === null || onOpen === void 0 || onOpen();
207
+ } else if (!newState && isDropdownOpen) {
208
+ onClose === null || onClose === void 0 || onClose();
209
+ }
210
+ setIsDropdownOpen(newState);
211
+ };
192
212
  const [internalSelectedItems, setInternalSelectedItems] = (0, _react.useState)([]);
193
213
  const selectedItems = externalSelectedItems || internalSelectedItems;
194
214
  const helpId = (0, _react.useId)();
215
+ const updateFilter = value => {
216
+ setFilter(value);
217
+ onSearchChange === null || onSearchChange === void 0 || onSearchChange(value);
218
+ };
219
+ const resetSearch = () => {
220
+ if (!filter.length) {
221
+ return;
222
+ }
223
+ updateFilter("");
224
+ };
195
225
  const updateItems = newItems => {
196
226
  const uniqueItems = Array.from(new Set(newItems));
197
227
  setInternalSelectedItems(uniqueItems);
@@ -222,12 +252,12 @@ const MultiSelect = _ref4 => {
222
252
  className: "multi-select",
223
253
  onToggleMenu: isOpen => {
224
254
  if (!isOpen) {
225
- setFilter("");
255
+ resetSearch();
226
256
  }
227
257
  // Handle syncing the state when toggling the menu from within the
228
258
  // contextual menu component e.g. when clicking outside.
229
259
  if (isOpen !== isDropdownOpen) {
230
- setIsDropdownOpen(isOpen);
260
+ handleSetDropdownOpen(isOpen);
231
261
  }
232
262
  },
233
263
  position: "left",
@@ -242,11 +272,12 @@ const MultiSelect = _ref4 => {
242
272
  disabled: disabled,
243
273
  autoComplete: "off",
244
274
  onChange: value => {
245
- setFilter(value);
275
+ updateFilter(value);
246
276
  // reopen if dropdown has been closed via ESC
247
- setIsDropdownOpen(true);
277
+ handleSetDropdownOpen(true);
248
278
  },
249
- onFocus: () => setIsDropdownOpen(true),
279
+ onClear: resetSearch,
280
+ onFocus: () => handleSetDropdownOpen(true),
250
281
  placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : "Search",
251
282
  required: required,
252
283
  type: "text",
@@ -260,7 +291,7 @@ const MultiSelect = _ref4 => {
260
291
  "aria-expanded": isDropdownOpen,
261
292
  className: "multi-select__select-button",
262
293
  onClick: () => {
263
- setIsDropdownOpen(!isDropdownOpen);
294
+ handleSetDropdownOpen(!isDropdownOpen);
264
295
  },
265
296
  onMouseDown: event => {
266
297
  // If the dropdown is open when this button is clicked the
@@ -295,6 +326,8 @@ const MultiSelect = _ref4 => {
295
326
  onSelectItem: onSelectItem,
296
327
  onDeselectItem: onDeselectItem,
297
328
  footer: footer,
329
+ emptyState: emptyState,
330
+ emptyMessage: emptyMessage,
298
331
  sortFn: isSortedAlphabetically ? sortAlphabetically : () => 0,
299
332
  hasSelectedItemsFirst: hasSelectedItemsFirst
300
333
  })), help && /*#__PURE__*/_react.default.createElement("p", {
@@ -333,6 +366,8 @@ MultiSelect.propTypes = {
333
366
  renderItem: _propTypes.default.func,
334
367
  dropdownHeader: _propTypes.default.node,
335
368
  dropdownFooter: _propTypes.default.node,
369
+ emptyState: _propTypes.default.node,
370
+ emptyMessage: _propTypes.default.string,
336
371
  showDropdownFooter: _propTypes.default.bool,
337
372
  variant: _propTypes.default.oneOf(["condensed", "search"]),
338
373
  scrollOverflow: _propTypes.default.bool,
@@ -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
  };
@@ -41,7 +41,10 @@ const SearchBox = /*#__PURE__*/_react.default.forwardRef((_ref, forwardedRef) =>
41
41
  } = _ref,
42
42
  props = _objectWithoutProperties(_ref, _excluded);
43
43
  const internalRef = (0, _react.useRef)(null);
44
+ const [internalValue, setInternalValue] = (0, _react.useState)(value !== null && value !== void 0 ? value : "");
45
+ const hasValue = externallyControlled ? Boolean(value) : Boolean(internalValue);
44
46
  const resetInput = () => {
47
+ setInternalValue("");
45
48
  onChange === null || onChange === void 0 || onChange("");
46
49
  onClear === null || onClear === void 0 || onClear();
47
50
  if (internalRef.current) {
@@ -71,7 +74,10 @@ const SearchBox = /*#__PURE__*/_react.default.forwardRef((_ref, forwardedRef) =>
71
74
  disabled: disabled,
72
75
  id: id,
73
76
  name: name,
74
- onChange: evt => onChange === null || onChange === void 0 ? void 0 : onChange(evt.target.value),
77
+ onChange: evt => {
78
+ setInternalValue(evt.target.value);
79
+ onChange === null || onChange === void 0 || onChange(evt.target.value);
80
+ },
75
81
  onKeyDown: onKeyDown,
76
82
  placeholder: placeholder,
77
83
  ref: input => {
@@ -86,7 +92,7 @@ const SearchBox = /*#__PURE__*/_react.default.forwardRef((_ref, forwardedRef) =>
86
92
  type: "search",
87
93
  defaultValue: externallyControlled ? undefined : value,
88
94
  value: externallyControlled ? value : undefined
89
- }, props)), value && /*#__PURE__*/_react.default.createElement("button", {
95
+ }, props)), hasValue && /*#__PURE__*/_react.default.createElement("button", {
90
96
  className: "p-search-box__reset",
91
97
  disabled: disabled,
92
98
  onClick: resetInput,
@@ -24,12 +24,17 @@ export type MultiSelectProps = {
24
24
  renderItem?: (item: MultiSelectItem) => ReactNode;
25
25
  dropdownHeader?: ReactNode;
26
26
  dropdownFooter?: ReactNode;
27
+ emptyState?: ReactNode;
28
+ emptyMessage?: string;
27
29
  showDropdownFooter?: boolean;
28
30
  variant?: "condensed" | "search";
29
31
  scrollOverflow?: boolean;
30
32
  isSortedAlphabetically?: boolean;
31
33
  hasSelectedItemsFirst?: boolean;
32
34
  id?: string;
35
+ onSearchChange?: (value: string) => void;
36
+ onOpen?: () => void;
37
+ onClose?: () => void;
33
38
  };
34
39
  type GroupFn = (items: Parameters<typeof getGroupedItems>[0]) => ReturnType<typeof getGroupedItems>;
35
40
  type SortFn = typeof sortAlphabetically;
@@ -43,6 +48,8 @@ type MultiSelectDropdownProps = {
43
48
  onDeselectItem?: (item: MultiSelectItem) => void;
44
49
  onSelectItem?: (item: MultiSelectItem) => void;
45
50
  footer?: ReactNode;
51
+ emptyState?: ReactNode;
52
+ emptyMessage?: string;
46
53
  groupFn?: GroupFn;
47
54
  sortFn?: SortFn;
48
55
  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", "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,8 @@ export var MultiSelectDropdown = _ref2 => {
49
49
  onDeselectItem,
50
50
  isOpen,
51
51
  footer,
52
+ emptyState,
53
+ emptyMessage,
52
54
  sortFn = sortAlphabetically,
53
55
  groupFn = getGroupedItems,
54
56
  hasSelectedItemsFirst = true
@@ -64,6 +66,7 @@ export var MultiSelectDropdown = _ref2 => {
64
66
  // eslint-disable-next-line react-hooks/exhaustive-deps
65
67
  }, [isOpen]);
66
68
  var hasGroup = useMemo(() => items.some(item => item.group), [items]);
69
+ var hasItems = items.length > 0;
67
70
  var groupedItems = useMemo(() => hasGroup ? groupFn(items) : [{
68
71
  group: "Ungrouped",
69
72
  items
@@ -92,7 +95,7 @@ export var MultiSelectDropdown = _ref2 => {
92
95
  }, /*#__PURE__*/React.createElement("div", _extends({
93
96
  className: "multi-select__dropdown",
94
97
  role: "listbox"
95
- }, props), header ? header : null, groupedItems.map(_ref3 => {
98
+ }, props), header ? header : null, hasItems ? groupedItems.map(_ref3 => {
96
99
  var {
97
100
  group,
98
101
  items
@@ -116,7 +119,9 @@ export var MultiSelectDropdown = _ref2 => {
116
119
  onChange: handleOnChange,
117
120
  key: item.value
118
121
  })))));
119
- }), footer ? /*#__PURE__*/React.createElement("div", {
122
+ }) : emptyState !== null && emptyState !== void 0 ? emptyState : emptyMessage ? /*#__PURE__*/React.createElement("p", {
123
+ className: "multi-select__empty-state"
124
+ }, emptyMessage) : null, footer ? /*#__PURE__*/React.createElement("div", {
120
125
  className: "multi-select__footer"
121
126
  }, footer) : null));
122
127
  };
@@ -149,6 +154,8 @@ MultiSelectDropdown.propTypes = {
149
154
  onDeselectItem: _pt.func,
150
155
  onSelectItem: _pt.func,
151
156
  footer: _pt.node,
157
+ emptyState: _pt.node,
158
+ emptyMessage: _pt.string,
152
159
  groupFn: _pt.func,
153
160
  sortFn: _pt.any,
154
161
  hasSelectedItemsFirst: _pt.bool
@@ -168,21 +175,44 @@ export var MultiSelect = _ref4 => {
168
175
  disabledItems = [],
169
176
  dropdownHeader,
170
177
  dropdownFooter,
178
+ emptyState,
179
+ emptyMessage,
171
180
  showDropdownFooter = true,
172
181
  variant = "search",
173
182
  scrollOverflow = false,
174
183
  isSortedAlphabetically = true,
175
184
  hasSelectedItemsFirst = true,
176
185
  id,
186
+ onSearchChange,
187
+ onOpen,
188
+ onClose,
177
189
  help,
178
190
  helpClassName
179
191
  } = _ref4;
180
192
  var buttonRef = useRef(null);
181
193
  var [isDropdownOpen, setIsDropdownOpen] = useState(false);
182
194
  var [filter, setFilter] = useState("");
195
+ var handleSetDropdownOpen = newState => {
196
+ if (newState && !isDropdownOpen) {
197
+ onOpen === null || onOpen === void 0 || onOpen();
198
+ } else if (!newState && isDropdownOpen) {
199
+ onClose === null || onClose === void 0 || onClose();
200
+ }
201
+ setIsDropdownOpen(newState);
202
+ };
183
203
  var [internalSelectedItems, setInternalSelectedItems] = useState([]);
184
204
  var selectedItems = externalSelectedItems || internalSelectedItems;
185
205
  var helpId = useId();
206
+ var updateFilter = value => {
207
+ setFilter(value);
208
+ onSearchChange === null || onSearchChange === void 0 || onSearchChange(value);
209
+ };
210
+ var resetSearch = () => {
211
+ if (!filter.length) {
212
+ return;
213
+ }
214
+ updateFilter("");
215
+ };
186
216
  var updateItems = newItems => {
187
217
  var uniqueItems = Array.from(new Set(newItems));
188
218
  setInternalSelectedItems(uniqueItems);
@@ -213,12 +243,12 @@ export var MultiSelect = _ref4 => {
213
243
  className: "multi-select",
214
244
  onToggleMenu: isOpen => {
215
245
  if (!isOpen) {
216
- setFilter("");
246
+ resetSearch();
217
247
  }
218
248
  // Handle syncing the state when toggling the menu from within the
219
249
  // contextual menu component e.g. when clicking outside.
220
250
  if (isOpen !== isDropdownOpen) {
221
- setIsDropdownOpen(isOpen);
251
+ handleSetDropdownOpen(isOpen);
222
252
  }
223
253
  },
224
254
  position: "left",
@@ -233,11 +263,12 @@ export var MultiSelect = _ref4 => {
233
263
  disabled: disabled,
234
264
  autoComplete: "off",
235
265
  onChange: value => {
236
- setFilter(value);
266
+ updateFilter(value);
237
267
  // reopen if dropdown has been closed via ESC
238
- setIsDropdownOpen(true);
268
+ handleSetDropdownOpen(true);
239
269
  },
240
- onFocus: () => setIsDropdownOpen(true),
270
+ onClear: resetSearch,
271
+ onFocus: () => handleSetDropdownOpen(true),
241
272
  placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : "Search",
242
273
  required: required,
243
274
  type: "text",
@@ -251,7 +282,7 @@ export var MultiSelect = _ref4 => {
251
282
  "aria-expanded": isDropdownOpen,
252
283
  className: "multi-select__select-button",
253
284
  onClick: () => {
254
- setIsDropdownOpen(!isDropdownOpen);
285
+ handleSetDropdownOpen(!isDropdownOpen);
255
286
  },
256
287
  onMouseDown: event => {
257
288
  // If the dropdown is open when this button is clicked the
@@ -286,6 +317,8 @@ export var MultiSelect = _ref4 => {
286
317
  onSelectItem: onSelectItem,
287
318
  onDeselectItem: onDeselectItem,
288
319
  footer: footer,
320
+ emptyState: emptyState,
321
+ emptyMessage: emptyMessage,
289
322
  sortFn: isSortedAlphabetically ? sortAlphabetically : () => 0,
290
323
  hasSelectedItemsFirst: hasSelectedItemsFirst
291
324
  })), help && /*#__PURE__*/React.createElement("p", {
@@ -323,6 +356,8 @@ MultiSelect.propTypes = {
323
356
  renderItem: _pt.func,
324
357
  dropdownHeader: _pt.node,
325
358
  dropdownFooter: _pt.node,
359
+ emptyState: _pt.node,
360
+ emptyMessage: _pt.string,
326
361
  showDropdownFooter: _pt.bool,
327
362
  variant: _pt.oneOf(["condensed", "search"]),
328
363
  scrollOverflow: _pt.bool,
@@ -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
  };
@@ -3,7 +3,7 @@ function _extends() { return _extends = Object.assign ? Object.assign.bind() : f
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; }
5
5
  import classNames from "classnames";
6
- import React, { useRef } from "react";
6
+ import React, { useRef, useState } from "react";
7
7
  import Icon from "../Icon";
8
8
  export var Label = /*#__PURE__*/function (Label) {
9
9
  Label["Clear"] = "Clear search field";
@@ -33,7 +33,10 @@ var SearchBox = /*#__PURE__*/React.forwardRef((_ref, forwardedRef) => {
33
33
  } = _ref,
34
34
  props = _objectWithoutProperties(_ref, _excluded);
35
35
  var internalRef = useRef(null);
36
+ var [internalValue, setInternalValue] = useState(value !== null && value !== void 0 ? value : "");
37
+ var hasValue = externallyControlled ? Boolean(value) : Boolean(internalValue);
36
38
  var resetInput = () => {
39
+ setInternalValue("");
37
40
  _onChange === null || _onChange === void 0 || _onChange("");
38
41
  onClear === null || onClear === void 0 || onClear();
39
42
  if (internalRef.current) {
@@ -63,7 +66,10 @@ var SearchBox = /*#__PURE__*/React.forwardRef((_ref, forwardedRef) => {
63
66
  disabled: disabled,
64
67
  id: id,
65
68
  name: name,
66
- onChange: evt => _onChange === null || _onChange === void 0 ? void 0 : _onChange(evt.target.value),
69
+ onChange: evt => {
70
+ setInternalValue(evt.target.value);
71
+ _onChange === null || _onChange === void 0 || _onChange(evt.target.value);
72
+ },
67
73
  onKeyDown: onKeyDown,
68
74
  placeholder: placeholder,
69
75
  ref: input => {
@@ -78,7 +84,7 @@ var SearchBox = /*#__PURE__*/React.forwardRef((_ref, forwardedRef) => {
78
84
  type: "search",
79
85
  defaultValue: externallyControlled ? undefined : value,
80
86
  value: externallyControlled ? value : undefined
81
- }, props)), value && /*#__PURE__*/React.createElement("button", {
87
+ }, props)), hasValue && /*#__PURE__*/React.createElement("button", {
82
88
  className: "p-search-box__reset",
83
89
  disabled: disabled,
84
90
  onClick: resetInput,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonical/react-components",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "author": {