diff --git a/packages/mui-material/src/Select/Select.test.js b/packages/mui-material/src/Select/Select.test.js
index ffbc0c53770756..d2245f932d1d5b 100644
--- a/packages/mui-material/src/Select/Select.test.js
+++ b/packages/mui-material/src/Select/Select.test.js
@@ -34,6 +34,75 @@ describe('', () => {
skip: ['componentProp', 'componentsProp', 'themeVariants', 'themeStyleOverrides'],
}));
+ describe('Pointer Cancellation', () => {
+ beforeEach(function beforeEachCallback() {
+ // Run these tests only in browser because JSDOM doesn't have getBoundingClientRect() API
+ if (/jsdom/.test(window.navigator.userAgent)) {
+ this.skip();
+ }
+ });
+
+ it('should close the menu when mouse is outside the select', () => {
+ render(
+ ,
+ );
+ const trigger = screen.getByRole('combobox');
+
+ // Open the menu
+ fireEvent.mouseDown(trigger);
+ expect(screen.getByRole('listbox')).not.to.equal(null);
+
+ // Simulate mouse up outside the menu. The mouseup target is the backdrop when the menu is opened.
+ fireEvent.mouseUp(screen.getByTestId('backdrop'), { clientX: 60, clientY: 10 });
+
+ // Menu should be closed now
+ expect(screen.queryByRole('listbox', { hidden: false })).to.equal(null);
+ });
+
+ it('should not close the menu when mouse is inside the trigger', () => {
+ render(
+ ,
+ );
+ const trigger = screen.getByRole('combobox');
+
+ // Open the menu
+ fireEvent.mouseDown(trigger);
+ expect(screen.getByRole('listbox')).not.to.equal(null);
+
+ // Simulate mouse up inside the trigger
+ fireEvent.mouseUp(trigger, { clientX: 20, clientY: 20 });
+
+ // Menu should still be open
+ expect(screen.queryByRole('listbox', { hidden: false })).not.to.equal(null);
+ });
+
+ it('should not close the menu when releasing on a menu item', () => {
+ render(
+ ,
+ );
+ const trigger = screen.getByRole('combobox');
+
+ // Open the menu
+ fireEvent.mouseDown(trigger);
+ const options = screen.getAllByRole('option');
+
+ // Simulate mouse up on a menu item
+ fireEvent.mouseUp(options[0]);
+
+ // Menu should still be open
+ expect(screen.getByRole('listbox')).not.to.equal(null);
+ });
+ });
+
describe('prop: inputProps', () => {
it('should be able to provide a custom classes property', () => {
render(
diff --git a/packages/mui-material/src/Select/SelectInput.js b/packages/mui-material/src/Select/SelectInput.js
index 0b272bb98b1131..641bb5fb5ddb82 100644
--- a/packages/mui-material/src/Select/SelectInput.js
+++ b/packages/mui-material/src/Select/SelectInput.js
@@ -146,6 +146,7 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) {
const inputRef = React.useRef(null);
const displayRef = React.useRef(null);
+ const paperRef = React.useRef(null);
const [displayNode, setDisplayNode] = React.useState(null);
const { current: isOpenControlled } = React.useRef(openProp != null);
const [menuMinWidthState, setMenuMinWidthState] = React.useState();
@@ -233,6 +234,36 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) {
event.preventDefault();
displayRef.current.focus();
+ const doc = ownerDocument(event.currentTarget);
+
+ function handleMouseUp(mouseEvent) {
+ if (!displayRef.current) {
+ return;
+ }
+
+ // mouse is over the options/menuitem, don't close the menu
+ if (paperRef.current.contains(mouseEvent.target)) {
+ return;
+ }
+
+ const triggerElement = displayRef.current.getBoundingClientRect();
+
+ // mouse is inside the trigger, don't close the menu
+ if (
+ mouseEvent.clientX >= triggerElement.left &&
+ mouseEvent.clientX <= triggerElement.right &&
+ mouseEvent.clientY >= triggerElement.top &&
+ mouseEvent.clientY <= triggerElement.bottom
+ ) {
+ return;
+ }
+
+ // close the menu
+ update(false, mouseEvent);
+ }
+
+ doc.addEventListener('mouseup', handleMouseUp, { once: true });
+
update(true, event);
};
@@ -571,6 +602,7 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) {
...listProps,
},
paper: {
+ ref: paperRef,
...paperProps,
style: {
minWidth: menuMinWidth,