diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 140a2e75..6734b883 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -21,7 +21,7 @@ import type { RefTriggerProps } from '../SelectTrigger'; import SelectTrigger from '../SelectTrigger'; import { getSeparatedContent, isValidCount } from '../utils/valueUtil'; import Polite from './Polite'; -import useOpen, { macroTask } from '../hooks/useOpen'; +import useOpen from '../hooks/useOpen'; import { useEvent } from '@rc-component/util'; import type { SelectInputRef } from '../SelectInput'; import SelectInput from '../SelectInput'; @@ -564,11 +564,12 @@ const BaseSelect = React.forwardRef((props, ref) }; const onRootBlur = () => { - macroTask(() => { - if (!isInside(getSelectElements(), document.activeElement as HTMLElement)) { - triggerOpen(false); - } - }); + // Delay close should check the activeElement + if (mergedOpen) { + triggerOpen(false, { + cancelFun: () => isInside(getSelectElements(), document.activeElement as HTMLElement), + }); + } }; const onInternalBlur: React.FocusEventHandler = (event) => { @@ -593,16 +594,14 @@ const BaseSelect = React.forwardRef((props, ref) } }; - const onInternalMouseDown: React.MouseEventHandler = (event, ...restArgs) => { + const onRootMouseDown: React.MouseEventHandler = (event, ...restArgs) => { const { target } = event; const popupElement: HTMLDivElement = triggerRef.current?.getPopupElement(); // We should give focus back to selector if clicked item is not focusable if (popupElement?.contains(target as HTMLElement) && triggerOpen) { // Tell `open` not to close since it's safe in the popup - triggerOpen(true, { - ignoreNext: true, - }); + triggerOpen(true); } onMouseDown?.(event, ...restArgs); @@ -747,7 +746,7 @@ const BaseSelect = React.forwardRef((props, ref) // Token handling tokenWithEnter={tokenWithEnter} // Open - onMouseDown={onInternalMouseDown} + onMouseDown={onRootMouseDown} // Components components={mergedComponents} /> @@ -774,7 +773,7 @@ const BaseSelect = React.forwardRef((props, ref) empty={emptyOptions} onPopupVisibleChange={onTriggerVisibleChange} onPopupMouseEnter={onPopupMouseEnter} - onPopupMouseDown={onInternalMouseDown} + onPopupMouseDown={onRootMouseDown} onPopupBlur={onRootBlur} > {renderNode} diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index 42555137..79980956 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -170,13 +170,14 @@ export default React.forwardRef(function Selec // ====================== Open ====================== const onInternalMouseDown: SelectInputProps['onMouseDown'] = useEvent((event) => { if (!disabled) { + const inputDOM = getDOM(inputRef.current); + // https://github.com/ant-design/ant-design/issues/56002 // Tell `useSelectTriggerControl` to ignore this event // When icon is dynamic render, the parentNode will miss // so we need to mark the event directly - (event.nativeEvent as any)._ignore_global_close = true; + (event.nativeEvent as any)._ori_target = inputDOM; - const inputDOM = getDOM(inputRef.current); if (inputDOM && event.target !== inputDOM && !inputDOM.contains(event.target as Node)) { event.preventDefault(); } diff --git a/src/hooks/useOpen.ts b/src/hooks/useOpen.ts index 4915ef66..53c6d92c 100644 --- a/src/hooks/useOpen.ts +++ b/src/hooks/useOpen.ts @@ -20,13 +20,12 @@ export const macroTask = (fn: VoidFunction, times = 1) => { /** * Trigger by latest open call, if nextOpen is undefined, means toggle. - * ignoreNext will skip next call in the macro task queue. + * `weak` means this call can be ignored if previous call exists. */ export type TriggerOpenType = ( nextOpen?: boolean, config?: { - ignoreNext?: boolean; - lazy?: boolean; + cancelFun?: () => boolean; }, ) => void; @@ -58,7 +57,6 @@ export default function useOpen( const mergedOpen = postOpen(ssrSafeOpen); const taskIdRef = useRef(0); - const taskLockRef = useRef(false); const triggerEvent = useEvent((nextOpen: boolean) => { if (onOpen && mergedOpen !== nextOpen) { @@ -68,35 +66,32 @@ export default function useOpen( }); const toggleOpen = useEvent((nextOpen, config = {}) => { - const { ignoreNext = false } = config; + const { cancelFun } = config; taskIdRef.current += 1; const id = taskIdRef.current; const nextOpenVal = typeof nextOpen === 'boolean' ? nextOpen : !mergedOpen; - // Since `mergedOpen` is post-processed, we need to check if the value really changed - if (nextOpenVal) { - if (!taskLockRef.current) { + function triggerUpdate() { + if ( + // Always check if id is match + id === taskIdRef.current && + // Check if need to cancel + !cancelFun?.() + ) { triggerEvent(nextOpenVal); - - // Lock if needed - if (ignoreNext) { - taskLockRef.current = ignoreNext; - - macroTask(() => { - taskLockRef.current = false; - }, 3); - } } - return; } - macroTask(() => { - if (id === taskIdRef.current && !taskLockRef.current) { - triggerEvent(nextOpenVal); - } - }); + // Weak update can be ignored + if (nextOpenVal) { + triggerUpdate(); + } else { + macroTask(() => { + triggerUpdate(); + }); + } }); return [mergedOpen, toggleOpen] as const; diff --git a/src/hooks/useSelectTriggerControl.ts b/src/hooks/useSelectTriggerControl.ts index 811c4dd2..347cd05d 100644 --- a/src/hooks/useSelectTriggerControl.ts +++ b/src/hooks/useSelectTriggerControl.ts @@ -1,5 +1,6 @@ import * as React from 'react'; import { useEvent } from '@rc-component/util'; +import type { TriggerOpenType } from './useOpen'; export function isInside(elements: (HTMLElement | SVGElement | undefined)[], target: HTMLElement) { return elements @@ -10,7 +11,7 @@ export function isInside(elements: (HTMLElement | SVGElement | undefined)[], tar export default function useSelectTriggerControl( elements: () => (HTMLElement | SVGElement | undefined)[], open: boolean, - triggerOpen: (open: boolean) => void, + triggerOpen: TriggerOpenType, customizedTrigger: boolean, ) { const onGlobalMouseDown = useEvent((event: MouseEvent) => { @@ -25,10 +26,13 @@ export default function useSelectTriggerControl( target = (event.composedPath()[0] || target) as HTMLElement; } + if ((event as any)._ori_target) { + target = (event as any)._ori_target; + } + if ( open && // Marked by SelectInput mouseDown event - !(event as any)._ignore_global_close && !isInside(elements(), target) ) { // Should trigger close diff --git a/tests/Select.test.tsx b/tests/Select.test.tsx index bac36751..27531303 100644 --- a/tests/Select.test.tsx +++ b/tests/Select.test.tsx @@ -603,7 +603,7 @@ describe('Select.Basic', () => { , ); - keyDown(container.querySelector('input'), 40); + keyDown(container.querySelector('input'), KeyCode.DOWN); expectOpen(container); }); @@ -2566,9 +2566,9 @@ describe('Select.Basic', () => { await waitFakeTimer(); expectOpen(container, true); - keyDown(inputElem!, 40); - keyUp(inputElem!, 40); - keyDown(inputElem!, 13); + keyDown(inputElem!, KeyCode.DOWN); + keyUp(inputElem!, KeyCode.DOWN); + keyDown(inputElem!, KeyCode.ENTER); await waitFakeTimer(); expect(onBlur).toHaveBeenCalledTimes(1); @@ -2579,9 +2579,9 @@ describe('Select.Basic', () => { await waitFakeTimer(); expectOpen(container, true); - keyDown(inputElem!, 40); - keyUp(inputElem!, 40); - keyDown(inputElem!, 13); + keyDown(inputElem!, KeyCode.DOWN); + keyUp(inputElem!, KeyCode.DOWN); + keyDown(inputElem!, KeyCode.ENTER); await waitFakeTimer(); expect(onBlur).toHaveBeenCalledTimes(2);