UNPKG

semantic-ui-react

Version:
1,454 lines (1,163 loc) 48.9 kB
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;