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 KeyCode from "@rc-component/util/es/KeyCode"; import useMemo from "@rc-component/util/es/hooks/useMemo"; import omit from "@rc-component/util/es/omit"; import pickAttrs from "@rc-component/util/es/pickAttrs"; import List from '@rc-component/virtual-list'; import * as React from 'react'; import { useEffect } from 'react'; import SelectContext from "./SelectContext"; import TransBtn from "./TransBtn"; import useBaseProps from "./hooks/useBaseProps"; import { isPlatformMac } from "./utils/platformUtil"; import { isValidCount } from "./utils/valueUtil"; // export interface OptionListProps { function isTitleType(content) { return typeof content === 'string' || typeof content === 'number'; } /** * Using virtual list of option display. * Will fallback to dom if use customize render. */ const OptionList = (_, ref) => { const { prefixCls, id, open, multiple, mode, searchValue, toggleOpen, notFoundContent, onPopupScroll, showScrollBar, lockOptions } = useBaseProps(); const { maxCount, flattenOptions, onActiveValue, defaultActiveFirstOption, onSelect, menuItemSelectedIcon, rawValues, fieldNames, virtual, direction, listHeight, listItemHeight, optionRender, classNames: contextClassNames, styles: contextStyles } = React.useContext(SelectContext); const itemPrefixCls = `${prefixCls}-item`; const memoFlattenOptions = useMemo(() => flattenOptions, [open, lockOptions], (prev, next) => next[0] && !next[1]); // =========================== List =========================== const listRef = React.useRef(null); const overMaxCount = React.useMemo(() => multiple && isValidCount(maxCount) && rawValues?.size >= maxCount, [multiple, maxCount, rawValues?.size]); const onListMouseDown = event => { event.preventDefault(); }; const scrollIntoView = args => { listRef.current?.scrollTo(typeof args === 'number' ? { index: args } : args); }; // https://github.com/ant-design/ant-design/issues/34975 const isSelected = React.useCallback(value => { if (mode === 'combobox') { return false; } return rawValues.has(value); }, [mode, [...rawValues].toString(), rawValues.size]); // ========================== Active ========================== const getEnabledActiveIndex = (index, offset = 1) => { const len = memoFlattenOptions.length; for (let i = 0; i < len; i += 1) { const current = (index + i * offset + len) % len; const { group, data } = memoFlattenOptions[current] || {}; if (!group && !data?.disabled && (isSelected(data.value) || !overMaxCount)) { return current; } } return -1; }; const [activeIndex, setActiveIndex] = React.useState(() => getEnabledActiveIndex(0)); const setActive = (index, fromKeyboard = false) => { setActiveIndex(index); const info = { source: fromKeyboard ? 'keyboard' : 'mouse' }; // Trigger active event const flattenItem = memoFlattenOptions[index]; if (!flattenItem) { onActiveValue(null, -1, info); return; } onActiveValue(flattenItem.value, index, info); }; // Auto active first item when list length or searchValue changed useEffect(() => { setActive(defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1); }, [memoFlattenOptions.length, searchValue]); // https://github.com/ant-design/ant-design/issues/48036 const isAriaSelected = React.useCallback(value => { if (mode === 'combobox') { return String(value).toLowerCase() === searchValue.toLowerCase(); } return rawValues.has(value); }, [mode, searchValue, [...rawValues].toString(), rawValues.size]); // Auto scroll to item position in single mode useEffect(() => { /** * React will skip `onChange` when component update. * `setActive` function will call root accessibility state update which makes re-render. * So we need to delay to let Input component trigger onChange first. */ let timeoutId; if (!multiple && open && rawValues.size === 1) { const value = Array.from(rawValues)[0]; // Scroll to the option closest to the searchValue if searching. const index = memoFlattenOptions.findIndex(({ data }) => searchValue ? String(data.value).startsWith(searchValue) : data.value === value); if (index !== -1) { setActive(index); timeoutId = setTimeout(() => { scrollIntoView(index); }); } } // Force trigger scrollbar visible when open if (open) { listRef.current?.scrollTo(undefined); } return () => clearTimeout(timeoutId); }, [open, searchValue]); // ========================== Values ========================== const onSelectValue = value => { if (value !== undefined) { onSelect(value, { selected: !rawValues.has(value) }); } // Single mode should always close by select if (!multiple) { toggleOpen(false); } }; // ========================= Keyboard ========================= React.useImperativeHandle(ref, () => ({ onKeyDown: event => { const { which, ctrlKey } = event; switch (which) { // >>> Arrow keys & ctrl + n/p on Mac case KeyCode.N: case KeyCode.P: case KeyCode.UP: case KeyCode.DOWN: { let offset = 0; if (which === KeyCode.UP) { offset = -1; } else if (which === KeyCode.DOWN) { offset = 1; } else if (isPlatformMac() && ctrlKey) { if (which === KeyCode.N) { offset = 1; } else if (which === KeyCode.P) { offset = -1; } } if (offset !== 0) { const nextActiveIndex = getEnabledActiveIndex(activeIndex + offset, offset); scrollIntoView(nextActiveIndex); setActive(nextActiveIndex, true); } break; } // >>> Select (Tab / Enter) case KeyCode.TAB: case KeyCode.ENTER: { // value const item = memoFlattenOptions[activeIndex]; if (!item || item.data.disabled) { return onSelectValue(undefined); } if (!overMaxCount || rawValues.has(item.value)) { onSelectValue(item.value); } else { onSelectValue(undefined); } if (open) { event.preventDefault(); } break; } // >>> Close case KeyCode.ESC: { toggleOpen(false); if (open) { event.stopPropagation(); } } } }, onKeyUp: () => {}, scrollTo: index => { scrollIntoView(index); } })); // ========================== Render ========================== if (memoFlattenOptions.length === 0) { return /*#__PURE__*/React.createElement("div", { role: "listbox", id: `${id}_list`, className: `${itemPrefixCls}-empty`, onMouseDown: onListMouseDown }, notFoundContent); } const omitFieldNameList = Object.keys(fieldNames).map(key => fieldNames[key]); const getLabel = item => item.label; function getItemAriaProps(item, index) { const { group } = item; return { role: group ? 'presentation' : 'option', id: `${id}_list_${index}` }; } const renderItem = index => { const item = memoFlattenOptions[index]; if (!item) { return null; } const itemData = item.data || {}; const { value, disabled } = itemData; const { group } = item; const attrs = pickAttrs(itemData, true); const mergedLabel = getLabel(item); return item ? /*#__PURE__*/React.createElement("div", _extends({ "aria-label": typeof mergedLabel === 'string' && !group ? mergedLabel : null }, attrs, { key: index }, getItemAriaProps(item, index), { "aria-selected": isAriaSelected(value), "aria-disabled": disabled }), value) : null; }; const a11yProps = { role: 'listbox', id: `${id}_list` }; return /*#__PURE__*/React.createElement(React.Fragment, null, virtual && /*#__PURE__*/React.createElement("div", _extends({}, a11yProps, { style: { height: 0, width: 0, overflow: 'hidden' } }), renderItem(activeIndex - 1), renderItem(activeIndex), renderItem(activeIndex + 1)), /*#__PURE__*/React.createElement(List, { itemKey: "key", ref: listRef, data: memoFlattenOptions, height: listHeight, itemHeight: listItemHeight, fullHeight: false, onMouseDown: onListMouseDown, onScroll: onPopupScroll, virtual: virtual, direction: direction, innerProps: virtual ? null : a11yProps, showScrollBar: showScrollBar, className: contextClassNames?.popup?.list, style: contextStyles?.popup?.list }, (item, itemIndex) => { const { group, groupOption, data, label, value } = item; const { key } = data; // Group if (group) { const groupTitle = data.title ?? (isTitleType(label) ? label.toString() : undefined); return /*#__PURE__*/React.createElement("div", { className: clsx(itemPrefixCls, `${itemPrefixCls}-group`, data.className), title: groupTitle }, label !== undefined ? label : key); } const { disabled, title, children, style, className, ...otherProps } = data; const passedProps = omit(otherProps, omitFieldNameList); // Option const selected = isSelected(value); const mergedDisabled = disabled || !selected && overMaxCount; const optionPrefixCls = `${itemPrefixCls}-option`; const optionClassName = clsx(itemPrefixCls, optionPrefixCls, className, contextClassNames?.popup?.listItem, { [`${optionPrefixCls}-grouped`]: groupOption, [`${optionPrefixCls}-active`]: activeIndex === itemIndex && !mergedDisabled, [`${optionPrefixCls}-disabled`]: mergedDisabled, [`${optionPrefixCls}-selected`]: selected }); const mergedLabel = getLabel(item); const iconVisible = !menuItemSelectedIcon || typeof menuItemSelectedIcon === 'function' || selected; // https://github.com/ant-design/ant-design/issues/34145 const content = typeof mergedLabel === 'number' ? mergedLabel : mergedLabel || value; // https://github.com/ant-design/ant-design/issues/26717 let optionTitle = isTitleType(content) ? content.toString() : undefined; if (title !== undefined) { optionTitle = title; } return /*#__PURE__*/React.createElement("div", _extends({}, pickAttrs(passedProps), !virtual ? getItemAriaProps(item, itemIndex) : {}, { "aria-selected": virtual ? undefined : isAriaSelected(value), "aria-disabled": mergedDisabled, className: optionClassName, title: optionTitle, onMouseMove: () => { if (activeIndex === itemIndex || mergedDisabled) { return; } setActive(itemIndex); }, onClick: () => { if (!mergedDisabled) { onSelectValue(value); } }, style: { ...contextStyles?.popup?.listItem, ...style } }), /*#__PURE__*/React.createElement("div", { className: `${optionPrefixCls}-content` }, typeof optionRender === 'function' ? optionRender(item, { index: itemIndex }) : content), /*#__PURE__*/React.isValidElement(menuItemSelectedIcon) || selected, iconVisible && /*#__PURE__*/React.createElement(TransBtn, { className: `${itemPrefixCls}-option-state`, customizeIcon: menuItemSelectedIcon, customizeIconProps: { value, disabled: mergedDisabled, isSelected: selected } }, selected ? '✓' : null)); })); }; const RefOptionList = /*#__PURE__*/React.forwardRef(OptionList); if (process.env.NODE_ENV !== 'production') { RefOptionList.displayName = 'OptionList'; } export default RefOptionList;