UNPKG

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