You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* feat(Dialog): support unmountOnHide
When set to false, Dialog content and overlay stay in the DOM when
closed (hidden via v-show) instead of being unmounted. Useful for SEO
and performance by avoiding remounts on every open.
Closes#1727
* test(Dialog): add tests for unmountOnHide
Also fixes focus restoration when unmountOnHide=false — since FocusScope
never unmounts, close-auto-focus doesn't fire. Added a watcher on present
to manually restore focus to trigger when the dialog hides.
* fix(Dialog): replace deprecated SpyInstance with MockInstance
* fix(Dialog): restore focus for non-modal when unmountOnHide=false
The focus-restoration watcher only lived in DialogContentModal, so a
non-modal dialog with unmountOnHide=false never returned focus to the
trigger on close (close-auto-focus doesn't fire while mounted). Mirror
the watcher in DialogContentNonModal, respecting interact-outside, and
document why the watcher exists in both.
* test(Dialog): exercise open→close cycle for aria-hidden check
Assert the rest of the page stays accessible after a full open/close
cycle (content remains mounted) rather than just checking initial state.
* fix(DismissableLayer): restore body pointer-events when toggled off while mounted
When `disableOutsidePointerEvents` toggles true→false without unmounting
(the unmountOnHide=false case), the watchEffect cleanup read the prop
reactively and saw the new false value, skipping the body pointer-events
restore and leaving the page frozen. Capture the value at run time for
cleanup, and read the layer set size via toRaw to avoid self-retriggering.
* fix(FocusScope): re-run mount auto-focus when re-trapped while mounted
With unmountOnHide=false the FocusScope stays mounted across open/close,
so the mount auto-focus (keyed off physical mount) never re-fires on
reopen and focus never enters the content. Watch the trapped false->true
transition and re-run the mount auto-focus. The default unmount-on-close
path mounts already trapped, so this transition never happens there and
behavior is unchanged.
* fix(FocusScope): retry auto-focus across v-show visibility frame
When a force-mounted scope (`unmountOnHide: false`) becomes trapped on
reopen, the `v-show` visibility can apply a frame after `trapped` flips,
so the first focus attempt runs while the container is still
`display: none` and no-ops. Retry the focus move on the next frame
without re-dispatching the auto-focus event.
* fix(FocusScope): drive auto-focus off visibility for force-mounted scopes
A force-mounted scope (Dialog `unmountOnHide: false`) physically mounts
once while hidden via `v-show`, so keying auto-focus off physical mount
fired `mountAutoFocus`/`openAutoFocus` and stole focus into a closed
dialog, and never re-focused non-modal content on open (it isn't
trapped). Gate the mount-time auto-focus on visibility and re-run it on
the hidden -> visible transition via a MutationObserver, covering first
open and reopen for both modal and non-modal. Replaces the trapped
watcher + rAF retry, which only handled the modal case.
* fix(Dialog): drive FocusScope and DismissableLayer off presence for unmountOnHide
When `unmountOnHide` is `false` the dialog content stays mounted while
closed, but FocusScope and DismissableLayer keyed their global stack
membership off physical mount. With more than one keep-mounted dialog on
a page, the later-mounted hidden layer was treated as the topmost layer
(swallowing Escape and outside interactions meant for the open dialog)
and its hidden scope permanently paused the open dialog's focus trap.
- FocusScope: replace `display: none` sniffing with an explicit
`present` prop; stack membership and mount auto-focus now follow
`present` transitions instead of mount. Drop the per-consumer
MutationObserver.
- DismissableLayer: add an internal `present` prop (kept out of the
public `DismissableLayerProps`); layer-stack membership, body
pointer-events locking and outside-interaction events now follow
presence, so hidden layers no longer swallow dismissal or emit
`interactOutside` while closed.
- Dialog: forward `present` through DialogContentImpl to both
primitives.
- Tests: regression coverage for the two-dialog scenario; replace
`setTimeout` magic waits with `vi.waitFor`.
* fix(DismissableLayer): gate Escape handling on present
A layer kept mounted while hidden (e.g. a Dialog with `unmountOnHide: false`)
is out of the layer stack, so its `index` is `-1`. When no layer is visible
(`size === 0`), `-1 === size - 1` made the hidden layer look like the highest
layer and emit `escapeKeyDown` / `dismiss` on Escape for an already-closed
dialog. Gate the Escape handler on `props.present`, consistent with the
existing `pointerDownOutside` / `focusOutside` guards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: zernonia <zernonia@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/content/meta/DialogRoot.md
+8Lines changed: 8 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -21,6 +21,13 @@
21
21
'description': '<p>The controlled open state of the dialog. Can be binded as <code>v-model:open</code>.</p>\n',
22
22
'type': 'boolean',
23
23
'required': false
24
+
},
25
+
{
26
+
'name': 'unmountOnHide',
27
+
'description': '<p>When set to <code>false</code>, the dialog content will not be unmounted when closed, but instead hidden with CSS. Useful for SEO or when you want to improve performance by not remounting the component on every open.</p>\n',
28
+
'type': 'boolean',
29
+
'required': false,
30
+
'default': 'true'
24
31
}
25
32
]" />
26
33
@@ -55,6 +62,7 @@
55
62
|`defaultOpen`| The open state of the dialog when it is initially rendered. Use when you do not need to control its open state. |`boolean`| No |`false`|
56
63
|`modal`| The modality of the dialog When set to true, <br> interaction with outside elements will be disabled and only dialog content will be visible to screen readers. |`boolean`| No |`true`|
57
64
|`open`| The controlled open state of the dialog. Can be binded as v-model:open. |`boolean`| No | - |
65
+
|`unmountOnHide`| When set to `false`, the dialog content will not be unmounted when closed, but instead hidden with CSS. Useful for SEO or when you want to improve performance by not remounting the component on every open. |`boolean`| No |`true`|
0 commit comments