diff --git a/packages/core/src/Select/Select.test.ts b/packages/core/src/Select/Select.test.ts index 95eb4d8671..792b309aff 100644 --- a/packages/core/src/Select/Select.test.ts +++ b/packages/core/src/Select/Select.test.ts @@ -38,6 +38,69 @@ describe('given default Select', () => { expect(selectTrigger.attributes('data-placeholder')).toBe('') }) + describe('trigger mouse interop', () => { + async function openSelectWithMouseClick() { + const button = wrapper.find('button') + // Open on pointerdown, then emit the compatibility mouse events that follow in browsers. + await button.trigger('pointerdown', { button: 0, ctrlKey: false }) + fireEvent.mouseDown(button.element, { button: 0, ctrlKey: false }) + fireEvent.mouseUp(button.element, { button: 0, ctrlKey: false }) + fireEvent.click(button.element, { button: 0, ctrlKey: false }) + await nextTick() + await nextTick() + } + + it('should not suppress window mousedown listeners when opening (#1773)', async () => { + const button = wrapper.find('button').element + const onWindowMousedown = vi.fn() + window.addEventListener('mousedown', onWindowMousedown, true) + + fireEvent.pointerDown(button, { button: 0, ctrlKey: false, pointerType: 'mouse' }) + const mousedownEvent = new MouseEvent('mousedown', { + bubbles: true, + cancelable: true, + button: 0, + ctrlKey: false, + }) + button.dispatchEvent(mousedownEvent) + + expect(onWindowMousedown).toHaveBeenCalled() + expect(mousedownEvent.defaultPrevented).toBe(true) + + window.removeEventListener('mousedown', onWindowMousedown, true) + }) + + it('should focus the trigger on click without a preceding pointerdown', async () => { + const trigger = wrapper.find('[role="combobox"]').element as HTMLElement + const focusSpy = vi.spyOn(trigger, 'focus') + + fireEvent.click(trigger, { button: 0, ctrlKey: false }) + + expect(focusSpy).toHaveBeenCalled() + focusSpy.mockRestore() + }) + + it('should not re-focus the trigger on click after opening via pointerdown', async () => { + const trigger = wrapper.find('[role="combobox"]').element as HTMLElement + const focusSpy = vi.spyOn(trigger, 'focus') + + await wrapper.find('button').trigger('pointerdown', { button: 0, ctrlKey: false }) + fireEvent.click(trigger, { button: 0, ctrlKey: false }) + + expect(focusSpy).not.toHaveBeenCalled() + focusSpy.mockRestore() + }) + + it('should not leave focus on the trigger after opening via mouse click', async () => { + const trigger = wrapper.find('[role="combobox"]').element + + await openSelectWithMouseClick() + + expect(wrapper.html()).toContain('Apple') + expect(document.activeElement).not.toBe(trigger) + }) + }) + describe('opening the modal', () => { beforeEach(async () => { await wrapper.find('button').trigger('pointerdown', { diff --git a/packages/core/src/Select/SelectTrigger.vue b/packages/core/src/Select/SelectTrigger.vue index c87a281a2e..7b5b243f25 100644 --- a/packages/core/src/Select/SelectTrigger.vue +++ b/packages/core/src/Select/SelectTrigger.vue @@ -47,6 +47,52 @@ function handlePointerOpen(event: PointerEvent) { y: Math.round(event.pageY), } } + +function isPlainLeftClick(event: MouseEvent) { + return event.button === 0 && event.ctrlKey === false +} + +// Tracks direct mouse presses handled in `pointerdown` so the Safari label +// `click` workaround below does not re-focus the trigger after opening. +let openedFromPointerDown = false + +function onTriggerPointerDown(event: PointerEvent) { + // Prevent opening on touch down. + // https://github.com/unovue/reka-ui/issues/804 + if (event.pointerType === 'touch') + return event.preventDefault() + + // prevent implicit pointer capture + // https://www.w3.org/TR/pointerevents3/#implicit-pointer-capture + const target = event.target as HTMLElement + if (target.hasPointerCapture(event.pointerId)) + target.releasePointerCapture(event.pointerId) + + // only call handler if it's the left button (mousedown gets triggered by all mouse buttons) + // but not when the control key is pressed (avoiding MacOS right click) + if (isPlainLeftClick(event)) { + handlePointerOpen(event) + openedFromPointerDown = true + } +} + +function onTriggerMouseDown(event: MouseEvent) { + // Prevent trigger from stealing focus from the active item after opening. + // We avoid calling `preventDefault` in `pointerdown` because that suppresses + // compatibility mouse events (`mousedown`, `mouseup`, `click`). + if (isPlainLeftClick(event)) + event.preventDefault() +} + +function onTriggerClick(event: MouseEvent) { + // Safari: label-associated clicks may not run `pointerdown` on the trigger. + // Direct mouse clicks open in `pointerdown` and must not re-focus the trigger + // here — `mousedown` `preventDefault` does not suppress `click`. + if (!openedFromPointerDown) + (event.currentTarget as HTMLElement)?.focus() + + openedFromPointerDown = false +}