UNPKG

baseui

Version:

A React Component library implementing the Base design language

308 lines (298 loc) • 11.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var React = _interopRequireWildcard(require("react")); var _input = require("../input"); var _utils = require("../menu/utils"); var _overrides = require("../helpers/overrides"); var _popover = require("../popover"); var _reactUid = require("react-uid"); var _styledComponents = require("./styled-components"); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } function _extends() { _extends = Object.assign ? Object.assign.bind() : 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); } /* Copyright (c) Uber Technologies, Inc. This source code is licensed under the MIT license found in the LICENSE file in the root directory of this source tree. */ const ENTER = 13; const ESCAPE = 27; const ARROW_UP = 38; const ARROW_DOWN = 40; // aria 1.1 spec: https://www.w3.org/TR/wai-aria-practices/#combobox // aria 1.2 spec: https://www.w3.org/TR/wai-aria-practices-1.2/#combobox function Combobox(props) { const { autocomplete = true, clearable = false, disabled = false, error = false, onBlur, onChange, onFocus, onSubmit, listBoxLabel, mapOptionToNode, mapOptionToString, id, name, options, overrides = {}, positive = false, inputRef: forwardInputRef, size = _input.SIZE.default, value } = props; const [selectionIndex, setSelectionIndex] = React.useState(-1); const [tempValue, setTempValue] = React.useState(value); const [isOpen, setIsOpen] = React.useState(false); const rootRef = React.useRef(null); const defaultInputRef = React.useRef(null); const inputRef = forwardInputRef || defaultInputRef; const listboxRef = React.useRef(null); const selectedOptionRef = React.useRef(null); const seed = (0, _reactUid.useUIDSeed)(); const activeDescendantId = seed('descendant'); const listboxId = seed('listbox'); // Handles case where an application wants to update the value in the input element // from outside of the combobox component. React.useEffect(() => { setTempValue(''); }, [value]); // Changing the 'selected' option temporarily updates the visible text string // in the input element until the user clicks an option or presses enter. React.useEffect(() => { // If no option selected, display the most recently user-edited string. if (selectionIndex === -1) { setTempValue(value); } else if (selectionIndex > options.length) { // Handles the case where option length is variable. After user clicks an // option and selection index is not in option bounds, reset it to default. setSelectionIndex(-1); } else { if (autocomplete) { let selectedOption = options[selectionIndex]; if (selectedOption) { setTempValue(mapOptionToString(selectedOption)); } } } }, [options, selectionIndex]); React.useEffect(() => { if (isOpen && selectedOptionRef.current && listboxRef.current) { (0, _utils.scrollItemIntoView)(selectedOptionRef.current, listboxRef.current, selectionIndex === 0, selectionIndex === options.length - 1); } }, [isOpen, selectedOptionRef.current, listboxRef.current]); const listboxWidth = React.useMemo(() => { if (rootRef.current) { // @ts-ignore return `${rootRef.current.clientWidth}px`; } return null; }, [rootRef.current]); function handleOpen() { if (!disabled) { setIsOpen(true); } } // @ts-ignore function handleKeyDown(event) { if (event.keyCode === ARROW_DOWN) { event.preventDefault(); handleOpen(); setSelectionIndex(prev => { let next = prev + 1; if (next > options.length - 1) { next = -1; } return next; }); } if (event.keyCode === ARROW_UP) { event.preventDefault(); setSelectionIndex(prev => { let next = prev - 1; if (next < -1) { next = options.length - 1; } return next; }); } if (event.keyCode === ENTER) { let clickedOption = options[selectionIndex]; if (clickedOption) { event.preventDefault(); setIsOpen(false); setSelectionIndex(-1); onChange(mapOptionToString(clickedOption), clickedOption); } else { if (onSubmit) { onSubmit({ closeListbox: () => setIsOpen(false), value }); } } } if (event.keyCode === ESCAPE) { // NOTE(chase): aria 1.2 spec outlines a pattern where when escape is // pressed, it closes the listbox and further presses will clear value. // Google search and some other examples I've seen do not implement this, // but something to consider when the 1.2 spec becomes more widespread. setIsOpen(false); setSelectionIndex(-1); setTempValue(value); } } // @ts-ignore function handleFocus(event) { if (!isOpen && options.length) { handleOpen(); } if (onFocus) onFocus(event); } // @ts-ignore function handleBlur(event) { if (listboxRef.current && event.relatedTarget && // NOTE(chase): Contains method expects a Node type, but relatedTarget is // EventTarget which is a super type of Node. Passing an EventTarget seems // to work fine, assuming the flow type is too strict. // eslint-disable-next-line @typescript-eslint/no-explicit-any // @ts-ignore listboxRef.current.contains(event.relatedTarget)) { return; } setIsOpen(false); setSelectionIndex(-1); setTempValue(value); if (onBlur) onBlur(event); } function handleInputClick(event) { if (inputRef.current) { // @ts-ignore inputRef.current.focus(); } if (!isOpen && options.length) { handleOpen(); } } // @ts-ignore function handleInputChange(event) { handleOpen(); setSelectionIndex(-1); onChange(event.target.value, null); setTempValue(event.target.value); } // @ts-ignore function handleOptionClick(index) { let clickedOption = options[index]; if (clickedOption) { const stringified = mapOptionToString(clickedOption); setIsOpen(false); setSelectionIndex(index); onChange(stringified, clickedOption); setTempValue(stringified); if (inputRef.current) { // @ts-ignore inputRef.current.focus(); } } } const [Root, rootProps] = (0, _overrides.getOverrides)(overrides.Root, _styledComponents.StyledRoot); const [InputContainer, inputContainerProps] = (0, _overrides.getOverrides)(overrides.InputContainer, _styledComponents.StyledInputContainer); const [ListBox, listBoxProps] = (0, _overrides.getOverrides)(overrides.ListBox, _styledComponents.StyledListBox); const [ListItem, listItemProps] = (0, _overrides.getOverrides)(overrides.ListItem, _styledComponents.StyledListItem); const [OverriddenInput, { overrides: inputOverrides = {}, ...restInputProps }] = (0, _overrides.getOverrides)(overrides.Input, _input.Input); const [OverriddenPopover, { overrides: popoverOverrides = {}, ...restPopoverProps }] = (0, _overrides.getOverrides)(overrides.Popover, _popover.Popover); return ( /*#__PURE__*/ // eslint-disable-next-line @typescript-eslint/no-explicit-any React.createElement(Root, _extends({ ref: rootRef }, rootProps), /*#__PURE__*/React.createElement(OverriddenPopover // React-focus-lock used in Popover used to skip non-tabbable elements (`tabIndex=-1`) elements for focus, we had ListBox with tabIndex to disable focus on // the ListBox, but we can just disable autoFocus (as ListBox / ListItem should not be focusable) (and input is also not autoFocused). // Select Component does the same thing , _extends({ autoFocus: false, isOpen: isOpen, overrides: popoverOverrides, placement: _popover.PLACEMENT.bottomLeft, onClick: handleInputClick, content: /*#__PURE__*/React.createElement(ListBox // TabIndex attribute exists to exclude option clicks from triggering onBlur event actions. , _extends({ tabIndex: "-1", id: listboxId // eslint-disable-next-line @typescript-eslint/no-explicit-any , ref: listboxRef, role: "listbox", "aria-label": listBoxLabel, $width: listboxWidth }, listBoxProps), options.map((option, index) => { const isSelected = selectionIndex === index; const ReplacementNode = mapOptionToNode; return ( /*#__PURE__*/ // List items are not focusable, therefore will never trigger a key event from it. // Secondly, key events are handled from the input element. // eslint-disable-next-line jsx-a11y/click-events-have-key-events React.createElement(ListItem, _extends({ "aria-selected": isSelected, id: isSelected ? activeDescendantId : null, key: index, onClick: () => handleOptionClick(index) // eslint-disable-next-line @typescript-eslint/no-explicit-any , ref: isSelected ? selectedOptionRef : null, role: "option", $isSelected: isSelected, $size: size }, listItemProps), ReplacementNode ? /*#__PURE__*/React.createElement(ReplacementNode, { isSelected: isSelected, option: option }) : mapOptionToString(option)) ); })) }, restPopoverProps), /*#__PURE__*/React.createElement(InputContainer, _extends({ "aria-expanded": isOpen, "aria-haspopup": "listbox", "aria-owns": listboxId // a11y linter implements the older 1.0 spec, suppressing to use updated 1.1 // https://github.com/A11yance/aria-query/issues/43 // https://github.com/evcohen/eslint-plugin-jsx-a11y/issues/442 // eslint-disable-next-line jsx-a11y/role-has-required-aria-props , role: "combobox" }, inputContainerProps), /*#__PURE__*/React.createElement(OverriddenInput, _extends({ inputRef: inputRef, "aria-activedescendant": isOpen && selectionIndex >= 0 ? activeDescendantId : undefined, "aria-autocomplete": "list", clearable: clearable, disabled: disabled, error: error, name: name, id: id, onBlur: handleBlur, onChange: handleInputChange, onFocus: handleFocus, onKeyDown: handleKeyDown, overrides: inputOverrides, positive: positive, size: size, value: tempValue ? tempValue : value }, isOpen ? { 'aria-controls': listboxId } : {}, restInputProps))))) ); } var _default = exports.default = Combobox;