fix(modal): clear stale data-closing flag when re-opened mid-close

If the modal is re-enabled within the 150ms close transition the
<dialog> element is still mounted and [open], so the dialogRef watcher
does not re-fire. Clear the leftover data-closing flag directly in
openDialog() so the dialog doesn't remain stuck at opacity 0.

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

View File

@ -153,4 +153,36 @@ describe('Modal.vue — open race condition (#2590)', () => {
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
// mounted and [open], so the dialogRef watcher does not re-fire. Make
// sure openDialog() clears the leftover data-closing flag itself;
// otherwise the dialog stays stuck at opacity 0.
const wrapper = mount(Modal, {
attachTo: document.body,
props: {enabled: true},
slots: {default: '<p class="test-body">hi</p>'},
})
await flushPromises()
await nextTick()
const dialog = document.querySelector('dialog.modal-dialog') as HTMLDialogElement
expect(dialog.hasAttribute('open')).toBe(true)
// Start closing — this sets data-closing and schedules the unmount.
await wrapper.setProps({enabled: false})
await nextTick()
expect(dialog.dataset.closing).toBe('')
// Re-open well before the 150ms close timer fires.
await wrapper.setProps({enabled: true})
await nextTick()
expect(dialog.dataset.closing).toBeUndefined()
expect(dialog.hasAttribute('open')).toBe(true)
wrapper.unmount()
})
})

View File

@ -97,7 +97,14 @@ function openDialog() {
previouslyFocused.value = document.activeElement
showDialog.value = true
document.body.style.overflow = 'hidden'
// The actual `showModal()` call happens in the `watch(dialogRef, )`
// If we're re-opening while the previous close transition is still in
// flight the <dialog> is still mounted and [open], so the dialogRef
// watcher below won't re-fire. Clear the data-closing flag here so the
// dialog doesn't stay stuck at opacity 0.
if (dialogRef.value) {
delete dialogRef.value.dataset.closing
}
// The initial `showModal()` call happens in the `watch(dialogRef, )`
// below, which fires the moment Vue mounts the <dialog>. We cannot call
// it synchronously here because the element is not in the DOM yet
// (v-if="showDialog" only just became true), and we cannot rely on a