Skip to content

[Bug Report][4.1.7] VDialog throws "Cannot read properties of null (reading 'activatorEl')" when it unmounts while closing #23053

Description

@d--j

Environment

Vuetify Version: 4.1.7
Vue Version: 3.5.40
OS: macOS 10.15.7 (current)

Steps to reproduce

  1. Open the reproduction link and switch to the preview
  2. Click Edit item to open the dialog
  3. Click Save to close it (pressing Esc or clicking the scrim does the same)
  4. The error is caught by onErrorCaptured and shown in the alert; it is also logged to the console

The dialog in the repro is rendered with v-if="selected" and a flush: 'post' watcher
clears selected once the dialog closes — a "deselect after close" cleanup. That
combination is what puts the unmount inside the window described below; any post-flush
work that unmounts the dialog does it.

Expected Behavior

Closing a dialog that is unmounted as part of closing should not throw. Returning focus to
the activator is best-effort — if the dialog is already gone by the time the watcher
resumes, there is nothing to focus and it should be a no-op.

Actual Behavior

TypeError: Cannot read properties of null (reading 'activatorEl')
at VDialog.js:61

Reproduction Link

https://play.vuetifyjs.com/#...

Other comments

watch(isActive, async val => {
if (!val) {
await nextTick()
overlay.value!.activatorEl?.focus({ preventScroll: true })
}
})

overlay is the template ref to the inner VOverlay, so Vue sets it to null as soon as
VDialog unmounts. The watcher awaits a tick before reading it, and post-flush callbacks of
that same tick run before the await resumes, so the ref can be nulled in between:

close()              -> isActive false, watcher body starts (overlay is still an object)
post-flush callback  -> selected = null -> VDialog unmounts -> overlay = null
await resumes        -> TypeError

Worth noting for anyone trying to reproduce it: closing and unmounting in the same tick
does not trigger it — the unmount disposes the watcher before its job ever runs.
Unmounting a microtask later (an await in the handler, Promise.resolve().then(...))
does not trigger it either — by then the watcher has already resumed with a live ref. Only
the post-flush window fails, which is probably why this shows up as an intermittent error
in production apps rather than something reliably reproducible.

The contentEl access a few lines above is guarded by its if (... && overlay.value?.contentEl ...);
this one is the only unguarded read.

Suggested fix, matching the surrounding style:

-        overlay.value!.activatorEl?.focus({ preventScroll: true })
+        overlay.value?.activatorEl?.focus({ preventScroll: true })

This was reported before in #22142 (same error, same line) but closed as not planned
without a reproduction. Happy to open a PR if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    C: VDialogT: bugFunctionality that does not work as intended/expected

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions