Skip to content

Commit 4226593

Browse files
committed
fix(Select): resolve focus, drag, pointer issues
- Fixed onFocus being triggered excessively (mui#44505) - Enabled drag-and-release to select items (mui#45374) - Addressed pointer cancellation (WCAG 2.5.2) failure (mui#45301)
1 parent 6fbfc62 commit 4226593

File tree

1 file changed

+110
-24
lines changed

1 file changed

+110
-24
lines changed

Diff for: packages/mui-material/src/Select/SelectInput.js

+110-24
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
'use client';
2-
import * as React from 'react';
3-
import { isFragment } from 'react-is';
4-
import PropTypes from 'prop-types';
5-
import clsx from 'clsx';
62
import composeClasses from '@mui/utils/composeClasses';
7-
import useId from '@mui/utils/useId';
83
import refType from '@mui/utils/refType';
9-
import ownerDocument from '../utils/ownerDocument';
10-
import capitalize from '../utils/capitalize';
11-
import Menu from '../Menu/Menu';
12-
import { StyledSelectSelect, StyledSelectIcon } from '../NativeSelect/NativeSelectInput';
4+
import useId from '@mui/utils/useId';
5+
import clsx from 'clsx';
6+
import PropTypes from 'prop-types';
7+
import * as React from 'react';
8+
import { isFragment } from 'react-is';
139
import { isFilled } from '../InputBase/utils';
14-
import { styled } from '../zero-styled';
10+
import Menu from '../Menu/Menu';
11+
import { StyledSelectIcon, StyledSelectSelect } from '../NativeSelect/NativeSelectInput';
1512
import slotShouldForwardProp from '../styles/slotShouldForwardProp';
16-
import useForkRef from '../utils/useForkRef';
13+
import capitalize from '../utils/capitalize';
14+
import ownerDocument from '../utils/ownerDocument';
1715
import useControlled from '../utils/useControlled';
16+
import useForkRef from '../utils/useForkRef';
17+
import { styled } from '../zero-styled';
1818
import selectClasses, { getSelectUtilityClasses } from './selectClasses';
1919

2020
const SelectSelect = styled(StyledSelectSelect, {
@@ -162,6 +162,17 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) {
162162

163163
const anchorElement = displayNode?.parentNode;
164164

165+
const [isPointerDown, setIsPointerDown] = React.useState(false);
166+
const dragSelectRef = React.useRef({
167+
isDragging: false,
168+
startedOn: null,
169+
});
170+
171+
const focusTrackingRef = React.useRef({
172+
isFocused: false,
173+
pendingBlur: false,
174+
});
175+
165176
React.useImperativeHandle(
166177
handleRef,
167178
() => ({
@@ -210,20 +221,23 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) {
210221
return undefined;
211222
}, [labelId]);
212223

213-
const update = (open, event) => {
214-
if (open) {
215-
if (onOpen) {
216-
onOpen(event);
224+
const update = React.useCallback(
225+
(open, event) => {
226+
if (open) {
227+
if (onOpen) {
228+
onOpen(event);
229+
}
230+
} else if (onClose) {
231+
onClose(event);
217232
}
218-
} else if (onClose) {
219-
onClose(event);
220-
}
221233

222-
if (!isOpenControlled) {
223-
setMenuMinWidthState(autoWidth ? null : anchorElement.clientWidth);
224-
setOpenState(open);
225-
}
226-
};
234+
if (!isOpenControlled) {
235+
setMenuMinWidthState(autoWidth ? null : anchorElement.clientWidth);
236+
setOpenState(open);
237+
}
238+
},
239+
[autoWidth, anchorElement, isOpenControlled, onOpen, onClose, setOpenState],
240+
);
227241

228242
const handleMouseDown = (event) => {
229243
// Ignore everything but left-click
@@ -234,6 +248,11 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) {
234248
event.preventDefault();
235249
displayRef.current.focus();
236250

251+
// Mark that we've initiated a pointer interaction
252+
setIsPointerDown(true);
253+
dragSelectRef.current.startedOn = displayRef.current;
254+
255+
// Open the menu immediately
237256
update(true, event);
238257
};
239258

@@ -258,6 +277,59 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) {
258277
}
259278
};
260279

280+
React.useEffect(() => {
281+
if (isPointerDown) {
282+
const doc = ownerDocument(displayRef.current);
283+
284+
const handleGlobalMouseUp = (event) => {
285+
setIsPointerDown(false);
286+
287+
if (dragSelectRef.current.isDragging) {
288+
// If we're dragging and the mouse is released, check where it was released
289+
dragSelectRef.current.isDragging = false;
290+
291+
// Check if mouse is over a menu item
292+
const targetElement = event.target;
293+
let menuItem = null;
294+
295+
// Find if we released on a menu item (checking up the parent chain)
296+
let current = targetElement;
297+
while (current && !menuItem) {
298+
// Check if this element has role="option"
299+
if (current.getAttribute && current.getAttribute('role') === 'option') {
300+
menuItem = current;
301+
}
302+
current = current.parentElement;
303+
}
304+
305+
if (menuItem) {
306+
// Simulate a click on the menu item if we released on one
307+
menuItem.click();
308+
} else {
309+
// If released outside menu items, close the menu
310+
update(false, event);
311+
}
312+
}
313+
};
314+
315+
const handleGlobalMouseMove = () => {
316+
if (isPointerDown) {
317+
dragSelectRef.current.isDragging = true;
318+
}
319+
};
320+
321+
doc.addEventListener('mouseup', handleGlobalMouseUp);
322+
doc.addEventListener('mousemove', handleGlobalMouseMove);
323+
324+
return () => {
325+
doc.removeEventListener('mouseup', handleGlobalMouseUp);
326+
doc.removeEventListener('mousemove', handleGlobalMouseMove);
327+
};
328+
}
329+
330+
return undefined;
331+
}, [isPointerDown, update]);
332+
261333
const handleItemClick = (child) => (event) => {
262334
let newValue;
263335

@@ -326,9 +398,23 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) {
326398

327399
const open = displayNode !== null && openState;
328400

401+
const handleFocus = (event) => {
402+
// Skip duplicate focus events
403+
if (!focusTrackingRef.current.isFocused) {
404+
focusTrackingRef.current.isFocused = true;
405+
focusTrackingRef.current.pendingBlur = false;
406+
407+
if (onFocus) {
408+
onFocus(event);
409+
}
410+
}
411+
};
412+
329413
const handleBlur = (event) => {
330414
// if open event.stopImmediatePropagation
331415
if (!open && onBlur) {
416+
focusTrackingRef.current.pendingBlur = false;
417+
focusTrackingRef.current.isFocused = false;
332418
// Preact support, target is read only property on a native event.
333419
Object.defineProperty(event, 'target', { writable: true, value: { value, name } });
334420
onBlur(event);
@@ -509,7 +595,7 @@ const SelectInput = React.forwardRef(function SelectInput(props, ref) {
509595
onKeyDown={handleKeyDown}
510596
onMouseDown={disabled || readOnly ? null : handleMouseDown}
511597
onBlur={handleBlur}
512-
onFocus={onFocus}
598+
onFocus={handleFocus}
513599
{...SelectDisplayProps}
514600
ownerState={ownerState}
515601
className={clsx(SelectDisplayProps.className, classes.select, className)}

0 commit comments

Comments
 (0)