@rhc-shared-components/form-multi-select-component 0.1.2 → 0.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,11 @@
1
- import * as React from 'react';
2
- import './styles.scss';
3
- export interface IMultiSelectItem {
4
- value: string;
5
- description?: string;
6
- isDisabled?: boolean;
7
- extraProps?: any;
8
- }
1
+ import React from 'react';
2
+ import { SelectOptionProps } from '@patternfly/react-core';
9
3
  export interface FormMultiSelectInputProps {
10
4
  name: string;
11
5
  label: string;
12
6
  isRequired?: boolean;
13
7
  placeholder?: string;
14
- selectOptions: IMultiSelectItem[];
8
+ selectMenuOptions: SelectOptionProps[];
15
9
  ariaLabel?: string;
16
10
  helperText?: string;
17
11
  maxHeight?: string | number;
@@ -20,5 +14,5 @@ export interface FormMultiSelectInputProps {
20
14
  menuAppendTo?: HTMLElement | (() => HTMLElement) | 'inline' | 'parent';
21
15
  extraProps?: any;
22
16
  }
23
- declare const FormMultiSelectInput: React.FC<FormMultiSelectInputProps>;
17
+ declare const FormMultiSelectInput: React.FunctionComponent<FormMultiSelectInputProps>;
24
18
  export default FormMultiSelectInput;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import FormMultiSelectInput, { IMultiSelectItem } from './FormMultiSelectInput';
2
- export { FormMultiSelectInput, IMultiSelectItem };
1
+ import FormMultiSelectInput from './FormMultiSelectInput';
2
+ export { FormMultiSelectInput };
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  var React = require('react');
2
- var formik = require('formik');
3
2
  var reactCore = require('@patternfly/react-core');
4
3
  var formGroupContainer = require('@rhc-shared-components/form-group-container');
5
- var lodash = require('lodash');
4
+ var formik = require('formik');
5
+
6
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
6
7
 
7
8
  function _interopNamespace(e) {
8
9
  if (e && e.__esModule) return e;
@@ -23,24 +24,7 @@ function _interopNamespace(e) {
23
24
  }
24
25
 
25
26
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
26
-
27
- function _extends() {
28
- _extends = Object.assign || function (target) {
29
- for (var i = 1; i < arguments.length; i++) {
30
- var source = arguments[i];
31
-
32
- for (var key in source) {
33
- if (Object.prototype.hasOwnProperty.call(source, key)) {
34
- target[key] = source[key];
35
- }
36
- }
37
- }
38
-
39
- return target;
40
- };
41
-
42
- return _extends.apply(this, arguments);
43
- }
27
+ var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
44
28
 
45
29
  function _objectWithoutPropertiesLoose(source, excluded) {
46
30
  if (source == null) return {};
@@ -57,121 +41,348 @@ function _objectWithoutPropertiesLoose(source, excluded) {
57
41
  return target;
58
42
  }
59
43
 
60
- var _excluded = ["label", "isRequired", "children", "selectOptions", "ariaLabel", "placeholder", "helperText", "maxHeight", "isDisabled", "classNames", "menuAppendTo", "extraProps"];
44
+ /*! *****************************************************************************
45
+ Copyright (c) Microsoft Corporation.
46
+
47
+ Permission to use, copy, modify, and/or distribute this software for any
48
+ purpose with or without fee is hereby granted.
49
+
50
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
51
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
52
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
53
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
54
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
55
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
56
+ PERFORMANCE OF THIS SOFTWARE.
57
+ ***************************************************************************** */
58
+
59
+ function __rest(s, e) {
60
+ var t = {};
61
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
62
+ t[p] = s[p];
63
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
64
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
65
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
66
+ t[p[i]] = s[p[i]];
67
+ }
68
+ return t;
69
+ }
70
+
71
+ var IconSize;
72
+ (function (IconSize) {
73
+ IconSize["sm"] = "sm";
74
+ IconSize["md"] = "md";
75
+ IconSize["lg"] = "lg";
76
+ IconSize["xl"] = "xl";
77
+ })(IconSize || (IconSize = {}));
78
+ const getSize = (size) => {
79
+ switch (size) {
80
+ case IconSize.sm:
81
+ return '1em';
82
+ case IconSize.md:
83
+ return '1.5em';
84
+ case IconSize.lg:
85
+ return '2em';
86
+ case IconSize.xl:
87
+ return '3em';
88
+ default:
89
+ return '1em';
90
+ }
91
+ };
92
+ let currentId = 0;
93
+ /**
94
+ * Factory to create Icon class components for consumers
95
+ */
96
+ function createIcon({ name, xOffset = 0, yOffset = 0, width, height, svgPath }) {
97
+ var _a;
98
+ return _a = class SVGIcon extends React__namespace.Component {
99
+ constructor() {
100
+ super(...arguments);
101
+ this.id = `icon-title-${currentId++}`;
102
+ }
103
+ render() {
104
+ const _a = this.props, { size, color, title, noVerticalAlign } = _a, props = __rest(_a, ["size", "color", "title", "noVerticalAlign"]);
105
+ const hasTitle = Boolean(title);
106
+ const heightWidth = getSize(size);
107
+ const baseAlign = -0.125 * Number.parseFloat(heightWidth);
108
+ const style = noVerticalAlign ? null : { verticalAlign: `${baseAlign}em` };
109
+ const viewBox = [xOffset, yOffset, width, height].join(' ');
110
+ return (React__namespace.createElement("svg", Object.assign({ style: style, fill: color, height: heightWidth, width: heightWidth, viewBox: viewBox, "aria-labelledby": hasTitle ? this.id : null, "aria-hidden": hasTitle ? null : true, role: "img" }, props),
111
+ hasTitle && React__namespace.createElement("title", { id: this.id }, title),
112
+ React__namespace.createElement("path", { d: svgPath })));
113
+ }
114
+ },
115
+ _a.displayName = name,
116
+ _a.defaultProps = {
117
+ color: 'currentColor',
118
+ size: IconSize.sm,
119
+ noVerticalAlign: false
120
+ },
121
+ _a;
122
+ }
123
+
124
+ const TimesIconConfig = {
125
+ name: 'TimesIcon',
126
+ height: 512,
127
+ width: 352,
128
+ svgPath: 'M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z',
129
+ yOffset: 0,
130
+ xOffset: 0,
131
+ };
132
+
133
+ const TimesIcon = createIcon(TimesIconConfig);
134
+
135
+ var TimesIcon$1 = TimesIcon;
136
+
137
+ var _excluded = ["label", "isRequired", "children", "selectMenuOptions", "ariaLabel", "placeholder", "helperText", "maxHeight", "isDisabled", "classNames", "menuAppendTo", "extraProps"];
61
138
 
62
139
  var FormMultiSelectInput = function FormMultiSelectInput(_ref) {
63
140
  var label = _ref.label,
64
141
  isRequired = _ref.isRequired,
65
- selectOptions = _ref.selectOptions,
66
- _ref$ariaLabel = _ref.ariaLabel,
67
- ariaLabel = _ref$ariaLabel === void 0 ? 'Select Input' : _ref$ariaLabel,
142
+ selectMenuOptions = _ref.selectMenuOptions,
68
143
  placeholder = _ref.placeholder,
69
144
  helperText = _ref.helperText,
70
- maxHeight = _ref.maxHeight,
71
145
  isDisabled = _ref.isDisabled,
72
- classNames = _ref.classNames,
73
146
  menuAppendTo = _ref.menuAppendTo,
74
- extraProps = _ref.extraProps,
75
147
  rest = _objectWithoutPropertiesLoose(_ref, _excluded);
76
148
 
77
149
  var _useField = formik.useField(rest),
78
150
  meta = _useField[1];
79
151
 
80
- var value = meta.value;
81
-
82
152
  var _useFormikContext = formik.useFormikContext(),
83
153
  isSubmitting = _useFormikContext.isSubmitting,
84
154
  setFieldValue = _useFormikContext.setFieldValue,
85
155
  setFieldTouched = _useFormikContext.setFieldTouched;
86
156
 
87
- var _React$useState = React__namespace.useState(false),
157
+ var _React$useState = React__default["default"].useState(false),
88
158
  isOpen = _React$useState[0],
89
159
  setIsOpen = _React$useState[1];
90
160
 
91
- var onSelect = function onSelect(_event, selection) {
92
- var newValues = value.includes(selection) ? value.filter(function (item) {
93
- return item !== selection;
94
- }) : [].concat(value, [selection]);
95
- setFieldTouched(rest.name, true, false);
96
- setFieldValue(rest.name, newValues, true);
97
- };
161
+ var _React$useState2 = React__default["default"].useState(''),
162
+ inputValue = _React$useState2[0],
163
+ setInputValue = _React$useState2[1];
164
+
165
+ var _React$useState3 = React__default["default"].useState([]),
166
+ selected = _React$useState3[0],
167
+ setSelected = _React$useState3[1];
168
+
169
+ var _React$useState4 = React__default["default"].useState(selectMenuOptions),
170
+ selectOptions = _React$useState4[0],
171
+ setSelectOptions = _React$useState4[1];
172
+
173
+ var _React$useState5 = React__default["default"].useState(null),
174
+ focusedItemIndex = _React$useState5[0],
175
+ setFocusedItemIndex = _React$useState5[1];
176
+
177
+ var _React$useState6 = React__default["default"].useState(null),
178
+ activeItem = _React$useState6[0],
179
+ setActiveItem = _React$useState6[1];
180
+
181
+ var textInputRef = React__default["default"].useRef();
182
+ React__default["default"].useEffect(function () {
183
+ var newSelectOptions = selectMenuOptions; // Filter menu items based on the text input value when one exists
184
+
185
+ if (inputValue) {
186
+ newSelectOptions = selectMenuOptions.filter(function (menuItem) {
187
+ return String(menuItem.children).toLowerCase().includes(inputValue.toLowerCase());
188
+ }); // When no options are found after filtering, display 'No results found'
189
+
190
+ if (!newSelectOptions.length) {
191
+ newSelectOptions = [{
192
+ isDisabled: false,
193
+ children: "No results found for \"" + inputValue + "\"",
194
+ value: 'no results'
195
+ }];
196
+ } // Open the menu when the input value changes and the new value is not empty
197
+
198
+
199
+ if (!isOpen) {
200
+ setIsOpen(true);
201
+ }
202
+ }
203
+
204
+ setSelectOptions(newSelectOptions);
205
+ setFocusedItemIndex(null);
206
+ setActiveItem(null);
207
+ }, [inputValue]);
98
208
 
99
- var disabledItems = selectOptions.filter(function (selectItem) {
100
- var _selectItem$extraProp;
101
-
102
- return (selectItem == null ? void 0 : selectItem.isDisabled) || (selectItem == null ? void 0 : (_selectItem$extraProp = selectItem.extraProps) == null ? void 0 : _selectItem$extraProp.isDisabled);
103
- });
104
-
105
- var CustomChipGroup = function CustomChipGroup() {
106
- return React__namespace.createElement(reactCore.ChipGroup, null, (value || []).map(function (currentValue) {
107
- return React__namespace.createElement(reactCore.Chip, {
108
- isReadOnly: !!disabledItems.find(function (item) {
109
- return item.value === currentValue;
110
- }),
111
- key: currentValue,
112
- onClick: function onClick(event) {
113
- return onSelect(event, currentValue);
209
+ var handleMenuArrowKeys = function handleMenuArrowKeys(key) {
210
+ var indexToFocus = 0;
211
+
212
+ if (isOpen) {
213
+ if (key === 'ArrowUp') {
214
+ // When no index is set or at the first index, focus to the last, otherwise decrement focus index
215
+ if (focusedItemIndex === null || focusedItemIndex === 0) {
216
+ indexToFocus = selectOptions.length - 1;
217
+ } else {
218
+ indexToFocus = focusedItemIndex - 1;
114
219
  }
115
- }, currentValue);
116
- }));
117
- };
220
+ }
118
221
 
119
- var onClear = function onClear() {
120
- var filtered = value == null ? void 0 : value.filter(function (item) {
121
- var _selectItem$extraProp2;
222
+ if (key === 'ArrowDown') {
223
+ // When no index is set or at the last index, focus to the first, otherwise increment focus index
224
+ if (focusedItemIndex === null || focusedItemIndex === selectOptions.length - 1) {
225
+ indexToFocus = 0;
226
+ } else {
227
+ indexToFocus = focusedItemIndex + 1;
228
+ }
229
+ }
122
230
 
123
- var selectItem = disabledItems.find(function (obj) {
124
- return obj.value === item;
125
- });
126
- return (selectItem == null ? void 0 : selectItem.isDisabled) || (selectItem == null ? void 0 : (_selectItem$extraProp2 = selectItem.extraProps) == null ? void 0 : _selectItem$extraProp2.isDisabled);
231
+ setFocusedItemIndex(indexToFocus);
232
+ var focusedItem = selectOptions.filter(function (option) {
233
+ return !option.isDisabled;
234
+ })[indexToFocus];
235
+ setActiveItem("select-multi-typeahead-" + focusedItem.value.replace(' ', '-'));
236
+ }
237
+ };
238
+
239
+ var onInputKeyDown = function onInputKeyDown(event) {
240
+ var enabledMenuItems = selectOptions.filter(function (menuItem) {
241
+ return !menuItem.isDisabled;
127
242
  });
128
- setFieldValue(rest.name, [].concat(filtered), true);
129
- setIsOpen(false);
243
+ var firstMenuItem = enabledMenuItems[0];
244
+ var focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem;
245
+
246
+ switch (event.key) {
247
+ // Select the first available option
248
+ case 'Enter':
249
+ if (!isOpen) {
250
+ setIsOpen(function (prevIsOpen) {
251
+ return !prevIsOpen;
252
+ });
253
+ } else if (isOpen && focusedItem.value !== 'no results') {
254
+ _onSelect(focusedItem.value);
255
+ }
256
+
257
+ break;
258
+
259
+ case 'Tab':
260
+ case 'Escape':
261
+ setIsOpen(false);
262
+ setActiveItem(null);
263
+ break;
264
+
265
+ case 'ArrowUp':
266
+ case 'ArrowDown':
267
+ event.preventDefault();
268
+ handleMenuArrowKeys(event.key);
269
+ break;
270
+ }
130
271
  };
131
272
 
132
- var onToggle = function onToggle() {
273
+ var onToggleClick = function onToggleClick() {
133
274
  setIsOpen(!isOpen);
134
275
  };
135
276
 
136
- return React__namespace.createElement(React__namespace.Fragment, null, React__namespace.createElement(formGroupContainer.FormGroupContainer, {
277
+ var onTextInputChange = function onTextInputChange(_event, value) {
278
+ setInputValue(value);
279
+ };
280
+
281
+ var _onSelect = function onSelect(value) {
282
+ var _textInputRef$current;
283
+
284
+ // eslint-disable-next-line no-console
285
+ console.log('selected', value);
286
+
287
+ if (value && value !== 'no results') {
288
+ var selectedValues = selected.includes(value) ? selected.filter(function (selection) {
289
+ return selection !== value;
290
+ }) : [].concat(selected, [value]);
291
+ setSelected(selectedValues);
292
+ setFieldTouched(rest.name, true, false);
293
+ setFieldValue(rest.name, selectedValues, true);
294
+ } // eslint-disable-next-line no-unused-expressions
295
+
296
+
297
+ textInputRef == null ? void 0 : (_textInputRef$current = textInputRef.current) == null ? void 0 : _textInputRef$current.focus();
298
+ };
299
+
300
+ var toggle = function toggle(toggleRef) {
301
+ return React__default["default"].createElement(reactCore.MenuToggle, {
302
+ variant: 'typeahead',
303
+ onClick: onToggleClick,
304
+ innerRef: toggleRef,
305
+ isExpanded: isOpen,
306
+ isDisabled: isSubmitting || isDisabled,
307
+ isFullWidth: true
308
+ }, React__default["default"].createElement(reactCore.TextInputGroup, {
309
+ isPlain: true
310
+ }, React__default["default"].createElement(reactCore.TextInputGroupMain, Object.assign({
311
+ value: inputValue,
312
+ onClick: onToggleClick,
313
+ onChange: onTextInputChange,
314
+ onKeyDown: onInputKeyDown,
315
+ id: 'multi-typeahead-select-input',
316
+ autoComplete: 'off',
317
+ innerRef: textInputRef,
318
+ placeholder: placeholder
319
+ }, activeItem && {
320
+ 'aria-activedescendant': activeItem
321
+ }, {
322
+ role: 'combobox',
323
+ isExpanded: isOpen,
324
+ "aria-controls": 'select-multi-typeahead-listbox'
325
+ }), React__default["default"].createElement(reactCore.ChipGroup, {
326
+ "aria-label": 'Current selections'
327
+ }, selected.map(function (selection, index) {
328
+ return React__default["default"].createElement(reactCore.Chip, {
329
+ key: index,
330
+ onClick: function onClick(ev) {
331
+ ev.stopPropagation();
332
+
333
+ _onSelect(selection);
334
+ }
335
+ }, selection);
336
+ }))), React__default["default"].createElement(reactCore.TextInputGroupUtilities, null, selected.length > 0 && React__default["default"].createElement(reactCore.Button, {
337
+ variant: 'plain',
338
+ onClick: function onClick() {
339
+ var _textInputRef$current2;
340
+
341
+ setInputValue('');
342
+ setSelected([]); // eslint-disable-next-line no-unused-expressions
343
+
344
+ textInputRef == null ? void 0 : (_textInputRef$current2 = textInputRef.current) == null ? void 0 : _textInputRef$current2.focus();
345
+ },
346
+ "aria-label": 'Clear input value'
347
+ }, React__default["default"].createElement(TimesIcon$1, {
348
+ "aria-hidden": true
349
+ })))));
350
+ };
351
+
352
+ return React__default["default"].createElement(React__default["default"].Fragment, null, React__default["default"].createElement(formGroupContainer.FormGroupContainer, {
137
353
  validated: meta.touched && meta.error ? reactCore.ValidatedOptions.error : reactCore.ValidatedOptions["default"],
138
354
  helperTextInvalid: meta.error,
139
355
  isRequired: isRequired,
140
356
  fieldId: rest.name,
141
357
  label: label,
142
358
  helperText: helperText
143
- }, React__namespace.createElement(reactCore.Select, Object.assign({
144
- variant: reactCore.SelectVariant.typeaheadMulti,
145
- "aria-labelledby": rest.name,
146
- typeAheadAriaLabel: placeholder || 'Select the options',
147
- placeholderText: placeholder || 'Select the options',
148
- onToggle: onToggle,
149
- onSelect: onSelect,
150
- onClear: onClear,
151
- selections: value,
152
- isOpen: isOpen
153
- }, maxHeight && {
154
- maxHeight: maxHeight
155
- }, {
156
- isDisabled: isSubmitting || isDisabled,
157
- "aria-label": ariaLabel
158
- }, disabledItems && {
159
- chipGroupComponent: React__namespace.createElement(CustomChipGroup, null)
160
- }, classNames && {
161
- className: classNames
359
+ }, React__default["default"].createElement(reactCore.Select, Object.assign({
360
+ id: 'multi-typeahead-select',
361
+ isOpen: isOpen,
362
+ selected: selected,
363
+ onSelect: function onSelect(_ev, selection) {
364
+ return _onSelect(selection);
365
+ },
366
+ onOpenChange: function onOpenChange() {
367
+ return setIsOpen(false);
368
+ },
369
+ toggle: toggle
162
370
  }, menuAppendTo && {
163
371
  menuAppendTo: menuAppendTo
164
- }, extraProps && _extends({}, extraProps), rest), lodash.map(selectOptions, function (option, index) {
165
- return React__namespace.createElement(reactCore.SelectOption, Object.assign({
166
- key: index,
167
- value: option.value,
168
- label: option.value
169
- }, option.description && {
170
- description: option.description
171
- }, option.isDisabled && {
372
+ }), React__default["default"].createElement(reactCore.SelectList, {
373
+ isAriaMultiselectable: true,
374
+ id: 'select-multi-typeahead-listbox'
375
+ }, selectOptions.map(function (option, index) {
376
+ return React__default["default"].createElement(reactCore.SelectOption, Object.assign({
377
+ key: option.value || option.children,
378
+ isFocused: focusedItemIndex === index,
379
+ className: option.className,
380
+ id: "select-multi-typeahead-" + option.value.replace(' ', '-'),
172
381
  isDisabled: option.isDisabled
173
- }, option.extraProps && _extends({}, option.extraProps)));
174
- }))));
382
+ }, option, {
383
+ ref: null
384
+ }));
385
+ })))));
175
386
  };
176
387
 
177
388
  exports.FormMultiSelectInput = FormMultiSelectInput;
@@ -1,26 +1,8 @@
1
1
  import * as React from 'react';
2
- import { useField, useFormikContext } from 'formik';
3
- import { ValidatedOptions, Select, SelectVariant, SelectOption, ChipGroup, Chip } from '@patternfly/react-core';
2
+ import React__default from 'react';
3
+ import { ValidatedOptions, Select, SelectList, SelectOption, MenuToggle, TextInputGroup, TextInputGroupMain, ChipGroup, Chip, TextInputGroupUtilities, Button } from '@patternfly/react-core';
4
4
  import { FormGroupContainer } from '@rhc-shared-components/form-group-container';
5
- import { map } from 'lodash';
6
-
7
- function _extends() {
8
- _extends = Object.assign || function (target) {
9
- for (var i = 1; i < arguments.length; i++) {
10
- var source = arguments[i];
11
-
12
- for (var key in source) {
13
- if (Object.prototype.hasOwnProperty.call(source, key)) {
14
- target[key] = source[key];
15
- }
16
- }
17
- }
18
-
19
- return target;
20
- };
21
-
22
- return _extends.apply(this, arguments);
23
- }
5
+ import { useField, useFormikContext } from 'formik';
24
6
 
25
7
  function _objectWithoutPropertiesLoose(source, excluded) {
26
8
  if (source == null) return {};
@@ -37,105 +19,310 @@ function _objectWithoutPropertiesLoose(source, excluded) {
37
19
  return target;
38
20
  }
39
21
 
40
- const _excluded = ["label", "isRequired", "children", "selectOptions", "ariaLabel", "placeholder", "helperText", "maxHeight", "isDisabled", "classNames", "menuAppendTo", "extraProps"];
22
+ /*! *****************************************************************************
23
+ Copyright (c) Microsoft Corporation.
24
+
25
+ Permission to use, copy, modify, and/or distribute this software for any
26
+ purpose with or without fee is hereby granted.
27
+
28
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
29
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
30
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
31
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
32
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
33
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
34
+ PERFORMANCE OF THIS SOFTWARE.
35
+ ***************************************************************************** */
36
+
37
+ function __rest(s, e) {
38
+ var t = {};
39
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
40
+ t[p] = s[p];
41
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
42
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
43
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
44
+ t[p[i]] = s[p[i]];
45
+ }
46
+ return t;
47
+ }
48
+
49
+ var IconSize;
50
+ (function (IconSize) {
51
+ IconSize["sm"] = "sm";
52
+ IconSize["md"] = "md";
53
+ IconSize["lg"] = "lg";
54
+ IconSize["xl"] = "xl";
55
+ })(IconSize || (IconSize = {}));
56
+ const getSize = (size) => {
57
+ switch (size) {
58
+ case IconSize.sm:
59
+ return '1em';
60
+ case IconSize.md:
61
+ return '1.5em';
62
+ case IconSize.lg:
63
+ return '2em';
64
+ case IconSize.xl:
65
+ return '3em';
66
+ default:
67
+ return '1em';
68
+ }
69
+ };
70
+ let currentId = 0;
71
+ /**
72
+ * Factory to create Icon class components for consumers
73
+ */
74
+ function createIcon({ name, xOffset = 0, yOffset = 0, width, height, svgPath }) {
75
+ var _a;
76
+ return _a = class SVGIcon extends React.Component {
77
+ constructor() {
78
+ super(...arguments);
79
+ this.id = `icon-title-${currentId++}`;
80
+ }
81
+ render() {
82
+ const _a = this.props, { size, color, title, noVerticalAlign } = _a, props = __rest(_a, ["size", "color", "title", "noVerticalAlign"]);
83
+ const hasTitle = Boolean(title);
84
+ const heightWidth = getSize(size);
85
+ const baseAlign = -0.125 * Number.parseFloat(heightWidth);
86
+ const style = noVerticalAlign ? null : { verticalAlign: `${baseAlign}em` };
87
+ const viewBox = [xOffset, yOffset, width, height].join(' ');
88
+ return (React.createElement("svg", Object.assign({ style: style, fill: color, height: heightWidth, width: heightWidth, viewBox: viewBox, "aria-labelledby": hasTitle ? this.id : null, "aria-hidden": hasTitle ? null : true, role: "img" }, props),
89
+ hasTitle && React.createElement("title", { id: this.id }, title),
90
+ React.createElement("path", { d: svgPath })));
91
+ }
92
+ },
93
+ _a.displayName = name,
94
+ _a.defaultProps = {
95
+ color: 'currentColor',
96
+ size: IconSize.sm,
97
+ noVerticalAlign: false
98
+ },
99
+ _a;
100
+ }
101
+
102
+ const TimesIconConfig = {
103
+ name: 'TimesIcon',
104
+ height: 512,
105
+ width: 352,
106
+ svgPath: 'M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z',
107
+ yOffset: 0,
108
+ xOffset: 0,
109
+ };
110
+
111
+ const TimesIcon = createIcon(TimesIconConfig);
112
+
113
+ var TimesIcon$1 = TimesIcon;
114
+
115
+ const _excluded = ["label", "isRequired", "children", "selectMenuOptions", "ariaLabel", "placeholder", "helperText", "maxHeight", "isDisabled", "classNames", "menuAppendTo", "extraProps"];
41
116
 
42
117
  const FormMultiSelectInput = _ref => {
43
118
  let {
44
119
  label,
45
120
  isRequired,
46
- selectOptions,
121
+ selectMenuOptions,
47
122
  ariaLabel = 'Select Input',
48
123
  placeholder,
49
124
  helperText,
50
- maxHeight,
51
125
  isDisabled,
52
- classNames,
53
- menuAppendTo,
54
- extraProps
126
+ menuAppendTo
55
127
  } = _ref,
56
128
  rest = _objectWithoutPropertiesLoose(_ref, _excluded);
57
129
 
58
130
  const [, meta] = useField(rest);
59
- const {
60
- value
61
- } = meta;
62
131
  const {
63
132
  isSubmitting,
64
133
  setFieldValue,
65
134
  setFieldTouched
66
135
  } = useFormikContext();
67
- const [isOpen, setIsOpen] = React.useState(false);
136
+ const [isOpen, setIsOpen] = React__default.useState(false);
137
+ const [inputValue, setInputValue] = React__default.useState('');
138
+ const [selected, setSelected] = React__default.useState([]);
139
+ const [selectOptions, setSelectOptions] = React__default.useState(selectMenuOptions);
140
+ const [focusedItemIndex, setFocusedItemIndex] = React__default.useState(null);
141
+ const [activeItem, setActiveItem] = React__default.useState(null);
142
+ const textInputRef = React__default.useRef();
143
+ React__default.useEffect(() => {
144
+ let newSelectOptions = selectMenuOptions; // Filter menu items based on the text input value when one exists
145
+
146
+ if (inputValue) {
147
+ newSelectOptions = selectMenuOptions.filter(menuItem => String(menuItem.children).toLowerCase().includes(inputValue.toLowerCase())); // When no options are found after filtering, display 'No results found'
148
+
149
+ if (!newSelectOptions.length) {
150
+ newSelectOptions = [{
151
+ isDisabled: false,
152
+ children: `No results found for "${inputValue}"`,
153
+ value: 'no results'
154
+ }];
155
+ } // Open the menu when the input value changes and the new value is not empty
156
+
157
+
158
+ if (!isOpen) {
159
+ setIsOpen(true);
160
+ }
161
+ }
68
162
 
69
- const onSelect = (_event, selection) => {
70
- const newValues = value.includes(selection) ? value.filter(item => item !== selection) : [...value, selection];
71
- setFieldTouched(rest.name, true, false);
72
- setFieldValue(rest.name, newValues, true);
163
+ setSelectOptions(newSelectOptions);
164
+ setFocusedItemIndex(null);
165
+ setActiveItem(null);
166
+ }, [inputValue]);
167
+
168
+ const handleMenuArrowKeys = key => {
169
+ let indexToFocus = 0;
170
+
171
+ if (isOpen) {
172
+ if (key === 'ArrowUp') {
173
+ // When no index is set or at the first index, focus to the last, otherwise decrement focus index
174
+ if (focusedItemIndex === null || focusedItemIndex === 0) {
175
+ indexToFocus = selectOptions.length - 1;
176
+ } else {
177
+ indexToFocus = focusedItemIndex - 1;
178
+ }
179
+ }
180
+
181
+ if (key === 'ArrowDown') {
182
+ // When no index is set or at the last index, focus to the first, otherwise increment focus index
183
+ if (focusedItemIndex === null || focusedItemIndex === selectOptions.length - 1) {
184
+ indexToFocus = 0;
185
+ } else {
186
+ indexToFocus = focusedItemIndex + 1;
187
+ }
188
+ }
189
+
190
+ setFocusedItemIndex(indexToFocus);
191
+ const focusedItem = selectOptions.filter(option => !option.isDisabled)[indexToFocus];
192
+ setActiveItem(`select-multi-typeahead-${focusedItem.value.replace(' ', '-')}`);
193
+ }
73
194
  };
74
195
 
75
- const disabledItems = selectOptions.filter(selectItem => {
76
- var _selectItem$extraProp;
196
+ const onInputKeyDown = event => {
197
+ const enabledMenuItems = selectOptions.filter(menuItem => !menuItem.isDisabled);
198
+ const [firstMenuItem] = enabledMenuItems;
199
+ const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem;
77
200
 
78
- return (selectItem == null ? void 0 : selectItem.isDisabled) || (selectItem == null ? void 0 : (_selectItem$extraProp = selectItem.extraProps) == null ? void 0 : _selectItem$extraProp.isDisabled);
79
- });
201
+ switch (event.key) {
202
+ // Select the first available option
203
+ case 'Enter':
204
+ if (!isOpen) {
205
+ setIsOpen(prevIsOpen => !prevIsOpen);
206
+ } else if (isOpen && focusedItem.value !== 'no results') {
207
+ onSelect(focusedItem.value);
208
+ }
80
209
 
81
- const CustomChipGroup = () => React.createElement(ChipGroup, null, (value || []).map(currentValue => React.createElement(Chip, {
82
- isReadOnly: !!disabledItems.find(item => item.value === currentValue),
83
- key: currentValue,
84
- onClick: event => onSelect(event, currentValue)
85
- }, currentValue)));
210
+ break;
86
211
 
87
- const onClear = () => {
88
- const filtered = value == null ? void 0 : value.filter(item => {
89
- var _selectItem$extraProp2;
212
+ case 'Tab':
213
+ case 'Escape':
214
+ setIsOpen(false);
215
+ setActiveItem(null);
216
+ break;
90
217
 
91
- const selectItem = disabledItems.find(obj => obj.value === item);
92
- return (selectItem == null ? void 0 : selectItem.isDisabled) || (selectItem == null ? void 0 : (_selectItem$extraProp2 = selectItem.extraProps) == null ? void 0 : _selectItem$extraProp2.isDisabled);
93
- });
94
- setFieldValue(rest.name, [...filtered], true);
95
- setIsOpen(false);
218
+ case 'ArrowUp':
219
+ case 'ArrowDown':
220
+ event.preventDefault();
221
+ handleMenuArrowKeys(event.key);
222
+ break;
223
+ }
96
224
  };
97
225
 
98
- const onToggle = () => {
226
+ const onToggleClick = () => {
99
227
  setIsOpen(!isOpen);
100
228
  };
101
229
 
102
- return React.createElement(React.Fragment, null, React.createElement(FormGroupContainer, {
230
+ const onTextInputChange = (_event, value) => {
231
+ setInputValue(value);
232
+ };
233
+
234
+ const onSelect = value => {
235
+ var _textInputRef$current;
236
+
237
+ // eslint-disable-next-line no-console
238
+ console.log('selected', value);
239
+
240
+ if (value && value !== 'no results') {
241
+ const selectedValues = selected.includes(value) ? selected.filter(selection => selection !== value) : [...selected, value];
242
+ setSelected(selectedValues);
243
+ setFieldTouched(rest.name, true, false);
244
+ setFieldValue(rest.name, selectedValues, true);
245
+ } // eslint-disable-next-line no-unused-expressions
246
+
247
+
248
+ textInputRef == null ? void 0 : (_textInputRef$current = textInputRef.current) == null ? void 0 : _textInputRef$current.focus();
249
+ };
250
+
251
+ const toggle = toggleRef => React__default.createElement(MenuToggle, {
252
+ variant: 'typeahead',
253
+ onClick: onToggleClick,
254
+ innerRef: toggleRef,
255
+ isExpanded: isOpen,
256
+ isDisabled: isSubmitting || isDisabled,
257
+ isFullWidth: true
258
+ }, React__default.createElement(TextInputGroup, {
259
+ isPlain: true
260
+ }, React__default.createElement(TextInputGroupMain, Object.assign({
261
+ value: inputValue,
262
+ onClick: onToggleClick,
263
+ onChange: onTextInputChange,
264
+ onKeyDown: onInputKeyDown,
265
+ id: 'multi-typeahead-select-input',
266
+ autoComplete: 'off',
267
+ innerRef: textInputRef,
268
+ placeholder: placeholder
269
+ }, activeItem && {
270
+ 'aria-activedescendant': activeItem
271
+ }, {
272
+ role: 'combobox',
273
+ isExpanded: isOpen,
274
+ "aria-controls": 'select-multi-typeahead-listbox'
275
+ }), React__default.createElement(ChipGroup, {
276
+ "aria-label": 'Current selections'
277
+ }, selected.map((selection, index) => React__default.createElement(Chip, {
278
+ key: index,
279
+ onClick: ev => {
280
+ ev.stopPropagation();
281
+ onSelect(selection);
282
+ }
283
+ }, selection)))), React__default.createElement(TextInputGroupUtilities, null, selected.length > 0 && React__default.createElement(Button, {
284
+ variant: 'plain',
285
+ onClick: () => {
286
+ var _textInputRef$current2;
287
+
288
+ setInputValue('');
289
+ setSelected([]); // eslint-disable-next-line no-unused-expressions
290
+
291
+ textInputRef == null ? void 0 : (_textInputRef$current2 = textInputRef.current) == null ? void 0 : _textInputRef$current2.focus();
292
+ },
293
+ "aria-label": 'Clear input value'
294
+ }, React__default.createElement(TimesIcon$1, {
295
+ "aria-hidden": true
296
+ })))));
297
+
298
+ return React__default.createElement(React__default.Fragment, null, React__default.createElement(FormGroupContainer, {
103
299
  validated: meta.touched && meta.error ? ValidatedOptions.error : ValidatedOptions.default,
104
300
  helperTextInvalid: meta.error,
105
301
  isRequired: isRequired,
106
302
  fieldId: rest.name,
107
303
  label: label,
108
304
  helperText: helperText
109
- }, React.createElement(Select, Object.assign({
110
- variant: SelectVariant.typeaheadMulti,
111
- "aria-labelledby": rest.name,
112
- typeAheadAriaLabel: placeholder || 'Select the options',
113
- placeholderText: placeholder || 'Select the options',
114
- onToggle: onToggle,
115
- onSelect: onSelect,
116
- onClear: onClear,
117
- selections: value,
118
- isOpen: isOpen
119
- }, maxHeight && {
120
- maxHeight
121
- }, {
122
- isDisabled: isSubmitting || isDisabled,
123
- "aria-label": ariaLabel
124
- }, disabledItems && {
125
- chipGroupComponent: React.createElement(CustomChipGroup, null)
126
- }, classNames && {
127
- className: classNames
305
+ }, React__default.createElement(Select, Object.assign({
306
+ id: 'multi-typeahead-select',
307
+ isOpen: isOpen,
308
+ selected: selected,
309
+ onSelect: (_ev, selection) => onSelect(selection),
310
+ onOpenChange: () => setIsOpen(false),
311
+ toggle: toggle
128
312
  }, menuAppendTo && {
129
313
  menuAppendTo
130
- }, extraProps && _extends({}, extraProps), rest), map(selectOptions, (option, index) => React.createElement(SelectOption, Object.assign({
131
- key: index,
132
- value: option.value,
133
- label: option.value
134
- }, option.description && {
135
- description: option.description
136
- }, option.isDisabled && {
314
+ }), React__default.createElement(SelectList, {
315
+ isAriaMultiselectable: true,
316
+ id: 'select-multi-typeahead-listbox'
317
+ }, selectOptions.map((option, index) => React__default.createElement(SelectOption, Object.assign({
318
+ key: option.value || option.children,
319
+ isFocused: focusedItemIndex === index,
320
+ className: option.className,
321
+ id: `select-multi-typeahead-${option.value.replace(' ', '-')}`,
137
322
  isDisabled: option.isDisabled
138
- }, option.extraProps && _extends({}, option.extraProps)))))));
323
+ }, option, {
324
+ ref: null
325
+ })))))));
139
326
  };
140
327
 
141
328
  export { FormMultiSelectInput };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhc-shared-components/form-multi-select-component",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "project description",
5
5
  "author": "shkale",
6
6
  "license": "MIT",
@@ -62,7 +62,7 @@
62
62
  "typescript": "^3.7.5"
63
63
  },
64
64
  "dependencies": {
65
- "@patternfly/react-core": "^4.276.6",
65
+ "@patternfly/react-core": "5.3.0",
66
66
  "@rhc-shared-components/form-group-container": "^0.3.2",
67
67
  "@types/lodash": "^4.14.177",
68
68
  "formik": "^2.1.4",