@fluentui/react-northstar
Version:
A themable React component library.
1,166 lines (1,152 loc) • 63.1 kB
JavaScript
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