UNPKG

@fluentui/react-northstar

Version:
1,146 lines (1,132 loc) 64.1 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); exports.__esModule = true; exports.dropdownSlotClassNames = exports.dropdownClassName = exports.Dropdown = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose")); var _findIndex2 = _interopRequireDefault(require("lodash/findIndex")); var _isNil2 = _interopRequireDefault(require("lodash/isNil")); var _isEmpty2 = _interopRequireDefault(require("lodash/isEmpty")); var _isNumber2 = _interopRequireDefault(require("lodash/isNumber")); var _isPlainObject2 = _interopRequireDefault(require("lodash/isPlainObject")); var _invoke2 = _interopRequireDefault(require("lodash/invoke")); var _debounce2 = _interopRequireDefault(require("lodash/debounce")); var _uniqueId2 = _interopRequireDefault(require("lodash/uniqueId")); var _get2 = _interopRequireDefault(require("lodash/get")); var _isFunction2 = _interopRequireDefault(require("lodash/isFunction")); var _map2 = _interopRequireDefault(require("lodash/map")); var _differenceBy2 = _interopRequireDefault(require("lodash/differenceBy")); var _reactBindings = require("@fluentui/react-bindings"); var _reactComponentRef = require("@fluentui/react-component-ref"); var customPropTypes = _interopRequireWildcard(require("@fluentui/react-proptypes")); var _accessibility = require("@fluentui/accessibility"); var React = _interopRequireWildcard(require("react")); var PropTypes = _interopRequireWildcard(require("prop-types")); var _classnames = _interopRequireDefault(require("classnames")); var _computeScrollIntoView = _interopRequireDefault(require("compute-scroll-into-view")); var _downshift = _interopRequireDefault(require("downshift")); var _utils = require("../../utils"); var _List = require("../List/List"); var _DropdownItem = require("./DropdownItem"); var _DropdownSelectedItem = require("./DropdownSelectedItem"); var _DropdownSearchInput = require("./DropdownSearchInput"); var _Button = require("../Button/Button"); var _accessibilityStyles = require("../../utils/accessibility/Styles/accessibilityStyles"); var _Box = require("../Box/Box"); var _Portal = require("../Portal/Portal"); var _positioner = require("../../utils/positioner"); var _reactIconsNorthstar = require("@fluentui/react-icons-northstar"); var _excluded = ["onClick", "onFocus", "onBlur", "onKeyDown"], _excluded2 = ["innerRef"], _excluded3 = ["innerRef"]; function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(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; } var dropdownClassName = 'ui-dropdown'; exports.dropdownClassName = dropdownClassName; var dropdownSlotClassNames = { clearIndicator: dropdownClassName + "__clear-indicator", container: dropdownClassName + "__container", toggleIndicator: dropdownClassName + "__toggle-indicator", item: dropdownClassName + "__item", itemsCount: dropdownClassName + "__items-count", itemsList: dropdownClassName + "__items-list", searchInput: dropdownClassName + "__searchinput", selectedItem: dropdownClassName + "__selecteditem", selectedItems: dropdownClassName + "__selected-items", triggerButton: dropdownClassName + "__trigger-button" }; exports.dropdownSlotClassNames = dropdownSlotClassNames; var a11yStatusCleanupTime = 500; var charKeyPressedCleanupTime = 500; /** `normalizedValue` should be normalized always as it can be received from props */ function normalizeValue(multiple, rawValue) { var normalizedValue = Array.isArray(rawValue) ? rawValue : [rawValue]; if (multiple) { return normalizedValue; } if (normalizedValue[0] === '') { return []; } return normalizedValue.slice(0, 1); } /** * Used to compute the filtered items (by value and search query) and, if needed, * their string equivalents, in order to be used throughout the component. */ function getFilteredValues(options) { var items = options.items, itemToString = options.itemToString, itemToValue = options.itemToValue, multiple = options.multiple, search = options.search, searchQuery = options.searchQuery, value = options.value; var filteredItemsByValue = multiple ? (0, _differenceBy2.default)(items, value, itemToValue) : items; var filteredItemStrings = (0, _map2.default)(filteredItemsByValue, function (filteredItem) { return itemToString(filteredItem).toLowerCase(); }); if (search) { if ((0, _isFunction2.default)(search)) { return { filteredItems: search(filteredItemsByValue, searchQuery), filteredItemStrings: filteredItemStrings }; } return { filteredItems: filteredItemsByValue.filter(function (item) { return itemToString(item).toLowerCase().indexOf(searchQuery.toLowerCase()) !== -1; }), filteredItemStrings: filteredItemStrings }; } return { filteredItems: filteredItemsByValue, filteredItemStrings: filteredItemStrings }; } var isEmpty = function isEmpty(prop) { return typeof prop === 'object' && !prop.props && !(0, _get2.default)(prop, 'children') && !(0, _get2.default)(prop, 'content'); }; /** * A Dropdown allows user to select one or more values from a list of options. * Can be created with search and multi-selection capabilities. * * @accessibility * Implements [ARIA Combo Box](https://www.w3.org/TR/wai-aria-practices-1.1/#combobox) design pattern, uses aria-live to announce state changes. * @accessibilityIssues * [Issue 991203: VoiceOver doesn't narrate properly elements in the input/combobox](https://bugs.chromium.org/p/chromium/issues/detail?id=991203) * [JAWS - ESC (ESCAPE) not closing collapsible listbox (dropdown) on first time #528](https://github.com/FreedomScientific/VFO-standards-support/issues/528) */ var Dropdown = /*#__PURE__*/React.forwardRef(function (props, ref) { var _context$target3; var context = (0, _reactBindings.useFluentContext)(); var _useTelemetry = (0, _reactBindings.useTelemetry)(Dropdown.displayName, context.telemetry), setStart = _useTelemetry.setStart, setEnd = _useTelemetry.setEnd; setStart(); var ariaLabelledby = props['aria-labelledby'], ariaDescribedby = props['aria-describedby'], ariaInvalid = props['aria-invalid'], allowFreeform = props.allowFreeform, clearable = props.clearable, clearIndicator = props.clearIndicator, checkable = props.checkable, checkableIndicator = props.checkableIndicator, className = props.className, design = props.design, disabled = props.disabled, error = props.error, fluid = props.fluid, getA11ySelectionMessage = props.getA11ySelectionMessage, a11ySelectedItemsMessage = props.a11ySelectedItemsMessage, getA11yStatusMessage = props.getA11yStatusMessage, inline = props.inline, inverted = props.inverted, itemToString = props.itemToString, itemToValue = props.itemToValue, items = props.items, highlightFirstItemOnOpen = props.highlightFirstItemOnOpen, multiple = props.multiple, headerMessage = props.headerMessage, moveFocusOnTab = props.moveFocusOnTab, noResultsMessage = props.noResultsMessage, loading = props.loading, loadingMessage = props.loadingMessage, placeholder = props.placeholder, renderItem = props.renderItem, renderSelectedItem = props.renderSelectedItem, search = props.search, searchInput = props.searchInput, styles = props.styles, toggleIndicator = props.toggleIndicator, triggerButton = props.triggerButton, variables = props.variables; var align = props.align, flipBoundary = props.flipBoundary, overflowBoundary = props.overflowBoundary, position = props.position, positionFixed = props.positionFixed, offset = props.offset, unstable_disableTether = props.unstable_disableTether, unstable_pinned = props.unstable_pinned, autoSize = props.autoSize; // PositioningProps passed directly to Dropdown var _partitionPopperProps = (0, _positioner.partitionPopperPropsFromShorthand)(props.list), list = _partitionPopperProps[0], positioningProps = _partitionPopperProps[1]; // PositioningProps passed to Dropdown `list` prop's `popper` key var buttonRef = React.useRef(); var _inputRef = React.useRef(); var listRef = React.useRef(); var selectedItemsRef = React.useRef(); var containerRef = React.useRef(); var defaultTriggerButtonId = React.useMemo(function () { return (0, _uniqueId2.default)('dropdown-trigger-button-'); }, []); var selectedItemsCountNarrationId = React.useMemo(function () { return (0, _uniqueId2.default)('dropdown-selected-items-count-'); }, []); var ElementType = (0, _reactBindings.getElementType)(props); var unhandledProps = (0, _reactBindings.useUnhandledProps)(Dropdown.handledProps, props); var _useAutoControlled = (0, _reactBindings.useAutoControlled)({ defaultValue: props.defaultActiveSelectedIndex, initialValue: multiple ? null : undefined, value: props.activeSelectedIndex }), activeSelectedIndex = _useAutoControlled[0], setActiveSelectedIndex = _useAutoControlled[1]; var _useAutoControlled2 = (0, _reactBindings.useAutoControlled)({ defaultValue: props.defaultHighlightedIndex, initialValue: highlightFirstItemOnOpen ? 0 : null, value: props.highlightedIndex }), highlightedIndex = _useAutoControlled2[0], setHighlightedIndex = _useAutoControlled2[1]; var _useAutoControlled3 = (0, _reactBindings.useAutoControlled)({ defaultValue: props.defaultOpen, initialValue: false, value: props.open }), open = _useAutoControlled3[0], setOpen = _useAutoControlled3[1]; var _useAutoControlled4 = (0, _reactBindings.useAutoControlled)({ defaultValue: props.defaultSearchQuery, initialValue: search ? '' : undefined, value: props.searchQuery }), searchQuery = _useAutoControlled4[0], setSearchQuery = _useAutoControlled4[1]; var _useAutoControlled5 = (0, _reactBindings.useAutoControlled)({ defaultValue: props.defaultValue, initialValue: [], value: props.value }), rawValue = _useAutoControlled5[0], setValue = _useAutoControlled5[1]; var value = normalizeValue(multiple, rawValue); var _React$useState = React.useState(''), a11ySelectionStatus = _React$useState[0], setA11ySelectionStatus = _React$useState[1]; var _React$useState2 = React.useState(false), focused = _React$useState2[0], setFocused = _React$useState2[1]; var _React$useState3 = React.useState(false), isFromKeyboard = _React$useState3[0], setIsFromKeyboard = _React$useState3[1]; var _React$useState4 = React.useState(false), itemIsFromKeyboard = _React$useState4[0], setItemIsFromKeyboard = _React$useState4[1]; var _React$useState5 = React.useState(search ? undefined : ''), startingString = _React$useState5[0], setStartingString = _React$useState5[1]; // used for keeping track of the source of the input, as Downshift does not pass events to the handlers // for free form dropdown: // - if the value is changed based on search query change (from input), accept any value even if not in the list // - if the value is changed based on selection from list, use the value from the list item var inListbox = React.useRef(false); var _getFilteredValues = getFilteredValues({ itemToString: itemToString, itemToValue: itemToValue, items: items, multiple: multiple, search: search, searchQuery: searchQuery, value: value }), filteredItems = _getFilteredValues.filteredItems, filteredItemStrings = _getFilteredValues.filteredItemStrings; var _useStyles = (0, _reactBindings.useStyles)(Dropdown.displayName, { className: dropdownClassName, mapPropsToStyles: function mapPropsToStyles() { var _positioningProps$pos; return { disabled: disabled, error: error, fluid: fluid, focused: focused, isEmptyClearIndicator: isEmpty(clearIndicator), hasToggleIndicator: !!toggleIndicator, inline: inline, inverted: inverted, isFromKeyboard: isFromKeyboard, multiple: multiple, open: open, position: (_positioningProps$pos = positioningProps == null ? void 0 : positioningProps.position) != null ? _positioningProps$pos : position, search: !!search, hasItemsSelected: value.length > 0 }; }, mapPropsToInlineStyles: function mapPropsToInlineStyles() { return { className: className, design: design, styles: styles, variables: variables }; }, rtl: context.rtl }), classes = _useStyles.classes, resolvedStyles = _useStyles.styles; var popperRef = (0, _reactBindings.useMergedRefs)(props.popperRef); (0, _reactBindings.useIsomorphicLayoutEffect)(function () { var _popperRef$current; (_popperRef$current = popperRef.current) == null ? void 0 : _popperRef$current.updatePosition(); }, [filteredItems == null ? void 0 : filteredItems.length, popperRef]); var clearA11ySelectionMessage = React.useMemo(function () { return (0, _debounce2.default)(function () { setA11ySelectionStatus(''); }, a11yStatusCleanupTime); }, []); var clearStartingString = React.useMemo(function () { return (0, _debounce2.default)(function () { setStartingString(''); }, charKeyPressedCleanupTime); }, []); var handleChange = function handleChange(e) { // Dropdown component doesn't present any `input` component in markup, however all of our // components should handle events transparently. (0, _invoke2.default)(props, 'onChange', e, Object.assign({}, props, { value: value })); }; var handleOnBlur = function handleOnBlur(e) { // Dropdown component doesn't present any `input` component in markup, however all of our // components should handle events transparently. if (e.target !== buttonRef.current) { (0, _invoke2.default)(props, 'onBlur', e, props); } }; var renderTriggerButton = function renderTriggerButton(getToggleButtonProps) { var content = getSelectedItemAsString(value[0]); var triggerButtonId = triggerButton['id'] || defaultTriggerButtonId; var triggerButtonContentId = triggerButtonId + "__content"; var triggerButtonProps = getToggleButtonProps(Object.assign({ disabled: disabled, onFocus: handleTriggerButtonOrListFocus, onBlur: handleTriggerButtonBlur, onKeyDown: function onKeyDown(e) { handleTriggerButtonKeyDown(e); }, 'aria-invalid': ariaInvalid, 'aria-label': undefined, 'aria-labelledby': [ariaLabelledby, triggerButtonContentId].filter(Boolean).join(' ') }, open && { 'aria-expanded': true })); var _onClick = triggerButtonProps.onClick, _onFocus = triggerButtonProps.onFocus, _onBlur = triggerButtonProps.onBlur, _onKeyDown = triggerButtonProps.onKeyDown, restTriggerButtonProps = (0, _objectWithoutPropertiesLoose2.default)(triggerButtonProps, _excluded); return /*#__PURE__*/React.createElement(_reactComponentRef.Ref, { innerRef: buttonRef }, (0, _utils.createShorthand)(_Button.Button, triggerButton, { defaultProps: function defaultProps() { return Object.assign({ className: dropdownSlotClassNames.triggerButton, disabled: disabled, id: triggerButtonId, fluid: true, styles: resolvedStyles.triggerButton }, restTriggerButtonProps); }, overrideProps: function overrideProps(predefinedProps) { // It can be a shorthand var resolvedContent = (0, _isPlainObject2.default)(predefinedProps.content) ? predefinedProps.content : predefinedProps.content ? { children: predefinedProps.content } : {}; return { content: // If `null` is passed we should not render the slot predefinedProps.content === null ? null : Object.assign({ content: content, id: triggerButtonContentId }, resolvedContent), onClick: function onClick(e) { _onClick(e); (0, _invoke2.default)(predefinedProps, 'onClick', e, predefinedProps); }, onFocus: function onFocus(e) { _onFocus(e); (0, _invoke2.default)(predefinedProps, 'onFocus', e, predefinedProps); }, onBlur: function onBlur(e) { if (!disabled) { _onBlur(e); } (0, _invoke2.default)(predefinedProps, 'onBlur', e, predefinedProps); }, onKeyDown: function onKeyDown(e) { if (!disabled) { _onKeyDown(e); } (0, _invoke2.default)(predefinedProps, 'onKeyDown', e, predefinedProps); } }; } })); }; var renderSearchInput = function renderSearchInput(accessibilityComboboxProps, highlightedIndex, getInputProps, selectItemAtIndex, toggleMenu, variables) { var noPlaceholder = (searchQuery == null ? void 0 : searchQuery.length) > 0 || multiple && value.length > 0; var isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform); var comboboxProps = isMac ? Object.assign({}, accessibilityComboboxProps, { 'aria-owns': undefined }) : accessibilityComboboxProps; return _DropdownSearchInput.DropdownSearchInput.create(searchInput || {}, { defaultProps: function defaultProps() { return { className: dropdownSlotClassNames.searchInput, placeholder: noPlaceholder ? '' : placeholder, inline: inline, variables: variables, disabled: disabled }; }, overrideProps: handleSearchInputOverrides(highlightedIndex, selectItemAtIndex, toggleMenu, comboboxProps, getInputProps) }); }; var renderSelectedItemsCountNarration = function renderSelectedItemsCountNarration(id) { // Get narration only if callback is provided, at least one item is selected and only in multiple case if (!getA11ySelectionMessage || !getA11ySelectionMessage.itemsCount || value.length === 0 || !multiple) { return null; } var narration = getA11ySelectionMessage.itemsCount(value.length); return /*#__PURE__*/React.createElement("span", { id: id, className: dropdownSlotClassNames.itemsCount, style: _accessibilityStyles.screenReaderContainerStyles }, narration); }; var renderItemsList = function renderItemsList(highlightedIndex, toggleMenu, selectItemAtIndex, getMenuProps, getItemProps, getInputProps) { var items = open ? renderItems(getItemProps) : []; var _getMenuProps = getMenuProps({ refKey: 'innerRef' }, { suppressRefError: true }), _innerRef = _getMenuProps.innerRef, accessibilityMenuProps = (0, _objectWithoutPropertiesLoose2.default)(_getMenuProps, _excluded2); // If it's just a selection, some attributes and listeners from Downshift input need to go on the menu list. if (!search) { var accessibilityInputProps = getInputProps(); accessibilityMenuProps['aria-activedescendant'] = accessibilityInputProps['aria-activedescendant']; accessibilityMenuProps['onKeyDown'] = function (e) { handleListKeyDown(e, highlightedIndex, accessibilityInputProps['onKeyDown'], toggleMenu, selectItemAtIndex); }; } return /*#__PURE__*/React.createElement(_reactComponentRef.Ref, { innerRef: function innerRef(listElement) { (0, _reactComponentRef.handleRef)(listRef, listElement); (0, _reactComponentRef.handleRef)(_innerRef, listElement); } }, /*#__PURE__*/React.createElement(_positioner.Popper, (0, _extends2.default)({ rtl: context.rtl, enabled: open, targetRef: containerRef, positioningDependencies: [items.length] // positioning props: , align: align, flipBoundary: flipBoundary, overflowBoundary: overflowBoundary, popperRef: popperRef, position: position, positionFixed: positionFixed, offset: offset, unstable_disableTether: unstable_disableTether, unstable_pinned: unstable_pinned, autoSize: autoSize }, positioningProps), _List.List.create(list, { defaultProps: function defaultProps() { return Object.assign({ className: dropdownSlotClassNames.itemsList }, accessibilityMenuProps, { styles: resolvedStyles.list, items: items, tabIndex: search ? undefined : -1, // needs to be focused when trigger button is activated. 'aria-hidden': !open }); }, overrideProps: function overrideProps(predefinedProps) { return { onFocus: function onFocus(e, listProps) { handleTriggerButtonOrListFocus(); (0, _invoke2.default)(predefinedProps, 'onClick', e, listProps); }, onBlur: function onBlur(e, listProps) { handleListBlur(e); (0, _invoke2.default)(predefinedProps, 'onBlur', e, listProps); } }; } }))); }; var renderItems = function renderItems(getItemProps) { var footerItem = renderItemsListFooter(); var headerItem = renderItemsListHeader(); var items = (0, _map2.default)(filteredItems, function (item, index) { return { children: function children() { var selected = value.indexOf(item) !== -1; return _DropdownItem.DropdownItem.create(item, { defaultProps: function defaultProps() { return Object.assign({ className: dropdownSlotClassNames.item, active: highlightedIndex === index, selected: selected, checkable: checkable, checkableIndicator: checkableIndicator, isFromKeyboard: itemIsFromKeyboard, variables: variables }, typeof item === 'object' && !item.hasOwnProperty('key') && { key: item.header }); }, overrideProps: handleItemOverrides(item, index, getItemProps, selected), render: renderItem }); } }; }); if (footerItem) { items.push(footerItem); } return headerItem ? [headerItem].concat(items) : items; }; var renderItemsListHeader = function renderItemsListHeader() { if (headerMessage) { return { children: function children() { return _DropdownItem.DropdownItem.create(headerMessage, { defaultProps: function defaultProps() { return { key: 'items-list-footer-message', styles: resolvedStyles.headerMessage }; } }); } }; } return null; }; var renderItemsListFooter = function renderItemsListFooter() { if (loading) { return { children: function children() { return _DropdownItem.DropdownItem.create(loadingMessage, { defaultProps: function defaultProps() { return { key: 'loading-message', styles: resolvedStyles.loadingMessage }; } }); } }; } if (filteredItems && filteredItems.length === 0) { return { children: function children() { return _DropdownItem.DropdownItem.create(noResultsMessage, { defaultProps: function defaultProps() { return { key: 'no-results-message', styles: resolvedStyles.noResultsMessage }; } }); } }; } return null; }; var selectedItemsCountNarration = renderSelectedItemsCountNarration(selectedItemsCountNarrationId); var renderSelectedItems = function renderSelectedItems() { if (value.length === 0) { return null; } var selectedItems = value.map(function (item, index) { return ( // (!) an item matches DropdownItemProps _DropdownSelectedItem.DropdownSelectedItem.create(item, { defaultProps: function defaultProps() { return Object.assign({ className: dropdownSlotClassNames.selectedItem, active: isSelectedItemActive(index), disabled: disabled, variables: variables }, typeof item === 'object' && !item.hasOwnProperty('key') && { key: item.header }); }, overrideProps: handleSelectedItemOverrides(item), render: renderSelectedItem }) ); }); return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", { role: "listbox", tabIndex: -1, "aria-label": a11ySelectedItemsMessage }, selectedItems), selectedItemsCountNarration); }; var downshiftStateReducer = function downshiftStateReducer(state, changes) { var activeElement = context.target.activeElement; switch (changes.type) { case _downshift.default.stateChangeTypes.blurButton: // Downshift closes the list by default on trigger blur. It does not support the case when dropdown is // single selection and focuses list on trigger click/up/down/space/enter. Treating that here. if (state.isOpen && activeElement === listRef.current) { return {}; // won't change state in this case. } (0, _invoke2.default)(props, 'onBlur', null); default: return changes; } }; var handleInputValueChange = function handleInputValueChange(inputValue, stateAndHelpers) { var itemSelected = stateAndHelpers.selectedItem && inputValue === itemToString(stateAndHelpers.selectedItem); if (inputValue !== searchQuery && !itemSelected // when item is selected, `handleStateChange` will update searchQuery. ) { setStateAndInvokeHandler(['onSearchQueryChange'], null, { searchQuery: inputValue }); } }; var handleStateChange = function handleStateChange(changes) { var _context$target2; var type = changes.type; var newState = {}; switch (type) { case _downshift.default.stateChangeTypes.changeInput: { var shouldValueChange = changes.inputValue === '' && !multiple && value.length > 0; if (allowFreeform) { // set highlighted index to first item starting with search query var itemIndex = items.findIndex(function (i) { var _itemToString, _changes$inputValue; return (_itemToString = itemToString(i)) == null ? void 0 : _itemToString.toLocaleLowerCase().startsWith((_changes$inputValue = changes.inputValue) == null ? void 0 : _changes$inputValue.toLowerCase()); }); if (itemIndex !== -1) { newState.highlightedIndex = itemIndex; // for free form always keep searchQuery and inputValue in sync // as state change might not be called after last letter was entered newState.searchQuery = changes.inputValue; } } else { newState.highlightedIndex = highlightFirstItemOnOpen ? 0 : null; } if (shouldValueChange) { newState.value = []; } if (open) { // we clear value when in single selection user cleared the query. var shouldMenuClose = changes.inputValue === '' || changes.selectedItem !== undefined; if (shouldMenuClose) { newState.open = false; } } else { newState.open = true; } break; } case _downshift.default.stateChangeTypes.keyDownEnter: case _downshift.default.stateChangeTypes.clickItem: var shouldAddHighlightedIndex = !multiple && items && items.length > 0; var isSameItemSelected = changes.selectedItem === undefined; var newValue = isSameItemSelected ? value[0] : changes.selectedItem; newState.searchQuery = getSelectedItemAsString(newValue); if (allowFreeform && !inListbox.current && type === _downshift.default.stateChangeTypes.keyDownEnter) { var _itemIndex = items.findIndex(function (i) { var _itemToString2; return (_itemToString2 = itemToString(i)) == null ? void 0 : _itemToString2.toLocaleLowerCase().startsWith(searchQuery == null ? void 0 : searchQuery.toLocaleLowerCase()); }); // if there is an item that starts with searchQuery, still apply the search query // to do auto complete (you enter '12:', can be completed to '12:00') if (_itemIndex === -1) { delete newState.searchQuery; } } newState.open = false; newState.highlightedIndex = shouldAddHighlightedIndex ? items.indexOf(newValue) : null; inListbox.current = false; if (!isSameItemSelected) { newState.value = multiple ? [].concat(value, [changes.selectedItem]) : [changes.selectedItem]; if (getA11ySelectionMessage && getA11ySelectionMessage.onAdd) { setA11ySelectionMessage(getA11ySelectionMessage.onAdd(newValue)); } } if (multiple) { var _context$target; (_context$target = context.target) == null ? void 0 : _context$target.defaultView.setTimeout(function () { return selectedItemsRef.current.scrollTop = selectedItemsRef.current.scrollHeight; }, 0); } // timeout because of NVDA, otherwise it narrates old button value/state (_context$target2 = context.target) == null ? void 0 : _context$target2.defaultView.setTimeout(function () { return tryFocusTriggerButton(); }, 100); break; case _downshift.default.stateChangeTypes.keyDownEscape: if (search && !multiple) { newState.value = []; } newState.open = false; newState.highlightedIndex = highlightFirstItemOnOpen ? 0 : null; break; case _downshift.default.stateChangeTypes.keyDownArrowDown: case _downshift.default.stateChangeTypes.keyDownArrowUp: if (changes.isOpen !== undefined) { newState.open = changes.isOpen; newState.highlightedIndex = changes.highlightedIndex; if (changes.isOpen) { var highlightedIndexOnArrowKeyOpen = getHighlightedIndexOnArrowKeyOpen(changes); if ((0, _isNumber2.default)(highlightedIndexOnArrowKeyOpen)) { newState.highlightedIndex = highlightedIndexOnArrowKeyOpen; } if (!search) { listRef.current.focus(); } } else { newState.highlightedIndex = null; } } case _downshift.default.stateChangeTypes['keyDownHome']: case _downshift.default.stateChangeTypes['keyDownEnd']: if (open && (0, _isNumber2.default)(changes.highlightedIndex)) { newState.highlightedIndex = changes.highlightedIndex; newState.itemIsFromKeyboard = true; } break; case _downshift.default.stateChangeTypes.mouseUp: if (open) { newState.open = false; if (allowFreeform) { var _itemIndex2 = items.findIndex(function (i) { var _itemToString3; return (_itemToString3 = itemToString(i)) == null ? void 0 : _itemToString3.toLowerCase().startsWith(searchQuery == null ? void 0 : searchQuery.toLowerCase()); }); // if there is an item that starts with searchQuery, still apply the search query // to do auto complete (you enter '12:', can be completed to '12:00') if (_itemIndex2 !== -1) { newState.searchQuery = itemToString(items[_itemIndex2]); } } else { newState.highlightedIndex = null; } } break; case _downshift.default.stateChangeTypes.clickButton: case _downshift.default.stateChangeTypes.keyDownSpaceButton: newState.open = changes.isOpen; newState.itemIsFromKeyboard = isFromKeyboard; if (changes.isOpen) { var _highlightedIndexOnArrowKeyOpen = getHighlightedIndexOnArrowKeyOpen(changes); if ((0, _isNumber2.default)(_highlightedIndexOnArrowKeyOpen)) { newState.highlightedIndex = _highlightedIndexOnArrowKeyOpen; } if (!search) { listRef.current.focus(); } } else if (allowFreeform) { var _itemIndex3 = items.findIndex(function (i) { var _itemToString4; return (_itemToString4 = itemToString(i)) == null ? void 0 : _itemToString4.toLocaleLowerCase().startsWith(searchQuery.toLowerCase()); }); // if there is an item that starts with searchQuery, still apply the search query // to do auto complete (you enter '12:', can be completed to '12:00') if (_itemIndex3 !== -1) { newState.searchQuery = itemToString(items[_itemIndex3]); } } else { newState.highlightedIndex = null; } break; case _downshift.default.stateChangeTypes.itemMouseEnter: newState.highlightedIndex = changes.highlightedIndex; newState.itemIsFromKeyboard = false; break; case _downshift.default.stateChangeTypes.unknown: if (changes.selectedItem) { newState.value = multiple ? [].concat(value, [changes.selectedItem]) : [changes.selectedItem]; newState.searchQuery = multiple ? '' : changes.inputValue; newState.open = false; newState.highlightedIndex = changes.highlightedIndex; tryFocusTriggerButton(); } else { newState.open = changes.isOpen; } default: break; } if ((0, _isEmpty2.default)(newState)) { return; } var handlers = [newState.highlightedIndex !== undefined && 'onHighlightedIndexChange', newState.open !== undefined && 'onOpenChange', newState.searchQuery !== undefined && 'onSearchQueryChange', newState.value !== undefined && 'onChange'].filter(Boolean); setStateAndInvokeHandler(handlers, null, newState); }; var isSelectedItemActive = function isSelectedItemActive(index) { return index === activeSelectedIndex; }; var handleItemOverrides = function handleItemOverrides(item, index, getItemProps, selected) { return function (predefinedProps) { return { accessibilityItemProps: Object.assign({}, getItemProps({ item: item, index: index, disabled: item['disabled'], onClick: function onClick(e) { e.stopPropagation(); e.nativeEvent.stopImmediatePropagation(); (0, _invoke2.default)(predefinedProps, 'onClick', e, predefinedProps); } }), !multiple && { 'aria-selected': selected }) }; }; }; var handleSelectedItemOverrides = function handleSelectedItemOverrides(item) { return function (predefinedProps) { return { onRemove: function onRemove(e, dropdownSelectedItemProps) { handleSelectedItemRemove(e, item, predefinedProps, dropdownSelectedItemProps); }, onClick: function onClick(e, dropdownSelectedItemProps) { setStateAndInvokeHandler(['onActiveSelectedIndexChange'], null, { activeSelectedIndex: value.indexOf(item) }); e.stopPropagation(); (0, _invoke2.default)(predefinedProps, 'onClick', e, dropdownSelectedItemProps); }, onKeyDown: function onKeyDown(e, dropdownSelectedItemProps) { handleSelectedItemKeyDown(e, item, predefinedProps, dropdownSelectedItemProps); } }; }; }; var handleSearchInputOverrides = function handleSearchInputOverrides(highlightedIndex, selectItemAtIndex, toggleMenu, accessibilityComboboxProps, getInputProps) { return function (predefinedProps) { var handleInputBlur = function handleInputBlur(e, searchInputProps) { if (!disabled) { setFocused(false); setIsFromKeyboard((0, _utils.isFromKeyboard)()); e.nativeEvent['preventDownshiftDefault'] = true; } (0, _invoke2.default)(predefinedProps, 'onInputBlur', e, searchInputProps); }; var handleInputKeyDown = function handleInputKeyDown(e, searchInputProps) { if (!disabled) { switch ((0, _accessibility.getCode)(e)) { // https://github.com/downshift-js/downshift/issues/1097 // Downshift skips Home/End if Deopdown is opened case _accessibility.keyboardKey.Home: e.nativeEvent['preventDownshiftDefault'] = filteredItems.length === 0; break; case _accessibility.keyboardKey.End: e.nativeEvent['preventDownshiftDefault'] = filteredItems.length === 0; break; case _accessibility.keyboardKey.Tab: e.stopPropagation(); handleTabSelection(e, highlightedIndex, selectItemAtIndex, toggleMenu); break; case _accessibility.keyboardKey.ArrowLeft: e.stopPropagation(); if (!context.rtl) { // https://github.com/testing-library/user-event/issues/709 // JSDOM does not implement `event.view` so prune this code path in test if (process.env.NODE_ENV !== 'test') { (0, _utils.setWhatInputSource)(e.view.document, 'keyboard'); } trySetLastSelectedItemAsActive(); } break; case _accessibility.keyboardKey.ArrowRight: e.stopPropagation(); if (context.rtl) { // https://github.com/testing-library/user-event/issues/709 // JSDOM does not implement `event.view` so prune this code path in test if (process.env.NODE_ENV !== 'test') { (0, _utils.setWhatInputSource)(e.view.document, 'keyboard'); } trySetLastSelectedItemAsActive(); } break; case _accessibility.keyboardKey.Backspace: e.stopPropagation(); tryRemoveItemFromValue(); break; case _accessibility.keyboardKey.Escape: // If dropdown list is open ESC should close it and not propagate to the parent // otherwise event should propagate if (open) { e.stopPropagation(); } case _accessibility.keyboardKey.ArrowUp: case _accessibility.keyboardKey.ArrowDown: if (allowFreeform) { inListbox.current = true; } break; default: if ((0, _accessibility.getCode)(e) !== _accessibility.keyboardKey.Enter) { inListbox.current = false; } break; } } (0, _invoke2.default)(predefinedProps, 'onInputKeyDown', e, Object.assign({}, searchInputProps, { highlightedIndex: highlightedIndex, selectItemAtIndex: selectItemAtIndex })); }; return { // getInputProps adds Downshift handlers. We also add our own by passing them as params to that function. // user handlers were also added to our handlers previously, at the beginning of this function. accessibilityInputProps: Object.assign({}, getInputProps({ disabled: disabled, onBlur: function onBlur(e) { handleInputBlur(e, predefinedProps); }, onKeyDown: function onKeyDown(e) { handleInputKeyDown(e, predefinedProps); }, onChange: function onChange(e) { // we prevent the onChange input event to bubble up to our Dropdown handler, // since in Dropdown it gets handled as onSearchQueryChange. e.stopPropagation(); // A state modification should be triggered there otherwise it will go to an another frame and will break // cursor position: // https://github.com/facebook/react/issues/955#issuecomment-469352730 setSearchQuery(e.target.value); }, 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedby || selectedItemsCountNarrationId })), // same story as above for getRootProps. accessibilityComboboxProps: accessibilityComboboxProps, inputRef: function inputRef(node) { (0, _reactComponentRef.handleRef)(predefinedProps.inputRef, node); _inputRef.current = node; }, onFocus: function onFocus(e, searchInputProps) { if (!disabled) { setFocused(true); setIsFromKeyboard((0, _utils.isFromKeyboard)()); } (0, _invoke2.default)(predefinedProps, 'onFocus', e, searchInputProps); }, onInputBlur: function onInputBlur(e, searchInputProps) { handleInputBlur(e, searchInputProps); }, onInputKeyDown: function onInputKeyDown(e, searchInputProps) { handleInputKeyDown(e, searchInputProps); } }; }; }; /** * Custom Tab selection logic, at least until Downshift will implement selection on blur. * Also keeps focus on multiple selection dropdown when selecting by Tab. */ var handleTabSelection = function handleTabSelection(e, highlightedIndex, selectItemAtIndex, toggleMenu) { if (open) { if (!(0, _isNil2.default)(highlightedIndex) && filteredItems.length && !items[highlightedIndex]['disabled']) { selectItemAtIndex(highlightedIndex); if (multiple && !moveFocusOnTab) { e.preventDefault(); } } else { toggleMenu(); } } }; var trySetLastSelectedItemAsActive = function trySetLastSelectedItemAsActive() { if (!multiple || _inputRef.current && _inputRef.current.selectionStart !== 0) { return; } if (value.length > 0) { // If last element was already active, perform a 'reset' of activeSelectedIndex. if (activeSelectedIndex === value.length - 1) { setStateAndInvokeHandler(['onActiveSelectedIndexChange'], null, { activeSelectedIndex: value.length - 1 }); } else { setStateAndInvokeHandler(['onActiveSelectedIndexChange'], null, { activeSelectedIndex: value.length - 1 }); } } }; var tryRemoveItemFromValue = function tryRemoveItemFromValue() { if (multiple && (searchQuery === '' || _inputRef.current.selectionStart === 0 && _inputRef.current.selectionEnd === 0) && value.length > 0) { removeItemFromValue(); } }; var handleClear = function handleClear(e) { setStateAndInvokeHandler(['onChange', 'onActiveSelectedIndexChange', 'onHighlightedIndexChange'], e, { activeSelectedIndex: multiple ? null : undefined, highlightedIndex: highlightFirstItemOnOpen ? 0 : null, open: false, searchQuery: search ? '' : undefined, value: [] }); tryFocusSearchInput(); tryFocusTriggerButton(); }; var handleContainerClick = function handleContainerClick() { tryFocusSearchInput(); }; var handleTriggerButtonKeyDown = function handleTriggerButtonKeyDown(e) { switch ((0, _accessibility.getCode)(e)) { case _accessibility.keyboardKey.ArrowLeft: if (!context.rtl) { trySetLastSelectedItemAsActive(); } return; case _accessibility.keyboardKey.ArrowRight: if (context.rtl) { trySetLastSelectedItemAsActive(); } return; default: return; } }; var handleListKeyDown = function handleListKeyDown(e, highlightedIndex, accessibilityInputPropsKeyDown, toggleMenu, selectItemAtIndex) { var keyCode = (0, _accessibility.getCode)(e); switch (keyCode) { case _accessibility.keyboardKey.Tab: handleTabSelection(e, highlightedIndex, selectItemAtIndex, toggleMenu); return; case _accessibility.keyboardKey.Escape: accessibilityInputPropsKeyDown(e); tryFocusTriggerButton(); e.stopPropagation(); return; default: var keyString = String.fromCharCode(keyCode); if (/[a-zA-Z0-9]/.test(keyString)) { setHighlightedIndexOnCharKeyDown(keyString); } accessibilityInputPropsKeyDown(e); return; } }; var handleSelectedItemKeyDown = function handleSelectedItemKeyDown(e, item, predefinedProps, dropdownSelectedItemProps) { var previousKey = context.rtl ? _accessibility.keyboardKey.ArrowRight : _accessibility.keyboardKey.ArrowLeft; var nextKey = context.rtl ? _accessibility.keyboardKey.ArrowLeft : _accessibility.keyboardKey.ArrowRight; switch ((0, _accessibility.getCode)(e)) { case _accessibility.keyboardKey.Delete: case _accessibility.keyboardKey.Backspace: handleSelectedItemRemove(e, item, predefinedProps, dropdownSelectedItemProps); break; case previousKey: if (value.length > 0 && !(0, _isNil2.default)(activeSelectedIndex) && activeSelectedIndex > 0) { setStateAndInvokeHandler(['onActiveSelectedIndexChange'], null, { activeSelectedIndex: activeSelectedIndex - 1 }); } break; case nextKey: if (value.length > 0 && !(0, _isNil2.default)(activeSelectedIndex)) { if (activeSelectedIndex < value.length - 1) { setStateAndInvokeHandler(['onActiveSelectedIndexChange'], null, { activeSelectedIndex: activeSelectedIndex + 1 }); } else { setStateAndInvokeHandler(['onActiveSelectedIndexChange'], null, { activeSelectedIndex: null }); if (search) { e.preventDefault(); // prevents caret to forward one position in input. _inputRef.current.focus(); } else { buttonRef.current.focus(); } } } break; default: break; } (0, _invoke2.default)(predefinedProps, 'onKeyDown', e, dropdownSelectedItemProps); }; var handleTriggerButtonOrListFocus = function handleTriggerButtonOrListFocus() { setFocused(true); setIsFromKeyboard((0, _utils.isFromKeyboard)()); }; var handleTriggerButtonBlur = function handleTriggerButtonBlur(e) { if (listRef.current !== e.relatedTarget) { setFocused(false); setIsFromKeyboard((0, _utils.isFromKeyboard)()); } }; var handleListBlur = function handleListBlur(e) { if (buttonRef.current !== e.relatedTarget) { setFocused(false); setIsFromKeyboard((0, _utils.isFromKeyboard)()); } }; /** * Sets highlightedIndex to be the item that starts with the character keys the * user has typed. Only used in non-search dropdowns. * * @param keystring - The string the item needs to start with. It is composed by typing keys in fast succession. */ var setHighlightedIndexOnCharKeyDown = function setHighlightedIndexOnCharKeyDown(keyString) { var newStartingString = "" + startingString + keyString.toLowerCase(); var newHighlightedIndex = -1; setStartingString(newStartingString); clearStartingString(); if ((0, _isNumber2.default)(highlightedIndex)) { newHighlightedIndex = (0, _findIndex2.default)(filteredItemStrings, function (item) { return item.startsWith(newStartingString); }, highlightedIndex + (startingString.length > 0 ? 0 : 1)); } if (newHighlightedIndex < 0) { newHighlightedIndex = (0, _findIndex2.default)(filteredItemStrings, function (item) { return item.startsWith(newStartingString); }); } if (newHighlightedIndex >= 0) { setStateAndInvokeHandler(['onHighlightedIndexChange'], null, { highlightedIndex: newHighlightedIndex }); } }; var handleSelectedItemRemove = function handleSelectedItemRemove(e, item, predefinedProps, dropdownSelectedItemProps) { setStateAndInvokeHandler(['onActiveSelectedIndexChange'], null, { activeSelectedIndex: null }); removeItemFromValue(item); tryFocusSearchInput(); tryFocusTriggerButton(); (0, _invoke2.default)(predefinedProps, 'onRemove', e, dropdownSelectedItemProps); }; var removeItemFromValue = function removeItemFromValue(item) { var poppedItem = item; var newValue = [].concat(value); if (poppedItem) { newValue =