diff --git a/src/OptionList.tsx b/src/OptionList.tsx index e151941f0..d3864c56d 100644 --- a/src/OptionList.tsx +++ b/src/OptionList.tsx @@ -10,6 +10,7 @@ import { FlattenOptionData as SelectFlattenOptionData, OptionData, RenderNode, + OnActiveValue, } from './interface'; import { RawValueType, FlattenOptionsType } from './interface/generator'; @@ -33,7 +34,7 @@ export interface OptionListProps { onSelect: (value: RawValueType, option: { selected: boolean }) => void; onToggleOpen: (open?: boolean) => void; /** Tell Select that some value is now active to make accessibility work */ - onActiveValue: (value: RawValueType, index: number) => void; + onActiveValue: OnActiveValue; onScroll: React.UIEventHandler; /** Tell Select that mouse enter the popup to force re-render */ @@ -115,17 +116,19 @@ const OptionList: React.RefForwardingComponent< }; const [activeIndex, setActiveIndex] = React.useState(() => getEnabledActiveIndex(0)); - const setActive = (index: number) => { + const setActive = (index: number, fromKeyboard = false) => { setActiveIndex(index); + const info = { source: fromKeyboard ? ('keyboard' as const) : ('mouse' as const) }; + // Trigger active event const flattenItem = memoFlattenOptions[index]; if (!flattenItem) { - onActiveValue(null, -1); + onActiveValue(null, -1, info); return; } - onActiveValue((flattenItem.data as OptionData).value, index); + onActiveValue((flattenItem.data as OptionData).value, index, info); }; // Auto active first item when list length or searchValue changed @@ -184,7 +187,7 @@ const OptionList: React.RefForwardingComponent< if (offset !== 0) { const nextActiveIndex = getEnabledActiveIndex(activeIndex + offset, offset); scrollIntoView(nextActiveIndex); - setActive(nextActiveIndex); + setActive(nextActiveIndex, true); } break; diff --git a/src/generate.tsx b/src/generate.tsx index 4dabeb8a5..5b053a630 100644 --- a/src/generate.tsx +++ b/src/generate.tsx @@ -14,7 +14,7 @@ import classNames from 'classnames'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; import Selector, { RefSelectorProps } from './Selector'; import SelectTrigger, { RefTriggerProps } from './SelectTrigger'; -import { RenderNode, Mode, RenderDOMFunc } from './interface'; +import { RenderNode, Mode, RenderDOMFunc, OnActiveValue } from './interface'; import { GetLabeledValue, FilterOptions, @@ -187,11 +187,10 @@ export interface GenerateConfig { /** Convert single raw value into { label, value } format. Will be called by each value */ getLabeledValue: GetLabeledValue>; filterOptions: FilterOptions; - findValueOption: - | (// Need still support legacy ts api - (values: RawValueType[], options: FlattenOptionsType) => OptionsType) - | (// New API add prevValueOptions support - ( + findValueOption: // Need still support legacy ts api + | ((values: RawValueType[], options: FlattenOptionsType) => OptionsType) + // New API add prevValueOptions support + | (( values: RawValueType[], options: FlattenOptionsType, info?: { prevValueOptions?: OptionsType[] }, @@ -874,10 +873,10 @@ export default function generateSelector< const mergedDefaultActiveFirstOption = defaultActiveFirstOption !== undefined ? defaultActiveFirstOption : mode !== 'combobox'; - const onActiveValue = (active: RawValueType, index: number) => { + const onActiveValue: OnActiveValue = (active, index, { source = 'keyboard' } = {}) => { setAccessibilityIndex(index); - if (backfill && mode === 'combobox' && active !== null) { + if (backfill && mode === 'combobox' && active !== null && source === 'keyboard') { setActiveValue(String(active)); } }; diff --git a/src/interface/index.ts b/src/interface/index.ts index f259f13a2..42456cae4 100644 --- a/src/interface/index.ts +++ b/src/interface/index.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Key } from './generator'; +import { Key, RawValueType } from './generator'; export type RenderDOMFunc = (props: any) => HTMLElement; @@ -8,6 +8,12 @@ export type RenderNode = React.ReactNode | ((props: any) => React.ReactNode); export type Mode = 'multiple' | 'tags' | 'combobox'; // ======================== Option ======================== +export type OnActiveValue = ( + active: RawValueType, + index: number, + info?: { source?: 'keyboard' | 'mouse' }, +) => void; + export interface OptionCoreData { key?: Key; disabled?: boolean; diff --git a/tests/Combobox.test.tsx b/tests/Combobox.test.tsx index ad4fba4e7..44ddc4cee 100644 --- a/tests/Combobox.test.tsx +++ b/tests/Combobox.test.tsx @@ -235,6 +235,22 @@ describe('Select.Combobox', () => { expect(handleSelect).toHaveBeenCalledWith('One', expect.objectContaining({ value: 'One' })); }); + it('mouse should not trigger', () => { + const wrapper = mount( + , + ); + + wrapper + .find('.rc-select-item-option') + .first() + .simulate('mouseMove'); + + expect(wrapper.find('input').props().value).toBeFalsy(); + }); + // https://github.com/ant-design/ant-design/issues/25345 it('dynamic options', () => { const onChange = jest.fn(); diff --git a/tests/OptionList.test.tsx b/tests/OptionList.test.tsx index fef0a65d3..350ba02f3 100644 --- a/tests/OptionList.test.tsx +++ b/tests/OptionList.test.tsx @@ -68,7 +68,7 @@ describe('OptionList', () => { }), ); - expect(onActiveValue).toHaveBeenCalledWith('1', expect.anything()); + expect(onActiveValue).toHaveBeenCalledWith('1', expect.anything(), expect.anything()); }); it('key operation', () => { @@ -86,13 +86,21 @@ describe('OptionList', () => { act(() => { listRef.current.onKeyDown({ which: KeyCode.DOWN } as any); }); - expect(onActiveValue).toHaveBeenCalledWith('2', expect.anything()); + expect(onActiveValue).toHaveBeenCalledWith( + '2', + expect.anything(), + expect.objectContaining({ source: 'keyboard' }), + ); onActiveValue.mockReset(); act(() => { listRef.current.onKeyDown({ which: KeyCode.UP } as any); }); - expect(onActiveValue).toHaveBeenCalledWith('1', expect.anything()); + expect(onActiveValue).toHaveBeenCalledWith( + '1', + expect.anything(), + expect.objectContaining({ source: 'keyboard' }), + ); }); it('hover to active', () => { @@ -109,7 +117,11 @@ describe('OptionList', () => { .find('.rc-select-item-option') .last() .simulate('mouseMove'); - expect(onActiveValue).toHaveBeenCalledWith('2', expect.anything()); + expect(onActiveValue).toHaveBeenCalledWith( + '2', + expect.anything(), + expect.objectContaining({ source: 'mouse' }), + ); // Same item not repeat trigger onActiveValue.mockReset();