diff --git a/packages/mui-material/src/Select/Select.test.js b/packages/mui-material/src/Select/Select.test.js index 4599548c848d92..c3818ad13c4d93 100644 --- a/packages/mui-material/src/Select/Select.test.js +++ b/packages/mui-material/src/Select/Select.test.js @@ -33,6 +33,54 @@ describe(' + none + Ten + , + ); + const trigger = getByRole('combobox'); + + // Open the menu with left mouse button + fireEvent.mouseDown(trigger, { button: 0 }); + expect(getByRole('listbox')).not.to.equal(null); + + // Simulate mouse move to initiate drag + fireEvent.mouseMove(document.body); + + // Simulate mouse up outside any menu items + fireEvent.mouseUp(document.body); + + // Menu should be closed now + expect(queryByRole('listbox', { hidden: false })).to.equal(null); + }); + + it('should not close the menu when releasing on a menu item after dragging', () => { + const { getByRole, getAllByRole } = render( + , + ); + const trigger = getByRole('combobox'); + + // Open the menu + fireEvent.mouseDown(trigger, { button: 0 }); + const options = getAllByRole('option'); + + // Simulate mouse move to initiate drag + fireEvent.mouseMove(document.body); + + // Simulate mouse up on a menu item + fireEvent.mouseUp(options[0]); + + // Menu should still be open + expect(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 58255517fe8eac..7fe7f28933eb86 100644 --- a/packages/mui-material/src/Select/SelectInput.js +++ b/packages/mui-material/src/Select/SelectInput.js @@ -162,6 +162,11 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) { const anchorElement = displayNode?.parentNode; + const [isPointerDown, setIsPointerDown] = React.useState(false); + const dragSelectRef = React.useRef({ + isDragging: false, + }); + React.useImperativeHandle( handleRef, () => ({ @@ -210,20 +215,23 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) { return undefined; }, [labelId]); - const update = (open, event) => { - if (open) { - if (onOpen) { - onOpen(event); + const update = React.useCallback( + (open, event) => { + if (open) { + if (onOpen) { + onOpen(event); + } + } else if (onClose) { + onClose(event); } - } else if (onClose) { - onClose(event); - } - if (!isOpenControlled) { - setMenuMinWidthState(autoWidth ? null : anchorElement.clientWidth); - setOpenState(open); - } - }; + if (!isOpenControlled) { + setMenuMinWidthState(autoWidth ? null : anchorElement.clientWidth); + setOpenState(open); + } + }, + [autoWidth, anchorElement, isOpenControlled, onOpen, onClose, setOpenState], + ); const handleMouseDown = (event) => { // Ignore everything but left-click @@ -234,6 +242,10 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) { event.preventDefault(); displayRef.current.focus(); + // Mark that we've initiated a pointer interaction + setIsPointerDown(true); + + // Open the menu immediately update(true, event); }; @@ -258,6 +270,56 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) { } }; + React.useEffect(() => { + if (isPointerDown) { + const doc = ownerDocument(displayRef.current); + + const handleGlobalMouseUp = (event) => { + setIsPointerDown(false); + + if (dragSelectRef.current.isDragging) { + // If we're dragging and the mouse is released, check where it was released + dragSelectRef.current.isDragging = false; + + // Check if mouse is over a menu item + const targetElement = event.target; + let menuItem = null; + + // Find if we released on a menu item (checking up the parent chain) + let current = targetElement; + while (current && !menuItem) { + // Check if this element has role="option" + if (current.getAttribute && current.getAttribute('role') === 'option') { + menuItem = current; + } + current = current.parentElement; + } + + if (!menuItem) { + // If released outside menu items, close the menu + update(false, event); + } + } + }; + + const handleGlobalMouseMove = () => { + if (isPointerDown) { + dragSelectRef.current.isDragging = true; + } + }; + + doc.addEventListener('mouseup', handleGlobalMouseUp); + doc.addEventListener('mousemove', handleGlobalMouseMove); + + return () => { + doc.removeEventListener('mouseup', handleGlobalMouseUp); + doc.removeEventListener('mousemove', handleGlobalMouseMove); + }; + } + + return undefined; + }, [isPointerDown, update]); + const handleItemClick = (child) => (event) => { let newValue;