UNPKG

20.6 kBJavaScriptView Raw
1"use strict";
2
3exports.__esModule = true;
4exports.default = void 0;
5
6var _classnames = _interopRequireDefault(require("classnames"));
7
8var _closest = _interopRequireDefault(require("dom-helpers/closest"));
9
10var _propTypes = _interopRequireDefault(require("prop-types"));
11
12var _react = _interopRequireWildcard(require("react"));
13
14var _uncontrollable = require("uncontrollable");
15
16var _AddToListOption = _interopRequireWildcard(require("./AddToListOption"));
17
18var _Icon = require("./Icon");
19
20var _List = _interopRequireDefault(require("./List"));
21
22var _FocusListContext = require("./FocusListContext");
23
24var _MultiselectInput = _interopRequireDefault(require("./MultiselectInput"));
25
26var _MultiselectTagList = _interopRequireDefault(require("./MultiselectTagList"));
27
28var _Popup = _interopRequireDefault(require("./Popup"));
29
30var _Widget = _interopRequireDefault(require("./Widget"));
31
32var _WidgetPicker = _interopRequireDefault(require("./WidgetPicker"));
33
34var _messages = require("./messages");
35
36var _A11y = require("./A11y");
37
38var _Filter = require("./Filter");
39
40var CustomPropTypes = _interopRequireWildcard(require("./PropTypes"));
41
42var _canShowCreate = _interopRequireDefault(require("./canShowCreate"));
43
44var _Accessors = require("./Accessors");
45
46var _useDropdownToggle = _interopRequireDefault(require("./useDropdownToggle"));
47
48var _useFocusManager = _interopRequireDefault(require("./useFocusManager"));
49
50var _WidgetHelpers = require("./WidgetHelpers");
51
52var _PickerCaret = _interopRequireDefault(require("./PickerCaret"));
53
54const _excluded = ["dataKey", "textField", "autoFocus", "id", "value", "defaultValue", "onChange", "open", "defaultOpen", "onToggle", "focusFirstItem", "searchTerm", "defaultSearchTerm", "onSearch", "filter", "allowCreate", "className", "containerClassName", "placeholder", "busy", "disabled", "readOnly", "selectIcon", "clearTagIcon", "busySpinner", "dropUp", "tabIndex", "popupTransition", "showPlaceholderWithValues", "showSelectedItemsInList", "onSelect", "onCreate", "onKeyDown", "onBlur", "onFocus", "inputProps", "listProps", "popupProps", "renderListItem", "renderListGroup", "renderTagValue", "optionComponent", "tagOptionComponent", "groupBy", "listComponent", "popupComponent", "tagListComponent", "data", "messages"];
55
56function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
57
58function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
59
60function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
61
62function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
63
64function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
65
66const ENTER = 13;
67const INSERT = 'insert';
68const REMOVE = 'remove';
69let propTypes = {
70 data: _propTypes.default.array,
71 //-- controlled props --
72 value: _propTypes.default.array,
73
74 /**
75 * @type {function (
76 * dataItems: ?any[],
77 * metadata: {
78 * dataItem: any,
79 * action: 'insert' | 'remove',
80 * originalEvent: SyntheticEvent,
81 * lastValue: ?any[],
82 * searchTerm: ?string
83 * }
84 * ): void}
85 */
86 onChange: _propTypes.default.func,
87 searchTerm: _propTypes.default.string,
88
89 /**
90 * @type {function (
91 * searchTerm: ?string,
92 * metadata: {
93 * action: 'clear' | 'input',
94 * lastSearchTerm: ?string,
95 * originalEvent: SyntheticEvent,
96 * }
97 * ): void}
98 */
99 onSearch: _propTypes.default.func,
100 open: _propTypes.default.bool,
101 handleOpen: _propTypes.default.func,
102 //-------------------------------------------
103 dataKey: CustomPropTypes.accessor,
104 textField: CustomPropTypes.accessor,
105 renderTagValue: _propTypes.default.func,
106 renderListItem: _propTypes.default.func,
107 renderListGroup: _propTypes.default.func,
108 groupBy: CustomPropTypes.accessor,
109 allowCreate: _propTypes.default.oneOf([true, false, 'onFilter']),
110
111 /**
112 *
113 * @type { (dataItem: ?any, metadata: { originalEvent: SyntheticEvent }) => void }
114 */
115 onSelect: _propTypes.default.func,
116
117 /**
118 * @type { (searchTerm: string) => void }
119 */
120 onCreate: _propTypes.default.func,
121 busy: _propTypes.default.bool,
122
123 /** Specify the element used to render the select (down arrow) icon. */
124 selectIcon: _propTypes.default.node,
125
126 /** Specify the element used to render tag clear icons. */
127 clearTagIcon: _propTypes.default.node,
128
129 /** Specify the element used to render the busy indicator */
130 busySpinner: _propTypes.default.node,
131 dropUp: _propTypes.default.bool,
132 popupTransition: _propTypes.default.elementType,
133
134 /** Adds a css class to the input container element. */
135 containerClassName: _propTypes.default.string,
136 inputProps: _propTypes.default.object,
137 listProps: _propTypes.default.object,
138 autoFocus: _propTypes.default.bool,
139 placeholder: _propTypes.default.string,
140
141 /** Continue to show the input placeholder even if tags are selected */
142 showPlaceholderWithValues: _propTypes.default.bool,
143
144 /** Continue to show the selected items in the dropdown list */
145 showSelectedItemsInList: _propTypes.default.bool,
146 disabled: CustomPropTypes.disabled.acceptsArray,
147 readOnly: CustomPropTypes.disabled,
148 messages: _propTypes.default.shape({
149 open: CustomPropTypes.message,
150 emptyList: CustomPropTypes.message,
151 emptyFilter: CustomPropTypes.message,
152 createOption: CustomPropTypes.message,
153 tagsLabel: CustomPropTypes.message,
154 selectedItems: CustomPropTypes.message,
155 noneSelected: CustomPropTypes.message,
156 removeLabel: CustomPropTypes.message
157 })
158};
159const EMPTY_ARRAY = [];
160
161function useMultiselectData(value = EMPTY_ARRAY, data, accessors, filter, searchTerm, showSelectedItemsInList) {
162 data = (0, _react.useMemo)(() => showSelectedItemsInList ? data : data.filter(i => !value.some(v => accessors.matches(i, v))), [data, showSelectedItemsInList, value, accessors]);
163 return [(0, _Filter.useFilteredData)(data, filter || false, searchTerm, accessors.text), data.length];
164}
165
166/**
167 * ---
168 * shortcuts:
169 * - { key: left arrow, label: move focus to previous tag }
170 * - { key: right arrow, label: move focus to next tag }
171 * - { key: delete, deselect focused tag }
172 * - { key: backspace, deselect next tag }
173 * - { key: alt + up arrow, label: close Multiselect }
174 * - { key: down arrow, label: open Multiselect, and move focus to next item }
175 * - { key: up arrow, label: move focus to previous item }
176 * - { key: home, label: move focus to first item }
177 * - { key: end, label: move focus to last item }
178 * - { key: enter, label: select focused item }
179 * - { key: ctrl + enter, label: create new tag from current searchTerm }
180 * - { key: any key, label: search list for item starting with key }
181 * ---
182 *
183 * A select listbox alternative.
184 *
185 * @public
186 */
187const Multiselect = /*#__PURE__*/_react.default.forwardRef(function Multiselect(_ref, outerRef) {
188 let {
189 dataKey,
190 textField,
191 autoFocus,
192 id,
193 value,
194 defaultValue = [],
195 onChange,
196 open,
197 defaultOpen = false,
198 onToggle,
199 focusFirstItem = false,
200 searchTerm,
201 defaultSearchTerm = '',
202 onSearch,
203 filter = 'startsWith',
204 allowCreate = false,
205 className,
206 containerClassName,
207 placeholder,
208 busy,
209 disabled,
210 readOnly,
211 selectIcon,
212 clearTagIcon = _Icon.times,
213 busySpinner,
214 dropUp,
215 tabIndex,
216 popupTransition,
217 showPlaceholderWithValues = false,
218 showSelectedItemsInList = false,
219 onSelect,
220 onCreate,
221 onKeyDown,
222 onBlur,
223 onFocus,
224 inputProps,
225 listProps,
226 popupProps,
227 renderListItem,
228 renderListGroup,
229 renderTagValue,
230 optionComponent,
231 tagOptionComponent,
232 groupBy,
233 listComponent: ListComponent = _List.default,
234 popupComponent: Popup = _Popup.default,
235 tagListComponent: TagList = _MultiselectTagList.default,
236 data: rawData = [],
237 messages: userMessages
238 } = _ref,
239 elementProps = _objectWithoutPropertiesLoose(_ref, _excluded);
240
241 let [currentValue, handleChange] = (0, _uncontrollable.useUncontrolledProp)(value, defaultValue, onChange);
242 const [currentOpen, handleOpen] = (0, _uncontrollable.useUncontrolledProp)(open, defaultOpen, onToggle);
243 const [currentSearch, handleSearch] = (0, _uncontrollable.useUncontrolledProp)(searchTerm, defaultSearchTerm, onSearch);
244 const ref = (0, _react.useRef)(null);
245 const inputRef = (0, _react.useRef)(null);
246 const listRef = (0, _react.useRef)(null);
247 const inputId = (0, _WidgetHelpers.useInstanceId)(id, '_input');
248 const tagsId = (0, _WidgetHelpers.useInstanceId)(id, '_taglist');
249 const listId = (0, _WidgetHelpers.useInstanceId)(id, '_listbox');
250 const createId = (0, _WidgetHelpers.useInstanceId)(id, '_createlist_option');
251 const activeTagId = (0, _WidgetHelpers.useInstanceId)(id, '_taglist_active_tag');
252 const activeOptionId = (0, _WidgetHelpers.useInstanceId)(id, '_listbox_active_option');
253 const accessors = (0, _Accessors.useAccessors)(textField, dataKey);
254 const messages = (0, _messages.useMessagesWithDefaults)(userMessages);
255 const toggle = (0, _useDropdownToggle.default)(currentOpen, handleOpen);
256 const isDisabled = disabled === true;
257 const isReadOnly = !!readOnly;
258 const [focusEvents, focused] = (0, _useFocusManager.default)(ref, {
259 disabled: isDisabled,
260 onBlur,
261 onFocus
262 }, {
263 didHandle(focused, event) {
264 if (focused) return focus();
265 toggle.close();
266 clearSearch(event);
267 tagList.focus(null);
268 }
269
270 });
271 const dataItems = (0, _react.useMemo)(() => currentValue.map(item => accessors.findOrSelf(rawData, item)), [rawData, currentValue, accessors]);
272 const [data, lengthWithoutValues] = useMultiselectData(dataItems, rawData, accessors, currentOpen ? filter : false, currentSearch, showSelectedItemsInList);
273 const list = (0, _FocusListContext.useFocusList)({
274 scope: ref,
275 scopeSelector: '.rw-popup',
276 focusFirstItem,
277 activeId: activeOptionId,
278 anchorItem: currentOpen ? dataItems[dataItems.length - 1] : undefined
279 });
280 const tagList = (0, _FocusListContext.useFocusList)({
281 scope: ref,
282 scopeSelector: '.rw-multiselect-taglist',
283 activeId: activeTagId
284 });
285 const showCreateOption = (0, _canShowCreate.default)(allowCreate, {
286 searchTerm: currentSearch,
287 data,
288 dataItems,
289 accessors
290 });
291 /**
292 * Update aria when it changes on update
293 */
294
295 const focusedTag = tagList.getFocused();
296 (0, _react.useEffect)(() => {
297 if (currentOpen) return;
298 (0, _A11y.setActiveDescendant)(inputRef.current, focusedTag ? activeTagId : '');
299 }, [activeTagId, currentOpen, focusedTag]);
300 const focusedItem = list.getFocused();
301 (0, _react.useEffect)(() => {
302 if (!currentOpen) return; // if (focusedItem) tagList.focus(null)
303
304 (0, _A11y.setActiveDescendant)(inputRef.current, focusedItem ? activeOptionId : '');
305 }, [activeOptionId, currentOpen, focusedItem]);
306 /**
307 * Event Handlers
308 */
309
310 const handleDelete = (dataItem, event) => {
311 if (isDisabled || readOnly || tagList.size() === 0) return;
312 focus();
313 change(dataItem, event, REMOVE);
314 };
315
316 const deletingRef = (0, _react.useRef)(false);
317
318 const handleSearchKeyDown = e => {
319 if (e.key === 'Backspace' && e.currentTarget.value && !deletingRef.current) deletingRef.current = true;
320 };
321
322 const handleSearchKeyUp = e => {
323 if (e.key === 'Backspace' && deletingRef.current) {
324 deletingRef.current = false;
325 }
326 };
327
328 const handleInputChange = e => {
329 search(e.target.value, e, 'input');
330 toggle.open();
331 };
332
333 const handleClick = e => {
334 if (isDisabled || readOnly) return; // prevents double clicks when in a <label>
335
336 e.preventDefault();
337 focus();
338
339 if ((0, _closest.default)(e.target, '.rw-select') && currentOpen) {
340 toggle.close();
341 } else toggle.open();
342 };
343
344 const handleDoubleClick = () => {
345 if (isDisabled || !inputRef.current) return;
346 focus();
347 if (inputRef.current) inputRef.current.select();
348 };
349
350 const handleSelect = (dataItem, originalEvent) => {
351 if (dataItem === undefined) return;
352 originalEvent.preventDefault();
353
354 if (dataItem === _AddToListOption.CREATE_OPTION) {
355 handleCreate(originalEvent);
356 return;
357 }
358
359 (0, _WidgetHelpers.notify)(onSelect, [dataItem, {
360 originalEvent
361 }]);
362
363 if (!showSelectedItemsInList || !dataItems.includes(dataItem)) {
364 change(dataItem, originalEvent, INSERT);
365 } else {
366 change(dataItem, originalEvent, REMOVE);
367 }
368
369 focus();
370 };
371
372 const handleCreate = event => {
373 (0, _WidgetHelpers.notify)(onCreate, [currentSearch]);
374 clearSearch(event);
375 focus();
376 };
377
378 const handleKeyDown = event => {
379 if (readOnly) {
380 event.preventDefault();
381 return;
382 }
383
384 let {
385 key,
386 keyCode,
387 altKey,
388 ctrlKey
389 } = event;
390 (0, _WidgetHelpers.notify)(onKeyDown, [event]);
391 if (event.defaultPrevented) return;
392
393 if (key === 'ArrowDown') {
394 event.preventDefault();
395
396 if (!currentOpen) {
397 toggle.open();
398 return;
399 }
400
401 list.focus(list.next());
402 tagList.focus(null);
403 } else if (key === 'ArrowUp' && (currentOpen || altKey)) {
404 event.preventDefault();
405
406 if (altKey) {
407 toggle.close();
408 return;
409 }
410
411 list.focus(list.prev());
412 tagList.focus(null);
413 } else if (key === 'End') {
414 event.preventDefault();
415
416 if (currentOpen) {
417 list.focus(list.last());
418 tagList.focus(null);
419 } else {
420 tagList.focus(tagList.last());
421 list.focus(null);
422 }
423 } else if (key === 'Home') {
424 event.preventDefault();
425 if (currentOpen) list.focus(list.first());else list.focus(tagList.first());
426 } else if (currentOpen && keyCode === ENTER) {
427 // using keyCode to ignore enter for japanese IME
428 event.preventDefault();
429
430 if (ctrlKey && showCreateOption) {
431 return handleCreate(event);
432 }
433
434 handleSelect(list.getFocused(), event);
435 } else if (key === 'Escape') {
436 if (currentOpen) toggle.close();else tagList.focus(null); //
437 } else if (!currentSearch && !deletingRef.current) {
438 //
439 if (key === 'ArrowLeft') {
440 tagList.focus(tagList.prev({
441 behavior: 'loop'
442 }));
443 } else if (key === 'ArrowRight') {
444 tagList.focus(tagList.next({
445 behavior: 'loop'
446 })); //
447 } else if (key === 'Delete' && tagList.getFocused()) {
448 handleDelete(tagList.getFocused(), event); //
449 } else if (key === 'Backspace') {
450 handleDelete(tagList.toDataItem(tagList.last()), event);
451 } else if (key === ' ' && !currentOpen) {
452 event.preventDefault();
453 toggle.open();
454 }
455 }
456 };
457 /**
458 * Methods
459 */
460
461
462 function change(dataItem, originalEvent, action) {
463 let nextDataItems = dataItems;
464
465 switch (action) {
466 case INSERT:
467 nextDataItems = nextDataItems.concat(dataItem);
468 break;
469
470 case REMOVE:
471 nextDataItems = nextDataItems.filter(d => d !== dataItem);
472 break;
473 }
474
475 handleChange(nextDataItems, {
476 action,
477 dataItem,
478 originalEvent,
479 searchTerm: currentSearch,
480 lastValue: currentValue
481 });
482 clearSearch(originalEvent);
483 }
484
485 function clearSearch(originalEvent) {
486 search('', originalEvent, 'clear');
487 }
488
489 function search(nextSearchTerm, originalEvent, action = 'input') {
490 if (nextSearchTerm !== currentSearch) handleSearch(nextSearchTerm, {
491 action,
492 originalEvent,
493 lastSearchTerm: currentSearch
494 });
495 }
496
497 function focus() {
498 if (inputRef.current) inputRef.current.focus();
499 }
500 /**
501 * Render
502 */
503
504
505 (0, _react.useImperativeHandle)(outerRef, () => ({
506 focus
507 }));
508 let shouldRenderPopup = (0, _WidgetHelpers.useFirstFocusedRender)(focused, currentOpen);
509 let shouldRenderTags = !!dataItems.length;
510 let inputOwns = `${listId} ` + (shouldRenderTags ? tagsId : '') + (showCreateOption ? createId : '');
511 return /*#__PURE__*/_react.default.createElement(_Widget.default, _extends({}, elementProps, {
512 ref: ref,
513 open: currentOpen,
514 dropUp: dropUp,
515 focused: focused,
516 disabled: isDisabled,
517 readOnly: isReadOnly,
518 onKeyDown: handleKeyDown
519 }, focusEvents, {
520 className: (0, _classnames.default)(className, 'rw-multiselect')
521 }), /*#__PURE__*/_react.default.createElement(_WidgetPicker.default, {
522 onClick: handleClick,
523 onTouchEnd: handleClick,
524 onDoubleClick: handleDoubleClick,
525 className: (0, _classnames.default)(containerClassName, 'rw-widget-input')
526 }, /*#__PURE__*/_react.default.createElement(_FocusListContext.FocusListContext.Provider, {
527 value: tagList.context
528 }, /*#__PURE__*/_react.default.createElement(TagList, {
529 id: tagsId,
530 textAccessor: accessors.text,
531 clearTagIcon: clearTagIcon,
532 label: messages.tagsLabel(),
533 value: dataItems,
534 readOnly: isReadOnly,
535 disabled: disabled,
536 onDelete: handleDelete,
537 tagOptionComponent: tagOptionComponent,
538 renderTagValue: renderTagValue
539 }, /*#__PURE__*/_react.default.createElement(_MultiselectInput.default, _extends({}, inputProps, {
540 role: "combobox",
541 autoFocus: autoFocus,
542 tabIndex: tabIndex || 0,
543 "aria-expanded": !!currentOpen,
544 "aria-busy": !!busy,
545 "aria-owns": inputOwns,
546 "aria-controls": listId,
547 "aria-haspopup": "listbox",
548 "aria-autocomplete": "list",
549 value: currentSearch,
550 disabled: isDisabled,
551 readOnly: isReadOnly,
552 placeholder: (currentValue.length && !showPlaceholderWithValues ? '' : placeholder) || '',
553 onKeyDown: handleSearchKeyDown,
554 onKeyUp: handleSearchKeyUp,
555 onChange: handleInputChange,
556 ref: inputRef
557 })))), /*#__PURE__*/_react.default.createElement(_PickerCaret.default, {
558 busy: busy,
559 spinner: busySpinner,
560 icon: selectIcon,
561 visible: focused
562 })), /*#__PURE__*/_react.default.createElement(_FocusListContext.FocusListContext.Provider, {
563 value: list.context
564 }, shouldRenderPopup && /*#__PURE__*/_react.default.createElement(Popup, _extends({}, popupProps, {
565 dropUp: dropUp,
566 open: currentOpen,
567 transition: popupTransition,
568 onEntering: () => listRef.current.scrollIntoView()
569 }), /*#__PURE__*/_react.default.createElement(ListComponent, _extends({}, listProps, {
570 id: listId,
571 data: data,
572 tabIndex: -1,
573 disabled: disabled,
574 searchTerm: currentSearch,
575 accessors: accessors,
576 renderItem: renderListItem,
577 renderGroup: renderListGroup,
578 value: dataItems,
579 groupBy: groupBy,
580 optionComponent: optionComponent,
581 onChange: (d, meta) => handleSelect(d, meta.originalEvent),
582 "aria-live": "polite",
583 "aria-labelledby": inputId,
584 "aria-hidden": !currentOpen,
585 ref: listRef,
586 messages: {
587 emptyList: lengthWithoutValues ? messages.emptyFilter : messages.emptyList
588 }
589 })), showCreateOption && /*#__PURE__*/_react.default.createElement(_AddToListOption.default, {
590 onSelect: handleCreate
591 }, messages.createOption(currentValue, currentSearch)))));
592});
593
594Multiselect.displayName = 'Multiselect';
595Multiselect.propTypes = propTypes;
596var _default = Multiselect;
597exports.default = _default;
\No newline at end of file