UNPKG

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