Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions src/BaseSelect/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
Expand Down Expand Up @@ -537,6 +537,15 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((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<boolean>(false);
Expand All @@ -554,6 +563,14 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
}
};

const onRootBlur = () => {
macroTask(() => {
if (!isInside(getSelectElements(), document.activeElement as HTMLElement)) {
triggerOpen(false);
}
});
};

const onInternalBlur: React.FocusEventHandler<HTMLElement> = (event) => {
setFocused(false);

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

onRootBlur();

if (!disabled) {
onBlur?.(event);
}
Expand Down Expand Up @@ -604,14 +623,6 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((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<BaseSelectContextProps>(
() => ({
Expand Down Expand Up @@ -764,6 +775,7 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
onPopupVisibleChange={onTriggerVisibleChange}
onPopupMouseEnter={onPopupMouseEnter}
onPopupMouseDown={onInternalMouseDown}
onPopupBlur={onRootBlur}
>
{renderNode}
</SelectTrigger>
Expand Down
17 changes: 0 additions & 17 deletions src/SelectInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -101,7 +100,6 @@ export default React.forwardRef<SelectInputRef, SelectInputProps>(function Selec

// Events
onMouseDown,
onBlur,
onClearMouseDown,
onInputKeyDown,
onSelectorRemove,
Expand Down Expand Up @@ -203,20 +201,6 @@ export default React.forwardRef<SelectInputRef, SelectInputProps>(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;

Expand Down Expand Up @@ -250,7 +234,6 @@ export default React.forwardRef<SelectInputRef, SelectInputProps>(function Selec
style={style}
// Mouse Events
onMouseDown={onInternalMouseDown}
onBlur={onInternalBlur}
>
{/* Prefix */}
<Affix className={clsx(`${prefixCls}-prefix`, classNames?.prefix)} style={styles?.prefix}>
Expand Down
4 changes: 3 additions & 1 deletion src/SelectTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export interface SelectTriggerProps {

onPopupMouseEnter: () => void;
onPopupMouseDown: React.MouseEventHandler<HTMLDivElement>;
onPopupBlur?: React.FocusEventHandler<HTMLDivElement>;
}

const SelectTrigger: React.ForwardRefRenderFunction<RefTriggerProps, SelectTriggerProps> = (
Expand Down Expand Up @@ -104,6 +105,7 @@ const SelectTrigger: React.ForwardRefRenderFunction<RefTriggerProps, SelectTrigg
onPopupVisibleChange,
onPopupMouseEnter,
onPopupMouseDown,
onPopupBlur,
...restProps
} = props;

Expand Down Expand Up @@ -165,7 +167,7 @@ const SelectTrigger: React.ForwardRefRenderFunction<RefTriggerProps, SelectTrigg
prefixCls={popupPrefixCls}
popupMotion={{ motionName: mergedTransitionName }}
popup={
<div onMouseEnter={onPopupMouseEnter} onMouseDown={onPopupMouseDown}>
<div onMouseEnter={onPopupMouseEnter} onMouseDown={onPopupMouseDown} onBlur={onPopupBlur}>
{popupNode}
</div>
}
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useOpen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export default function useOpen(

macroTask(() => {
taskLockRef.current = false;
}, 2);
}, 3);
}
}
return;
Expand Down
10 changes: 7 additions & 3 deletions src/hooks/useSelectTriggerControl.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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);
Expand Down
66 changes: 59 additions & 7 deletions tests/focus.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Select />);
Expand All @@ -24,13 +30,9 @@ describe('Select.Focus', () => {
jest.runAllTimers();
});
expect(container.querySelector('.rc-select-focused')).toBeFalsy();

jest.useRealTimers();
});

it('after onBlur is triggered the focused does not need to be reset', () => {
jest.useFakeTimers();

const onFocus = jest.fn();

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

expect(onFocus).toHaveBeenCalled();
});

jest.useRealTimers();
it('when popupRender has custom input, focus it and trigger SelectInput blur should not close the popup', () => {
const onPopupVisibleChange = jest.fn();

const { container } = render(
<Select
open
onPopupVisibleChange={onPopupVisibleChange}
popupRender={() => (
<div className="bamboo">
<input className="custom-input" />
</div>
)}
/>,
);

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);
});
});
2 changes: 1 addition & 1 deletion tests/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ window.MessageChannel = class {
if (port._target && typeof port._target.onmessage === 'function') {
port._target.onmessage({ data: message });
}
}, 0);
}, 10);
},
_target: null,
};
Expand Down
Loading