UNPKG

14.7 kBJavaScriptView Raw
1"use strict";
2
3exports.__esModule = true;
4exports.default = void 0;
5
6var _classnames = _interopRequireDefault(require("classnames"));
7
8var PropTypes = _interopRequireWildcard(require("prop-types"));
9
10var React = _interopRequireWildcard(require("react"));
11
12var _uncontrollable = require("uncontrollable");
13
14var _Icon = require("./Icon");
15
16var _Input = _interopRequireDefault(require("./Input"));
17
18var _List = _interopRequireDefault(require("./List"));
19
20var _FocusListContext = require("./FocusListContext");
21
22var _Popup = _interopRequireDefault(require("./Popup"));
23
24var _InputAddon = _interopRequireDefault(require("./InputAddon"));
25
26var _Widget = _interopRequireDefault(require("./Widget"));
27
28var _WidgetPicker = _interopRequireDefault(require("./WidgetPicker"));
29
30var _messages = require("./messages");
31
32var _A11y = require("./A11y");
33
34var CustomPropTypes = _interopRequireWildcard(require("./PropTypes"));
35
36var _Accessors = require("./Accessors");
37
38var _Filter = require("./Filter");
39
40var _useDropdownToggle = _interopRequireDefault(require("./useDropdownToggle"));
41
42var _useFocusManager = _interopRequireDefault(require("./useFocusManager"));
43
44var _WidgetHelpers = require("./WidgetHelpers");
45
46const _excluded = ["id", "className", "containerClassName", "placeholder", "autoFocus", "textField", "dataKey", "autoSelectMatches", "focusFirstItem", "value", "defaultValue", "onChange", "open", "defaultOpen", "onToggle", "filter", "busy", "disabled", "readOnly", "selectIcon", "hideCaret", "hideEmptyPopup", "busySpinner", "dropUp", "tabIndex", "popupTransition", "name", "onSelect", "onKeyDown", "onBlur", "onFocus", "inputProps", "listProps", "groupBy", "renderListItem", "renderListGroup", "optionComponent", "listComponent", "popupComponent", "data", "messages"];
47
48function _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); }
49
50function _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; }
51
52function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
53
54function _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); }
55
56function _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; }
57
58function indexOf(data, searchTerm, text) {
59 if (!searchTerm.trim()) return -1;
60
61 for (let idx = 0; idx < data.length; idx++) if (text(data[idx]).toLowerCase() === searchTerm) return idx;
62
63 return -1;
64}
65
66let propTypes = {
67 value: PropTypes.any,
68 onChange: PropTypes.func,
69 open: PropTypes.bool,
70 onToggle: PropTypes.func,
71 renderListItem: PropTypes.func,
72 listComponent: PropTypes.elementType,
73 renderListGroup: PropTypes.func,
74 groupBy: CustomPropTypes.accessor,
75 data: PropTypes.array,
76 dataKey: CustomPropTypes.accessor,
77 textField: CustomPropTypes.accessor,
78 name: PropTypes.string,
79
80 /** Do not show the auto complete list when it returns no results. */
81 hideEmptyPopup: PropTypes.bool,
82
83 /** Hide the combobox dropdown indicator. */
84 hideCaret: PropTypes.bool,
85
86 /**
87 *
88 * @type {(dataItem: ?any, metadata: { originalEvent: SyntheticEvent }) => void}
89 */
90 onSelect: PropTypes.func,
91 autoFocus: PropTypes.bool,
92 disabled: CustomPropTypes.disabled.acceptsArray,
93 readOnly: CustomPropTypes.disabled,
94 busy: PropTypes.bool,
95
96 /** Specify the element used to render the select (down arrow) icon. */
97 selectIcon: PropTypes.node,
98
99 /** Specify the element used to render the busy indicator */
100 busySpinner: PropTypes.node,
101 dropUp: PropTypes.bool,
102 popupTransition: PropTypes.elementType,
103 placeholder: PropTypes.string,
104
105 /** Adds a css class to the input container element. */
106 containerClassName: PropTypes.string,
107 inputProps: PropTypes.object,
108 listProps: PropTypes.object,
109 messages: PropTypes.shape({
110 openCombobox: CustomPropTypes.message,
111 emptyList: CustomPropTypes.message,
112 emptyFilter: CustomPropTypes.message
113 })
114};
115
116/**
117 * ---
118 * shortcuts:
119 * - { key: alt + down arrow, label: open combobox }
120 * - { key: alt + up arrow, label: close combobox }
121 * - { key: down arrow, label: move focus to next item }
122 * - { key: up arrow, label: move focus to previous item }
123 * - { key: home, label: move focus to first item }
124 * - { key: end, label: move focus to last item }
125 * - { key: enter, label: select focused item }
126 * - { key: any key, label: search list for item starting with key }
127 * ---
128 *
129 * Select an item from the list, or input a custom value. The Combobox can also make suggestions as you type.
130
131 * @public
132 */
133const ComboboxImpl = /*#__PURE__*/React.forwardRef(function Combobox(_ref, outerRef) {
134 let {
135 id,
136 className,
137 containerClassName,
138 placeholder,
139 autoFocus,
140 textField,
141 dataKey,
142 autoSelectMatches,
143 focusFirstItem = false,
144 value,
145 defaultValue = '',
146 onChange,
147 open,
148 defaultOpen = false,
149 onToggle,
150 filter = true,
151 busy,
152 disabled,
153 readOnly,
154 selectIcon = _Icon.caretDown,
155 hideCaret,
156 hideEmptyPopup,
157 busySpinner,
158 dropUp,
159 tabIndex,
160 popupTransition,
161 name,
162 onSelect,
163 onKeyDown,
164 onBlur,
165 onFocus,
166 inputProps,
167 listProps,
168 groupBy,
169 renderListItem,
170 renderListGroup,
171 optionComponent,
172 listComponent: ListComponent = _List.default,
173 popupComponent: Popup = _Popup.default,
174 data: rawData = [],
175 messages: userMessages
176 } = _ref,
177 elementProps = _objectWithoutPropertiesLoose(_ref, _excluded);
178
179 let [currentValue, handleChange] = (0, _uncontrollable.useUncontrolledProp)(value, defaultValue, onChange);
180 const [currentOpen, handleOpen] = (0, _uncontrollable.useUncontrolledProp)(open, defaultOpen, onToggle);
181 const ref = (0, React.useRef)(null);
182 const inputRef = (0, React.useRef)(null);
183 const listRef = (0, React.useRef)(null);
184 const [suggestion, setSuggestion] = (0, React.useState)(null);
185 const shouldFilter = (0, React.useRef)(false);
186 const inputId = (0, _WidgetHelpers.useInstanceId)(id, '_input');
187 const listId = (0, _WidgetHelpers.useInstanceId)(id, '_listbox');
188 const activeId = (0, _WidgetHelpers.useInstanceId)(id, '_listbox_active_option');
189 const accessors = (0, _Accessors.useAccessors)(textField, dataKey);
190 const messages = (0, _messages.useMessagesWithDefaults)(userMessages);
191 const toggle = (0, _useDropdownToggle.default)(currentOpen, handleOpen);
192 const isDisabled = disabled === true;
193 const isReadOnly = !!readOnly;
194 const data = (0, _Filter.useFilteredData)(rawData, filter, shouldFilter.current ? accessors.text(currentValue) : void 0, accessors.text);
195 const selectedItem = (0, React.useMemo)(() => data[accessors.indexOf(data, currentValue)], [data, currentValue, accessors]);
196 const list = (0, _FocusListContext.useFocusList)({
197 activeId,
198 scope: ref,
199 focusFirstItem,
200 anchorItem: currentOpen ? selectedItem : undefined
201 });
202 const [focusEvents, focused] = (0, _useFocusManager.default)(ref, {
203 disabled: isDisabled,
204 onBlur,
205 onFocus
206 }, {
207 didHandle(focused) {
208 if (!focused) {
209 shouldFilter.current = false;
210 toggle.close();
211 setSuggestion(null);
212 list.focus(undefined);
213 } else {
214 focus({
215 preventScroll: true
216 });
217 }
218 }
219
220 });
221 (0, _A11y.useActiveDescendant)(ref, activeId, currentOpen, [list.getFocused()]);
222 /**
223 * Handlers
224 */
225
226 const handleClick = e => {
227 if (readOnly || isDisabled) return; // prevents double clicks when in a <label>
228
229 e.preventDefault();
230 focus();
231 toggle();
232 };
233
234 const handleSelect = (data, originalEvent) => {
235 toggle.close();
236 shouldFilter.current = false;
237 setSuggestion(null);
238 (0, _WidgetHelpers.notify)(onSelect, [data, {
239 originalEvent
240 }]);
241 change(data, originalEvent, true);
242 focus({
243 preventScroll: true
244 });
245 };
246
247 const handleInputKeyDown = ({
248 key
249 }) => {
250 if (key === 'Backspace' || key === 'Delete') {
251 list.focus(null);
252 }
253 };
254
255 const handleInputChange = event => {
256 let idx = autoSelectMatches ? indexOf(rawData, event.target.value.toLowerCase(), accessors.text) : -1;
257 shouldFilter.current = true;
258 setSuggestion(null);
259 const nextValue = idx === -1 ? event.target.value : rawData[idx];
260 change(nextValue, event);
261 if (!nextValue) toggle.close();else toggle.open();
262 };
263
264 const handleKeyDown = e => {
265 if (readOnly) return;
266 let {
267 key,
268 altKey,
269 shiftKey
270 } = e;
271 (0, _WidgetHelpers.notify)(onKeyDown, [e]);
272 if (e.defaultPrevented) return;
273
274 const select = item => item != null && handleSelect(item, e);
275
276 const setFocused = el => {
277 if (!el) return;
278 setSuggestion(list.toDataItem(el));
279 list.focus(el);
280 };
281
282 if (key === 'End' && currentOpen && !shiftKey) {
283 e.preventDefault();
284 setFocused(list.last());
285 } else if (key === 'Home' && currentOpen && !shiftKey) {
286 e.preventDefault();
287 setFocused(list.first());
288 } else if (key === 'Escape' && currentOpen) {
289 e.preventDefault();
290 setSuggestion(null);
291 toggle.close();
292 } else if (key === 'Enter' && currentOpen) {
293 e.preventDefault();
294 select(list.getFocused());
295 } else if (key === 'ArrowDown') {
296 e.preventDefault();
297
298 if (currentOpen) {
299 setFocused(list.next());
300 } else {
301 return toggle.open();
302 }
303 } else if (key === 'ArrowUp') {
304 e.preventDefault();
305 if (altKey) return toggle.close();
306
307 if (currentOpen) {
308 setFocused(list.prev());
309 }
310 }
311 };
312 /**
313 * Methods
314 */
315
316
317 function focus(opts) {
318 if (inputRef.current) inputRef.current.focus(opts);
319 }
320
321 function change(nextValue, originalEvent, selected = false) {
322 handleChange(nextValue, {
323 lastValue: currentValue,
324 originalEvent,
325 source: selected ? 'listbox' : 'input'
326 });
327 }
328 /**
329 * Rendering
330 */
331
332
333 (0, React.useImperativeHandle)(outerRef, () => ({
334 focus
335 }));
336 let shouldRenderPopup = (0, _WidgetHelpers.useFirstFocusedRender)(focused, currentOpen);
337 let valueItem = accessors.findOrSelf(data, currentValue);
338 let inputValue = accessors.text(suggestion || valueItem);
339 let completeType = filter ? 'list' : 'none';
340 let popupOpen = currentOpen && (!hideEmptyPopup || !!data.length);
341 let inputReadOnly = // @ts-ignore
342 (inputProps == null ? void 0 : inputProps.readOnly) != null ? inputProps == null ? void 0 : inputProps.readOnly : readOnly;
343 let inputAddon = false;
344
345 if (!hideCaret) {
346 inputAddon = /*#__PURE__*/React.createElement(_InputAddon.default, {
347 busy: busy,
348 icon: selectIcon,
349 spinner: busySpinner,
350 onClick: handleClick,
351 disabled: !!isDisabled || isReadOnly // FIXME
352 ,
353 label: messages.openCombobox()
354 });
355 } else if (busy) {
356 inputAddon = /*#__PURE__*/React.createElement("span", {
357 "aria-hidden": "true",
358 className: "rw-btn rw-picker-caret"
359 }, busySpinner || _Icon.Spinner);
360 }
361
362 return /*#__PURE__*/React.createElement(_Widget.default, _extends({}, elementProps, {
363 ref: ref,
364 open: currentOpen,
365 dropUp: dropUp,
366 focused: focused,
367 disabled: isDisabled,
368 readOnly: isReadOnly
369 }, focusEvents, {
370 onKeyDown: handleKeyDown,
371 className: (0, _classnames.default)(className, 'rw-combobox')
372 }), /*#__PURE__*/React.createElement(_WidgetPicker.default, {
373 className: (0, _classnames.default)(containerClassName, hideCaret && 'rw-widget-input', hideCaret && !busy && 'rw-hide-caret')
374 }, /*#__PURE__*/React.createElement(_Input.default, _extends({}, inputProps, {
375 role: "combobox",
376 name: name,
377 id: inputId,
378 className: (0, _classnames.default)( // @ts-ignore
379 inputProps && inputProps.className, 'rw-combobox-input', !hideCaret && 'rw-widget-input'),
380 autoFocus: autoFocus,
381 tabIndex: tabIndex,
382 disabled: isDisabled,
383 readOnly: inputReadOnly,
384 "aria-busy": !!busy,
385 "aria-owns": listId,
386 "aria-autocomplete": completeType,
387 "aria-expanded": currentOpen,
388 "aria-haspopup": true,
389 placeholder: placeholder,
390 value: inputValue,
391 onChange: handleInputChange,
392 onKeyDown: handleInputKeyDown,
393 ref: inputRef
394 })), inputAddon), /*#__PURE__*/React.createElement(_FocusListContext.FocusListContext.Provider, {
395 value: list.context
396 }, shouldRenderPopup && /*#__PURE__*/React.createElement(Popup, {
397 dropUp: dropUp,
398 open: popupOpen,
399 transition: popupTransition,
400 onEntering: () => listRef.current.scrollIntoView()
401 }, /*#__PURE__*/React.createElement(ListComponent, _extends({}, listProps, {
402 id: listId,
403 tabIndex: -1,
404 data: data,
405 groupBy: groupBy,
406 disabled: disabled,
407 accessors: accessors,
408 renderItem: renderListItem,
409 renderGroup: renderListGroup,
410 optionComponent: optionComponent,
411 value: selectedItem,
412 searchTerm: valueItem && accessors.text(valueItem) || '',
413 "aria-hidden": !popupOpen,
414 "aria-labelledby": inputId,
415 "aria-live": popupOpen ? 'polite' : void 0,
416 onChange: (d, meta) => handleSelect(d, meta.originalEvent),
417 ref: listRef,
418 messages: {
419 emptyList: rawData.length ? messages.emptyFilter : messages.emptyList
420 }
421 })))));
422});
423ComboboxImpl.displayName = 'Combobox';
424ComboboxImpl.propTypes = propTypes;
425var _default = ComboboxImpl;
426exports.default = _default;
\No newline at end of file