function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } import { clsx } from 'clsx'; import { getDOM } from "@rc-component/util/es/Dom/findDOMNode"; import * as React from 'react'; import { useAllowClear } from "../hooks/useAllowClear"; import { BaseSelectContext } from "../hooks/useBaseProps"; import useLock from "../hooks/useLock"; import useSelectTriggerControl, { isInside } from "../hooks/useSelectTriggerControl"; import SelectTrigger from "../SelectTrigger"; import { getSeparatedContent, isValidCount } from "../utils/valueUtil"; import Polite from "./Polite"; import useOpen, { macroTask } from "../hooks/useOpen"; import { useEvent } from '@rc-component/util'; import SelectInput from "../SelectInput"; import useComponents from "../hooks/useComponents"; /** * ZombieJ: * We are currently refactoring the semantic structure of the component. Changelog: * - Remove `suffixIcon` and change to `suffix`. * - Add `components.root` for replacing response element. * - Remove `getInputElement` and `getRawInputElement` since we can use `components.input` instead. */ export const isMultiple = mode => mode === 'tags' || mode === 'multiple'; const BaseSelect = /*#__PURE__*/React.forwardRef((props, ref) => { const { id, prefixCls, className, styles, classNames, showSearch, tagRender, showScrollBar = 'optional', direction, omitDomProps, // Value displayValues, onDisplayValuesChange, emptyOptions, notFoundContent = 'Not Found', onClear, maxCount, placeholder, // Mode mode, // Status disabled, loading, // Customize Input getInputElement, getRawInputElement, // Open open, defaultOpen, onPopupVisibleChange, // Active activeValue, onActiveValueChange, activeDescendantId, // Search searchValue, autoClearSearchValue, onSearch, onSearchSplit, tokenSeparators, // Icons allowClear, prefix, suffix, suffixIcon, clearIcon, // Dropdown OptionList, animation, transitionName, popupStyle, popupClassName, popupMatchSelectWidth, popupRender, popupAlign, placement, builtinPlacements, getPopupContainer, // Focus showAction = [], onFocus, onBlur, // Rest Events onKeyUp, onKeyDown, onMouseDown, // Components components, // Rest Props ...restProps } = props; // ============================== MISC ============================== const multiple = isMultiple(mode); // ============================== Refs ============================== const containerRef = React.useRef(null); const triggerRef = React.useRef(null); const listRef = React.useRef(null); /** Used for component focused management */ const [focused, setFocused] = React.useState(false); // =========================== Imperative =========================== React.useImperativeHandle(ref, () => ({ focus: containerRef.current?.focus, blur: containerRef.current?.blur, scrollTo: arg => listRef.current?.scrollTo(arg), nativeElement: getDOM(containerRef.current) })); // =========================== Components =========================== const mergedComponents = useComponents(components, getInputElement, getRawInputElement); // ========================== Search Value ========================== const mergedSearchValue = React.useMemo(() => { if (mode !== 'combobox') { return searchValue; } const val = displayValues[0]?.value; return typeof val === 'string' || typeof val === 'number' ? String(val) : ''; }, [searchValue, mode, displayValues]); // ========================== Custom Input ========================== // Only works in `combobox` const customizeInputElement = mode === 'combobox' && typeof getInputElement === 'function' && getInputElement() || null; // ============================== Open ============================== // Not trigger `open` when `notFoundContent` is empty const emptyListContent = !notFoundContent && emptyOptions; const [rawOpen, mergedOpen, triggerOpen, lockOptions] = useOpen(defaultOpen || false, open, onPopupVisibleChange, nextOpen => disabled || emptyListContent ? false : nextOpen); // ============================= Search ============================= const tokenWithEnter = React.useMemo(() => (tokenSeparators || []).some(tokenSeparator => ['\n', '\r\n'].includes(tokenSeparator)), [tokenSeparators]); const onInternalSearch = (searchText, fromTyping, isCompositing) => { if (multiple && isValidCount(maxCount) && displayValues.length >= maxCount) { return; } let ret = true; let newSearchText = searchText; onActiveValueChange?.(null); const separatedList = getSeparatedContent(searchText, tokenSeparators, isValidCount(maxCount) ? maxCount - displayValues.length : undefined); // Check if match the `tokenSeparators` const patchLabels = isCompositing ? null : separatedList; // Ignore combobox since it's not split-able if (mode !== 'combobox' && patchLabels) { newSearchText = ''; onSearchSplit?.(patchLabels); // Should close when paste finish triggerOpen(false); // Tell Selector that break next actions ret = false; } if (onSearch && mergedSearchValue !== newSearchText) { onSearch(newSearchText, { source: fromTyping ? 'typing' : 'effect' }); } // Open if from typing if (searchText && fromTyping && ret) { triggerOpen(true); } return ret; }; // Only triggered when menu is closed & mode is tags // If menu is open, OptionList will take charge // If mode isn't tags, press enter is not meaningful when you can't see any option const onInternalSearchSubmit = searchText => { // prevent empty tags from appearing when you click the Enter button if (!searchText || !searchText.trim()) { return; } onSearch(searchText, { source: 'submit' }); }; // Clean up search value when the dropdown is closed. // We use `rawOpen` here to avoid clearing the search input when the dropdown is // programmatically closed due to `notFoundContent={null}` and no matching options. // This allows the user to continue typing their search query. React.useEffect(() => { if (!rawOpen && !multiple && mode !== 'combobox') { onInternalSearch('', false, false); } }, [rawOpen]); // ============================ Disabled ============================ // Close dropdown & remove focus state when disabled change React.useEffect(() => { // After onBlur is triggered, the focused does not need to be reset if (disabled) { triggerOpen(false); setFocused(false); } }, [disabled, mergedOpen]); // ============================ Keyboard ============================ /** * We record input value here to check if can press to clean up by backspace * - null: Key is not down, this is reset by key up * - true: Search text is empty when first time backspace down * - false: Search text is not empty when first time backspace down */ const [getClearLock, setClearLock] = useLock(); const keyLockRef = React.useRef(false); // KeyDown const onInternalKeyDown = event => { const clearLock = getClearLock(); const { key } = event; const isEnterKey = key === 'Enter'; const isSpaceKey = key === ' '; // Enter or Space opens dropdown (ARIA combobox: spacebar should open) if (isEnterKey || isSpaceKey) { // Do not submit form when type in the input; prevent Space from scrolling page const isCombobox = mode === 'combobox'; const isEditable = isCombobox || showSearch; if (isSpaceKey && !isEditable || isEnterKey && !isCombobox) { event.preventDefault(); } // We only manage open state here, close logic should handle by list component if (!mergedOpen) { triggerOpen(true); } } setClearLock(!!mergedSearchValue); // Remove value by `backspace` if (key === 'Backspace' && !clearLock && multiple && !mergedSearchValue && displayValues.length) { const cloneDisplayValues = [...displayValues]; let removedDisplayValue = null; for (let i = cloneDisplayValues.length - 1; i >= 0; i -= 1) { const current = cloneDisplayValues[i]; if (!current.disabled) { cloneDisplayValues.splice(i, 1); removedDisplayValue = current; break; } } if (removedDisplayValue) { onDisplayValuesChange(cloneDisplayValues, { type: 'remove', values: [removedDisplayValue] }); } } if (mergedOpen && (!isEnterKey || !keyLockRef.current) && !isSpaceKey) { // Lock the Enter key after it is pressed to avoid repeated triggering of the onChange event. if (isEnterKey) { keyLockRef.current = true; } listRef.current?.onKeyDown(event); } onKeyDown?.(event); }; // KeyUp const onInternalKeyUp = (event, ...rest) => { if (mergedOpen) { listRef.current?.onKeyUp(event, ...rest); } if (event.key === 'Enter') { keyLockRef.current = false; } onKeyUp?.(event, ...rest); }; // ============================ Selector ============================ const onSelectorRemove = useEvent(val => { const newValues = displayValues.filter(i => i !== val); onDisplayValuesChange(newValues, { type: 'remove', values: [val] }); }); const onInputBlur = () => { // Unlock the Enter key after the input blur; otherwise, the Enter key needs to be pressed twice to trigger the correct effect. keyLockRef.current = false; }; // ========================== Focus / Blur ========================== const getSelectElements = () => [getDOM(containerRef.current), triggerRef.current?.getPopupElement()]; // Close when click on non-select element useSelectTriggerControl(getSelectElements, mergedOpen, triggerOpen, !!mergedComponents.root); // ========================== Focus / Blur ========================== const internalMouseDownRef = React.useRef(false); const onInternalFocus = event => { setFocused(true); if (!disabled) { // `showAction` should handle `focus` if set if (showAction.includes('focus')) { triggerOpen(true); } onFocus?.(event); } }; const onRootBlur = () => { // Delay close should check the activeElement if (mergedOpen && !internalMouseDownRef.current) { triggerOpen(false, { cancelFun: () => isInside(getSelectElements(), document.activeElement) }); } }; const onInternalBlur = event => { setFocused(false); if (mergedSearchValue) { // `tags` mode should move `searchValue` into values if (mode === 'tags') { onSearch(mergedSearchValue, { source: 'submit' }); } else if (mode === 'multiple') { // `multiple` mode only clean the search value but not trigger event onSearch('', { source: 'blur' }); } } onRootBlur(); if (!disabled) { onBlur?.(event); } }; const onRootMouseDown = (event, ...restArgs) => { const { target } = event; const popupElement = triggerRef.current?.getPopupElement(); // We should give focus back to selector if clicked item is not focusable if (popupElement?.contains(target) && triggerOpen) { // Tell `open` not to close since it's safe in the popup triggerOpen(true); } onMouseDown?.(event, ...restArgs); internalMouseDownRef.current = true; macroTask(() => { internalMouseDownRef.current = false; }); }; // ============================ Dropdown ============================ const [, forceUpdate] = React.useState({}); // We need force update here since popup dom is render async function onPopupMouseEnter() { forceUpdate({}); } // Used for raw custom input trigger let onTriggerVisibleChange; if (!!mergedComponents.root) { onTriggerVisibleChange = newOpen => { triggerOpen(newOpen); }; } // ============================ Context ============================= const baseSelectContext = React.useMemo(() => ({ ...props, notFoundContent, open: mergedOpen, triggerOpen: mergedOpen, rawOpen, id, showSearch, multiple, toggleOpen: triggerOpen, showScrollBar, styles, classNames, lockOptions }), [props, notFoundContent, triggerOpen, id, showSearch, multiple, mergedOpen, rawOpen, showScrollBar, styles, classNames, lockOptions]); // ================================================================== // == Render == // ================================================================== // ============================= Suffix ============================= const mergedSuffixIcon = React.useMemo(() => { const nextSuffix = suffix ?? suffixIcon; if (typeof nextSuffix === 'function') { return nextSuffix({ searchValue: mergedSearchValue, open: mergedOpen, focused, showSearch, loading }); } return nextSuffix; }, [suffix, suffixIcon, mergedSearchValue, mergedOpen, focused, showSearch, loading]); // ============================= Clear ============================== const onClearMouseDown = () => { onClear?.(); containerRef.current?.focus(); onDisplayValuesChange([], { type: 'clear', values: displayValues }); onInternalSearch('', false, false); }; const { allowClear: mergedAllowClear, clearIcon: clearNode } = useAllowClear(prefixCls, displayValues, allowClear, clearIcon, disabled, mergedSearchValue, mode); // =========================== OptionList =========================== const optionList = /*#__PURE__*/React.createElement(OptionList, { ref: listRef }); // ============================= Select ============================= const mergedClassName = clsx(prefixCls, className, { [`${prefixCls}-focused`]: focused, [`${prefixCls}-multiple`]: multiple, [`${prefixCls}-single`]: !multiple, [`${prefixCls}-allow-clear`]: mergedAllowClear, [`${prefixCls}-show-arrow`]: mergedSuffixIcon !== undefined && mergedSuffixIcon !== null, [`${prefixCls}-disabled`]: disabled, [`${prefixCls}-loading`]: loading, [`${prefixCls}-open`]: mergedOpen, [`${prefixCls}-customize-input`]: customizeInputElement, [`${prefixCls}-show-search`]: showSearch }); // >>> Render let renderNode = /*#__PURE__*/React.createElement(SelectInput, _extends({}, restProps, { // Ref ref: containerRef // Style , prefixCls: prefixCls, className: mergedClassName // Focus state , focused: focused // UI , prefix: prefix, suffix: mergedSuffixIcon, clearIcon: clearNode // Type or mode , multiple: multiple, mode: mode // Values , displayValues: displayValues, placeholder: placeholder, searchValue: mergedSearchValue, activeValue: activeValue, onSearch: onInternalSearch, onSearchSubmit: onInternalSearchSubmit, onInputBlur: onInputBlur, onFocus: onInternalFocus, onBlur: onInternalBlur, onClearMouseDown: onClearMouseDown, onKeyDown: onInternalKeyDown, onKeyUp: onInternalKeyUp, onSelectorRemove: onSelectorRemove // Token handling , tokenWithEnter: tokenWithEnter // Open , onMouseDown: onRootMouseDown // Components , components: mergedComponents })); renderNode = /*#__PURE__*/React.createElement(SelectTrigger, { ref: triggerRef, disabled: disabled, prefixCls: prefixCls, visible: mergedOpen, popupElement: optionList, animation: animation, transitionName: transitionName, popupStyle: popupStyle, popupClassName: popupClassName, direction: direction, popupMatchSelectWidth: popupMatchSelectWidth, popupRender: popupRender, popupAlign: popupAlign, placement: placement, builtinPlacements: builtinPlacements, getPopupContainer: getPopupContainer, empty: emptyOptions, onPopupVisibleChange: onTriggerVisibleChange, onPopupMouseEnter: onPopupMouseEnter, onPopupMouseDown: onRootMouseDown, onPopupBlur: onRootBlur }, renderNode); return /*#__PURE__*/React.createElement(BaseSelectContext.Provider, { value: baseSelectContext }, /*#__PURE__*/React.createElement(Polite, { visible: focused && !mergedOpen, values: displayValues }), renderNode); }); // Set display name for dev if (process.env.NODE_ENV !== 'production') { BaseSelect.displayName = 'BaseSelect'; } export default BaseSelect;