Skip to content

feat(Dialog): support unmountOnHide#2494

Open
MickL wants to merge 4 commits intounovue:v2from
MickL:feat/dialog-unmount-on-hide
Open

feat(Dialog): support unmountOnHide#2494
MickL wants to merge 4 commits intounovue:v2from
MickL:feat/dialog-unmount-on-hide

Conversation

@MickL
Copy link
Copy Markdown

@MickL MickL commented Mar 8, 2026

resolves #1727

Summary by CodeRabbit

  • New Features

    • Added unmountOnHide to Dialog, letting users choose whether dialog content is unmounted or simply hidden when closed (defaults to unmount).
  • Bug Fixes

    • Modal pointer-event behavior now updates dynamically based on dialog visibility.
    • Dialog content visibility now supports smoother mount/show coordination for animated transitions.
  • Documentation

    • Docs updated for unmountOnHide and corrected a small typo in dialog API docs.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 8, 2026

📝 Walkthrough

Walkthrough

Adds a new public prop unmountOnHide to DialogRoot and updates Dialog content to allow hiding with v-show (instead of unmounting) by wiring Presence to a present flag and using force-mount derived from unmountOnHide.

Changes

Cohort / File(s) Summary
Docs
docs/content/meta/DialogRoot.md
Added unmountOnHide boolean prop documentation (default: true).
Dialog Root
packages/core/src/Dialog/DialogRoot.vue
Added public prop unmountOnHide?: boolean (default true) and exposed it via DialogRootContext as Ref<boolean>; included in props-to-refs extraction and provided to consumers.
Dialog Presence / Content
packages/core/src/Dialog/DialogContent.vue, packages/core/src/Dialog/DialogContentImpl.vue
Reworked Presence usage to a scoped slot that provides present; set `:force-mount="forceMount
Dialog Modal Props
packages/core/src/Dialog/DialogContentModal.vue
Extended props to include present: boolean and bound :disable-outside-pointer-events to present so outside-pointer-events follow the present state.
Docs (examples)
docs/content/docs/guides/animation.md
Removed multiple attribute from AnimatePresence example, making it a single-presence instance for the shown example.

Sequence Diagram

sequenceDiagram
    participant User
    participant DialogRoot
    participant DialogContent
    participant Presence
    participant DOM

    User->>DialogRoot: Toggle open (props include unmountOnHide)
    DialogRoot->>DialogContent: Provide context (unmountOnHide ref)
    DialogContent->>Presence: Render with :force-mount = forceMount || !unmountOnHide
    Presence->>Presence: Determine `present` (enter/leave)
    Presence-->>DialogContent: Supply `present` via scoped slot
    DialogContent->>DOM: Render content and apply v-show="present"
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble through props with glee and hop,
I keep the dialog present, never stop.
With unmountOnHide I quietly stay,
Visible by show, not whisked away.
Little rabbit cheers—hops all the way! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(Dialog): support unmountOnHide' directly and clearly describes the main feature addition in the changeset.
Linked Issues check ✅ Passed The PR implements the unmountOnHide feature requested in issue #1727, allowing Dialog content to remain in DOM with v-show instead of v-if for SEO purposes.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the unmountOnHide feature; documentation updates and typo fixes are supporting changes for the feature.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/Dialog/DialogOverlay.vue (1)

26-41: ⚠️ Potential issue | 🔴 Critical

Critical: Body scroll locks immediately when unmountOnHide=false.

When unmountOnHide is false, the force-mount prop evaluates to true (line 30: forceMount || !rootContext.unmountOnHide.value), causing DialogOverlayImpl to mount immediately. However, DialogOverlayImpl calls useBodyScrollLock(true) unconditionally on mount (line 16 of DialogOverlayImpl.vue). This locks body scroll as soon as the component renders, even when the dialog is closed and only visually hidden via v-show="present".

This breaks page scrolling for any page that includes a closed dialog with unmountOnHide=false.

The scroll lock should be tied to the present state rather than component mount. Consider conditionally enabling the scroll lock based on whether the dialog is open:

// In DialogOverlayImpl.vue
-useBodyScrollLock(true)
+const scrollLocked = useBodyScrollLock(false)
+watch(() => rootContext.open.value, (isOpen) => {
+  scrollLocked.value = isOpen
+}, { immediate: true })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/Dialog/DialogOverlay.vue` around lines 26 - 41, The
DialogOverlayImpl currently calls useBodyScrollLock(true) on mount which locks
scrolling whenever force-mounting (when rootContext.unmountOnHide is false);
change the behavior so the scroll lock is driven by the Presence "present" state
instead of mount: pass the present state into DialogOverlayImpl (or expose it
via a prop like "open" / "present") from DialogOverlay.vue and update
DialogOverlayImpl to call useBodyScrollLock(present) or watch the present prop
and enable/disable the lock accordingly so body scroll is only locked when
present is true.
🧹 Nitpick comments (1)
docs/content/meta/DialogRoot.md (1)

