UNPKG

react-widgets

Version:

An à la carte set of polished, extensible, and accessible inputs built for React

426 lines (367 loc) 14.7 kB
"use strict"; exports.__esModule = true; exports.default = void 0; var _classnames = _interopRequireDefault(require("classnames")); var PropTypes = _interopRequireWildcard(require("prop-types")); var React = _interopRequireWildcard(require("react")); var _uncontrollable = require("uncontrollable"); var _Icon = require("./Icon"); var _Input = _interopRequireDefault(require("./Input")); var _List = _interopRequireDefault(require("./List")); var _FocusListContext = require("./FocusListContext"); var _Popup = _interopRequireDefault(require("./Popup")); var _InputAddon = _interopRequireDefault(require("./InputAddon")); var _Widget = _interopRequireDefault(require("./Widget")); var _WidgetPicker = _interopRequireDefault(require("./WidgetPicker")); var _messages = require("./messages"); var _A11y = require("./A11y"); var CustomPropTypes = _interopRequireWildcard(require("./PropTypes")); var _Accessors = require("./Accessors"); var _Filter = require("./Filter"); var _useDropdownToggle = _interopRequireDefault(require("./useDropdownToggle")); var _useFocusManager = _interopRequireDefault(require("./useFocusManager")); var _WidgetHelpers = require("./WidgetHelpers"); 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"]; function _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); } function _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; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 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); } 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; } function indexOf(data, searchTerm, text) { if (!searchTerm.trim()) return -1; for (let idx = 0; idx < data.length; idx++) if (text(data[idx]).toLowerCase() === searchTerm) return idx; return -1; } let propTypes = { value: PropTypes.any, onChange: PropTypes.func, open: PropTypes.bool, onToggle: PropTypes.func, renderListItem: PropTypes.func, listComponent: PropTypes.elementType, renderListGroup: PropTypes.func, groupBy: CustomPropTypes.accessor, data: PropTypes.array, dataKey: CustomPropTypes.accessor, textField: CustomPropTypes.accessor, name: PropTypes.string, /** Do not show the auto complete list when it returns no results. */ hideEmptyPopup: PropTypes.bool, /** Hide the combobox dropdown indicator. */ hideCaret: PropTypes.bool, /** * * @type {(dataItem: ?any, metadata: { originalEvent: SyntheticEvent }) => void} */ onSelect: PropTypes.func, autoFocus: PropTypes.bool, disabled: CustomPropTypes.disabled.acceptsArray, readOnly: CustomPropTypes.disabled, busy: PropTypes.bool, /** Specify the element used to render the select (down arrow) icon. */ selectIcon: PropTypes.node, /** Specify the element used to render the busy indicator */ busySpinner: PropTypes.node, dropUp: PropTypes.bool, popupTransition: PropTypes.elementType, placeholder: PropTypes.string, /** Adds a css class to the input container element. */ containerClassName: PropTypes.string, inputProps: PropTypes.object, listProps: PropTypes.object, messages: PropTypes.shape({ openCombobox: CustomPropTypes.message, emptyList: CustomPropTypes.message, emptyFilter: CustomPropTypes.message }) }; /** * --- * shortcuts: * - { key: alt + down arrow, label: open combobox } * - { key: alt + up arrow, label: close combobox } * - { key: down arrow, label: move focus to next item } * - { key: up arrow, label: move focus to previous item } * - { key: home, label: move focus to first item } * - { key: end, label: move focus to last item } * - { key: enter, label: select focused item } * - { key: any key, label: search list for item starting with key } * --- * * Select an item from the list, or input a custom value. The Combobox can also make suggestions as you type. * @public */ const ComboboxImpl = /*#__PURE__*/React.forwardRef(function Combobox(_ref, outerRef) { let { id, className, containerClassName, placeholder, autoFocus, textField, dataKey, autoSelectMatches, focusFirstItem = false, value, defaultValue = '', onChange, open, defaultOpen = false, onToggle, filter = true, busy, disabled, readOnly, selectIcon = _Icon.caretDown, hideCaret, hideEmptyPopup, busySpinner, dropUp, tabIndex, popupTransition, name, onSelect, onKeyDown, onBlur, onFocus, inputProps, listProps, groupBy, renderListItem, renderListGroup, optionComponent, listComponent: ListComponent = _List.default, popupComponent: Popup = _Popup.default, data: rawData = [], messages: userMessages } = _ref, elementProps = _objectWithoutPropertiesLoose(_ref, _excluded); let [currentValue, handleChange] = (0, _uncontrollable.useUncontrolledProp)(value, defaultValue, onChange); const [currentOpen, handleOpen] = (0, _uncontrollable.useUncontrolledProp)(open, defaultOpen, onToggle); const ref = (0, React.useRef)(null); const inputRef = (0, React.useRef)(null); const listRef = (0, React.useRef)(null); const [suggestion, setSuggestion] = (0, React.useState)(null); const shouldFilter = (0, React.useRef)(false); const inputId = (0, _WidgetHelpers.useInstanceId)(id, '_input'); const listId = (0, _WidgetHelpers.useInstanceId)(id, '_listbox'); const activeId = (0, _WidgetHelpers.useInstanceId)(id, '_listbox_active_option'); const accessors = (0, _Accessors.useAccessors)(textField, dataKey); const messages = (0, _messages.useMessagesWithDefaults)(userMessages); const toggle = (0, _useDropdownToggle.default)(currentOpen, handleOpen); const isDisabled = disabled === true; const isReadOnly = !!readOnly; const data = (0, _Filter.useFilteredData)(rawData, filter, shouldFilter.current ? accessors.text(currentValue) : void 0, accessors.text); const selectedItem = (0, React.useMemo)(() => data[accessors.indexOf(data, currentValue)], [data, currentValue, accessors]); const list = (0, _FocusListContext.useFocusList)({ activeId, scope: ref, focusFirstItem, anchorItem: currentOpen ? selectedItem : undefined }); const [focusEvents, focused] = (0, _useFocusManager.default)(ref, { disabled: isDisabled, onBlur, onFocus }, { didHandle(focused) { if (!focused) { shouldFilter.current = false; toggle.close(); setSuggestion(null); list.focus(undefined); } else { focus({ preventScroll: true }); } } }); (0, _A11y.useActiveDescendant)(ref, activeId, currentOpen, [list.getFocused()]); /** * Handlers */ const handleClick = e => { if (readOnly || isDisabled) return; // prevents double clicks when in a <label> e.preventDefault(); focus(); toggle(); }; const handleSelect = (data, originalEvent) => { toggle.close(); shouldFilter.current = false; setSuggestion(null); (0, _WidgetHelpers.notify)(onSelect, [data, { originalEvent }]); change(data, originalEvent, true); focus({ preventScroll: true }); }; const handleInputKeyDown = ({ key }) => { if (key === 'Backspace' || key === 'Delete') { list.focus(null); } }; const handleInputChange = event => { let idx = autoSelectMatches ? indexOf(rawData, event.target.value.toLowerCase(), accessors.text) : -1; shouldFilter.current = true; setSuggestion(null); const nextValue = idx === -1 ? event.target.value : rawData[idx]; change(nextValue, event); if (!nextValue) toggle.close();else toggle.open(); }; const handleKeyDown = e => { if (readOnly) return; let { key, altKey, shiftKey } = e; (0, _WidgetHelpers.notify)(onKeyDown, [e]); if (e.defaultPrevented) return; const select = item => item != null && handleSelect(item, e); const setFocused = el => { if (!el) return; setSuggestion(list.toDataItem(el)); list.focus(el); }; if (key === 'End' && currentOpen && !shiftKey) { e.preventDefault(); setFocused(list.last()); } else if (key === 'Home' && currentOpen && !shiftKey) { e.preventDefault(); setFocused(list.first()); } else if (key === 'Escape' && currentOpen) { e.preventDefault(); setSuggestion(null); toggle.close(); } else if (key === 'Enter' && currentOpen) { e.preventDefault(); select(list.getFocused()); } else if (key === 'ArrowDown') { e.preventDefault(); if (currentOpen) { setFocused(list.next()); } else { return toggle.open(); } } else if (key === 'ArrowUp') { e.preventDefault(); if (altKey) return toggle.close(); if (currentOpen) { setFocused(list.prev()); } } }; /** * Methods */ function focus(opts) { if (inputRef.current) inputRef.current.focus(opts); } function change(nextValue, originalEvent, selected = false) { handleChange(nextValue, { lastValue: currentValue, originalEvent, source: selected ? 'listbox' : 'input' }); } /** * Rendering */ (0, React.useImperativeHandle)(outerRef, () => ({ focus })); let shouldRenderPopup = (0, _WidgetHelpers.useFirstFocusedRender)(focused, currentOpen); let valueItem = accessors.findOrSelf(data, currentValue); let inputValue = accessors.text(suggestion || valueItem); let completeType = filter ? 'list' : 'none'; let popupOpen = currentOpen && (!hideEmptyPopup || !!data.length); let inputReadOnly = // @ts-ignore (inputProps == null ? void 0 : inputProps.readOnly) != null ? inputProps == null ? void 0 : inputProps.readOnly : readOnly; let inputAddon = false; if (!hideCaret) { inputAddon = /*#__PURE__*/React.createElement(_InputAddon.default, { busy: busy, icon: selectIcon, spinner: busySpinner, onClick: handleClick, disabled: !!isDisabled || isReadOnly // FIXME , label: messages.openCombobox() }); } else if (busy) { inputAddon = /*#__PURE__*/React.createElement("span", { "aria-hidden": "true", className: "rw-btn rw-picker-caret" }, busySpinner || _Icon.Spinner); } return /*#__PURE__*/React.createElement(_Widget.default, _extends({}, elementProps, { ref: ref, open: currentOpen, dropUp: dropUp, focused: focused, disabled: isDisabled, readOnly: isReadOnly }, focusEvents, { onKeyDown: handleKeyDown, className: (0, _classnames.default)(className, 'rw-combobox') }), /*#__PURE__*/React.createElement(_WidgetPicker.default, { className: (0, _classnames.default)(containerClassName, hideCaret && 'rw-widget-input', hideCaret && !busy && 'rw-hide-caret') }, /*#__PURE__*/React.createElement(_Input.default, _extends({}, inputProps, { role: "combobox", name: name, id: inputId, className: (0, _classnames.default)( // @ts-ignore inputProps && inputProps.className, 'rw-combobox-input', !hideCaret && 'rw-widget-input'), autoFocus: autoFocus, tabIndex: tabIndex, disabled: isDisabled, readOnly: inputReadOnly, "aria-busy": !!busy, "aria-owns": listId, "aria-autocomplete": completeType, "aria-expanded": currentOpen, "aria-haspopup": true, placeholder: placeholder, value: inputValue, onChange: handleInputChange, onKeyDown: handleInputKeyDown, ref: inputRef })), inputAddon), /*#__PURE__*/React.createElement(_FocusListContext.FocusListContext.Provider, { value: list.context }, shouldRenderPopup && /*#__PURE__*/React.createElement(Popup, { dropUp: dropUp, open: popupOpen, transition: popupTransition, onEntering: () => listRef.current.scrollIntoView() }, /*#__PURE__*/React.createElement(ListComponent, _extends({}, listProps, { id: listId, tabIndex: -1, data: data, groupBy: groupBy, disabled: disabled, accessors: accessors, renderItem: renderListItem, renderGroup: renderListGroup, optionComponent: optionComponent, value: selectedItem, searchTerm: valueItem && accessors.text(valueItem) || '', "aria-hidden": !popupOpen, "aria-labelledby": inputId, "aria-live": popupOpen ? 'polite' : void 0, onChange: (d, meta) => handleSelect(d, meta.originalEvent), ref: listRef, messages: { emptyList: rawData.length ? messages.emptyFilter : messages.emptyList } }))))); }); ComboboxImpl.displayName = 'Combobox'; ComboboxImpl.propTypes = propTypes; var _default = ComboboxImpl; exports.default = _default;