|
1 | 1 | import type { DOMWrapper, VueWrapper } from '@vue/test-utils' |
2 | 2 | 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' |
4 | 4 | import { mount } from '@vue/test-utils' |
5 | 5 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' |
6 | 6 | import { axe } from 'vitest-axe' |
@@ -50,6 +50,23 @@ const DialogTest = defineComponent({ |
50 | 50 | </DialogRoot>`, |
51 | 51 | }) |
52 | 52 |
|
| 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 | + |
53 | 70 | describe('given a default Dialog', () => { |
54 | 71 | let wrapper: VueWrapper<InstanceType<typeof DialogTest>> |
55 | 72 | let trigger: DOMWrapper<HTMLElement> |
@@ -162,3 +179,45 @@ describe('given a default Dialog', () => { |
162 | 179 | }) |
163 | 180 | }) |
164 | 181 | }) |
| 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 | +}) |
0 commit comments