Skip to content

Commit 6a22b76

Browse files
authored
fix: popupRender close the Select (#1180)
* chore: extract func * fix: blur logic * test: add test case * chore: clean up
1 parent f32cdd8 commit 6a22b76

File tree

7 files changed

+93
-40
lines changed

7 files changed

+93
-40
lines changed

src/BaseSelect/index.tsx

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useAllowClear } from '../hooks/useAllowClear';
77
import { BaseSelectContext } from '../hooks/useBaseProps';
88
import type { BaseSelectContextProps } from '../hooks/useBaseProps';
99
import useLock from '../hooks/useLock';
10-
import useSelectTriggerControl from '../hooks/useSelectTriggerControl';
10+
import useSelectTriggerControl, { isInside } from '../hooks/useSelectTriggerControl';
1111
import type {
1212
DisplayInfoType,
1313
DisplayValueType,
@@ -21,7 +21,7 @@ import type { RefTriggerProps } from '../SelectTrigger';
2121
import SelectTrigger from '../SelectTrigger';
2222
import { getSeparatedContent, isValidCount } from '../utils/valueUtil';
2323
import Polite from './Polite';
24-
import useOpen from '../hooks/useOpen';
24+
import useOpen, { macroTask } from '../hooks/useOpen';
2525
import { useEvent } from '@rc-component/util';
2626
import type { SelectInputRef } from '../SelectInput';
2727
import SelectInput from '../SelectInput';
@@ -537,6 +537,15 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
537537
keyLockRef.current = false;
538538
};
539539

540+
// ========================== Focus / Blur ==========================
541+
const getSelectElements = () => [
542+
getDOM(containerRef.current),
543+
triggerRef.current?.getPopupElement(),
544+
];
545+
546+
// Close when click on non-select element
547+
useSelectTriggerControl(getSelectElements, mergedOpen, triggerOpen, !!mergedComponents.root);
548+
540549
// ========================== Focus / Blur ==========================
541550
/** Record real focus status */
542551
// const focusRef = React.useRef<boolean>(false);
@@ -554,6 +563,14 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
554563
}
555564
};
556565

566+
const onRootBlur = () => {
567+
macroTask(() => {
568+
if (!isInside(getSelectElements(), document.activeElement as HTMLElement)) {
569+
triggerOpen(false);
570+
}
571+
});
572+
};
573+
557574
const onInternalBlur: React.FocusEventHandler<HTMLElement> = (event) => {
558575
setFocused(false);
559576

@@ -569,6 +586,8 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
569586
}
570587
}
571588

589+
onRootBlur();
590+
572591
if (!disabled) {
573592
onBlur?.(event);
574593
}
@@ -604,14 +623,6 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
604623
};
605624
}
606625

