diff --git a/packages/react/src/select/item/SelectItem.test.tsx b/packages/react/src/select/item/SelectItem.test.tsx
index 44e7bc203de..2dbe08aa357 100644
--- a/packages/react/src/select/item/SelectItem.test.tsx
+++ b/packages/react/src/select/item/SelectItem.test.tsx
@@ -203,6 +203,37 @@ describe('', () => {
expect(handleClick).toHaveBeenCalledOnce();
});
+ it('should select an unhighlighted item with the mouse', async () => {
+ await render(
+
+
+
+
+
+
+
+ one
+ two
+
+
+
+ ,
+ );
+
+ const option = screen.getByRole('option', { name: 'two' });
+
+ expect(option).not.toHaveAttribute('data-highlighted');
+
+ fireEvent.pointerDown(option, { pointerType: 'mouse' });
+ fireEvent.mouseDown(option);
+ fireEvent.mouseUp(option);
+ fireEvent.click(option, { detail: 1 });
+
+ await waitFor(() => {
+ expect(screen.getByTestId('value').textContent).toBe('two');
+ });
+ });
+
it('should focus the selected item upon opening the popup', async () => {
const { user } = await render(
@@ -372,6 +403,73 @@ describe('', () => {
expect(handleClick).toHaveBeenCalledOnce();
});
+ it('should select via drag-to-select when hover highlighting is disabled', async () => {
+ ignoreActWarnings();
+
+ await renderFakeTimers(
+
+
+
+
+
+
+
+ one
+ two
+
+
+
+ ,
+ );
+
+ const trigger = screen.getByTestId('trigger');
+ fireEvent.mouseDown(trigger);
+ await waitFor(() => expect(screen.queryByRole('listbox')).not.toBe(null));
+
+ const option = screen.getByRole('option', { name: 'two' });
+ fireEvent.pointerEnter(option, { pointerType: 'mouse' });
+ fireEvent.pointerMove(option, { pointerType: 'mouse' });
+
+ expect(option).not.toHaveAttribute('data-highlighted');
+
+ await act(async () => {
+ await clock.tickAsync(500);
+ });
+ fireEvent.mouseUp(option);
+
+ await waitFor(() => expect(screen.getByTestId('value').textContent).toBe('two'));
+ });
+
+ it('should ignore an opening click that did not start on the item', async () => {
+ ignoreActWarnings();
+
+ await renderFakeTimers(
+
+
+
+
+
+
+
+ one
+ two
+
+
+
+ ,
+ );
+
+ const trigger = screen.getByTestId('trigger');
+ fireEvent.pointerDown(trigger, { pointerType: 'mouse' });
+ fireEvent.mouseDown(trigger);
+ await waitFor(() => expect(screen.queryByRole('listbox')).not.toBe(null));
+
+ const option = screen.getByRole('option', { name: 'one' });
+ fireEvent.click(option, { detail: 1 });
+
+ expect(screen.getByTestId('value').textContent).toBe('Select font');
+ });
+
it('should not select item when onClick calls preventBaseUIHandler during drag-to-select', async () => {
ignoreActWarnings();
const handleClick = vi.fn((event) => event.preventBaseUIHandler());
diff --git a/packages/react/src/select/item/SelectItem.tsx b/packages/react/src/select/item/SelectItem.tsx
index be719665685..5e2cc43cc3d 100644
--- a/packages/react/src/select/item/SelectItem.tsx
+++ b/packages/react/src/select/item/SelectItem.tsx
@@ -114,7 +114,7 @@ export const SelectItem = React.memo(
const lastKeyRef = React.useRef(null);
const pointerTypeRef = React.useRef<'mouse' | 'touch' | 'pen'>('mouse');
- const didPointerDownRef = React.useRef(false);
+ const allowMouseSelectionRef = React.useRef(false);
const { getButtonProps, buttonRef } = useButton({
disabled,
@@ -151,6 +151,7 @@ export const SelectItem = React.memo(
'aria-selected': selected,
tabIndex: highlighted ? 0 : -1,
onTouchStart() {
+ allowMouseSelectionRef.current = false;
selectionRef.current = {
allowSelectedMouseUp: false,
allowUnselectedMouseUp: false,
@@ -165,7 +166,14 @@ export const SelectItem = React.memo(
}
},
onClick(event) {
- didPointerDownRef.current = false;
+ const isMouseClick = event.type === 'click' && pointerTypeRef.current !== 'touch';
+ const isVirtualClick = isMouseClick && event.nativeEvent.detail === 0;
+ // With alignItemWithTrigger, opening can place an item under the cursor. Real mouse
+ // clicks must start on the item, while virtual clicks keep the highlighted-item guard.
+ const isInvalidMouseClick =
+ isMouseClick && !allowMouseSelectionRef.current && (isVirtualClick ? !highlighted : true);
+
+ allowMouseSelectionRef.current = false;
// Prevent double commit on {Enter}
if (event.type === 'keydown' && lastKeyRef.current === null) {
@@ -175,7 +183,7 @@ export const SelectItem = React.memo(
if (
disabled ||
(event.type === 'keydown' && lastKeyRef.current === ' ' && typingRef.current) ||
- (pointerTypeRef.current !== 'touch' && !highlighted)
+ isInvalidMouseClick
) {
return;
}
@@ -188,31 +196,28 @@ export const SelectItem = React.memo(
},
onPointerDown(event) {
pointerTypeRef.current = event.pointerType;
- didPointerDownRef.current = true;
+ allowMouseSelectionRef.current = true;
},
onMouseUp() {
if (disabled) {
return;
}
- // Regular click (pointerdown on this element) if didPointerDownRef is set, otherwise drag-to-select
- if (didPointerDownRef.current) {
- didPointerDownRef.current = false;
+ // Regular clicks are committed by the click event.
+ if (allowMouseSelectionRef.current) {
return;
}
const disallowSelectedMouseUp = !selectionRef.current.allowSelectedMouseUp && selected;
const disallowUnselectedMouseUp = !selectionRef.current.allowUnselectedMouseUp && !selected;
- if (
- disallowSelectedMouseUp ||
- disallowUnselectedMouseUp ||
- (pointerTypeRef.current !== 'touch' && !highlighted)
- ) {
+ if (disallowSelectedMouseUp || disallowUnselectedMouseUp) {
return;
}
+ allowMouseSelectionRef.current = true;
itemRef.current?.click();
+ allowMouseSelectionRef.current = false;
},
};