UNPKG

@fluentui/react-northstar

Version:
1,166 lines (1,152 loc) 63.1 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose"; import _findIndex from "lodash/findIndex"; import _isNil from "lodash/isNil"; import _isEmpty from "lodash/isEmpty"; import _isNumber from "lodash/isNumber"; import _isPlainObject from "lodash/isPlainObject"; import _invoke from "lodash/invoke"; import _debounce from "lodash/debounce"; import _uniqueId from "lodash/uniqueId"; import _get from "lodash/get"; import _isFunction from "lodash/isFunction"; import _map from "lodash/map"; import _differenceBy from "lodash/differenceBy"; var _excluded = ["onClick", "onFocus", "onBlur", "onKeyDown"], _excluded2 = ["innerRef"], _excluded3 = ["innerRef"]; import { getElementType, useAutoControlled, useStyles, useUnhandledProps, useFluentContext, useTelemetry, useMergedRefs, useIsomorphicLayoutEffect } from '@fluentui/react-bindings'; import { handleRef, Ref } from '@fluentui/react-component-ref'; import * as customPropTypes from '@fluentui/react-proptypes'; import { indicatorBehavior, getCode, keyboardKey, SpacebarKey } from '@fluentui/accessibility'; import * as React from 'react'; import * as PropTypes from 'prop-types'; import cx from 'classnames'; import computeScrollIntoView from 'compute-scroll-into-view'; import Downshift from 'downshift'; import { commonPropTypes, isFromKeyboard as detectIsFromKeyboard, createShorthand, setWhatInputSource } from '../../utils'; import { List } from '../List/List'; import { DropdownItem } from './DropdownItem'; import { DropdownSelectedItem } from './DropdownSelectedItem'; import { DropdownSearchInput } from './DropdownSearchInput'; import { Button } from '../Button/Button'; import { screenReaderContainerStyles } from '../../utils/accessibility/Styles/accessibilityStyles'; import { Box } from '../Box/Box'; import { Portal } from '../Portal/Portal'; import { ALIGNMENTS, POSITIONS, Popper, partitionPopperPropsFromShorthand, AUTOSIZES } from '../../utils/positioner'; import { CloseIcon, ChevronDownIcon } from '@fluentui/react-icons-northstar'; export var dropdownClassName = 'ui-dropdown'; export 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" }; 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 ? _differenceBy(items, value, itemToValue) : items; var filteredItemStrings = _map(filteredItemsByValue, function (filteredItem) { return itemToString(filteredItem).toLowerCase(); }); if (search) { if (_isFunction(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 && !_get(prop, 'children') && !_get(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) */ export var Dropdown = /*#__PURE__*/function () { var Dropdown = /*#__PURE__*/React.forwardRef(function (props, ref) { var _context$target3; var context = useFluentContext(); var _useTelemetry = 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 = 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 _uniqueId('dropdown-trigger-button-'); }, []); var selectedItemsCountNarrationId = React.useMemo(function () { return _uniqueId('dropdown-selected-items-count-'); }, []); var ElementType = getElementType(props); var unhandledProps = useUnhandledProps(Dropdown.handledProps, props); var _useAutoControlled = useAutoControlled({ defaultValue: props.defaultActiveSelectedIndex, initialValue: multiple ? null : undefined, value: props.activeSelectedIndex }), activeSelectedIndex = _useAutoControlled[0], setActiveSelectedIndex = _useAutoControlled[1]; var _useAutoControlled2 = useAutoControlled({ defaultValue: props.defaultHighlightedIndex, initialValue: highlightFirstItemOnOpen ? 0 : null, value: props.highlightedIndex }), highlightedIndex = _useAutoControlled2[0], setHighlightedIndex = _useAutoControlled2[1]; var _useAutoControlled3 = useAutoControlled({ defaultValue: props.defaultOpen, initialValue: false, value: props.open }), open = _useAutoControlled3[0], setOpen = _useAutoControlled3[1]; var _useAutoControlled4 = useAutoControlled({ defaultValue: props.defaultSearchQuery, initialValue: search ? '' : undefined, value: props.searchQuery }), searchQuery = _useAutoControlled4[0], setSearchQuery = _useAutoControlled4[1]; var _useAutoControlled5 = 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 = 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 = useMergedRefs(props.popperRef); 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 _debounce(function () { setA11ySelectionStatus(''); }, a11yStatusCleanupTime); }, []); var clearStartingString = React.useMemo(function () { return _debounce(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. _invoke(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) { _invoke(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 = _objectWithoutPropertiesLoose(triggerButtonProps, _excluded); return /*#__PURE__*/React.createElement(Ref, { innerRef: buttonRef }, createShorthand(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 = _isPlainObject(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); _invoke(predefinedProps, 'onClick', e, predefinedProps); }, onFocus: function onFocus(e) { _onFocus(e); _invoke(predefinedProps, 'onFocus', e, predefinedProps); }, onBlur: function onBlur(e) { if (!disabled) { _onBlur(e); } _invoke(predefinedProps, 'onBlur', e, predefinedProps); }, onKeyDown: function onKeyDown(e) { if (!disabled) { _onKeyDown(e); } _invoke(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.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: 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 = _objectWithoutPropertiesLoose(_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(Ref, { innerRef: function innerRef(listElement) { handleRef(listRef, listElement); handleRef(_innerRef, listElement); } }, /*#__PURE__*/React.createElement(Popper, _extends({ 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.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(); _invoke(predefinedProps, 'onClick', e, listProps); }, onBlur: function onBlur(e, listProps) { handleListBlur(e); _invoke(predefinedProps, 'onBlur', e, listProps); } }; } }))); }; var renderItems = function renderItems(getItemProps) { var footerItem = renderItemsListFooter(); var headerItem = renderItemsListHeader(); var items = _map(filteredItems, function (item, index) { return { children: function children() { var selected = value.indexOf(item) !== -1; return 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.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.create(loadingMessage, { defaultProps: function defaultProps() { return { key: 'loading-message', styles: resolvedStyles.loadingMessage }; } }); } }; } if (filteredItems && filteredItems.length === 0) { return { children: function children() { return 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.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.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. } _invoke(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.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.stateChangeTypes.keyDownEnter: case Downshift.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.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.stateChangeTypes.keyDownEscape: if (search && !multiple) { newState.value = []; } newState.open = false; newState.highlightedIndex = highlightFirstItemOnOpen ? 0 : null; break; case Downshift.stateChangeTypes.keyDownArrowDown: case Downshift.stateChangeTypes.keyDownArrowUp: if (changes.isOpen !== undefined) { newState.open = changes.isOpen; newState.highlightedIndex = changes.highlightedIndex; if (changes.isOpen) { var highlightedIndexOnArrowKeyOpen = getHighlightedIndexOnArrowKeyOpen(changes); if (_isNumber(highlightedIndexOnArrowKeyOpen)) { newState.highlightedIndex = highlightedIndexOnArrowKeyOpen; } if (!search) { listRef.current.focus(); } } else { newState.highlightedIndex = null; } } case Downshift.stateChangeTypes['keyDownHome']: case Downshift.stateChangeTypes['keyDownEnd']: if (open && _isNumber(changes.highlightedIndex)) { newState.highlightedIndex = changes.highlightedIndex; newState.itemIsFromKeyboard = true; } break; case Downshift.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.stateChangeTypes.clickButton: case Downshift.stateChangeTypes.keyDownSpaceButton: newState.open = changes.isOpen; newState.itemIsFromKeyboard = isFromKeyboard; if (changes.isOpen) { var _highlightedIndexOnArrowKeyOpen = getHighlightedIndexOnArrowKeyOpen(changes); if (_isNumber(_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.stateChangeTypes.itemMouseEnter: newState.highlightedIndex = changes.highlightedIndex; newState.itemIsFromKeyboard = false; break; case Downshift.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 (_isEmpty(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(); _invoke(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(); _invoke(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(detectIsFromKeyboard()); e.nativeEvent['preventDownshiftDefault'] = true; } _invoke(predefinedProps, 'onInputBlur', e, searchInputProps); }; var handleInputKeyDown = function handleInputKeyDown(e, searchInputProps) { if (!disabled) { switch (getCode(e)) { // https://github.com/downshift-js/downshift/issues/1097 // Downshift skips Home/End if Deopdown is opened case keyboardKey.Home: e.nativeEvent['preventDownshiftDefault'] = filteredItems.length === 0; break; case keyboardKey.End: e.nativeEvent['preventDownshiftDefault'] = filteredItems.length === 0; break; case keyboardKey.Tab: e.stopPropagation(); handleTabSelection(e, highlightedIndex, selectItemAtIndex, toggleMenu); break; case 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') { setWhatInputSource(e.view.document, 'keyboard'); } trySetLastSelectedItemAsActive(); } break; case 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') { setWhatInputSource(e.view.document, 'keyboard'); } trySetLastSelectedItemAsActive(); } break; case keyboardKey.Backspace: e.stopPropagation(); tryRemoveItemFromValue(); break; case 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 keyboardKey.ArrowUp: case keyboardKey.ArrowDown: if (allowFreeform) { inListbox.current = true; } break; default: if (getCode(e) !== keyboardKey.Enter) { inListbox.current = false; } break; } } _invoke(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) { handleRef(predefinedProps.inputRef, node); _inputRef.current = node; }, onFocus: function onFocus(e, searchInputProps) { if (!disabled) { setFocused(true); setIsFromKeyboard(detectIsFromKeyboard()); } _invoke(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 (!_isNil(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 (getCode(e)) { case keyboardKey.ArrowLeft: if (!context.rtl) { trySetLastSelectedItemAsActive(); } return; case keyboardKey.ArrowRight: if (context.rtl) { trySetLastSelectedItemAsActive(); } return; default: return; } }; var handleListKeyDown = function handleListKeyDown(e, highlightedIndex, accessibilityInputPropsKeyDown, toggleMenu, selectItemAtIndex) { var keyCode = getCode(e); switch (keyCode) { case keyboardKey.Tab: handleTabSelection(e, highlightedIndex, selectItemAtIndex, toggleMenu); return; case 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 ? keyboardKey.ArrowRight : keyboardKey.ArrowLeft; var nextKey = context.rtl ? keyboardKey.ArrowLeft : keyboardKey.ArrowRight; switch (getCode(e)) { case keyboardKey.Delete: case keyboardKey.Backspace: handleSelectedItemRemove(e, item, predefinedProps, dropdownSelectedItemProps); break; case previousKey: if (value.length > 0 && !_isNil(activeSelectedIndex) && activeSelectedIndex > 0) { setStateAndInvokeHandler(['onActiveSelectedIndexChange'], null, { activeSelectedIndex: activeSelectedIndex - 1 }); } break; case nextKey: if (value.length > 0 && !_isNil(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; } _invoke(predefinedProps, 'onKeyDown', e, dropdownSelectedItemProps); }; var handleTriggerButtonOrListFocus = function handleTriggerButtonOrListFocus() { setFocused(true); setIsFromKeyboard(detectIsFromKeyboard()); }; var handleTriggerButtonBlur = function handleTriggerButtonBlur(e) { if (listRef.current !== e.relatedTarget) { setFocused(false); setIsFromKeyboard(detectIsFromKeyboard()); } }; var handleListBlur = function handleListBlur(e) { if (buttonRef.current !== e.relatedTarget) { setFocused(false); setIsFromKeyboard(detectIsFromKeyboard()); } }; /** * 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 (_isNumber(highlightedIndex)) { newHighlightedIndex = _findIndex(filteredItemStrings, function (item) { return item.startsWith(newStartingString); }, highlightedIndex + (startingString.length > 0 ? 0 : 1)); } if (newHighlightedIndex < 0) { newHighlightedIndex = _findIndex(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(); _invoke(predefinedProps, 'onRemove', e, dropdownSelectedItemProps); }; var removeItemFromValue = function removeItemFromValue(item) { var poppedItem = item; var newValue = [].concat(value); if (poppedItem) { newValue = newValue.filter(function (currentElement) { return currentElement !== item; }); } else { poppedItem = newValue.pop(); } if (getA11ySelectionMessage && getA11ySelectionMessage.onRemove) { setA11ySelectionMessage(getA11ySelectionMessage.onRemove(poppedItem)); } setStateAndInvokeHandler(['onChange'], null, { value: newValue }); }; /** * Calls setState and invokes event handler exposed to user. * We don't have the event object for most events coming from Downshift se we send an empty event * because we want to keep the event handling interface */ var setStateAndInvokeHandler = function setStateAndInvokeHandler(handlerNames, event, newState) { var proposedValue = _isNil(newState.value) ? value : newState.value; // `proposedValue` should be normalized for single/multiple variations, `null` condition is // required as first item can be undefined var newValue = multiple ? proposedValue : proposedValue[0] || null; if (newState.hasOwnProperty('activeSelectedIndex')) { setActiveSelectedIndex(newState.activeSelectedIndex); } if (newState.hasOwnProperty('highlightedIndex')) { setHighlighted