semantic-ui-react
Version:
The official Semantic-UI-React integration.
1,454 lines (1,163 loc) • 48.9 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
import _inheritsLoose from "@babel/runtime/helpers/esm/inheritsLoose";
import _includes from "lodash-es/includes";
import _compact from "lodash-es/compact";
import _map from "lodash-es/map";
import _every from "lodash-es/every";
import _without from "lodash-es/without";
import _find from "lodash-es/find";
import _dropRight from "lodash-es/dropRight";
import _isEmpty from "lodash-es/isEmpty";
import _size from "lodash-es/size";
import _difference from "lodash-es/difference";
import _union from "lodash-es/union";
import _get from "lodash-es/get";
import _noop from "lodash-es/noop";
import _isUndefined from "lodash-es/isUndefined";
import _invoke from "lodash-es/invoke";
import _has from "lodash-es/has";
import _isEqual from "lodash-es/isEqual";
import _isFunction from "lodash-es/isFunction";
import _pick from "lodash-es/pick";
import _isNil from "lodash-es/isNil";
import EventStack from '@semantic-ui-react/event-stack';
import { Ref } from '@fluentui/react-component-ref';
import cx from 'clsx';
import keyboardKey from 'keyboard-key';
import PropTypes from 'prop-types';
import React, { Children, cloneElement, createRef } from 'react';
import shallowEqual from 'shallowequal';
import { ModernAutoControlledComponent as Component, childrenUtils, customPropTypes, doesNodeContainClick, getElementType, getUnhandledProps, objectDiff, useKeyOnly, useKeyOrValueAndKey } from '../../lib';
import Icon from '../../elements/Icon';
import Label from '../../elements/Label';
import Flag from '../../elements/Flag';
import Image from '../../elements/Image';
import DropdownDivider from './DropdownDivider';
import DropdownItem from './DropdownItem';
import DropdownHeader from './DropdownHeader';
import DropdownMenu from './DropdownMenu';
import DropdownSearchInput from './DropdownSearchInput';
import DropdownText from './DropdownText';
import getMenuOptions from './utils/getMenuOptions';
import getSelectedIndex from './utils/getSelectedIndex';
var getKeyOrValue = function getKeyOrValue(key, value) {
return _isNil(key) ? value : key;
};
var getKeyAndValues = function getKeyAndValues(options) {
return options ? options.map(function (option) {
return _pick(option, ['key', 'value']);
}) : options;
};
function renderItemContent(item) {
var flag = item.flag,
image = item.image,
text = item.text; // TODO: remove this in v3
// This maintains compatibility with Shorthand API in v1 as this might be called in "Label.create()"
if (_isFunction(text)) {
return text;
}
return {
content: /*#__PURE__*/React.createElement(React.Fragment, null, Flag.create(flag), Image.create(image), text)
};
}
/**
* A dropdown allows a user to select a value from a series of options.
* @see Form
* @see Select
* @see Menu
*/
var Dropdown = /*#__PURE__*/function (_Component) {
_inheritsLoose(Dropdown, _Component);
function Dropdown() {
var _this;
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
_this = _Component.call.apply(_Component, [this].concat(args)) || this;
_this.searchRef = /*#__PURE__*/createRef();
_this.sizerRef = /*#__PURE__*/createRef();
_this.ref = /*#__PURE__*/createRef();
_this.handleChange = function (e, value) {
_invoke(_this.props, 'onChange', e, _extends({}, _this.props, {
value: value
}));
};
_this.closeOnChange = function (e) {
var _this$props = _this.props,
closeOnChange = _this$props.closeOnChange,
multiple = _this$props.multiple;
var shouldClose = _isUndefined(closeOnChange) ? !multiple : closeOnChange;
if (shouldClose) {
_this.close(e, _noop);
}
};
_this.closeOnEscape = function (e) {
if (!_this.props.closeOnEscape) return;
if (keyboardKey.getCode(e) !== keyboardKey.Escape) return;
e.preventDefault();
_this.close(e);
};
_this.moveSelectionOnKeyDown = function (e) {
var _moves;
var _this$props2 = _this.props,
multiple = _this$props2.multiple,
selectOnNavigation = _this$props2.selectOnNavigation;
var open = _this.state.open;
if (!open) {
return;
}
var moves = (_moves = {}, _moves[keyboardKey.ArrowDown] = 1, _moves[keyboardKey.ArrowUp] = -1, _moves);
var move = moves[keyboardKey.getCode(e)];
if (move === undefined) {
return;
}
e.preventDefault();
var nextIndex = _this.getSelectedIndexAfterMove(move);
if (!multiple && selectOnNavigation) {
_this.makeSelectedItemActive(e, nextIndex);
}
_this.setState({
selectedIndex: nextIndex
});
};
_this.openOnSpace = function (e) {
var _e$target, _e$target2, _e$target3;
var shouldHandleEvent = _this.state.focus && !_this.state.open && keyboardKey.getCode(e) === keyboardKey.Spacebar;
var shouldPreventDefault = ((_e$target = e.target) == null ? void 0 : _e$target.tagName) !== 'INPUT' && ((_e$target2 = e.target) == null ? void 0 : _e$target2.tagName) !== 'TEXTAREA' && ((_e$target3 = e.target) == null ? void 0 : _e$target3.isContentEditable) !== true;
if (shouldHandleEvent) {
if (shouldPreventDefault) {
e.preventDefault();
}
_this.open(e);
}
};
_this.openOnArrow = function (e) {
var _this$state = _this.state,
focus = _this$state.focus,
open = _this$state.open;
if (focus && !open) {
var code = keyboardKey.getCode(e);
if (code === keyboardKey.ArrowDown || code === keyboardKey.ArrowUp) {
e.preventDefault();
_this.open(e);
}
}
};
_this.makeSelectedItemActive = function (e, selectedIndex) {
var _this$state2 = _this.state,
open = _this$state2.open,
value = _this$state2.value;
var multiple = _this.props.multiple;
var item = _this.getSelectedItem(selectedIndex);
var selectedValue = _get(item, 'value');
var disabled = _get(item, 'disabled'); // prevent selecting null if there was no selected item value
// prevent selecting duplicate items when the dropdown is closed
// prevent selecting disabled items
if (_isNil(selectedValue) || !open || disabled) {
return value;
} // state value may be undefined
var newValue = multiple ? _union(value, [selectedValue]) : selectedValue;
var valueHasChanged = multiple ? !!_difference(newValue, value).length : newValue !== value;
if (valueHasChanged) {
// notify the onChange prop that the user is trying to change value
_this.setState({
value: newValue
});
_this.handleChange(e, newValue); // Heads up! This event handler should be called after `onChange`
// Notify the onAddItem prop if this is a new value
if (item['data-additional']) {
_invoke(_this.props, 'onAddItem', e, _extends({}, _this.props, {
value: selectedValue
}));
}
}
return value;
};
_this.selectItemOnEnter = function (e) {
var search = _this.props.search;
var _this$state3 = _this.state,
open = _this$state3.open,
selectedIndex = _this$state3.selectedIndex;
if (!open) {
return;
}
var shouldSelect = keyboardKey.getCode(e) === keyboardKey.Enter || // https://github.com/Semantic-Org/Semantic-UI-React/pull/3766
!search && keyboardKey.getCode(e) === keyboardKey.Spacebar;
if (!shouldSelect) {
return;
}
e.preventDefault();
var optionSize = _size(getMenuOptions({
value: _this.state.value,
options: _this.props.options,
searchQuery: _this.state.searchQuery,
additionLabel: _this.props.additionLabel,
additionPosition: _this.props.additionPosition,
allowAdditions: _this.props.allowAdditions,
deburr: _this.props.deburr,
multiple: _this.props.multiple,
search: _this.props.search
}));
if (search && optionSize === 0) {
return;
}
var nextValue = _this.makeSelectedItemActive(e, selectedIndex); // This is required as selected value may be the same
_this.setState({
selectedIndex: getSelectedIndex({
additionLabel: _this.props.additionLabel,
additionPosition: _this.props.additionPosition,
allowAdditions: _this.props.allowAdditions,
deburr: _this.props.deburr,
multiple: _this.props.multiple,
search: _this.props.search,
selectedIndex: selectedIndex,
value: nextValue,
options: _this.props.options,
searchQuery: ''
})
});
_this.closeOnChange(e);
_this.clearSearchQuery();
if (search) {
_invoke(_this.searchRef.current, 'focus');
}
};
_this.removeItemOnBackspace = function (e) {
var _this$props3 = _this.props,
multiple = _this$props3.multiple,
search = _this$props3.search;
var _this$state4 = _this.state,
searchQuery = _this$state4.searchQuery,
value = _this$state4.value;
if (keyboardKey.getCode(e) !== keyboardKey.Backspace) return;
if (searchQuery || !search || !multiple || _isEmpty(value)) return;
e.preventDefault(); // remove most recent value
var newValue = _dropRight(value);
_this.setState({
value: newValue
});
_this.handleChange(e, newValue);
};
_this.closeOnDocumentClick = function (e) {
if (!_this.props.closeOnBlur) return; // If event happened in the dropdown, ignore it
if (_this.ref.current && doesNodeContainClick(_this.ref.current, e)) return;
_this.close();
};
_this.handleMouseDown = function (e) {
_this.isMouseDown = true;
_invoke(_this.props, 'onMouseDown', e, _this.props);
document.addEventListener('mouseup', _this.handleDocumentMouseUp);
};
_this.handleDocumentMouseUp = function () {
_this.isMouseDown = false;
document.removeEventListener('mouseup', _this.handleDocumentMouseUp);
};
_this.handleClick = function (e) {
var _this$props4 = _this.props,
minCharacters = _this$props4.minCharacters,
search = _this$props4.search;
var _this$state5 = _this.state,
open = _this$state5.open,
searchQuery = _this$state5.searchQuery;
_invoke(_this.props, 'onClick', e, _this.props); // prevent closeOnDocumentClick()
e.stopPropagation();
if (!search) return _this.toggle(e);
if (open) {
_invoke(_this.searchRef.current, 'focus');
return;
}
if (searchQuery.length >= minCharacters || minCharacters === 1) {
_this.open(e);
return;
}
_invoke(_this.searchRef.current, 'focus');
};
_this.handleIconClick = function (e) {
var clearable = _this.props.clearable;
var hasValue = _this.hasValue();
_invoke(_this.props, 'onClick', e, _this.props); // prevent handleClick()
e.stopPropagation();
if (clearable && hasValue) {
_this.clearValue(e);
} else {
_this.toggle(e);
}
};
_this.handleItemClick = function (e, item) {
var _this$props5 = _this.props,
multiple = _this$props5.multiple,
search = _this$props5.search;
var currentValue = _this.state.value;
var value = item.value; // prevent toggle() in handleClick()
e.stopPropagation(); // prevent closeOnDocumentClick() if multiple or item is disabled
if (multiple || item.disabled) {
e.nativeEvent.stopImmediatePropagation();
}
if (item.disabled) {
return;
}
var isAdditionItem = item['data-additional'];
var newValue = multiple ? _union(_this.state.value, [value]) : value;
var valueHasChanged = multiple ? !!_difference(newValue, currentValue).length : newValue !== currentValue; // notify the onChange prop that the user is trying to change value
if (valueHasChanged) {
_this.setState({
value: newValue
});
_this.handleChange(e, newValue);
}
_this.clearSearchQuery();
if (search) {
_invoke(_this.searchRef.current, 'focus');
} else {
_invoke(_this.ref.current, 'focus');
}
_this.closeOnChange(e); // Heads up! This event handler should be called after `onChange`
// Notify the onAddItem prop if this is a new value
if (isAdditionItem) {
_invoke(_this.props, 'onAddItem', e, _extends({}, _this.props, {
value: value
}));
}
};
_this.handleFocus = function (e) {
var focus = _this.state.focus;
if (focus) return;
_invoke(_this.props, 'onFocus', e, _this.props);
_this.setState({
focus: true
});
};
_this.handleBlur = function (e) {
// Heads up! Don't remove this.
// https://github.com/Semantic-Org/Semantic-UI-React/issues/1315
var currentTarget = _get(e, 'currentTarget');
if (currentTarget && currentTarget.contains(document.activeElement)) return;
var _this$props6 = _this.props,
closeOnBlur = _this$props6.closeOnBlur,
multiple = _this$props6.multiple,
selectOnBlur = _this$props6.selectOnBlur; // do not "blur" when the mouse is down inside of the Dropdown
if (_this.isMouseDown) return;
_invoke(_this.props, 'onBlur', e, _this.props);
if (selectOnBlur && !multiple) {
_this.makeSelectedItemActive(e, _this.state.selectedIndex);
if (closeOnBlur) _this.close();
}
_this.setState({
focus: false
});
_this.clearSearchQuery();
};
_this.handleSearchChange = function (e, _ref) {
var value = _ref.value;
// prevent propagating to this.props.onChange()
e.stopPropagation();
var minCharacters = _this.props.minCharacters;
var open = _this.state.open;
var newQuery = value;
_invoke(_this.props, 'onSearchChange', e, _extends({}, _this.props, {
searchQuery: newQuery
}));
_this.setState({
searchQuery: newQuery,
selectedIndex: 0
}); // open search dropdown on search query
if (!open && newQuery.length >= minCharacters) {
_this.open();
return;
} // close search dropdown if search query is too small
if (open && minCharacters !== 1 && newQuery.length < minCharacters) _this.close();
};
_this.handleKeyDown = function (e) {
_this.moveSelectionOnKeyDown(e);
_this.openOnArrow(e);
_this.openOnSpace(e);
_this.selectItemOnEnter(e);
_invoke(_this.props, 'onKeyDown', e);
};
_this.getSelectedItem = function (selectedIndex) {
var options = getMenuOptions({
value: _this.state.value,
options: _this.props.options,
searchQuery: _this.state.searchQuery,
additionLabel: _this.props.additionLabel,
additionPosition: _this.props.additionPosition,
allowAdditions: _this.props.allowAdditions,
deburr: _this.props.deburr,
multiple: _this.props.multiple,
search: _this.props.search
});
return _get(options, "[" + selectedIndex + "]");
};
_this.getItemByValue = function (value) {
var options = _this.props.options;
return _find(options, {
value: value
});
};
_this.getDropdownAriaOptions = function () {
var _this$props7 = _this.props,
loading = _this$props7.loading,
disabled = _this$props7.disabled,
search = _this$props7.search,
multiple = _this$props7.multiple;
var open = _this.state.open;
var ariaOptions = {
role: search ? 'combobox' : 'listbox',
'aria-busy': loading,
'aria-disabled': disabled,
'aria-expanded': !!open
};
if (ariaOptions.role === 'listbox') {
ariaOptions['aria-multiselectable'] = multiple;
}
return ariaOptions;
};
_this.clearSearchQuery = function () {
var searchQuery = _this.state.searchQuery;
if (searchQuery === undefined || searchQuery === '') return;
_this.setState({
searchQuery: ''
});
};
_this.handleLabelClick = function (e, labelProps) {
// prevent focusing search input on click
e.stopPropagation();
_this.setState({
selectedLabel: labelProps.value
});
_invoke(_this.props, 'onLabelClick', e, labelProps);
};
_this.handleLabelRemove = function (e, labelProps) {
// prevent focusing search input on click
e.stopPropagation();
var value = _this.state.value;
var newValue = _without(value, labelProps.value);
_this.setState({
value: newValue
});
_this.handleChange(e, newValue);
};
_this.getSelectedIndexAfterMove = function (offset, startIndex) {
if (startIndex === void 0) {
startIndex = _this.state.selectedIndex;
}
var options = getMenuOptions({
value: _this.state.value,
options: _this.props.options,
searchQuery: _this.state.searchQuery,
additionLabel: _this.props.additionLabel,
additionPosition: _this.props.additionPosition,
allowAdditions: _this.props.allowAdditions,
deburr: _this.props.deburr,
multiple: _this.props.multiple,
search: _this.props.search
}); // Prevent infinite loop
// TODO: remove left part of condition after children API will be removed
if (options === undefined || _every(options, 'disabled')) return;
var lastIndex = options.length - 1;
var wrapSelection = _this.props.wrapSelection; // next is after last, wrap to beginning
// next is before first, wrap to end
var nextIndex = startIndex + offset; // if 'wrapSelection' is set to false and selection is after last or before first, it just does not change
if (!wrapSelection && (nextIndex > lastIndex || nextIndex < 0)) {
nextIndex = startIndex;
} else if (nextIndex > lastIndex) {
nextIndex = 0;
} else if (nextIndex < 0) {
nextIndex = lastIndex;
}
if (options[nextIndex].disabled) {
return _this.getSelectedIndexAfterMove(offset, nextIndex);
}
return nextIndex;
};
_this.handleIconOverrides = function (predefinedProps) {
var clearable = _this.props.clearable;
var classes = cx(clearable && _this.hasValue() && 'clear', predefinedProps.className);
return {
className: classes,
onClick: function onClick(e) {
_invoke(predefinedProps, 'onClick', e, predefinedProps);
_this.handleIconClick(e);
}
};
};
_this.clearValue = function (e) {
var multiple = _this.props.multiple;
var newValue = multiple ? [] : '';
_this.setState({
value: newValue
});
_this.handleChange(e, newValue);
};
_this.computeSearchInputTabIndex = function () {
var _this$props8 = _this.props,
disabled = _this$props8.disabled,
tabIndex = _this$props8.tabIndex;
if (!_isNil(tabIndex)) return tabIndex;
return disabled ? -1 : 0;
};
_this.computeSearchInputWidth = function () {
var searchQuery = _this.state.searchQuery;
if (_this.sizerRef.current && searchQuery) {
// resize the search input, temporarily show the sizer so we can measure it
_this.sizerRef.current.style.display = 'inline';
_this.sizerRef.current.textContent = searchQuery;
var searchWidth = Math.ceil(_this.sizerRef.current.getBoundingClientRect().width);
_this.sizerRef.current.style.removeProperty('display');
return searchWidth;
}
};
_this.computeTabIndex = function () {
var _this$props9 = _this.props,
disabled = _this$props9.disabled,
search = _this$props9.search,
tabIndex = _this$props9.tabIndex; // don't set a root node tabIndex as the search input has its own tabIndex
if (search) return undefined;
if (disabled) return -1;
return _isNil(tabIndex) ? 0 : tabIndex;
};
_this.handleSearchInputOverrides = function (predefinedProps) {
return {
onChange: function onChange(e, inputProps) {
_invoke(predefinedProps, 'onChange', e, inputProps);
_this.handleSearchChange(e, inputProps);
}
};
};
_this.hasValue = function () {
var multiple = _this.props.multiple;
var value = _this.state.value;
return multiple ? !_isEmpty(value) : !_isNil(value) && value !== '';
};
_this.scrollSelectedItemIntoView = function () {
if (!_this.ref.current) return;
var menu = _this.ref.current.querySelector('.menu.visible');
if (!menu) return;
var item = menu.querySelector('.item.selected');
if (!item) return;
var isOutOfUpperView = item.offsetTop < menu.scrollTop;
var isOutOfLowerView = item.offsetTop + item.clientHeight > menu.scrollTop + menu.clientHeight;
if (isOutOfUpperView) {
menu.scrollTop = item.offsetTop;
} else if (isOutOfLowerView) {
// eslint-disable-next-line no-mixed-operators
menu.scrollTop = item.offsetTop + item.clientHeight - menu.clientHeight;
}
};
_this.setOpenDirection = function () {
if (!_this.ref.current) return;
var menu = _this.ref.current.querySelector('.menu.visible');
if (!menu) return;
var dropdownRect = _this.ref.current.getBoundingClientRect();
var menuHeight = menu.clientHeight;
var spaceAtTheBottom = document.documentElement.clientHeight - dropdownRect.top - dropdownRect.height - menuHeight;
var spaceAtTheTop = dropdownRect.top - menuHeight;
var upward = spaceAtTheBottom < 0 && spaceAtTheTop > spaceAtTheBottom; // set state only if there's a relevant difference
if (!upward !== !_this.state.upward) {
_this.setState({
upward: upward
});
}
};
_this.open = function (e, triggerSetState) {
if (e === void 0) {
e = null;
}
if (triggerSetState === void 0) {
triggerSetState = true;
}
var _this$props10 = _this.props,
disabled = _this$props10.disabled,
search = _this$props10.search;
if (disabled) return;
if (search) _invoke(_this.searchRef.current, 'focus');
_invoke(_this.props, 'onOpen', e, _this.props);
if (triggerSetState) {
_this.setState({
open: true
});
}
_this.scrollSelectedItemIntoView();
};
_this.close = function (e, callback) {
if (callback === void 0) {
callback = _this.handleClose;
}
if (_this.state.open) {
_invoke(_this.props, 'onClose', e, _this.props);
_this.setState({
open: false
}, callback);
}
};
_this.handleClose = function () {
var hasSearchFocus = document.activeElement === _this.searchRef.current; // https://github.com/Semantic-Org/Semantic-UI-React/issues/627
// Blur the Dropdown on close so it is blurred after selecting an item.
// This is to prevent it from re-opening when switching tabs after selecting an item.
if (!hasSearchFocus && _this.ref.current) {
_this.ref.current.blur();
}
var hasDropdownFocus = document.activeElement === _this.ref.current;
var hasFocus = hasSearchFocus || hasDropdownFocus; // We need to keep the virtual model in sync with the browser focus change
// https://github.com/Semantic-Org/Semantic-UI-React/issues/692
_this.setState({
focus: hasFocus
});
};
_this.toggle = function (e) {
return _this.state.open ? _this.close(e) : _this.open(e);
};
_this.renderText = function () {
var _this$props11 = _this.props,
multiple = _this$props11.multiple,
placeholder = _this$props11.placeholder,
search = _this$props11.search,
text = _this$props11.text;
var _this$state6 = _this.state,
searchQuery = _this$state6.searchQuery,
selectedIndex = _this$state6.selectedIndex,
value = _this$state6.value,
open = _this$state6.open;
var hasValue = _this.hasValue();
var classes = cx(placeholder && !hasValue && 'default', 'text', search && searchQuery && 'filtered');
var _text = placeholder;
var selectedItem;
if (text) {
_text = text;
} else if (open && !multiple) {
selectedItem = _this.getSelectedItem(selectedIndex);
} else if (hasValue) {
selectedItem = _this.getItemByValue(value);
}
return DropdownText.create(selectedItem ? renderItemContent(selectedItem) : _text, {
defaultProps: {
className: classes
}
});
};
_this.renderSearchInput = function () {
var _this$props12 = _this.props,
search = _this$props12.search,
searchInput = _this$props12.searchInput;
var searchQuery = _this.state.searchQuery;
return search && /*#__PURE__*/React.createElement(Ref, {
innerRef: _this.searchRef
}, DropdownSearchInput.create(searchInput, {
defaultProps: {
style: {
width: _this.computeSearchInputWidth()
},
tabIndex: _this.computeSearchInputTabIndex(),
value: searchQuery
},
overrideProps: _this.handleSearchInputOverrides
}));
};
_this.renderSearchSizer = function () {
var _this$props13 = _this.props,
search = _this$props13.search,
multiple = _this$props13.multiple;
return search && multiple && /*#__PURE__*/React.createElement("span", {
className: "sizer",
ref: _this.sizerRef
});
};
_this.renderLabels = function () {
var _this$props14 = _this.props,
multiple = _this$props14.multiple,
renderLabel = _this$props14.renderLabel;
var _this$state7 = _this.state,
selectedLabel = _this$state7.selectedLabel,
value = _this$state7.value;
if (!multiple || _isEmpty(value)) {
return;
}
var selectedItems = _map(value, _this.getItemByValue);
// if no item could be found for a given state value the selected item will be undefined
// compact the selectedItems so we only have actual objects left
return _map(_compact(selectedItems), function (item, index) {
var defaultProps = {
active: item.value === selectedLabel,
as: 'a',
key: getKeyOrValue(item.key, item.value),
onClick: _this.handleLabelClick,
onRemove: _this.handleLabelRemove,
value: item.value
};
return Label.create(renderLabel(item, index, defaultProps), {
defaultProps: defaultProps
});
});
};
_this.renderOptions = function () {
var _this$props15 = _this.props,
lazyLoad = _this$props15.lazyLoad,
multiple = _this$props15.multiple,
search = _this$props15.search,
noResultsMessage = _this$props15.noResultsMessage;
var _this$state8 = _this.state,
open = _this$state8.open,
selectedIndex = _this$state8.selectedIndex,
value = _this$state8.value; // lazy load, only render options when open
if (lazyLoad && !open) return null;
var options = getMenuOptions({
value: _this.state.value,
options: _this.props.options,
searchQuery: _this.state.searchQuery,
additionLabel: _this.props.additionLabel,
additionPosition: _this.props.additionPosition,
allowAdditions: _this.props.allowAdditions,
deburr: _this.props.deburr,
multiple: _this.props.multiple,
search: _this.props.search
});
if (noResultsMessage !== null && search && _isEmpty(options)) {
return /*#__PURE__*/React.createElement("div", {
className: "message"
}, noResultsMessage);
}
var isActive = multiple ? function (optValue) {
return _includes(value, optValue);
} : function (optValue) {
return optValue === value;
};
return _map(options, function (opt, i) {
return DropdownItem.create(_extends({
active: isActive(opt.value),
selected: selectedIndex === i
}, opt, {
key: getKeyOrValue(opt.key, opt.value),
// Needed for handling click events on disabled items
style: _extends({}, opt.style, {
pointerEvents: 'all'
})
}), {
generateKey: false,
overrideProps: function overrideProps(predefinedProps) {
return {
onClick: function onClick(e, item) {
predefinedProps.onClick == null ? void 0 : predefinedProps.onClick(e, item);
_this.handleItemClick(e, item);
}
};
}
});
});
};
_this.renderMenu = function () {
var _this$props16 = _this.props,
children = _this$props16.children,
direction = _this$props16.direction,
header = _this$props16.header;
var open = _this.state.open;
var ariaOptions = _this.getDropdownMenuAriaOptions(); // single menu child
if (!childrenUtils.isNil(children)) {
var menuChild = Children.only(children);
var className = cx(direction, useKeyOnly(open, 'visible'), menuChild.props.className);
return /*#__PURE__*/cloneElement(menuChild, _extends({
className: className
}, ariaOptions));
}
return /*#__PURE__*/React.createElement(DropdownMenu, _extends({}, ariaOptions, {
direction: direction,
open: open
}), DropdownHeader.create(header, {
autoGenerateKey: false
}), _this.renderOptions());
};
return _this;
}
var _proto = Dropdown.prototype;
_proto.getInitialAutoControlledState = function getInitialAutoControlledState() {
return {
focus: false,
searchQuery: ''
};
};
Dropdown.getAutoControlledStateFromProps = function getAutoControlledStateFromProps(nextProps, computedState, prevState) {
// These values are stored only for a comparison on next getAutoControlledStateFromProps()
var derivedState = {
__options: nextProps.options,
__value: computedState.value
}; // The selected index is only dependent:
var shouldComputeSelectedIndex = // On value change
!shallowEqual(prevState.__value, computedState.value) || // On option keys/values, we only check those properties to avoid recursive performance impacts.
// https://github.com/Semantic-Org/Semantic-UI-React/issues/3000
!_isEqual(getKeyAndValues(nextProps.options), getKeyAndValues(prevState.__options));
if (shouldComputeSelectedIndex) {
derivedState.selectedIndex = getSelectedIndex({
additionLabel: nextProps.additionLabel,
additionPosition: nextProps.additionPosition,
allowAdditions: nextProps.allowAdditions,
deburr: nextProps.deburr,
multiple: nextProps.multiple,
search: nextProps.search,
selectedIndex: computedState.selectedIndex,
value: computedState.value,
options: nextProps.options,
searchQuery: computedState.searchQuery
});
}
return derivedState;
};
_proto.componentDidMount = function componentDidMount() {
var open = this.state.open;
if (open) {
this.open(null, false);
}
};
_proto.shouldComponentUpdate = function shouldComponentUpdate(nextProps, nextState) {
return !shallowEqual(nextProps, this.props) || !shallowEqual(nextState, this.state);
};
_proto.componentDidUpdate = function componentDidUpdate(prevProps, prevState) {
// eslint-disable-line complexity
var _this$props17 = this.props,
closeOnBlur = _this$props17.closeOnBlur,
minCharacters = _this$props17.minCharacters,
openOnFocus = _this$props17.openOnFocus,
search = _this$props17.search;
/* eslint-disable no-console */
if (process.env.NODE_ENV !== 'production') {
// in development, validate value type matches dropdown type
var isNextValueArray = Array.isArray(this.props.value);
var hasValue = _has(this.props, 'value');
if (hasValue && this.props.multiple && !isNextValueArray) {
console.error('Dropdown `value` must be an array when `multiple` is set.' + (" Received type: `" + Object.prototype.toString.call(this.props.value) + "`."));
} else if (hasValue && !this.props.multiple && isNextValueArray) {
console.error('Dropdown `value` must not be an array when `multiple` is not set.' + ' Either set `multiple={true}` or use a string or number value.');
}
}
/* eslint-enable no-console */
// focused / blurred
if (!prevState.focus && this.state.focus) {
if (!this.isMouseDown) {
var openable = !search || search && minCharacters === 1 && !this.state.open;
if (openOnFocus && openable) this.open();
}
} else if (prevState.focus && !this.state.focus) {
if (!this.isMouseDown && closeOnBlur) {
this.close();
}
} // opened / closed
if (!prevState.open && this.state.open) {
this.setOpenDirection();
this.scrollSelectedItemIntoView();
} else if (prevState.open && !this.state.open) {}
if (prevState.selectedIndex !== this.state.selectedIndex) {
this.scrollSelectedItemIntoView();
}
} // ----------------------------------------
// Document Event Handlers
// ----------------------------------------
// onChange needs to receive a value
// can't rely on props.value if we are controlled
;
_proto.getDropdownMenuAriaOptions = function getDropdownMenuAriaOptions() {
var _this$props18 = this.props,
search = _this$props18.search,
multiple = _this$props18.multiple;
var ariaOptions = {};
if (search) {
ariaOptions['aria-multiselectable'] = multiple;
ariaOptions.role = 'listbox';
}
return ariaOptions;
} // ----------------------------------------
// Setters
// ----------------------------------------
;
_proto.render = function render() {
var _this$props19 = this.props,
basic = _this$props19.basic,
button = _this$props19.button,
className = _this$props19.className,
compact = _this$props19.compact,
disabled = _this$props19.disabled,
error = _this$props19.error,
fluid = _this$props19.fluid,
floating = _this$props19.floating,
icon = _this$props19.icon,
inline = _this$props19.inline,
item = _this$props19.item,
labeled = _this$props19.labeled,
loading = _this$props19.loading,
multiple = _this$props19.multiple,
pointing = _this$props19.pointing,
search = _this$props19.search,
selection = _this$props19.selection,
scrolling = _this$props19.scrolling,
simple = _this$props19.simple,
trigger = _this$props19.trigger;
var _this$state9 = this.state,
focus = _this$state9.focus,
open = _this$state9.open,
upward = _this$state9.upward; // Classes
var classes = cx('ui', useKeyOnly(open, 'active visible'), useKeyOnly(disabled, 'disabled'), useKeyOnly(error, 'error'), useKeyOnly(loading, 'loading'), useKeyOnly(basic, 'basic'), useKeyOnly(button, 'button'), useKeyOnly(compact, 'compact'), useKeyOnly(fluid, 'fluid'), useKeyOnly(floating, 'floating'), useKeyOnly(inline, 'inline'), // TODO: consider augmentation to render Dropdowns as Button/Menu, solves icon/link item issues
// https://github.com/Semantic-Org/Semantic-UI-React/issues/401#issuecomment-240487229
// TODO: the icon class is only required when a dropdown is a button
// useKeyOnly(icon, 'icon'),
useKeyOnly(labeled, 'labeled'), useKeyOnly(item, 'item'), useKeyOnly(multiple, 'multiple'), useKeyOnly(search, 'search'), useKeyOnly(selection, 'selection'), useKeyOnly(simple, 'simple'), useKeyOnly(scrolling, 'scrolling'), useKeyOnly(upward, 'upward'), useKeyOrValueAndKey(pointing, 'pointing'), 'dropdown', className);
var rest = getUnhandledProps(Dropdown, this.props);
var ElementType = getElementType(Dropdown, this.props);
var ariaOptions = this.getDropdownAriaOptions(ElementType, this.props);
return /*#__PURE__*/React.createElement(Ref, {
innerRef: this.ref
}, /*#__PURE__*/React.createElement(ElementType, _extends({}, rest, ariaOptions, {
className: classes,
onBlur: this.handleBlur,
onClick: this.handleClick,
onKeyDown: this.handleKeyDown,
onMouseDown: this.handleMouseDown,
onFocus: this.handleFocus,
onChange: this.handleChange,
tabIndex: this.computeTabIndex()
}), this.renderLabels(), this.renderSearchInput(), this.renderSearchSizer(), trigger || this.renderText(), Icon.create(icon, {
overrideProps: this.handleIconOverrides,
autoGenerateKey: false
}), this.renderMenu(), open && /*#__PURE__*/React.createElement(EventStack, {
name: "keydown",
on: this.closeOnEscape
}), open && /*#__PURE__*/React.createElement(EventStack, {
name: "click",
on: this.closeOnDocumentClick
}), focus && /*#__PURE__*/React.createElement(EventStack, {
name: "keydown",
on: this.removeItemOnBackspace
})));
};
return Dropdown;
}(Component);
Dropdown.handledProps = ["additionLabel", "additionPosition", "allowAdditions", "as", "basic", "button", "children", "className", "clearable", "closeOnBlur", "closeOnChange", "closeOnEscape", "compact", "deburr", "defaultOpen", "defaultSearchQuery", "defaultSelectedLabel", "defaultUpward", "defaultValue", "direction", "disabled", "error", "floating", "fluid", "header", "icon", "inline", "item", "labeled", "lazyLoad", "loading", "minCharacters", "multiple", "noResultsMessage", "onAddItem", "onBlur", "onChange", "onClick", "onClose", "onFocus", "onLabelClick", "onMouseDown", "onOpen", "onSearchChange", "open", "openOnFocus", "options", "placeholder", "pointing", "renderLabel", "scrolling", "search", "searchInput", "searchQuery", "selectOnBlur", "selectOnNavigation", "selectedLabel", "selection", "simple", "tabIndex", "text", "trigger", "upward", "value", "wrapSelection"];
export { Dropdown as default };
Dropdown.propTypes = process.env.NODE_ENV !== "production" ? {
/** An element type to render as (string or function). */
as: PropTypes.elementType,
/** Label prefixed to an option added by a user. */
additionLabel: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
/** Position of the `Add: ...` option in the dropdown list ('top' or 'bottom'). */
additionPosition: PropTypes.oneOf(['top', 'bottom']),
/**
* Allow user additions to the list of options (boolean).
* Requires the use of `selection`, `options` and `search`.
*/
allowAdditions: customPropTypes.every([customPropTypes.demand(['options', 'selection', 'search']), PropTypes.bool]),
/** A Dropdown can reduce its complexity. */
basic: PropTypes.bool,
/** Format the Dropdown to appear as a button. */
button: PropTypes.bool,
/** Primary content. */
children: customPropTypes.every([customPropTypes.disallow(['options', 'selection']), customPropTypes.givenProps({
children: PropTypes.any.isRequired
}, PropTypes.element.isRequired)]),
/** Additional classes. */
className: PropTypes.string,
/** Using the clearable setting will let users remove their selection from a dropdown. */
clearable: PropTypes.bool,
/** Whether or not the menu should close when the dropdown is blurred. */
closeOnBlur: PropTypes.bool,
/** Whether or not the dropdown should close when the escape key is pressed. */
closeOnEscape: PropTypes.bool,
/**
* Whether or not the menu should close when a value is selected from the dropdown.
* By default, multiple selection dropdowns will remain open on change, while single
* selection dropdowns will close on change.
*/
closeOnChange: PropTypes.bool,
/** A compact dropdown has no minimum width. */
compact: PropTypes.bool,
/** Whether or not the dropdown should strip diacritics in options and input search */
deburr: PropTypes.bool,
/** Initial value of open. */
defaultOpen: PropTypes.bool,
/** Initial value of searchQuery. */
defaultSearchQuery: PropTypes.string,
/** Currently selected label in multi-select. */
defaultSelectedLabel: customPropTypes.every([customPropTypes.demand(['multiple']), PropTypes.oneOfType([PropTypes.number, PropTypes.string])]),
/** Initial value of upward. */
defaultUpward: PropTypes.bool,
/** Initial value or value array if multiple. */
defaultValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.bool, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]))]),
/** A dropdown menu can open to the left or to the right. */
direction: PropTypes.oneOf(['left', 'right']),
/** A disabled dropdown menu or item does not allow user interaction. */
disabled: PropTypes.bool,
/** An errored dropdown can alert a user to a problem. */
error: PropTypes.bool,
/** A dropdown menu can contain floated content. */
floating: PropTypes.bool,
/** A dropdown can take the full width of its parent */
fluid: PropTypes.bool,
/** A dropdown menu can contain a header. */
header: PropTypes.node,
/** Shorthand for Icon. */
icon: PropTypes.oneOfType([PropTypes.node, PropTypes.object]),
/** A dropdown can be formatted to appear inline in other content. */
inline: PropTypes.bool,
/** A dropdown can be formatted as a Menu item. */
item: PropTypes.bool,
/** A dropdown can be labeled. */
labeled: PropTypes.bool,
/** A dropdown can defer rendering its options until it is open. */
lazyLoad: PropTypes.bool,
/** A dropdown can show that it is currently loading data. */
loading: PropTypes.bool,
/** The minimum characters for a search to begin showing results. */
minCharacters: PropTypes.number,
/** A selection dropdown can allow multiple selections. */
multiple: PropTypes.bool,
/** Message to display when there are no results. */
noResultsMessage: PropTypes.node,
/**
* Called when a user adds a new item. Use this to update the options list.
*
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All props and the new item's value.
*/
onAddItem: PropTypes.func,
/**
* Called on blur.
*
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All props.
*/
onBlur: PropTypes.func,
/**
* Called when the user attempts to change the value.
*
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All props and proposed value.
*/
onChange: PropTypes.func,
/**
* Called on click.
*
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All props.
*/
onClick: PropTypes.func,
/**
* Called when a close event happens.
*
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All props.
*/
onClose: PropTypes.func,
/**
* Called on focus.
*
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All props.
*/
onFocus: PropTypes.func,
/**
* Called when a multi-select label is clicked.
*
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All label props.
*/
onLabelClick: PropTypes.func,
/**
* Called on mousedown.
*
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All props.
*/
onMouseDown: PropTypes.func,
/**
* Called when an open event happens.
*
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All props.
*/
onOpen: PropTypes.func,
/**
* Called on search input change.
*
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All props, includes current value of searchQuery.
*/
onSearchChange: PropTypes.func,
/** Controls whether or not the dropdown menu is displayed. */
open: PropTypes.bool,
/** Whether or not the menu should open when the dropdown is focused. */
openOnFocus: PropTypes.bool,
/** Array of Dropdown.Item props e.g. `{ text: '', value: '' }` */
options: customPropTypes.every([customPropTypes.disallow(['children']), PropTypes.arrayOf(PropTypes.shape(DropdownItem.propTypes))]),
/** Placeholder text. */
placeholder: PropTypes.string,
/** A dropdown can be formatted so that its menu is pointing. */
pointing: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['left', 'right', 'top', 'top left', 'top right', 'bottom', 'bottom left', 'bottom right'])]),
/**
* Mapped over the active items and returns shorthand for the active item Labels.
* Only applies to `multiple` Dropdowns.
*
* @param {object} item - A currently active dropdown item.
* @param {number} index - The current index.
* @param {object} defaultLabelProps - The default props for an active item Label.
* @returns {*} Shorthand for a Label.
*/
renderLabel: PropTypes.func,
/** A dropdown can have its menu scroll. */
scrolling: PropTypes.bool,
/**
* A selection dropdown can allow a user to search through a large list of choices.
* Pass a function here to replace the default search.
*/
search: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
/** A shorthand for a search input. */
searchInput: PropTypes.oneOfType([PropTypes.array, PropTypes.node, PropTypes.object]),
/** Current value of searchQuery. Creates a controlled component. */
searchQuery: PropTypes.string,
// TODO 'searchInMenu' or 'search='in menu' or ??? How to handle this markup and functionality?
/** Define whether the highlighted item should be selected on blur. */
selectOnBlur: PropTypes.bool,
/**
* Whether or not to change the value when navigating the menu using arrow keys.
* Setting to false will require enter or left click to confirm a choice.
*/
selectOnNavigation: PropTypes.bool,
/** Currently selected label in multi-select. */
selectedLabel: customPropTypes.every([customPropTypes.demand(['multiple']), PropTypes.oneOfType([PropTypes.string, PropTypes.number])]),
/** A dropdown can be used to select between choices in a form. */
selection: customPropTypes.every([customPropTypes.disallow(['children']), customPropTypes.demand(['options']), PropTypes.bool]),
/** A simple dropdown can open without Javascript. */
simple: PropTypes.bool,
/** A dropdown can receive focus. */
tabIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
/** The text displayed in the dropdown, usually for the active item. */
text: PropTypes.string,
/** Custom element to trigger the menu to become visible. Takes place of 'text'. */
trigger: customPropTypes.every([customPropTypes.disallow(['selection', 'text']), PropTypes.node]),
/** Current value or value array if multiple. Creates a controlled component. */
value: PropTypes.oneOfType([PropTypes.bool, PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.bool, PropTypes.string, PropTypes.number]))]),
/** Controls whether the dropdown will open upward. */
upward: PropTypes.bool,
/**
* A dropdown will go to the last element when ArrowUp is pressed on the first,
* or go to the first when ArrowDown is pressed on the last( aka infinite selection )
*/
wrapSelection: PropTypes.bool
} : {};
Dropdown.defaultProps = {
additionLabel: 'Add ',
additionPosition: 'top',
closeOnBlur: true,
closeOnEscape: true,
deburr: false,
icon: 'dropdown',
minCharacters: 1,
noResultsMessage: 'No results found.',
openOnFocus: true,
renderLabel: renderItemContent,
searchInput: 'text',
selectOnBlur: true,
selectOnNavigation: true,
wrapSelection: true
};
Dropdown.autoControlledProps = ['open', 'searchQuery', 'selectedLabel', 'value', 'upward'];
Dropdown.Divider = DropdownDivider;
Dropdown.Header = DropdownHeader;
Dropdown.Item = DropdownItem;
Dropdown.Menu = DropdownMenu;
Dropdown.SearchInput = DropdownSearchInput;
Dropdown.Text = DropdownText;