import TransBtn from './TransBtn'; import PropTypes from '../_util/vue-types'; import KeyCode from '../_util/KeyCode'; import classNames from '../_util/classNames'; import pickAttrs from '../_util/pickAttrs'; import { isValidElement } from '../_util/props-util'; import createRef from '../_util/createRef'; import { computed, reactive, watch } from 'vue'; import List from '../vc-virtual-list/List'; const OptionListProps = { prefixCls: PropTypes.string, id: PropTypes.string, options: PropTypes.array, flattenOptions: PropTypes.array, height: PropTypes.number, itemHeight: PropTypes.number, values: PropTypes.any, multiple: PropTypes.bool, open: PropTypes.bool, defaultActiveFirstOption: PropTypes.bool, notFoundContent: PropTypes.any, menuItemSelectedIcon: PropTypes.any, childrenAsData: PropTypes.bool, searchValue: PropTypes.string, virtual: PropTypes.bool, onSelect: PropTypes.func, onToggleOpen: PropTypes.func, /** Tell Select that some value is now active to make accessibility work */ onActiveValue: PropTypes.func, onScroll: PropTypes.func, /** Tell Select that mouse enter the popup to force re-render */ onMouseenter: PropTypes.func, }; /** * Using virtual list of option display. * Will fallback to dom if use customize render. */ const OptionList = { props: OptionListProps, name: 'OptionList', inheritAttrs: false, setup(props) { const itemPrefixCls = computed(() => `${props.prefixCls}-item`); // =========================== List =========================== const listRef = createRef(); const onListMouseDown = event => { event.preventDefault(); }; const scrollIntoView = index => { if (listRef.current) { listRef.current.scrollTo({ index }); } }; // ========================== Active ========================== const getEnabledActiveIndex = (index, offset = 1) => { const len = props.flattenOptions.length; for (let i = 0; i < len; i += 1) { const current = (index + i * offset + len) % len; const { group, data } = props.flattenOptions[current]; if (!group && !data.disabled) { return current; } } return -1; }; const state = reactive({ activeIndex: getEnabledActiveIndex(0), }); const setActive = (index, fromKeyboard = false) => { state.activeIndex = index; const info = { source: fromKeyboard ? 'keyboard' : 'mouse' }; // Trigger active event const flattenItem = props.flattenOptions[index]; if (!flattenItem) { props.onActiveValue(null, -1, info); return; } props.onActiveValue(flattenItem.data.value, index, info); }; // Auto active first item when list length or searchValue changed watch([props.flattenOptions.length, props.searchValue], () => { setActive(props.defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1); }); // Auto scroll to item position in single mode watch(props.open, () => { /** * 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. */ const timeoutId = setTimeout(() => { if (!props.multiple && props.open && props.values.size === 1) { const value = Array.from(props.values)[0]; const index = props.flattenOptions.findIndex(({ data }) => data.value === value); setActive(index); scrollIntoView(index); } }); return () => clearTimeout(timeoutId); }); // ========================== Values ========================== const onSelectValue = value => { if (value !== undefined) { props.onSelect(value, { selected: !props.values.has(value) }); } // Single mode should always close by select if (!props.multiple) { props.onToggleOpen(false); } }; function renderItem(index) { const item = props.flattenOptions[index]; if (!item) return null; const itemData = item.data || {}; const { value, label, children } = itemData; const attrs = pickAttrs(itemData, true); const mergedLabel = props.childrenAsData ? children : label; return item ? (
{value}
) : null; } return { renderItem, listRef, state, onListMouseDown, itemPrefixCls, setActive, onSelectValue, onKeydown: event => { const { which } = event; switch (which) { // >>> Arrow keys case KeyCode.UP: case KeyCode.DOWN: { let offset = 0; if (which === KeyCode.UP) { offset = -1; } else if (which === KeyCode.DOWN) { offset = 1; } if (offset !== 0) { const nextActiveIndex = getEnabledActiveIndex(state.activeIndex + offset, offset); scrollIntoView(nextActiveIndex); setActive(nextActiveIndex, true); } break; } // >>> Select case KeyCode.ENTER: { // value const item = props.flattenOptions[state.activeIndex]; if (item && !item.data.disabled) { onSelectValue(item.data.value); } else { onSelectValue(undefined); } if (props.open) { event.preventDefault(); } break; } // >>> Close case KeyCode.ESC: { props.onToggleOpen(false); } } }, onKeyup: () => {}, scrollTo: index => { scrollIntoView(index); }, }; }, render() { const { renderItem, listRef, onListMouseDown, itemPrefixCls, setActive, onSelectValue } = this; const { id, childrenAsData, values, height, itemHeight, flattenOptions, menuItemSelectedIcon, notFoundContent, virtual, onScroll, onMouseenter, } = this.$props; const { activeIndex } = this.state; // ========================== Render ========================== if (flattenOptions.length === 0) { return (
{notFoundContent}
); } return ( <>
{renderItem(activeIndex - 1)} {renderItem(activeIndex)} {renderItem(activeIndex + 1)}
{({ group, groupOption, data }, itemIndex) => { const { label, key } = data; // Group if (group) { return (
{label !== undefined ? label : key}
); } const { disabled, value, title, children, style, class: cls, className, ...otherProps } = data; // Option const selected = values.has(value); const optionPrefixCls = `${itemPrefixCls}-option`; const optionClassName = classNames(itemPrefixCls, optionPrefixCls, cls, className, { [`${optionPrefixCls}-grouped`]: groupOption, [`${optionPrefixCls}-active`]: activeIndex === itemIndex && !disabled, [`${optionPrefixCls}-disabled`]: disabled, [`${optionPrefixCls}-selected`]: selected, }); const mergedLabel = childrenAsData ? children : label; const iconVisible = !menuItemSelectedIcon || typeof menuItemSelectedIcon === 'function' || selected; const content = mergedLabel || value; // https://github.com/ant-design/ant-design/issues/26717 let optionTitle = typeof content === 'string' || typeof content === 'number' ? content.toString() : undefined; if (title !== undefined) { optionTitle = title; } return (
{ if (activeIndex === itemIndex || disabled) { return; } setActive(itemIndex); }} onClick={() => { if (!disabled) { onSelectValue(value); } }} style={style} >
{content}
{isValidElement(menuItemSelectedIcon) || selected} {iconVisible && ( {selected ? '✓' : null} )}
); }}
); }, }; export default OptionList;