baseui
Version:
A React Component library implementing the Base design language
308 lines (298 loc) • 11.7 kB
JavaScript
;
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;