UNPKG

20.4 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", "renderListItem", "renderListGroup", "renderTagValue", "optionComponent", "tagOptionComponent", "groupBy", "listComponent", "popupComponent", "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 renderListItem,
227 renderListGroup,
228 renderTagValue,
229 optionComponent,
230 tagOptionComponent,
231 groupBy,
232 listComponent: ListComponent = _List.default,
233 popupComponent: Popup = _Popup.default,
234 data: rawData = [],
235 messages: userMessages
236 } = _ref,
237 elementProps = _objectWithoutPropertiesLoose(_ref, _excluded);
238
239 let [currentValue, handleChange] = (0, _uncontrollable.useUncontrolledProp)(value, defaultValue, onChange);
240 const [currentOpen, handleOpen] = (0, _uncontrollable.useUncontrolledProp)(open, defaultOpen, onToggle);
241 const [currentSearch, handleSearch] = (0, _uncontrollable.useUncontrolledProp)(searchTerm, defaultSearchTerm, onSearch);
242 const ref = (0, _react.useRef)(null);
243 const inputRef = (0, _react.useRef)(null);
244 const listRef = (0, _react.useRef)(null);
245 const inputId = (0, _WidgetHelpers.useInstanceId)(id, '_input');
246 const tagsId = (0, _WidgetHelpers.useInstanceId)(id, '_taglist');
247 const listId = (0, _WidgetHelpers.useInstanceId)(id, '_listbox');
248 const createId = (0, _WidgetHelpers.useInstanceId)(id, '_createlist_option');
249 const activeTagId = (0, _WidgetHelpers.useInstanceId)(id, '_taglist_active_tag');
250 const activeOptionId = (0, _WidgetHelpers.useInstanceId)(id, '_listbox_active_option');
251 const accessors = (0, _Accessors.useAccessors)(textField, dataKey);
252 const messages = (0, _messages.useMessagesWithDefaults)(userMessages);
253 const toggle = (0, _useDropdownToggle.default)(currentOpen, handleOpen);
254 const isDisabled = disabled === true;
255 const isReadOnly = !!readOnly;
256 const [focusEvents, focused] = (0, _useFocusManager.default)(ref, {
257 disabled: isDisabled,
258 onBlur,
259 onFocus
260 }, {
261 didHandle(focused, event) {
262 if (focused) return focus();
263 toggle.close();
264 clearSearch(event);
265 tagList.focus(null);
266 }
267
268 });
269 const dataItems = (0, _react.useMemo)(() => currentValue.map(item => accessors.findOrSelf(rawData, item)), [rawData, currentValue, accessors]);
270 const [data, lengthWithoutValues] = useMultiselectData(dataItems, rawData, accessors, currentOpen ? filter : false, currentSearch, showSelectedItemsInList);
271 const list = (0, _FocusListContext.useFocusList)({
272 scope: ref,
273 scopeSelector: '.rw-popup',
274 focusFirstItem,
275 activeId: activeOptionId,
276 anchorItem: currentOpen ? dataItems[dataItems.length - 1] : undefined
277 });
278 const tagList = (0, _FocusListContext.useFocusList)({
279 scope: ref,
280 scopeSelector: '.rw-multiselect-taglist',
281 activeId: activeTagId
282 });
283 const showCreateOption = (0, _canShowCreate.default)(allowCreate, {
284 searchTerm: currentSearch,
285 data,
286 dataItems,
287 accessors
288 });
289 /**
290 * Update aria when it changes on update
291 */
292
293 const focusedTag = tagList.getFocused();
294 (0, _react.useEffect)(() => {
295 if (currentOpen) return;
296 (0, _A11y.setActiveDescendant)(inputRef.current, focusedTag ? activeTagId : '');
297 }, [activeTagId, currentOpen, focusedTag]);
298 const focusedItem = list.getFocused();
299 (0, _react.useEffect)(() => {
300 if (!currentOpen) return; // if (focusedItem) tagList.focus(null)
301
302 (0, _A11y.setActiveDescendant)(inputRef.current, focusedItem ? activeOptionId : '');
303 }, [activeOptionId, currentOpen, focusedItem]);
304 /**
305 * Event Handlers
306 */
307
308 const handleDelete = (dataItem, event) => {
309 if (isDisabled || readOnly || tagList.size() === 0) return;
310 focus();
311 change(dataItem, event, REMOVE);
312 };
313
314 const deletingRef = (0, _react.useRef)(false);
315
316 const handleSearchKeyDown = e => {
317 if (e.key === 'Backspace' && e.currentTarget.value && !deletingRef.current) deletingRef.current = true;
318 };
319
320 const handleSearchKeyUp = e => {
321 if (e.key === 'Backspace' && deletingRef.current) {
322 deletingRef.current = false;
323 }
324 };
325
326 const handleInputChange = e => {
327 search(e.target.value, e, 'input');
328 toggle.open();
329 };
330
331 const handleClick = e => {
332 if (isDisabled || readOnly) return; // prevents double clicks when in a <label>
333
334 e.preventDefault();
335 focus();
336
337 if ((0, _closest.default)(e.target, '.rw-select') && currentOpen) {
338 toggle.close();
339 } else toggle.open();
340 };
341
342 const handleDoubleClick = () => {
343 if (isDisabled || !inputRef.current) return;
344 focus();
345 if (inputRef.current) inputRef.current.select();
346 };
347
348 const handleSelect = (dataItem, originalEvent) => {
349 if (dataItem === undefined) return;
350 originalEvent.preventDefault();
351
352 if (dataItem === _AddToListOption.CREATE_OPTION) {
353 handleCreate(originalEvent);
354 return;
355 }
356
357 (0, _WidgetHelpers.notify)(onSelect, [dataItem, {
358 originalEvent
359 }]);
360
361 if (!showSelectedItemsInList || !dataItems.includes(dataItem)) {
362 change(dataItem, originalEvent, INSERT);
363 } else {
364 change(dataItem, originalEvent, REMOVE);
365 }
366
367 focus();
368 };
369
370 const handleCreate = event => {
371 (0, _WidgetHelpers.notify)(onCreate, [currentSearch]);
372 clearSearch(event);
373 focus();
374 };
375
376 const handleKeyDown = event => {
377 if (readOnly) {
378 event.preventDefault();
379 return;
380 }
381
382 let {
383 key,
384 keyCode,
385 altKey,
386 ctrlKey
387 } = event;
388 (0, _WidgetHelpers.notify)(onKeyDown, [event]);
389 if (event.defaultPrevented) return;
390
391 if (key === 'ArrowDown') {
392 event.preventDefault();
393
394 if (!currentOpen) {
395 toggle.open();
396 return;
397 }
398
399 list.focus(list.next());
400 tagList.focus(null);
401 } else if (key === 'ArrowUp' && (currentOpen || altKey)) {
402 event.preventDefault();
403
404 if (altKey) {
405 toggle.close();
406 return;
407 }
408
409 list.focus(list.prev());
410 tagList.focus(null);
411 } else if (key === 'End') {
412 event.preventDefault();
413
414 if (currentOpen) {
415 list.focus(list.last());
416 tagList.focus(null);
417 } else {
418 tagList.focus(tagList.last());
419 list.focus(null);
420 }
421 } else if (key === 'Home') {
422 event.preventDefault();
423 if (currentOpen) list.focus(list.first());else list.focus(tagList.first());
424 } else if (currentOpen && keyCode === ENTER) {
425 // using keyCode to ignore enter for japanese IME
426 event.preventDefault();
427
428 if (ctrlKey && showCreateOption) {
429 return handleCreate(event);
430 }
431
432 handleSelect(list.getFocused(), event);
433 } else if (key === 'Escape') {
434 if (currentOpen) toggle.close();else tagList.focus(null); //
435 } else if (!currentSearch && !deletingRef.current) {
436 //
437 if (key === 'ArrowLeft') {
438 tagList.focus(tagList.prev({
439 behavior: 'loop'
440 }));
441 } else if (key === 'ArrowRight') {
442 tagList.focus(tagList.next({
443 behavior: 'loop'
444 })); //
445 } else if (key === 'Delete' && tagList.getFocused()) {
446 handleDelete(tagList.getFocused(), event); //
447 } else if (key === 'Backspace') {
448 handleDelete(tagList.toDataItem(tagList.last()), event);
449 } else if (key === ' ' && !currentOpen) {
450 event.preventDefault();
451 toggle.open();
452 }
453 }
454 };
455 /**
456 * Methods
457 */
458
459
460 function change(dataItem, originalEvent, action) {
461 let nextDataItems = dataItems;
462
463 switch (action) {
464 case INSERT:
465 nextDataItems = nextDataItems.concat(dataItem);
466 break;
467
468 case REMOVE:
469 nextDataItems = nextDataItems.filter(d => d !== dataItem);
470 break;
471 }
472
473 handleChange(nextDataItems, {
474 action,
475 dataItem,
476 originalEvent,
477 searchTerm: currentSearch,
478 lastValue: currentValue
479 });
480 clearSearch(originalEvent);
481 }
482
483 function clearSearch(originalEvent) {
484 search('', originalEvent, 'clear');
485 }
486
487 function search(nextSearchTerm, originalEvent, action = 'input') {
488 if (nextSearchTerm !== currentSearch) handleSearch(nextSearchTerm, {
489 action,
490 originalEvent,
491 lastSearchTerm: currentSearch
492 });
493 }
494
495 function focus() {
496 if (inputRef.current) inputRef.current.focus();
497 }
498 /**
499 * Render
500 */
501
502
503 (0, _react.useImperativeHandle)(outerRef, () => ({
504 focus
505 }));
506 let shouldRenderPopup = (0, _WidgetHelpers.useFirstFocusedRender)(focused, currentOpen);
507 let shouldRenderTags = !!dataItems.length;
508 let inputOwns = `${listId} ` + (shouldRenderTags ? tagsId : '') + (showCreateOption ? createId : '');
509 return /*#__PURE__*/_react.default.createElement(_Widget.default, _extends({}, elementProps, {
510 ref: ref,
511 open: currentOpen,
512 dropUp: dropUp,
513 focused: focused,
514 disabled: isDisabled,
515 readOnly: isReadOnly,
516 onKeyDown: handleKeyDown
517 }, focusEvents, {
518 className: (0, _classnames.default)(className, 'rw-multiselect')
519 }), /*#__PURE__*/_react.default.createElement(_WidgetPicker.default, {
520 onClick: handleClick,
521 onTouchEnd: handleClick,
522 onDoubleClick: handleDoubleClick,
523 className: (0, _classnames.default)(containerClassName, 'rw-widget-input')
524 }, /*#__PURE__*/_react.default.createElement(_FocusListContext.FocusListContext.Provider, {
525 value: tagList.context
526 }, /*#__PURE__*/_react.default.createElement(_MultiselectTagList.default, {
527 id: tagsId,
528 textAccessor: accessors.text,
529 clearTagIcon: clearTagIcon,
530 label: messages.tagsLabel(),
531 value: dataItems,
532 readOnly: isReadOnly,
533 disabled: disabled,
534 onDelete: handleDelete,
535 tagOptionComponent: tagOptionComponent,
536 renderTagValue: renderTagValue
537 }, /*#__PURE__*/_react.default.createElement(_MultiselectInput.default, _extends({}, inputProps, {
538 role: "combobox",
539 autoFocus: autoFocus,
540 tabIndex: tabIndex || 0,
541 "aria-expanded": !!currentOpen,
542 "aria-busy": !!busy,
543 "aria-owns": inputOwns,
544 "aria-controls": listId,
545 "aria-haspopup": "listbox",
546 "aria-autocomplete": "list",
547 value: currentSearch,
548 disabled: isDisabled,
549 readOnly: isReadOnly,
550 placeholder: (currentValue.length && !showPlaceholderWithValues ? '' : placeholder) || '',
551 onKeyDown: handleSearchKeyDown,
552 onKeyUp: handleSearchKeyUp,
553 onChange: handleInputChange,
554 ref: inputRef
555 })))), /*#__PURE__*/_react.default.createElement(_PickerCaret.default, {
556 busy: busy,
557 spinner: busySpinner,
558 icon: selectIcon,
559 visible: focused
560 })), /*#__PURE__*/_react.default.createElement(_FocusListContext.FocusListContext.Provider, {
561 value: list.context
562 }, shouldRenderPopup && /*#__PURE__*/_react.default.createElement(Popup, {
563 dropUp: dropUp,
564 open: currentOpen,
565 transition: popupTransition,
566 onEntering: () => listRef.current.scrollIntoView()
567 }, /*#__PURE__*/_react.default.createElement(ListComponent, _extends({}, listProps, {
568 id: listId,
569 data: data,
570 tabIndex: -1,
571 disabled: disabled,
572 searchTerm: currentSearch,
573 accessors: accessors,
574 renderItem: renderListItem,
575 renderGroup: renderListGroup,
576 value: dataItems,
577 groupBy: groupBy,
578 optionComponent: optionComponent,
579 onChange: (d, meta) => handleSelect(d, meta.originalEvent),
580 "aria-live": "polite",
581 "aria-labelledby": inputId,
582 "aria-hidden": !currentOpen,
583 ref: listRef,
584 messages: {
585 emptyList: lengthWithoutValues ? messages.emptyFilter : messages.emptyList
586 }
587 })), showCreateOption && /*#__PURE__*/_react.default.createElement(_AddToListOption.default, {
588 onSelect: handleCreate
589 }, messages.createOption(currentValue, currentSearch)))));
590});
591
592Multiselect.displayName = 'Multiselect';
593Multiselect.propTypes = propTypes;
594var _default = Multiselect;
595exports.default = _default;
\No newline at end of file