diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 1cd445cd..dc5be9dd 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -7,7 +7,7 @@ import { useAllowClear } from '../hooks/useAllowClear'; import { BaseSelectContext } from '../hooks/useBaseProps'; import type { BaseSelectContextProps } from '../hooks/useBaseProps'; import useLock from '../hooks/useLock'; -import useSelectTriggerControl from '../hooks/useSelectTriggerControl'; +import useSelectTriggerControl, { isInside } from '../hooks/useSelectTriggerControl'; import type { DisplayInfoType, DisplayValueType, @@ -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 from '../hooks/useOpen'; +import useOpen, { macroTask } from '../hooks/useOpen'; import { useEvent } from '@rc-component/util'; import type { SelectInputRef } from '../SelectInput'; import SelectInput from '../SelectInput'; @@ -537,6 +537,15 @@ const BaseSelect = React.forwardRef((props, ref) keyLockRef.current = false; }; + // ========================== Focus / Blur ========================== + const getSelectElements = () => [ + getDOM(containerRef.current), + triggerRef.current?.getPopupElement(), + ]; + + // Close when click on non-select element + useSelectTriggerControl(getSelectElements, mergedOpen, triggerOpen, !!mergedComponents.root); + // ========================== Focus / Blur ========================== /** Record real focus status */ // const focusRef = React.useRef(false); @@ -554,6 +563,14 @@ const BaseSelect = React.forwardRef((props, ref) } }; + const onRootBlur = () => { + macroTask(() => { + if (!isInside(getSelectElements(), document.activeElement as HTMLElement)) { + triggerOpen(false); + } + }); + }; + const onInternalBlur: React.FocusEventHandler = (event) => { setFocused(false); @@ -569,6 +586,8 @@ const BaseSelect = React.forwardRef((props, ref) } } + onRootBlur(); + if (!disabled) { onBlur?.(event); } @@ -604,14 +623,6 @@ const BaseSelect = React.forwardRef((props, ref) }; } - // Close when click on non-select element - useSelectTriggerControl( - () => [getDOM(containerRef.current), triggerRef.current?.getPopupElement()], - mergedOpen, - triggerOpen, - !!mergedComponents.root, - ); - // ============================ Context ============================= const baseSelectContext = React.useMemo( () => ({ @@ -764,6 +775,7 @@ const BaseSelect = React.forwardRef((props, ref) onPopupVisibleChange={onTriggerVisibleChange} onPopupMouseEnter={onPopupMouseEnter} onPopupMouseDown={onInternalMouseDown} + onPopupBlur={onRootBlur} > {renderNode} diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index 619be16e..42555137 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -11,7 +11,6 @@ import { clsx } from 'clsx'; import type { ComponentsConfig } from '../hooks/useComponents'; import { getDOM } from '@rc-component/util/lib/Dom/findDOMNode'; import { composeRef } from '@rc-component/util/lib/ref'; -import { macroTask } from '../hooks/useOpen'; export interface SelectInputRef { focus: (options?: FocusOptions) => void; @@ -101,7 +100,6 @@ export default React.forwardRef(function Selec // Events onMouseDown, - onBlur, onClearMouseDown, onInputKeyDown, onSelectorRemove, @@ -203,20 +201,6 @@ export default React.forwardRef(function Selec onMouseDown?.(event); }); - const onInternalBlur: SelectInputProps['onBlur'] = (event) => { - macroTask(() => { - const inputNode = getDOM(inputRef.current); - if ( - !inputNode || - (inputNode !== document.activeElement && !inputNode.contains(document.activeElement)) - ) { - toggleOpen(false); - } - }); - - onBlur?.(event); - }; - // =================== Components =================== const { root: RootComponent } = components; @@ -250,7 +234,6 @@ export default React.forwardRef(function Selec style={style} // Mouse Events onMouseDown={onInternalMouseDown} - onBlur={onInternalBlur} > {/* Prefix */} diff --git a/src/SelectTrigger.tsx b/src/SelectTrigger.tsx index a44df91a..bbf719fa 100644 --- a/src/SelectTrigger.tsx +++ b/src/SelectTrigger.tsx @@ -77,6 +77,7 @@ export interface SelectTriggerProps { onPopupMouseEnter: () => void; onPopupMouseDown: React.MouseEventHandler; + onPopupBlur?: React.FocusEventHandler; } const SelectTrigger: React.ForwardRefRenderFunction = ( @@ -104,6 +105,7 @@ const SelectTrigger: React.ForwardRefRenderFunction +
{popupNode}
} diff --git a/src/hooks/useOpen.ts b/src/hooks/useOpen.ts index bf08663f..4915ef66 100644 --- a/src/hooks/useOpen.ts +++ b/src/hooks/useOpen.ts @@ -86,7 +86,7 @@ export default function useOpen( macroTask(() => { taskLockRef.current = false; - }, 2); + }, 3); } } return; diff --git a/src/hooks/useSelectTriggerControl.ts b/src/hooks/useSelectTriggerControl.ts index 3d1de438..811c4dd2 100644 --- a/src/hooks/useSelectTriggerControl.ts +++ b/src/hooks/useSelectTriggerControl.ts @@ -1,6 +1,12 @@ import * as React from 'react'; import { useEvent } from '@rc-component/util'; +export function isInside(elements: (HTMLElement | SVGElement | undefined)[], target: HTMLElement) { + return elements + .filter((element) => element) + .some((element) => element.contains(target) || element === target); +} + export default function useSelectTriggerControl( elements: () => (HTMLElement | SVGElement | undefined)[], open: boolean, @@ -23,9 +29,7 @@ export default function useSelectTriggerControl( open && // Marked by SelectInput mouseDown event !(event as any)._ignore_global_close && - elements() - .filter((element) => element) - .every((element) => !element.contains(target) && element !== target) + !isInside(elements(), target) ) { // Should trigger close triggerOpen(false); diff --git a/tests/focus.test.tsx b/tests/focus.test.tsx index af042e14..104352fe 100644 --- a/tests/focus.test.tsx +++ b/tests/focus.test.tsx @@ -3,10 +3,16 @@ import Select from '../src'; import { fireEvent, render } from '@testing-library/react'; describe('Select.Focus', () => { - it('disabled should reset focused', () => { - jest.clearAllTimers(); + beforeEach(() => { jest.useFakeTimers(); + }); + afterEach(() => { + jest.clearAllTimers(); + jest.useRealTimers(); + }); + + it('disabled should reset focused', () => { jest.clearAllTimers(); const { container, rerender } = render( ( +
+ +
+ )} + />, + ); + + const selectInput = container.querySelector('input.rc-select-input') as HTMLElement; + const customInput = container.querySelector('.custom-input') as HTMLElement; + + fireEvent.focus(selectInput); + selectInput.focus(); + fireEvent.blur(selectInput); + + // Focus custom input should not close popup + fireEvent.focus(customInput); + selectInput.focus(); + + act(() => { + jest.runAllTimers(); + }); + + expect(onPopupVisibleChange).not.toHaveBeenCalled(); + + // Click on the popup element will blur to document but should not close + fireEvent.mouseDown(container.querySelector('.bamboo')); + fireEvent.blur(customInput); + document.body.focus(); + + act(() => { + jest.runAllTimers(); + }); + + expect(onPopupVisibleChange).not.toHaveBeenCalled(); + + // Click on the body should close the popup + fireEvent.mouseDown(document.body); + act(() => { + jest.runAllTimers(); + }); + + expect(onPopupVisibleChange).toHaveBeenCalledWith(false); }); }); diff --git a/tests/setup.ts b/tests/setup.ts index 1aba7af6..f6b5042e 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -11,7 +11,7 @@ window.MessageChannel = class { if (port._target && typeof port._target.onmessage === 'function') { port._target.onmessage({ data: message }); } - }, 0); + }, 10); }, _target: null, };