607-
// Close when click on non-select element
608-
useSelectTriggerControl(
609-
() => [getDOM(containerRef.current), triggerRef.current?.getPopupElement()],
610-
mergedOpen,
611-
triggerOpen,
612-
!!mergedComponents.root,
613-
);
614-
615626
// ============================ Context =============================
616627
const baseSelectContext = React.useMemo<BaseSelectContextProps>(
617628
() => ({
@@ -764,6 +775,7 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
764775
onPopupVisibleChange={onTriggerVisibleChange}
765776
onPopupMouseEnter={onPopupMouseEnter}
766777
onPopupMouseDown={onInternalMouseDown}
778+
onPopupBlur={onRootBlur}
767779
>
768780
{renderNode}
769781
</SelectTrigger>

src/SelectInput/index.tsx

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { clsx } from 'clsx';
1111
import type { ComponentsConfig } from '../hooks/useComponents';
1212
import { getDOM } from '@rc-component/util/lib/Dom/findDOMNode';
1313
import { composeRef } from '@rc-component/util/lib/ref';
14-
import { macroTask } from '../hooks/useOpen';
1514

1615
export interface SelectInputRef {
1716
focus: (options?: FocusOptions) => void;
@@ -101,7 +100,6 @@ export default React.forwardRef<SelectInputRef, SelectInputProps>(function Selec
101100

102101
// Events
103102
onMouseDown,
104-
onBlur,
105103
onClearMouseDown,
106104
onInputKeyDown,
107105
onSelectorRemove,
@@ -203,20 +201,6 @@ export default React.forwardRef<SelectInputRef, SelectInputProps>(function Selec
203201
onMouseDown?.(event);
204202
});
205203

206-
const onInternalBlur: SelectInputProps['onBlur'] = (event) => {
207-
macroTask(() => {
208-
const inputNode = getDOM(inputRef.current);
209-
if (
210-
!inputNode ||
211-
(inputNode !== document.activeElement && !inputNode.contains(document.activeElement))
212-
) {
213-
toggleOpen(false);
214-
}
215-
});
216-
217-
onBlur?.(event);
218-
};
219-
220204
// =================== Components ===================
221205
const { root: RootComponent } = components;
222206

@@ -250,7 +234,6 @@ export default React.forwardRef<SelectInputRef, SelectInputProps>(function Selec
250234
style={style}
251235
// Mouse Events
252236
onMouseDown={onInternalMouseDown}
253-
onBlur={onInternalBlur}
254237
>
255238
{/* Prefix */}
256239
<Affix className={clsx(`${prefixCls}-prefix`, classNames?.prefix)} style={styles?.prefix}>

src/SelectTrigger.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export interface SelectTriggerProps {
7777

7878
onPopupMouseEnter: () => void;
7979
onPopupMouseDown: React.MouseEventHandler<HTMLDivElement>;
80+
onPopupBlur?: React.FocusEventHandler<HTMLDivElement>;
8081
}
8182

8283
const SelectTrigger: React.ForwardRefRenderFunction<RefTriggerProps, SelectTriggerProps> = (
@@ -104,6 +105,7 @@ const SelectTrigger: React.ForwardRefRenderFunction<RefTriggerProps, SelectTrigg
104105
onPopupVisibleChange,
105106
onPopupMouseEnter,
106107
onPopupMouseDown,
108+
onPopupBlur,
107109
...restProps
108110
} = props;
109111

@@ -165,7 +167,7 @@ const SelectTrigger: React.ForwardRefRenderFunction<RefTriggerProps, SelectTrigg
165167
prefixCls={popupPrefixCls}
166168
popupMotion={{ motionName: mergedTransitionName }}
167169
popup={
168-
<div onMouseEnter={onPopupMouseEnter} onMouseDown={onPopupMouseDown}>
170+
<div onMouseEnter={onPopupMouseEnter} onMouseDown={onPopupMouseDown} onBlur={onPopupBlur}>
169171
{popupNode}
170172
</div>
171173
}

src/hooks/useOpen.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export default function useOpen(
8686

8787
macroTask(() => {
8888
taskLockRef.current = false;
89-
}, 2);
89+
}, 3);
9090
}
9191
}
9292
return;

src/hooks/useSelectTriggerControl.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import * as React from 'react';
22
import { useEvent } from '@rc-component/util';
33

4+
export function isInside(elements: (HTMLElement | SVGElement | undefined)[], target: HTMLElement) {
5+
return elements
6+
.filter((element) => element)
7+
.some((element) => element.contains(target) || element === target);
8+
}
9+
410
export default function useSelectTriggerControl(
511
elements: () => (HTMLElement | SVGElement | undefined)[],
612
open: boolean,
@@ -23,9 +29,7 @@ export default function useSelectTriggerControl(
2329
open &&
2430
// Marked by SelectInput mouseDown event
2531
!(event as any)._ignore_global_close &&
26-
elements()
27-
.filter((element) => element)
28-
.every((element) => !element.contains(target) && element !== target)
32+
!isInside(elements(), target)
2933
) {
3034
// Should trigger close
3135
triggerOpen(false);

tests/focus.test.tsx

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@ import Select from '../src';
33
import { fireEvent, render } from '@testing-library/react';
44

55
describe('Select.Focus', () => {
6-
it('disabled should reset focused', () => {
7-
jest.clearAllTimers();
6+
beforeEach(() => {
87
jest.useFakeTimers();
8+
});
99

10+
afterEach(() => {
11+
jest.clearAllTimers();
12+
jest.useRealTimers();
13+
});
14+
15+
it('disabled should reset focused', () => {
1016
jest.clearAllTimers();
1117

1218
const { container, rerender } = render(<Select />);
@@ -24,13 +30,9 @@ describe('Select.Focus', () => {
2430
jest.runAllTimers();
2531
});
2632
expect(container.querySelector('.rc-select-focused')).toBeFalsy();
27-
28-
jest.useRealTimers();
2933
});
3034

3135
it('after onBlur is triggered the focused does not need to be reset', () => {
32-
jest.useFakeTimers();
33-
3436
const onFocus = jest.fn();
3537

3638
const Demo: React.FC = () => {
@@ -63,7 +65,57 @@ describe('Select.Focus', () => {
6365
jest.runAllTimers();
6466

6567
expect(onFocus).toHaveBeenCalled();
68+
});
6669

67-
jest.useRealTimers();
70+
it('when popupRender has custom input, focus it and trigger SelectInput blur should not close the popup', () => {
71+
const onPopupVisibleChange = jest.fn();
72+
73+
const { container } = render(
74+
<Select
75+
open
76+
onPopupVisibleChange={onPopupVisibleChange}
77+
popupRender={() => (
78+
<div className="bamboo">
79+
<input className="custom-input" />
80+
</div>
81+
)}
82+
/>,
83+
);
84+
85+
const selectInput = container.querySelector('input.rc-select-input') as HTMLElement;
86+
const customInput = container.querySelector('.custom-input') as HTMLElement;
87+
88+
fireEvent.focus(selectInput);
89+
selectInput.focus();
90+
fireEvent.blur(selectInput);
91+
92+
// Focus custom input should not close popup
93+
fireEvent.focus(customInput);
94+
selectInput.focus();
95+
96+
act(() => {
97+
jest.runAllTimers();
98+
});
99+
100+
expect(onPopupVisibleChange).not.toHaveBeenCalled();
101+
102+
// Click on the popup element will blur to document but should not close
103+
fireEvent.mouseDown(container.querySelector('.bamboo'));
104+
fireEvent.blur(customInput);
105+
document.body.focus();
106+
107+
act(() => {
108+
jest.runAllTimers();
109+
});
110+
111+
expect(onPopupVisibleChange).not.toHaveBeenCalled();
112+
113+
// Click on the body should close the popup
114+
fireEvent.mouseDown(document.body);
115+
act(() => {
116+
jest.runAllTimers();
117+
});
118+
119+
expect(onPopupVisibleChange).toHaveBeenCalledWith(false);
68120
});
69121
});

tests/setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ window.MessageChannel = class {
1111
if (port._target && typeof port._target.onmessage === 'function') {
1212
port._target.onmessage({ data: message });
1313
}
14-
}, 0);
14+
}, 10);
1515
},
1616
_target: null,
1717
};

0 commit comments

Comments
 (0)