24-29: Documentation description could be more informative.

The description here is minimal compared to the JSDoc in DialogRoot.vue. Consider aligning the documentation to mention the SEO/performance benefits:

   {
     'name': 'unmountOnHide',
-    'description': '<p>When <code>true</code>, the element will be unmounted on closed state.</p>\n',
+    'description': '<p>When set to <code>false</code>, the dialog content and overlay 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',
     'type': 'boolean',
     'required': false,
     'default': 'true'
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/content/meta/DialogRoot.md` around lines 24 - 29, Update the
documentation entry for the prop 'unmountOnHide' to match the richer JSDoc in
DialogRoot.vue: explain that when true the dialog's DOM is removed on close (not
just "unmounted"), and note the SEO and performance benefits (reduces DOM size,
prevents hidden content from being indexed, and lowers memory/paint costs). Keep
the type/default/required fields the same but expand the 'description' text to
include these SEO/perf details and a brief example of when to set it to false
(e.g., to preserve state or for animations), referencing 'unmountOnHide' and
'DialogRoot.vue' for context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/core/src/Dialog/DialogOverlay.vue`:
- Around line 26-41: The DialogOverlayImpl currently calls
useBodyScrollLock(true) on mount which locks scrolling whenever force-mounting
(when rootContext.unmountOnHide is false); change the behavior so the scroll
lock is driven by the Presence "present" state instead of mount: pass the
present state into DialogOverlayImpl (or expose it via a prop like "open" /
"present") from DialogOverlay.vue and update DialogOverlayImpl to call
useBodyScrollLock(present) or watch the present prop and enable/disable the lock
accordingly so body scroll is only locked when present is true.

---

Nitpick comments:
In `@docs/content/meta/DialogRoot.md`:
- Around line 24-29: Update the documentation entry for the prop 'unmountOnHide'
to match the richer JSDoc in DialogRoot.vue: explain that when true the dialog's
DOM is removed on close (not just "unmounted"), and note the SEO and performance
benefits (reduces DOM size, prevents hidden content from being indexed, and
lowers memory/paint costs). Keep the type/default/required fields the same but
expand the 'description' text to include these SEO/perf details and a brief
example of when to set it to false (e.g., to preserve state or for animations),
referencing 'unmountOnHide' and 'DialogRoot.vue' for context.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bf1570ca-06c5-4ebb-9324-ba2334be1db8

📥 Commits

Reviewing files that changed from the base of the PR and between 5262438 and 4750aaf.

📒 Files selected for processing (4)
  • docs/content/meta/DialogRoot.md
  • packages/core/src/Dialog/DialogContent.vue
  • packages/core/src/Dialog/DialogOverlay.vue
  • packages/core/src/Dialog/DialogRoot.vue

@MickL MickL mentioned this pull request Mar 10, 2026
@MickL MickL marked this pull request as draft March 11, 2026 19:17
@MickL MickL marked this pull request as ready for review March 11, 2026 19:25
@MickL
Copy link
Copy Markdown
Author

MickL commented Mar 11, 2026

I just tested my changes in production and made small tweaks. It is working fine now with both modal true/false.

I published it to npm: @mickl/reka-ui@latest

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/Dialog/DialogContentModal.vue`:
- Line 23: The disable-outside-pointer-events binding currently uses
rootContext.open.value which releases the global pointer-events lock during the
Presence exit phase; change it to use the Presence "present" state used to keep
content mounted/visible instead. Thread the parent's scoped-slot present flag
through into DialogContentModal (same pattern used for other Presence-controlled
behavior) and bind :disable-outside-pointer-events to that present value so the
body pointer-events lock stays active for the entire visible lifetime; locate
references to rootContext.open.value in DialogContentModal and the parent slot
that renders it, add a present prop/slot param, and ensure
DismissableLayer-style present-driven behavior is followed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0a4b16a6-08d9-4341-ac45-5738d95c9545

📥 Commits

Reviewing files that changed from the base of the PR and between 4750aaf and 0f9787f.

📒 Files selected for processing (1)
  • packages/core/src/Dialog/DialogContentModal.vue

Comment thread packages/core/src/Dialog/DialogContentModal.vue Outdated
docs:Fixed typo and removed non-existent Motion prop "multiple"
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/Dialog/DialogContentModal.vue (1)

7-15: ⚠️ Potential issue | 🟠 Major

Gate useHideOthers by actual visibility.

With unmountOnHide=false, this component can stay mounted after close. useHideOthers(currentElement) at Line 15 is still mount-scoped, so the rest of the page can remain aria-hidden while the dialog is only hidden, which blocks screen-reader access to the underlying content. Please make that effect track present/visible state instead of mount state alone.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/Dialog/DialogContentModal.vue` around lines 7 - 15, The
call to useHideOthers(currentElement) must be gated by the dialog's visibility
(props.present) so aria-hidden is applied only while visible: call useHideOthers
inside a watcher on props.present (or a computed) so when present becomes true
you invoke useHideOthers(currentElement) and capture its cleanup/stop function,
and when present becomes false you call that cleanup (and also call it on
component unmount); update DialogContentModal.vue to store and dispose the
returned teardown from useHideOthers instead of invoking it unconditionally at
mount.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/Dialog/DialogContentModal.vue`:
- Line 7: The props object currently forwards the local present prop to
DialogContentImpl causing it to leak as a fallthrough attribute; destructure
props to remove present (e.g., const { present, ...restProps } = props) and then
use v-bind with restProps and emitsAsProps when rendering DialogContentImpl so
that DialogContentImpl (and DismissableLayer) no longer receive the present
prop.

---

Outside diff comments:
In `@packages/core/src/Dialog/DialogContentModal.vue`:
- Around line 7-15: The call to useHideOthers(currentElement) must be gated by
the dialog's visibility (props.present) so aria-hidden is applied only while
visible: call useHideOthers inside a watcher on props.present (or a computed) so
when present becomes true you invoke useHideOthers(currentElement) and capture
its cleanup/stop function, and when present becomes false you call that cleanup
(and also call it on component unmount); update DialogContentModal.vue to store
and dispose the returned teardown from useHideOthers instead of invoking it
unconditionally at mount.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 71cc589b-910e-4cd3-99af-fb84bad7171c

📥 Commits

Reviewing files that changed from the base of the PR and between 0f9787f and 1f17380.

📒 Files selected for processing (4)
  • docs/content/docs/guides/animation.md
  • packages/core/src/Dialog/DialogContent.vue
  • packages/core/src/Dialog/DialogContentImpl.vue
  • packages/core/src/Dialog/DialogContentModal.vue
✅ Files skipped from review due to trivial changes (2)
  • docs/content/docs/guides/animation.md
  • packages/core/src/Dialog/DialogContentImpl.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/Dialog/DialogContent.vue

Comment thread packages/core/src/Dialog/DialogContentModal.vue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Dont remove Dialog from DOM (use v-show instead of v-if)

1 participant