Skip to content

Commit 396ba29

Browse files
zernoniaclaude
andauthored
fix(Dialog): scope overlay pointerdown prevention to the overlay itself (#2668)
#2655 added `@pointerdown.left.prevent` on the overlay so focus stays on the trigger after the dialog closes. When `DialogContent` is nested inside `DialogOverlay` (a common centering pattern), that handler also fired for pointerdown events bubbling up from controls inside the content, calling `preventDefault()` on them. That suppressed native behavior — `<select>` dropdowns wouldn't open and `<input>`/`<textarea>` wouldn't focus on click. Add the `.self` modifier so the prevention only applies when the pointer lands directly on the overlay backdrop, preserving the #2655 behavior while leaving descendant interactions untouched. Fixes #2660 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d3df692 commit 396ba29

2 files changed

Lines changed: 61 additions & 2 deletions

File tree

packages/core/src/Dialog/Dialog.test.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { DOMWrapper, VueWrapper } from '@vue/test-utils'
22
import type { Mock, SpyInstance } from 'vitest'
3-
import { findByText, fireEvent, render } from '@testing-library/vue'
3+
import { createEvent, findByText, fireEvent, render } from '@testing-library/vue'
44
import { mount } from '@vue/test-utils'
55
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
66
import { axe } from 'vitest-axe'
@@ -50,6 +50,23 @@ const DialogTest = defineComponent({
5050
</DialogRoot>`,
5151
})
5252

53+
// Reproduces https://github.com/unovue/reka-ui/issues/2660 — the content is
54+
// nested *inside* the overlay (a common centering pattern), so pointerdown
55+
// events from controls in the content bubble up to the overlay.
56+
const NestedContentDialogTest = defineComponent({
57+
components: { DialogRoot, DialogTrigger, DialogOverlay, DialogContent, DialogClose, DialogTitle },
58+
template: `<DialogRoot>
59+
<DialogTrigger>${OPEN_TEXT}</DialogTrigger>
60+
<DialogOverlay>
61+
<DialogContent>
62+
<DialogTitle>${TITLE_TEXT}</DialogTitle>
63+
<input data-testid="text-input" type="text">
64+
<DialogClose>${CLOSE_TEXT}</DialogClose>
65+
</DialogContent>
66+
</DialogOverlay>
67+
</DialogRoot>`,
68+
})
69+
5370
describe('given a default Dialog', () => {
5471
let wrapper: VueWrapper<InstanceType<typeof DialogTest>>
5572
let trigger: DOMWrapper<HTMLElement>
@@ -162,3 +179,45 @@ describe('given a default Dialog', () => {
162179
})
163180
})
164181
})
182+
183+
// Regression test for https://github.com/unovue/reka-ui/issues/2660
184+
// The overlay calls `preventDefault()` on its own `pointerdown` to keep focus
185+
// on the trigger after the dialog closes (#2655). When the content is nested
186+
// inside the overlay, that handler must NOT prevent the default action of
187+
// pointerdown events bubbling up from controls inside the content — otherwise
188+
// native `<select>`/`<input>` interactions break.
189+
describe('given a Dialog with content nested inside the overlay', () => {
190+
let wrapper: VueWrapper<InstanceType<typeof NestedContentDialogTest>>
191+
let consoleWarnMock: SpyInstance
192+
193+
beforeEach(async () => {
194+
document.body.innerHTML = ''
195+
wrapper = mount(NestedContentDialogTest, { attachTo: document.body })
196+
consoleWarnMock = vi.spyOn(console, 'warn').mockImplementation(() => {})
197+
fireEvent.click(wrapper.find('button').element)
198+
await findByText(document.body, CLOSE_TEXT)
199+
})
200+
201+
afterEach(() => {
202+
consoleWarnMock.mockRestore()
203+
})
204+
205+
describe('when pressing down on a control inside the content', () => {
206+
it('should not prevent the default pointerdown action', async () => {
207+
const input = document.querySelector<HTMLInputElement>('[data-testid="text-input"]')!
208+
const event = createEvent.pointerDown(input, { button: 0 })
209+
input.dispatchEvent(event)
210+
await nextTick()
211+
212+
expect(event.defaultPrevented).toBe(false)
213+
})
214+
215+
it('should keep the dialog open', async () => {
216+
const input = document.querySelector<HTMLInputElement>('[data-testid="text-input"]')!
217+
await fireEvent.pointerDown(input, { button: 0 })
218+
await nextTick()
219+
220+
expect(document.body.innerHTML).toContain(CLOSE_TEXT)
221+
})
222+
})
223+
})

packages/core/src/Dialog/DialogOverlayImpl.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ useForwardExpose()
2323
:as-child="asChild"
2424
:data-state="rootContext.open.value ? 'open' : 'closed'"
2525
style="pointer-events: auto"
26-
@pointerdown.left.prevent
26+
@pointerdown.left.self.prevent
2727
>
2828
<slot />
2929
</Primitive>

0 commit comments

Comments
 (0)