UNPKG

15 kBJavaScriptView Raw
1"use strict";
2
3function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
4
5Object.defineProperty(exports, "__esModule", {
6 value: true
7});
8exports.default = void 0;
9
10var React = _interopRequireWildcard(require("react"));
11
12var _index = require("../input/index.js");
13
14var _utils = require("../menu/utils.js");
15
16var _overrides = require("../helpers/overrides.js");
17
18var _index2 = require("../popover/index.js");
19
20var _getBuiId = _interopRequireDefault(require("../utils/get-bui-id.js"));
21
22var _styledComponents = require("./styled-components.js");
23
24function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
25
26function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; }
27
28function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (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; }
29
30function _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); }
31
32function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
33
34function _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; }
35
36function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
37
38function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }
39
40function _iterableToArrayLimit(arr, i) { if (!(Symbol.iterator in Object(arr) || Object.prototype.toString.call(arr) === "[object Arguments]")) { return; } var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
41
42function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
43
44var ENTER = 13;
45var ESCAPE = 27;
46var ARROW_UP = 38;
47var ARROW_DOWN = 40; // aria 1.1 spec: https://www.w3.org/TR/wai-aria-practices/#combobox
48// aria 1.2 spec: https://www.w3.org/TR/wai-aria-practices-1.2/#combobox
49
50function Combobox(props) {
51 var _props$autocomplete = props.autocomplete,
52 autocomplete = _props$autocomplete === void 0 ? true : _props$autocomplete,
53 _props$disabled = props.disabled,
54 disabled = _props$disabled === void 0 ? false : _props$disabled,
55 _props$error = props.error,
56 error = _props$error === void 0 ? false : _props$error,
57 onBlur = props.onBlur,
58 onChange = props.onChange,
59 onFocus = props.onFocus,
60 onSubmit = props.onSubmit,
61 mapOptionToNode = props.mapOptionToNode,
62 mapOptionToString = props.mapOptionToString,
63 id = props.id,
64 name = props.name,
65 options = props.options,
66 _props$overrides = props.overrides,
67 overrides = _props$overrides === void 0 ? {} : _props$overrides,
68 _props$positive = props.positive,
69 positive = _props$positive === void 0 ? false : _props$positive,
70 _props$size = props.size,
71 size = _props$size === void 0 ? _index.SIZE.default : _props$size,
72 value = props.value;
73
74 var _React$useState = React.useState(-1),
75 _React$useState2 = _slicedToArray(_React$useState, 2),
76 selectionIndex = _React$useState2[0],
77 setSelectionIndex = _React$useState2[1];
78
79 var _React$useState3 = React.useState(value),
80 _React$useState4 = _slicedToArray(_React$useState3, 2),
81 tempValue = _React$useState4[0],
82 setTempValue = _React$useState4[1];
83
84 var _React$useState5 = React.useState(false),
85 _React$useState6 = _slicedToArray(_React$useState5, 2),
86 isOpen = _React$useState6[0],
87 setIsOpen = _React$useState6[1];
88
89 var rootRef = React.useRef(null);
90 var inputRef = React.useRef(null);
91 var listboxRef = React.useRef(null);
92 var selectedOptionRef = React.useRef(null);
93 var activeDescendantId = React.useMemo(function () {
94 return (0, _getBuiId.default)();
95 }, []);
96 var listboxId = React.useMemo(function () {
97 return (0, _getBuiId.default)();
98 }, []); // Handles case where an application wants to update the value in the input element
99 // from outside of the combobox component.
100
101 React.useEffect(function () {
102 setTempValue('');
103 }, [value]); // Changing the 'selected' option temporarily updates the visible text string
104 // in the input element until the user clicks an option or presses enter.
105
106 React.useEffect(function () {
107 // If no option selected, display the most recently user-edited string.
108 if (selectionIndex === -1) {
109 setTempValue(value);
110 } else if (selectionIndex > options.length) {
111 // Handles the case where option length is variable. After user clicks an
112 // option and selection index is not in option bounds, reset it to default.
113 setSelectionIndex(-1);
114 } else {
115 if (autocomplete) {
116 var selectedOption = options[selectionIndex];
117
118 if (selectedOption) {
119 setTempValue(mapOptionToString(selectedOption));
120 }
121 }
122 }
123 }, [options, selectionIndex]);
124 React.useEffect(function () {
125 if (isOpen && selectedOptionRef.current && listboxRef.current) {
126 (0, _utils.scrollItemIntoView)(selectedOptionRef.current, listboxRef.current, selectionIndex === 0, selectionIndex === options.length - 1);
127 }
128 }, [isOpen, selectedOptionRef.current, listboxRef.current]);
129 var listboxWidth = React.useMemo(function () {
130 if (rootRef.current) {
131 return "".concat(rootRef.current.clientWidth, "px");
132 }
133
134 return null;
135 }, [rootRef.current]);
136
137 function handleOpen() {
138 if (!disabled) {
139 setIsOpen(true);
140 }
141 }
142
143 function handleKeyDown(event) {
144 if (event.keyCode === ARROW_DOWN) {
145 event.preventDefault();
146 handleOpen();
147 setSelectionIndex(function (prev) {
148 var next = prev + 1;
149
150 if (next > options.length - 1) {
151 next = -1;
152 }
153
154 return next;
155 });
156 }
157
158 if (event.keyCode === ARROW_UP) {
159 event.preventDefault();
160 setSelectionIndex(function (prev) {
161 var next = prev - 1;
162
163 if (next < -1) {
164 next = options.length - 1;
165 }
166
167 return next;
168 });
169 }
170
171 if (event.keyCode === ENTER) {
172 var clickedOption = options[selectionIndex];
173
174 if (clickedOption) {
175 event.preventDefault();
176 setIsOpen(false);
177 setSelectionIndex(-1);
178 onChange(mapOptionToString(clickedOption), clickedOption);
179 } else {
180 if (onSubmit) {
181 onSubmit({
182 closeListbox: function closeListbox() {
183 return setIsOpen(false);
184 },
185 value: value
186 });
187 }
188 }
189 }
190
191 if (event.keyCode === ESCAPE) {
192 // NOTE(chase): aria 1.2 spec outlines a pattern where when escape is
193 // pressed, it closes the listbox and further presses will clear value.
194 // Google search and some other examples I've seen do not implement this,
195 // but something to consider when the 1.2 spec becomes more widespread.
196 setIsOpen(false);
197 setSelectionIndex(-1);
198 setTempValue(value);
199 }
200 }
201
202 function handleFocus(event) {
203 if (!isOpen && options.length) {
204 handleOpen();
205 }
206
207 if (onFocus) onFocus(event);
208 }
209
210 function handleBlur(event) {
211 if (listboxRef.current && event.relatedTarget && // NOTE(chase): Contains method expects a Node type, but relatedTarget is
212 // EventTarget which is a super type of Node. Passing an EventTarget seems
213 // to work fine, assuming the flow type is too strict.
214 // eslint-disable-next-line flowtype/no-weak-types
215 listboxRef.current.contains(event.relatedTarget)) {
216 return;
217 }
218
219 setIsOpen(false);
220 setSelectionIndex(-1);
221 setTempValue(value);
222 if (onBlur) onBlur(event);
223 }
224
225 function handleInputChange(event) {
226 handleOpen();
227 setSelectionIndex(-1);
228 onChange(event.target.value, null);
229 setTempValue(event.target.value);
230 }
231
232 function handleOptionClick(index) {
233 var clickedOption = options[index];
234
235 if (clickedOption) {
236 var stringified = mapOptionToString(clickedOption);
237 setIsOpen(false);
238 setSelectionIndex(index);
239 onChange(stringified, clickedOption);
240 setTempValue(stringified);
241
242 if (inputRef.current) {
243 inputRef.current.focus();
244 }
245 }
246 }
247
248 var _getOverrides = (0, _overrides.getOverrides)(overrides.Root, _styledComponents.StyledRoot),
249 _getOverrides2 = _slicedToArray(_getOverrides, 2),
250 Root = _getOverrides2[0],
251 rootProps = _getOverrides2[1];
252
253 var _getOverrides3 = (0, _overrides.getOverrides)(overrides.InputContainer, _styledComponents.StyledInputContainer),
254 _getOverrides4 = _slicedToArray(_getOverrides3, 2),
255 InputContainer = _getOverrides4[0],
256 inputContainerProps = _getOverrides4[1];
257
258 var _getOverrides5 = (0, _overrides.getOverrides)(overrides.ListBox, _styledComponents.StyledListBox),
259 _getOverrides6 = _slicedToArray(_getOverrides5, 2),
260 ListBox = _getOverrides6[0],
261 listBoxProps = _getOverrides6[1];
262
263 var _getOverrides7 = (0, _overrides.getOverrides)(overrides.ListItem, _styledComponents.StyledListItem),
264 _getOverrides8 = _slicedToArray(_getOverrides7, 2),
265 ListItem = _getOverrides8[0],
266 listItemProps = _getOverrides8[1];
267
268 var _getOverrides9 = (0, _overrides.getOverrides)(overrides.Input, _index.Input),
269 _getOverrides10 = _slicedToArray(_getOverrides9, 2),
270 OverriddenInput = _getOverrides10[0],
271 _getOverrides10$ = _getOverrides10[1],
272 _getOverrides10$$over = _getOverrides10$.overrides,
273 inputOverrides = _getOverrides10$$over === void 0 ? {} : _getOverrides10$$over,
274 restInputProps = _objectWithoutProperties(_getOverrides10$, ["overrides"]);
275
276 var _getOverrides11 = (0, _overrides.getOverrides)(overrides.Popover, _index2.Popover),
277 _getOverrides12 = _slicedToArray(_getOverrides11, 2),
278 OverriddenPopover = _getOverrides12[0],
279 _getOverrides12$ = _getOverrides12[1],
280 _getOverrides12$$over = _getOverrides12$.overrides,
281 popoverOverrides = _getOverrides12$$over === void 0 ? {} : _getOverrides12$$over,
282 restPopoverProps = _objectWithoutProperties(_getOverrides12$, ["overrides"]);
283
284 return React.createElement(Root // eslint-disable-next-line flowtype/no-weak-types
285 , _extends({
286 ref: rootRef
287 }, rootProps), React.createElement(OverriddenPopover, _extends({
288 isOpen: isOpen,
289 overrides: popoverOverrides,
290 placement: _index2.PLACEMENT.bottomLeft,
291 content: React.createElement(ListBox // TabIndex attribute exists to exclude option clicks from triggering onBlur event actions.
292 , _extends({
293 tabIndex: "-1",
294 id: listboxId // eslint-disable-next-line flowtype/no-weak-types
295 ,
296 ref: listboxRef,
297 role: "listbox",
298 $width: listboxWidth
299 }, listBoxProps), options.map(function (option, index) {
300 var isSelected = selectionIndex === index;
301 var ReplacementNode = mapOptionToNode;
302 return (// List items are not focusable, therefore will never trigger a key event from it.
303 // Secondly, key events are handled from the input element.
304 // eslint-disable-next-line jsx-a11y/click-events-have-key-events
305 React.createElement(ListItem, _extends({
306 "aria-selected": isSelected,
307 id: isSelected ? activeDescendantId : null,
308 key: index,
309 onClick: function onClick() {
310 return handleOptionClick(index);
311 } // eslint-disable-next-line flowtype/no-weak-types
312 ,
313 ref: isSelected ? selectedOptionRef : null,
314 role: "option",
315 $isSelected: isSelected,
316 $size: size
317 }, listItemProps), ReplacementNode ? React.createElement(ReplacementNode, {
318 isSelected: isSelected,
319 option: option
320 }) : mapOptionToString(option))
321 );
322 }))
323 }, restPopoverProps), React.createElement(InputContainer, _extends({
324 "aria-expanded": isOpen,
325 "aria-haspopup": "listbox",
326 "aria-owns": listboxId // a11y linter implements the older 1.0 spec, suppressing to use updated 1.1
327 // https://github.com/A11yance/aria-query/issues/43
328 // https://github.com/evcohen/eslint-plugin-jsx-a11y/issues/442
329 // eslint-disable-next-line jsx-a11y/role-has-required-aria-props
330 ,
331 role: "combobox"
332 }, inputContainerProps), React.createElement(OverriddenInput, _extends({
333 inputRef: inputRef,
334 "aria-activedescendant": selectionIndex >= 0 ? activeDescendantId : undefined,
335 "aria-autocomplete": "list",
336 "aria-controls": listboxId,
337 disabled: disabled,
338 error: error,
339 name: name,
340 id: id,
341 onBlur: handleBlur,
342 onChange: handleInputChange,
343 onFocus: handleFocus,
344 onKeyDown: handleKeyDown,
345 overrides: inputOverrides,
346 positive: positive,
347 size: size,
348 value: tempValue ? tempValue : value
349 }, restInputProps)))));
350}
351
352var _default = Combobox;
353exports.default = _default;
\No newline at end of file