import TransBtn from './TransBtn'; 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, defineComponent, nextTick, reactive, toRaw, watch } from 'vue'; import List from '../vc-virtual-list'; import useMemo from '../_util/hooks/useMemo'; import { isPlatformMac } from './utils/platformUtil'; import type { EventHandler } from '../_util/EventInterface'; import omit from '../_util/omit'; import useBaseProps from './hooks/useBaseProps'; import type { RawValueType } from './Select'; import useSelectProps from './SelectContext'; import type { ScrollConfig } from '../vc-virtual-list/List'; export interface RefOptionListProps { onKeydown: (e?: KeyboardEvent) => void; onKeyup: (e?: KeyboardEvent) => void; scrollTo?: (index: number | ScrollConfig) => void; } function isTitleType(content: any) { return typeof content === 'string' || typeof content === 'number'; } // export interface OptionListProps { export type OptionListProps = Record; /** * Using virtual list of option display. * Will fallback to dom if use customize render. */ const OptionList = defineComponent({ compatConfig: { MODE: 3 }, name: 'OptionList', inheritAttrs: false, setup(_, { expose, slots }) { const baseProps = useBaseProps(); const props = useSelectProps(); const itemPrefixCls = computed(() => `${baseProps.prefixCls}-item`); const memoFlattenOptions = useMemo( () => props.flattenOptions, [() => baseProps.open, () => props.flattenOptions], next => next[0], ); // =========================== List =========================== const listRef = createRef(); const onListMouseDown: EventHandler = event => { event.preventDefault(); }; const scrollIntoView = (args: number | ScrollConfig) => { if (listRef.current) { listRef.current.scrollTo(typeof args === 'number' ? { index: args } : args); } }; // ========================== Active ========================== const getEnabledActiveIndex = (index: number, offset = 1) => { const len = memoFlattenOptions.value.length; for (let i = 0; i < len; i += 1) { const current = (index + i * offset + len) % len; const { group, data } = memoFlattenOptions.value[current]; if (!group && !data.disabled) { return current; } } return -1; }; const state = reactive({ activeIndex: getEnabledActiveIndex(0), }); const setActive = (index: number, fromKeyboard = false) => { state.activeIndex = index; const info = { source: fromKeyboard ? ('keyboard' as const) : ('mouse' as const) }; // Trigger active event const flattenItem = memoFlattenOptions.value[index]; if (!flattenItem) { props.onActiveValue(null, -1, info); return; } props.onActiveValue(flattenItem.value, index, info); }; // Auto active first item when list length or searchValue changed watch( [() => memoFlattenOptions.value.length, () => baseProps.searchValue], () => { setActive(props.defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1); }, { immediate: true }, ); // https://github.com/ant-design/ant-design/issues/34975 const isSelected = (value: RawValueType) => props.rawValues.has(value) && baseProps.mode !== 'combobox'; // Auto scroll to item position in single mode watch( [() => baseProps.open, () => baseProps.searchValue], () => { if (!baseProps.multiple && baseProps.open && props.rawValues.size === 1) { const value = Array.from(props.rawValues)[0]; const index = toRaw(memoFlattenOptions.value).findIndex( ({ data }) => data[props.fieldNames.value] === value, ); if (index !== -1) { setActive(index); nextTick(() => { scrollIntoView(index); }); } } // Force trigger scrollbar visible when open if (baseProps.open) { nextTick(() => { listRef.current?.scrollTo(undefined); }); } }, { immediate: true, flush: 'post' }, ); // ========================== Values ========================== const onSelectValue = (value?: RawValueType) => { if (value !== undefined) { props.onSelect(value, { selected: !props.rawValues.has(value) }); } // Single mode should always close by select if (!baseProps.multiple) { baseProps.toggleOpen(false); } }; const getLabel = (item: Record) => typeof item.label === 'function' ? item.label() : item.label; function renderItem(index: number) { const item = memoFlattenOptions.value[index]; if (!item) return null; const itemData = item.data || {}; const { value } = itemData; const { group } = item; const attrs = pickAttrs(itemData, true); const mergedLabel = getLabel(item); return item ? (
{value}
) : null; } const onKeydown = (event: KeyboardEvent) => { 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(state.activeIndex + offset, offset); scrollIntoView(nextActiveIndex); setActive(nextActiveIndex, true); } break; } // >>> Select case KeyCode.ENTER: { // value const item = memoFlattenOptions.value[state.activeIndex]; if (item && !item.data.disabled) { onSelectValue(item.value); } else { onSelectValue(undefined); } if (baseProps.open) { event.preventDefault(); } break; } // >>> Close case KeyCode.ESC: { baseProps.toggleOpen(false); if (baseProps.open) { event.stopPropagation(); } } } }; const onKeyup = () => {}; const scrollTo = (index: number) => { scrollIntoView(index); }; expose({ onKeydown, onKeyup, scrollTo, }); return () => { // const { // renderItem, // listRef, // onListMouseDown, // itemPrefixCls, // setActive, // onSelectValue, // memoFlattenOptions, // $slots, // } = this as any; const { id, notFoundContent, onPopupScroll } = baseProps; const { menuItemSelectedIcon, fieldNames, virtual, listHeight, listItemHeight } = props; const renderOption = slots.option; const { activeIndex } = state; const omitFieldNameList = Object.keys(fieldNames).map(key => fieldNames[key]); // ========================== Render ========================== if (memoFlattenOptions.value.length === 0) { return (
{notFoundContent}
); } return ( <>
{renderItem(activeIndex - 1)} {renderItem(activeIndex)} {renderItem(activeIndex + 1)}
{ const { group, groupOption, data, value } = item; const { key } = data; const label = typeof item.label === 'function' ? item.label() : item.label; // Group if (group) { const groupTitle = data.title ?? (isTitleType(label) && label); return (
{renderOption ? renderOption(data) : label !== undefined ? label : key}
); } const { disabled, title, children, style, class: cls, className, ...otherProps } = data; const passedProps = omit(otherProps, omitFieldNameList); // Option const selected = isSelected(value); const optionPrefixCls = `${itemPrefixCls.value}-option`; const optionClassName = classNames( itemPrefixCls.value, optionPrefixCls, cls, className, { [`${optionPrefixCls}-grouped`]: groupOption, [`${optionPrefixCls}-active`]: activeIndex === itemIndex && !disabled, [`${optionPrefixCls}-disabled`]: disabled, [`${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 (
{ if (otherProps.onMousemove) { otherProps.onMousemove(e); } if (activeIndex === itemIndex || disabled) { return; } setActive(itemIndex); }} onClick={e => { if (!disabled) { onSelectValue(value); } if (otherProps.onClick) { otherProps.onClick(e); } }} style={style} >
{renderOption ? renderOption(data) : content}
{isValidElement(menuItemSelectedIcon) || selected} {iconVisible && ( {selected ? '✓' : null} )}
); }, }} >
); }; }, }); export default OptionList;