From aba78022dfe30f00866bfe9697ccec5ffb1002da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 5 Dec 2025 11:37:37 +0800 Subject: [PATCH 1/5] chore: use weak instead --- src/BaseSelect/index.tsx | 14 ++++---- src/hooks/useOpen.ts | 48 +++++++++++++++------------- src/hooks/useSelectTriggerControl.ts | 7 ++-- 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 140a2e75..ebb6f827 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -566,7 +566,9 @@ const BaseSelect = React.forwardRef((props, ref) const onRootBlur = () => { macroTask(() => { if (!isInside(getSelectElements(), document.activeElement as HTMLElement)) { - triggerOpen(false); + triggerOpen(false, { + weak: true, + }); } }); }; @@ -593,16 +595,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 +747,7 @@ const BaseSelect = React.forwardRef((props, ref) // Token handling tokenWithEnter={tokenWithEnter} // Open - onMouseDown={onInternalMouseDown} + onMouseDown={onRootMouseDown} // Components components={mergedComponents} /> @@ -774,7 +774,7 @@ const BaseSelect = React.forwardRef((props, ref) empty={emptyOptions} onPopupVisibleChange={onTriggerVisibleChange} onPopupMouseEnter={onPopupMouseEnter} - onPopupMouseDown={onInternalMouseDown} + onPopupMouseDown={onRootMouseDown} onPopupBlur={onRootBlur} > {renderNode} diff --git a/src/hooks/useOpen.ts b/src/hooks/useOpen.ts index 4915ef66..8e6f9805 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; + weak?: boolean; }, ) => void; @@ -58,7 +57,7 @@ export default function useOpen( const mergedOpen = postOpen(ssrSafeOpen); const taskIdRef = useRef(0); - const taskLockRef = useRef(false); + const weakLockRef = useRef(false); const triggerEvent = useEvent((nextOpen: boolean) => { if (onOpen && mergedOpen !== nextOpen) { @@ -68,35 +67,38 @@ export default function useOpen( }); const toggleOpen = useEvent((nextOpen, config = {}) => { - const { ignoreNext = false } = config; + const { weak = false } = 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 && + // Only weak update when not locked + (!weak || !weakLockRef.current) + ) { triggerEvent(nextOpenVal); - - // Lock if needed - if (ignoreNext) { - taskLockRef.current = ignoreNext; - - macroTask(() => { - taskLockRef.current = false; - }, 3); - } + weakLockRef.current = false; } - return; } - macroTask(() => { - if (id === taskIdRef.current && !taskLockRef.current) { - triggerEvent(nextOpenVal); - } - }); + // Since `mergedOpen` is post-processed, we need to check if the value really changed + if (!weak) { + weakLockRef.current = true; + } + + // 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..1031adff 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) => { @@ -32,7 +33,9 @@ export default function useSelectTriggerControl( !isInside(elements(), target) ) { // Should trigger close - triggerOpen(false); + triggerOpen(false, { + weak: true, + }); } }); From 1cffebdb21c2ab456be3bb2a9eb5be4979fc6c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 5 Dec 2025 11:50:43 +0800 Subject: [PATCH 2/5] chore: adjust --- src/hooks/useOpen.ts | 3 ++- tests/Select.test.tsx | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/hooks/useOpen.ts b/src/hooks/useOpen.ts index 8e6f9805..18236608 100644 --- a/src/hooks/useOpen.ts +++ b/src/hooks/useOpen.ts @@ -88,7 +88,8 @@ export default function useOpen( // Since `mergedOpen` is post-processed, we need to check if the value really changed if (!weak) { - weakLockRef.current = true; + // Only `true` need lock + weakLockRef.current = nextOpenVal; } // Weak update can be ignored 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); From fa4407bb662d7e4d9c63f93b9ededc20031d5f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 5 Dec 2025 14:42:40 +0800 Subject: [PATCH 3/5] chore: adjust --- src/BaseSelect/index.tsx | 13 ++++++------- src/SelectInput/index.tsx | 5 +++-- src/hooks/useOpen.ts | 11 +++++++---- src/hooks/useSelectTriggerControl.ts | 9 +++++---- tests/Select.test.tsx | 1 + 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index ebb6f827..05e7736e 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -564,13 +564,12 @@ const BaseSelect = React.forwardRef((props, ref) }; const onRootBlur = () => { - macroTask(() => { - if (!isInside(getSelectElements(), document.activeElement as HTMLElement)) { - triggerOpen(false, { - weak: true, - }); - } - }); + // Delay close should check the activeElement + if (mergedOpen) { + triggerOpen(false, { + cancelFun: () => isInside(getSelectElements(), document.activeElement as HTMLElement), + }); + } }; const onInternalBlur: React.FocusEventHandler = (event) => { 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 18236608..2657b50f 100644 --- a/src/hooks/useOpen.ts +++ b/src/hooks/useOpen.ts @@ -26,6 +26,7 @@ export type TriggerOpenType = ( nextOpen?: boolean, config?: { weak?: boolean; + cancelFun?: () => boolean; }, ) => void; @@ -67,7 +68,7 @@ export default function useOpen( }); const toggleOpen = useEvent((nextOpen, config = {}) => { - const { weak = false } = config; + const { weak = false, cancelFun } = config; taskIdRef.current += 1; const id = taskIdRef.current; @@ -79,17 +80,18 @@ export default function useOpen( // Always check if id is match id === taskIdRef.current && // Only weak update when not locked - (!weak || !weakLockRef.current) + (!weak || !weakLockRef.current) && + // Check if need to cancel + !cancelFun?.() ) { triggerEvent(nextOpenVal); - weakLockRef.current = false; } } // Since `mergedOpen` is post-processed, we need to check if the value really changed if (!weak) { // Only `true` need lock - weakLockRef.current = nextOpenVal; + weakLockRef.current = true; } // Weak update can be ignored @@ -98,6 +100,7 @@ export default function useOpen( } else { macroTask(() => { triggerUpdate(); + weakLockRef.current = false; }); } }); diff --git a/src/hooks/useSelectTriggerControl.ts b/src/hooks/useSelectTriggerControl.ts index 1031adff..347cd05d 100644 --- a/src/hooks/useSelectTriggerControl.ts +++ b/src/hooks/useSelectTriggerControl.ts @@ -26,16 +26,17 @@ 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 - triggerOpen(false, { - weak: true, - }); + triggerOpen(false); } }); diff --git a/tests/Select.test.tsx b/tests/Select.test.tsx index 27531303..d4456de8 100644 --- a/tests/Select.test.tsx +++ b/tests/Select.test.tsx @@ -1951,6 +1951,7 @@ describe('Select.Basic', () => { toggleOpen(container); + console.log('!~~~~~~~~'); const clickEvent = new Event('mousedown'); Object.defineProperty(clickEvent, 'target', { get: () => document.body, From 8cfd9ca28806f461af7c93a2c98f837cef8822a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 5 Dec 2025 14:43:56 +0800 Subject: [PATCH 4/5] chore: clean up --- src/hooks/useOpen.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/hooks/useOpen.ts b/src/hooks/useOpen.ts index 2657b50f..53c6d92c 100644 --- a/src/hooks/useOpen.ts +++ b/src/hooks/useOpen.ts @@ -25,7 +25,6 @@ export const macroTask = (fn: VoidFunction, times = 1) => { export type TriggerOpenType = ( nextOpen?: boolean, config?: { - weak?: boolean; cancelFun?: () => boolean; }, ) => void; @@ -58,7 +57,6 @@ export default function useOpen( const mergedOpen = postOpen(ssrSafeOpen); const taskIdRef = useRef(0); - const weakLockRef = useRef(false); const triggerEvent = useEvent((nextOpen: boolean) => { if (onOpen && mergedOpen !== nextOpen) { @@ -68,7 +66,7 @@ export default function useOpen( }); const toggleOpen = useEvent((nextOpen, config = {}) => { - const { weak = false, cancelFun } = config; + const { cancelFun } = config; taskIdRef.current += 1; const id = taskIdRef.current; @@ -79,8 +77,6 @@ export default function useOpen( if ( // Always check if id is match id === taskIdRef.current && - // Only weak update when not locked - (!weak || !weakLockRef.current) && // Check if need to cancel !cancelFun?.() ) { @@ -88,19 +84,12 @@ export default function useOpen( } } - // Since `mergedOpen` is post-processed, we need to check if the value really changed - if (!weak) { - // Only `true` need lock - weakLockRef.current = true; - } - // Weak update can be ignored if (nextOpenVal) { triggerUpdate(); } else { macroTask(() => { triggerUpdate(); - weakLockRef.current = false; }); } }); From 8c4da6476a1121c1a5ff65d1ba46dc50f8ea99b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 5 Dec 2025 14:49:21 +0800 Subject: [PATCH 5/5] chore: clean up --- src/BaseSelect/index.tsx | 2 +- tests/Select.test.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 05e7736e..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'; diff --git a/tests/Select.test.tsx b/tests/Select.test.tsx index d4456de8..27531303 100644 --- a/tests/Select.test.tsx +++ b/tests/Select.test.tsx @@ -1951,7 +1951,6 @@ describe('Select.Basic', () => { toggleOpen(container); - console.log('!~~~~~~~~'); const clickEvent = new Event('mousedown'); Object.defineProperty(clickEvent, 'target', { get: () => document.body,