fix(modal): skip showModal if enabled flipped false before mount

Re-check props.enabled inside the dialogRef watcher. The watcher fires
once Vue mounts the <dialog>, but the caller may have flipped enabled
back to false between the openDialog() call and the mount flush. In that
case the prop state is disabled and we must not open the dialog.

Addresses augmentcode review on #2604.
This commit is contained in:
kolaente 2026-04-11 20:41:48 +02:00 committed by kolaente
parent e01a599418
commit 113b77e92f
2 changed files with 32 additions and 1 deletions

View File

@ -154,6 +154,34 @@ describe('Modal.vue — open race condition (#2590)', () => {
wrapper.unmount()
})
it('does not open the dialog if enabled flips back to false before mount', async () => {
// Regression guard: the dialogRef watcher fires once the <dialog>
// element mounts. If props.enabled has flipped back to false by the
// time the mount happens, the watcher must not call showModal().
const wrapper = mount(Modal, {
attachTo: document.body,
props: {enabled: false},
slots: {default: '<p class="test-body">hi</p>'},
})
// Flip enabled true then false within the same tick, before the mount
// flush can complete.
wrapper.setProps({enabled: true})
wrapper.setProps({enabled: false})
await flushPromises()
await nextTick()
await new Promise(resolve => setTimeout(resolve, 200))
await flushPromises()
await nextTick()
// showModal must not have been called — the final prop state is
// disabled.
expect(showModalSpy).not.toHaveBeenCalled()
expect(document.querySelector('dialog.modal-dialog')).toBeNull()
wrapper.unmount()
})
it('clears data-closing when re-opened mid-close transition', async () => {
// Regression guard: if the user toggles enabled back to true while the
// 150ms close transition is still in flight, the <dialog> is still

View File

@ -147,9 +147,12 @@ watch(
// Actually call showModal() the moment the <dialog> element is mounted.
// `dialogRef` is populated by Vue during the render flush after
// `showDialog.value = true`, so this fires deterministically, no matter
// how many flushes the renderer needs (see #2590).
// how many flushes the renderer needs (see #2590). We re-check
// `props.enabled` here because the prop can flip back to `false` between
// `openDialog()` and the mount flush, in which case we must not open.
watch(dialogRef, (dialog) => {
if (!dialog) return
if (!props.enabled) return
delete dialog.dataset.closing
dialog.showModal()
})