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