1 | const _excluded = ["id", "autoFocus", "textField", "dataKey", "value", "defaultValue", "onChange", "open", "defaultOpen", "onToggle", "searchTerm", "defaultSearchTerm", "onSearch", "filter", "allowCreate", "delay", "focusFirstItem", "className", "containerClassName", "placeholder", "busy", "disabled", "readOnly", "selectIcon", "busySpinner", "dropUp", "tabIndex", "popupTransition", "name", "autoComplete", "onSelect", "onCreate", "onKeyPress", "onKeyDown", "onClick", "inputProps", "listProps", "renderListItem", "renderListGroup", "optionComponent", "renderValue", "groupBy", "onBlur", "onFocus", "listComponent", "popupComponent", "data", "messages"];
|
2 |
|
3 | function _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); }
|
4 |
|
5 | function _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; }
|
6 |
|
7 | import cn from 'classnames';
|
8 | import PropTypes from 'prop-types';
|
9 | import React, { useImperativeHandle, useMemo, useRef, useState } from 'react';
|
10 | import { useUncontrolledProp } from 'uncontrollable';
|
11 | import useTimeout from '@restart/hooks/useTimeout';
|
12 | import AddToListOption, { CREATE_OPTION } from './AddToListOption';
|
13 | import DropdownListInput from './DropdownListInput';
|
14 | import { caretDown } from './Icon';
|
15 | import List from './List';
|
16 | import { FocusListContext, useFocusList } from './FocusListContext';
|
17 | import BasePopup from './Popup';
|
18 | import Widget from './Widget';
|
19 | import WidgetPicker from './WidgetPicker';
|
20 | import { useMessagesWithDefaults } from './messages';
|
21 | import { useActiveDescendant } from './A11y';
|
22 | import { useFilteredData, presets } from './Filter';
|
23 | import * as CustomPropTypes from './PropTypes';
|
24 | import canShowCreate from './canShowCreate';
|
25 | import { useAccessors } from './Accessors';
|
26 | import useAutoFocus from './useAutoFocus';
|
27 | import useDropdownToggle from './useDropdownToggle';
|
28 | import useFocusManager from './useFocusManager';
|
29 | import { notify, useFirstFocusedRender, useInstanceId } from './WidgetHelpers';
|
30 | import PickerCaret from './PickerCaret';
|
31 | const propTypes = {
|
32 | value: PropTypes.any,
|
33 |
|
34 | |
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 | onChange: PropTypes.func,
|
45 | open: PropTypes.bool,
|
46 | onToggle: PropTypes.func,
|
47 | data: PropTypes.array,
|
48 | dataKey: CustomPropTypes.accessor,
|
49 | textField: CustomPropTypes.accessor,
|
50 | allowCreate: PropTypes.oneOf([true, false, 'onFilter']),
|
51 |
|
52 | |
53 |
|
54 |
|
55 |
|
56 | renderValue: PropTypes.func,
|
57 | renderListItem: PropTypes.func,
|
58 | listComponent: CustomPropTypes.elementType,
|
59 | optionComponent: CustomPropTypes.elementType,
|
60 | renderPopup: PropTypes.func,
|
61 | renderListGroup: PropTypes.func,
|
62 | groupBy: CustomPropTypes.accessor,
|
63 |
|
64 | |
65 |
|
66 |
|
67 |
|
68 | onSelect: PropTypes.func,
|
69 | onCreate: PropTypes.func,
|
70 |
|
71 | |
72 |
|
73 |
|
74 | onSearch: PropTypes.func,
|
75 | searchTerm: PropTypes.string,
|
76 | busy: PropTypes.bool,
|
77 |
|
78 |
|
79 | selectIcon: PropTypes.node,
|
80 |
|
81 |
|
82 | busySpinner: PropTypes.node,
|
83 | placeholder: PropTypes.string,
|
84 | dropUp: PropTypes.bool,
|
85 | popupTransition: CustomPropTypes.elementType,
|
86 | disabled: CustomPropTypes.disabled.acceptsArray,
|
87 | readOnly: CustomPropTypes.disabled,
|
88 |
|
89 |
|
90 | containerClassName: PropTypes.string,
|
91 | inputProps: PropTypes.object,
|
92 | listProps: PropTypes.object,
|
93 | messages: PropTypes.shape({
|
94 | open: PropTypes.string,
|
95 | emptyList: CustomPropTypes.message,
|
96 | emptyFilter: CustomPropTypes.message,
|
97 | createOption: CustomPropTypes.message
|
98 | })
|
99 | };
|
100 |
|
101 | function useSearchWordBuilder(delay) {
|
102 | const timeout = useTimeout();
|
103 | const wordRef = useRef('');
|
104 |
|
105 | function search(character, cb) {
|
106 | let word = (wordRef.current + character).toLowerCase();
|
107 | if (!character) return;
|
108 | wordRef.current = word;
|
109 | timeout.set(() => {
|
110 | wordRef.current = '';
|
111 | cb(word);
|
112 | }, delay);
|
113 | }
|
114 |
|
115 | return search;
|
116 | }
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 | const DropdownListImpl = React.forwardRef(function DropdownList(_ref, outerRef) {
|
123 | let {
|
124 | id,
|
125 | autoFocus,
|
126 | textField,
|
127 | dataKey,
|
128 | value,
|
129 | defaultValue,
|
130 | onChange,
|
131 | open,
|
132 | defaultOpen = false,
|
133 | onToggle,
|
134 | searchTerm,
|
135 | defaultSearchTerm = '',
|
136 | onSearch,
|
137 | filter = true,
|
138 | allowCreate = false,
|
139 | delay = 500,
|
140 | focusFirstItem,
|
141 | className,
|
142 | containerClassName,
|
143 | placeholder,
|
144 | busy,
|
145 | disabled,
|
146 | readOnly,
|
147 | selectIcon = caretDown,
|
148 | busySpinner,
|
149 | dropUp,
|
150 | tabIndex,
|
151 | popupTransition,
|
152 | name,
|
153 | autoComplete,
|
154 | onSelect,
|
155 | onCreate,
|
156 | onKeyPress,
|
157 | onKeyDown,
|
158 | onClick,
|
159 | inputProps,
|
160 | listProps,
|
161 | renderListItem,
|
162 | renderListGroup,
|
163 | optionComponent,
|
164 | renderValue,
|
165 | groupBy,
|
166 | onBlur,
|
167 | onFocus,
|
168 | listComponent: ListComponent = List,
|
169 | popupComponent: Popup = BasePopup,
|
170 | data: rawData = [],
|
171 | messages: userMessages
|
172 | } = _ref,
|
173 | elementProps = _objectWithoutPropertiesLoose(_ref, _excluded);
|
174 |
|
175 | const [currentValue, handleChange] = useUncontrolledProp(value, defaultValue, onChange);
|
176 | const [currentOpen, handleOpen] = useUncontrolledProp(open, defaultOpen, onToggle);
|
177 | const [currentSearch, handleSearch] = useUncontrolledProp(searchTerm, defaultSearchTerm, onSearch);
|
178 | const ref = useRef(null);
|
179 | const filterRef = useRef(null);
|
180 | const listRef = useRef(null);
|
181 | const inputId = useInstanceId(id, '_input');
|
182 | const listId = useInstanceId(id, '_listbox');
|
183 | const activeId = useInstanceId(id, '_listbox_active_option');
|
184 | const accessors = useAccessors(textField, dataKey);
|
185 | const messages = useMessagesWithDefaults(userMessages);
|
186 | useAutoFocus(!!autoFocus, ref);
|
187 | const toggle = useDropdownToggle(currentOpen, handleOpen);
|
188 | const isDisabled = disabled === true;
|
189 |
|
190 | const isReadOnly = !!readOnly;
|
191 | const [focusEvents, focused] = useFocusManager(ref, {
|
192 | disabled: isDisabled,
|
193 | onBlur,
|
194 | onFocus
|
195 | }, {
|
196 | didHandle(focused) {
|
197 | if (focused) {
|
198 | if (filter) focus();
|
199 | return;
|
200 | }
|
201 |
|
202 | toggle.close();
|
203 | clearSearch();
|
204 | }
|
205 |
|
206 | });
|
207 | const data = useFilteredData(rawData, currentOpen ? filter : false, currentSearch, accessors.text);
|
208 | const selectedItem = useMemo(() => data[accessors.indexOf(data, currentValue)], [data, currentValue, accessors]);
|
209 | const list = useFocusList({
|
210 | activeId,
|
211 | scope: ref,
|
212 | focusFirstItem,
|
213 | anchorItem: currentOpen ? selectedItem : undefined
|
214 | });
|
215 | const [autofilling, setAutofilling] = useState(false);
|
216 | const nextSearchChar = useSearchWordBuilder(delay);
|
217 | const focusedItem = list.getFocused();
|
218 | useActiveDescendant(ref, activeId, focusedItem && currentOpen, [focusedItem]);
|
219 | const showCreateOption = canShowCreate(allowCreate, {
|
220 | searchTerm: currentSearch,
|
221 | data,
|
222 | accessors
|
223 | });
|
224 |
|
225 | const handleCreate = event => {
|
226 | notify(onCreate, [currentSearch]);
|
227 | clearSearch(event);
|
228 | toggle.close();
|
229 | focus();
|
230 | };
|
231 |
|
232 | const handleSelect = (dataItem, originalEvent) => {
|
233 | if (readOnly || isDisabled) return;
|
234 | if (dataItem === undefined) return;
|
235 | originalEvent == null ? void 0 : originalEvent.preventDefault();
|
236 |
|
237 | if (dataItem === CREATE_OPTION) {
|
238 | handleCreate(originalEvent);
|
239 | return;
|
240 | }
|
241 |
|
242 | notify(onSelect, [dataItem, {
|
243 | originalEvent
|
244 | }]);
|
245 | change(dataItem, originalEvent, true);
|
246 | toggle.close();
|
247 | focus();
|
248 | };
|
249 |
|
250 | const handleClick = e => {
|
251 | if (readOnly || isDisabled) return;
|
252 |
|
253 | e.preventDefault();
|
254 | focus();
|
255 | toggle();
|
256 | notify(onClick, [e]);
|
257 | };
|
258 |
|
259 | const handleKeyDown = e => {
|
260 | if (readOnly || isDisabled) return;
|
261 | let {
|
262 | key,
|
263 | altKey,
|
264 | ctrlKey,
|
265 | shiftKey
|
266 | } = e;
|
267 | notify(onKeyDown, [e]);
|
268 |
|
269 | let closeWithFocus = () => {
|
270 | clearSearch();
|
271 | toggle.close();
|
272 | if (currentOpen) setTimeout(focus);
|
273 | };
|
274 |
|
275 | if (e.defaultPrevented) return;
|
276 |
|
277 | if (key === 'End' && currentOpen && !shiftKey) {
|
278 | e.preventDefault();
|
279 | list.focus(list.last());
|
280 | } else if (key === 'Home' && currentOpen && !shiftKey) {
|
281 | e.preventDefault();
|
282 | list.focus(list.first());
|
283 | } else if (key === 'Escape' && (currentOpen || currentSearch)) {
|
284 | e.preventDefault();
|
285 | closeWithFocus();
|
286 | } else if (key === 'Enter' && currentOpen && ctrlKey && showCreateOption) {
|
287 | e.preventDefault();
|
288 | handleCreate(e);
|
289 | } else if ((key === 'Enter' || key === ' ' && !filter) && currentOpen) {
|
290 | e.preventDefault();
|
291 | if (list.hasFocused()) handleSelect(list.getFocused(), e);
|
292 | } else if (key === 'ArrowDown') {
|
293 | e.preventDefault();
|
294 |
|
295 | if (!currentOpen) {
|
296 | toggle.open();
|
297 | return;
|
298 | }
|
299 |
|
300 | list.focus(list.next());
|
301 | } else if (key === 'ArrowUp') {
|
302 | e.preventDefault();
|
303 | if (altKey) return closeWithFocus();
|
304 | list.focus(list.prev());
|
305 | }
|
306 | };
|
307 |
|
308 | const handleKeyPress = e => {
|
309 | if (readOnly || isDisabled) return;
|
310 | notify(onKeyPress, [e]);
|
311 | if (e.defaultPrevented || filter) return;
|
312 | nextSearchChar(String.fromCharCode(e.which), word => {
|
313 | if (!currentOpen) return;
|
314 |
|
315 | let isValid = item => presets.startsWith(accessors.text(item).toLowerCase(), word.toLowerCase());
|
316 |
|
317 | const [items, focusedItem] = list.get();
|
318 | const len = items.length;
|
319 | const startIdx = items.indexOf(focusedItem) + 1;
|
320 | const offset = startIdx >= len ? 0 : startIdx;
|
321 | let idx = 0;
|
322 | let pointer = offset;
|
323 |
|
324 | while (idx < len) {
|
325 | pointer = (idx + offset) % len;
|
326 | let item = items[pointer];
|
327 | if (isValid(list.toDataItem(item))) break;
|
328 | idx++;
|
329 | }
|
330 |
|
331 | if (idx === len) return;
|
332 | list.focus(items[pointer]);
|
333 | });
|
334 | };
|
335 |
|
336 | const handleInputChange = e => {
|
337 |
|
338 | if (!currentOpen && !e.target.value.trim()) {
|
339 | e.preventDefault();
|
340 | } else {
|
341 | search(e.target.value, e, 'input');
|
342 | }
|
343 |
|
344 | toggle.open();
|
345 | };
|
346 |
|
347 | const handleAutofillChange = e => {
|
348 | let filledValue = e.target.value.toLowerCase();
|
349 | if (filledValue === '') return void change(null);
|
350 |
|
351 | for (const item of rawData) {
|
352 | if (String(accessors.value(item)).toLowerCase() === filledValue || accessors.text(item).toLowerCase() === filledValue) {
|
353 | change(item, e);
|
354 | break;
|
355 | }
|
356 | }
|
357 | };
|
358 |
|
359 | function change(nextValue, originalEvent, selected = false) {
|
360 | if (!accessors.matches(nextValue, currentValue)) {
|
361 | notify(handleChange, [nextValue, {
|
362 | originalEvent,
|
363 | source: selected ? 'listbox' : 'input',
|
364 | lastValue: currentValue,
|
365 | searchTerm: currentSearch
|
366 | }]);
|
367 | clearSearch(originalEvent);
|
368 | toggle.close();
|
369 | }
|
370 | }
|
371 |
|
372 | function focus() {
|
373 | if (filter) filterRef.current.focus();else ref.current.focus();
|
374 | }
|
375 |
|
376 | function clearSearch(originalEvent) {
|
377 | search('', originalEvent, 'clear');
|
378 | }
|
379 |
|
380 | function search(nextSearchTerm, originalEvent, action = 'input') {
|
381 | if (currentSearch !== nextSearchTerm) handleSearch(nextSearchTerm, {
|
382 | action,
|
383 | originalEvent,
|
384 | lastSearchTerm: currentSearch
|
385 | });
|
386 | }
|
387 | |
388 |
|
389 |
|
390 |
|
391 |
|
392 | useImperativeHandle(outerRef, () => ({
|
393 | focus
|
394 | }));
|
395 | let valueItem = accessors.findOrSelf(data, currentValue);
|
396 | let shouldRenderPopup = useFirstFocusedRender(focused, currentOpen);
|
397 | const widgetProps = Object.assign({}, elementProps, {
|
398 | role: 'combobox',
|
399 | id: inputId,
|
400 |
|
401 | tabIndex: filter ? -1 : tabIndex || 0,
|
402 |
|
403 | 'aria-owns': listId,
|
404 | 'aria-expanded': !!currentOpen,
|
405 | 'aria-haspopup': true,
|
406 | 'aria-busy': !!busy,
|
407 | 'aria-live': currentOpen ? 'polite' : undefined,
|
408 | 'aria-autocomplete': 'list',
|
409 | 'aria-disabled': isDisabled,
|
410 | 'aria-readonly': isReadOnly
|
411 | });
|
412 | return React.createElement(FocusListContext.Provider, {
|
413 | value: list.context
|
414 | }, React.createElement(Widget, _extends({}, widgetProps, {
|
415 | open: !!currentOpen,
|
416 | dropUp: !!dropUp,
|
417 | focused: !!focused,
|
418 | disabled: isDisabled,
|
419 | readOnly: isReadOnly,
|
420 | autofilling: autofilling
|
421 | }, focusEvents, {
|
422 | onKeyDown: handleKeyDown,
|
423 | onKeyPress: handleKeyPress,
|
424 | className: cn(className, 'rw-dropdown-list'),
|
425 | ref: ref
|
426 | }), React.createElement(WidgetPicker, {
|
427 | onClick: handleClick,
|
428 | tabIndex: filter ? -1 : 0,
|
429 | className: cn(containerClassName, 'rw-widget-input')
|
430 | }, React.createElement(DropdownListInput, _extends({}, inputProps, {
|
431 | value: valueItem,
|
432 | dataKeyAccessor: accessors.value,
|
433 | textAccessor: accessors.text,
|
434 | name: name,
|
435 | readOnly: readOnly,
|
436 | disabled: isDisabled,
|
437 | allowSearch: !!filter,
|
438 | searchTerm: currentSearch,
|
439 | ref: filterRef,
|
440 | autoComplete: autoComplete,
|
441 | onSearch: handleInputChange,
|
442 | onAutofill: setAutofilling,
|
443 | onAutofillChange: handleAutofillChange,
|
444 | placeholder: placeholder,
|
445 | renderValue: renderValue
|
446 | })), React.createElement(PickerCaret, {
|
447 | visible: true,
|
448 | busy: busy,
|
449 | icon: selectIcon,
|
450 | spinner: busySpinner
|
451 | })), shouldRenderPopup && React.createElement(Popup, {
|
452 | dropUp: dropUp,
|
453 | open: currentOpen,
|
454 | transition: popupTransition,
|
455 | onEntered: focus,
|
456 | onEntering: () => listRef.current.scrollIntoView()
|
457 | }, React.createElement(ListComponent, _extends({}, listProps, {
|
458 | id: listId,
|
459 | data: data,
|
460 | tabIndex: -1,
|
461 | disabled: disabled,
|
462 | groupBy: groupBy,
|
463 | searchTerm: currentSearch,
|
464 | accessors: accessors,
|
465 | renderItem: renderListItem,
|
466 | renderGroup: renderListGroup,
|
467 | optionComponent: optionComponent,
|
468 | value: selectedItem,
|
469 | onChange: (d, meta) => handleSelect(d, meta.originalEvent),
|
470 | "aria-live": currentOpen ? 'polite' : undefined,
|
471 | "aria-labelledby": inputId,
|
472 | "aria-hidden": !currentOpen,
|
473 | ref: listRef,
|
474 | messages: {
|
475 | emptyList: rawData.length ? messages.emptyFilter : messages.emptyList
|
476 | }
|
477 | })), showCreateOption && React.createElement(AddToListOption, {
|
478 | onSelect: handleCreate
|
479 | }, messages.createOption(currentValue, currentSearch || '')))));
|
480 | });
|
481 | DropdownListImpl.displayName = 'DropdownList';
|
482 | DropdownListImpl.propTypes = propTypes;
|
483 | export default DropdownListImpl; |
\ | No newline at end of file |