UNPKG

react-widgets

Version:

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

520 lines (445 loc) 17.7 kB
"use strict"; exports.__esModule = true; exports.default = void 0; var _classnames = _interopRequireDefault(require("classnames")); var _propTypes = _interopRequireDefault(require("prop-types")); var _react = _interopRequireWildcard(require("react")); var _uncontrollable = require("uncontrollable"); var _useTimeout = _interopRequireDefault(require("@restart/hooks/useTimeout")); var _AddToListOption = _interopRequireWildcard(require("./AddToListOption")); var _DropdownListInput = _interopRequireDefault(require("./DropdownListInput")); var _Icon = require("./Icon"); var _List = _interopRequireDefault(require("./List")); var _FocusListContext = require("./FocusListContext"); var _Popup = _interopRequireDefault(require("./Popup")); var _Widget = _interopRequireDefault(require("./Widget")); var _WidgetPicker = _interopRequireDefault(require("./WidgetPicker")); var _messages = require("./messages"); var _A11y = require("./A11y"); var _Filter = require("./Filter"); var CustomPropTypes = _interopRequireWildcard(require("./PropTypes")); var _canShowCreate = _interopRequireDefault(require("./canShowCreate")); var _Accessors = require("./Accessors"); var _useAutoFocus = _interopRequireDefault(require("./useAutoFocus")); var _useDropdownToggle = _interopRequireDefault(require("./useDropdownToggle")); var _useFocusManager = _interopRequireDefault(require("./useFocusManager")); var _WidgetHelpers = require("./WidgetHelpers"); var _PickerCaret = _interopRequireDefault(require("./PickerCaret")); const _excluded = ["id", "autoFocus", "textField", "dataKey", "value", "defaultValue", "onChange", "open", "defaultOpen", "onToggle", "searchTerm", "defaultSearchTerm", "onSearch", "filter", "allowCreate", "delay", "focusFirstItem", "className", "containerClassName", "placeholder", "busy", "disabled", "readOnly", "selectIcon", "busySpinner", "dropUp", "tabIndex", "popupTransition", "name", "autoComplete", "onSelect", "onCreate", "onKeyPress", "onKeyDown", "onClick", "inputProps", "listProps", "renderListItem", "renderListGroup", "optionComponent", "renderValue", "groupBy", "onBlur", "onFocus", "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; } const propTypes = { value: _propTypes.default.any, /** * @type {function ( * dataItems: ?any, * metadata: { * lastValue: ?any, * searchTerm: ?string * originalEvent: SyntheticEvent, * } * ): void} */ onChange: _propTypes.default.func, open: _propTypes.default.bool, onToggle: _propTypes.default.func, data: _propTypes.default.array, dataKey: CustomPropTypes.accessor, textField: CustomPropTypes.accessor, allowCreate: _propTypes.default.oneOf([true, false, 'onFilter']), /** * A React render prop for customizing the rendering of the DropdownList * value */ renderValue: _propTypes.default.func, renderListItem: _propTypes.default.func, listComponent: CustomPropTypes.elementType, optionComponent: CustomPropTypes.elementType, renderPopup: _propTypes.default.func, renderListGroup: _propTypes.default.func, groupBy: CustomPropTypes.accessor, /** * * @type {(dataItem: ?any, metadata: { originalEvent: SyntheticEvent }) => void} */ onSelect: _propTypes.default.func, onCreate: _propTypes.default.func, /** * @type function(searchTerm: string, metadata: { action, lastSearchTerm, originalEvent? }) */ onSearch: _propTypes.default.func, searchTerm: _propTypes.default.string, busy: _propTypes.default.bool, /** Specify the element used to render the select (down arrow) icon. */ selectIcon: _propTypes.default.node, /** Specify the element used to render the busy indicator */ busySpinner: _propTypes.default.node, placeholder: _propTypes.default.string, dropUp: _propTypes.default.bool, popupTransition: CustomPropTypes.elementType, disabled: CustomPropTypes.disabled.acceptsArray, readOnly: CustomPropTypes.disabled, /** Adds a css class to the input container element. */ containerClassName: _propTypes.default.string, inputProps: _propTypes.default.object, listProps: _propTypes.default.object, messages: _propTypes.default.shape({ open: _propTypes.default.string, emptyList: CustomPropTypes.message, emptyFilter: CustomPropTypes.message, createOption: CustomPropTypes.message }) }; function useSearchWordBuilder(delay) { const timeout = (0, _useTimeout.default)(); const wordRef = (0, _react.useRef)(''); function search(character, cb) { let word = (wordRef.current + character).toLowerCase(); if (!character) return; wordRef.current = word; timeout.set(() => { wordRef.current = ''; cb(word); }, delay); } return search; } /** * A `<select>` replacement for single value lists. * @public */ const DropdownListImpl = /*#__PURE__*/_react.default.forwardRef(function DropdownList(_ref, outerRef) { let { id, autoFocus, textField, dataKey, value, defaultValue, onChange, open, defaultOpen = false, onToggle, searchTerm, defaultSearchTerm = '', onSearch, filter = true, allowCreate = false, delay = 500, focusFirstItem, className, containerClassName, placeholder, busy, disabled, readOnly, selectIcon = _Icon.caretDown, busySpinner, dropUp, tabIndex, popupTransition, name, autoComplete, onSelect, onCreate, onKeyPress, onKeyDown, onClick, inputProps, listProps, renderListItem, renderListGroup, optionComponent, renderValue, groupBy, onBlur, onFocus, listComponent: ListComponent = _List.default, popupComponent: Popup = _Popup.default, data: rawData = [], messages: userMessages } = _ref, elementProps = _objectWithoutPropertiesLoose(_ref, _excluded); const [currentValue, handleChange] = (0, _uncontrollable.useUncontrolledProp)(value, defaultValue, onChange); const [currentOpen, handleOpen] = (0, _uncontrollable.useUncontrolledProp)(open, defaultOpen, onToggle); const [currentSearch, handleSearch] = (0, _uncontrollable.useUncontrolledProp)(searchTerm, defaultSearchTerm, onSearch); const ref = (0, _react.useRef)(null); const filterRef = (0, _react.useRef)(null); const listRef = (0, _react.useRef)(null); 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); (0, _useAutoFocus.default)(!!autoFocus, ref); const toggle = (0, _useDropdownToggle.default)(currentOpen, handleOpen); const isDisabled = disabled === true; // const disabledItems = toItemArray(disabled) const isReadOnly = !!readOnly; const [focusEvents, focused] = (0, _useFocusManager.default)(ref, { disabled: isDisabled, onBlur, onFocus }, { didHandle(focused) { if (focused) { if (filter) focus(); return; } toggle.close(); clearSearch(); } }); const data = (0, _Filter.useFilteredData)(rawData, currentOpen ? filter : false, currentSearch, 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 [autofilling, setAutofilling] = (0, _react.useState)(false); const nextSearchChar = useSearchWordBuilder(delay); const focusedItem = list.getFocused(); (0, _A11y.useActiveDescendant)(ref, activeId, focusedItem && currentOpen, [focusedItem]); const showCreateOption = (0, _canShowCreate.default)(allowCreate, { searchTerm: currentSearch, data, accessors }); const handleCreate = event => { (0, _WidgetHelpers.notify)(onCreate, [currentSearch]); clearSearch(event); toggle.close(); focus(); }; const handleSelect = (dataItem, originalEvent) => { if (readOnly || isDisabled) return; if (dataItem === undefined) return; originalEvent == null ? void 0 : originalEvent.preventDefault(); if (dataItem === _AddToListOption.CREATE_OPTION) { handleCreate(originalEvent); return; } (0, _WidgetHelpers.notify)(onSelect, [dataItem, { originalEvent }]); change(dataItem, originalEvent, true); toggle.close(); focus(); }; const handleClick = e => { if (readOnly || isDisabled) return; // prevents double clicks when in a <label> e.preventDefault(); focus(); toggle(); (0, _WidgetHelpers.notify)(onClick, [e]); }; const handleKeyDown = e => { if (readOnly || isDisabled) return; let { key, altKey, ctrlKey, shiftKey } = e; (0, _WidgetHelpers.notify)(onKeyDown, [e]); let closeWithFocus = () => { clearSearch(); toggle.close(); if (currentOpen) setTimeout(focus); }; if (e.defaultPrevented) return; if (key === 'End' && currentOpen && !shiftKey) { e.preventDefault(); list.focus(list.last()); } else if (key === 'Home' && currentOpen && !shiftKey) { e.preventDefault(); list.focus(list.first()); } else if (key === 'Escape' && (currentOpen || currentSearch)) { e.preventDefault(); closeWithFocus(); } else if (key === 'Enter' && currentOpen && ctrlKey && showCreateOption) { e.preventDefault(); handleCreate(e); } else if ((key === 'Enter' || key === ' ' && !filter) && currentOpen) { e.preventDefault(); if (list.hasFocused()) handleSelect(list.getFocused(), e); } else if (key === 'ArrowDown') { e.preventDefault(); if (!currentOpen) { toggle.open(); return; } list.focus(list.next()); } else if (key === 'ArrowUp') { e.preventDefault(); if (altKey) return closeWithFocus(); list.focus(list.prev()); } }; const handleKeyPress = e => { if (readOnly || isDisabled) return; (0, _WidgetHelpers.notify)(onKeyPress, [e]); if (e.defaultPrevented || filter) return; nextSearchChar(String.fromCharCode(e.which), word => { if (!currentOpen) return; let isValid = item => _Filter.presets.startsWith(accessors.text(item).toLowerCase(), word.toLowerCase()); const [items, focusedItem] = list.get(); const len = items.length; const startIdx = items.indexOf(focusedItem) + 1; const offset = startIdx >= len ? 0 : startIdx; let idx = 0; let pointer = offset; while (idx < len) { pointer = (idx + offset) % len; let item = items[pointer]; if (isValid(list.toDataItem(item))) break; idx++; } if (idx === len) return; list.focus(items[pointer]); }); }; const handleInputChange = e => { // hitting space to open if (!currentOpen && !e.target.value.trim()) { e.preventDefault(); } else { search(e.target.value, e, 'input'); } toggle.open(); }; const handleAutofillChange = e => { let filledValue = e.target.value.toLowerCase(); if (filledValue === '') return void change(null); for (const item of rawData) { if (String(accessors.value(item)).toLowerCase() === filledValue || accessors.text(item).toLowerCase() === filledValue) { change(item, e); break; } } }; function change(nextValue, originalEvent, selected = false) { if (!accessors.matches(nextValue, currentValue)) { (0, _WidgetHelpers.notify)(handleChange, [nextValue, { originalEvent, source: selected ? 'listbox' : 'input', lastValue: currentValue, searchTerm: currentSearch }]); clearSearch(originalEvent); toggle.close(); } } function focus() { if (filter) filterRef.current.focus();else ref.current.focus(); } function clearSearch(originalEvent) { search('', originalEvent, 'clear'); } function search(nextSearchTerm, originalEvent, action = 'input') { if (currentSearch !== nextSearchTerm) handleSearch(nextSearchTerm, { action, originalEvent, lastSearchTerm: currentSearch }); } /** * Render */ (0, _react.useImperativeHandle)(outerRef, () => ({ focus })); let valueItem = accessors.findOrSelf(data, currentValue); let shouldRenderPopup = (0, _WidgetHelpers.useFirstFocusedRender)(focused, currentOpen); const widgetProps = Object.assign({}, elementProps, { role: 'combobox', id: inputId, //tab index when there is no filter input to take focus tabIndex: filter ? -1 : tabIndex || 0, // FIXME: only when item exists 'aria-owns': listId, 'aria-expanded': !!currentOpen, 'aria-haspopup': true, 'aria-busy': !!busy, 'aria-live': currentOpen ? 'polite' : undefined, 'aria-autocomplete': 'list', 'aria-disabled': isDisabled, 'aria-readonly': isReadOnly }); return /*#__PURE__*/_react.default.createElement(_FocusListContext.FocusListContext.Provider, { value: list.context }, /*#__PURE__*/_react.default.createElement(_Widget.default, _extends({}, widgetProps, { open: !!currentOpen, dropUp: !!dropUp, focused: !!focused, disabled: isDisabled, readOnly: isReadOnly, autofilling: autofilling }, focusEvents, { onKeyDown: handleKeyDown, onKeyPress: handleKeyPress, className: (0, _classnames.default)(className, 'rw-dropdown-list'), ref: ref }), /*#__PURE__*/_react.default.createElement(_WidgetPicker.default, { onClick: handleClick, tabIndex: filter ? -1 : 0, className: (0, _classnames.default)(containerClassName, 'rw-widget-input') }, /*#__PURE__*/_react.default.createElement(_DropdownListInput.default, _extends({}, inputProps, { value: valueItem, dataKeyAccessor: accessors.value, textAccessor: accessors.text, name: name, readOnly: readOnly, disabled: isDisabled, allowSearch: !!filter, searchTerm: currentSearch, ref: filterRef, autoComplete: autoComplete, onSearch: handleInputChange, onAutofill: setAutofilling, onAutofillChange: handleAutofillChange, placeholder: placeholder, renderValue: renderValue })), /*#__PURE__*/_react.default.createElement(_PickerCaret.default, { visible: true, busy: busy, icon: selectIcon, spinner: busySpinner })), shouldRenderPopup && /*#__PURE__*/_react.default.createElement(Popup, { dropUp: dropUp, open: currentOpen, transition: popupTransition, onEntered: focus, onEntering: () => listRef.current.scrollIntoView() }, /*#__PURE__*/_react.default.createElement(ListComponent, _extends({}, listProps, { id: listId, data: data, tabIndex: -1, disabled: disabled, groupBy: groupBy, searchTerm: currentSearch, accessors: accessors, renderItem: renderListItem, renderGroup: renderListGroup, optionComponent: optionComponent, value: selectedItem, onChange: (d, meta) => handleSelect(d, meta.originalEvent), "aria-live": currentOpen ? 'polite' : undefined, "aria-labelledby": inputId, "aria-hidden": !currentOpen, ref: listRef, messages: { emptyList: rawData.length ? messages.emptyFilter : messages.emptyList } })), showCreateOption && /*#__PURE__*/_react.default.createElement(_AddToListOption.default, { onSelect: handleCreate }, messages.createOption(currentValue, currentSearch || ''))))); }); DropdownListImpl.displayName = 'DropdownList'; DropdownListImpl.propTypes = propTypes; var _default = DropdownListImpl; exports.default = _default;