Skip to content

fix(popover, collapsible): DP-185811 cancel transitions on unmount - #1241

Merged
Brad Paugh (braddialpad) merged 4 commits into
stagingfrom
fix-transitions-on-unmounted
May 1, 2026
Merged

fix(popover, collapsible): DP-185811 cancel transitions on unmount#1241
Brad Paugh (braddialpad) merged 4 commits into
stagingfrom
fix-transitions-on-unmounted

Conversation

@braddialpad

@braddialpad Brad Paugh (braddialpad) commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

🛠️ Type Of Change

  • Fix

📖 Jira Ticket

DP-185811

📖 Description

Fixes a race condition where a CSS leave transition (fade/collapse) could complete after DtPopover or DtCollapsible was already unmounted, causing Vue to attempt updates on a dead component instance.

💡 Context

Reported from the product side — implementing components were unmounting while a popover/collapsible leave animation was in progress. The after-leave hook then fired on the already-torn-down component, producing warnings and potential errors about updating unmounted component instances.

📝 Checklist

For all PRs:

  • I have ensured no private Dialpad links or info are in the code or pull request description (Dialtone is a public repo!).
  • I have reviewed my changes.
  • I have added all relevant documentation.
  • I have considered the performance impact of my change.

For all Vue changes:

  • I have added / updated unit tests.
  • I have validated components with a screen reader.
  • I have validated components keyboard navigation.

@github-actions

Copy link
Copy Markdown
Contributor

Please add either the visual-test-ready or no-visual-test label to this PR depending on whether you want to run visual tests or not.
It is recommended to run visual tests if your PR changes any UI. ‼️

@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: d648f82d-8f23-48ed-aad8-fd5314b1d57e

📥 Commits

Reviewing files that changed from the base of the PR and between fd8d559 and af6f660.

📒 Files selected for processing (2)
  • packages/dialtone-vue/components/collapsible/collapsible.vue
  • packages/dialtone-vue/components/popover/popover.test.js

Prevents post-unmount CSS transition callbacks from running by adding beforeUnmount hooks that set _isUnmounting, guarding transition-complete handlers in DtPopover and DtCollapsible, and (for DtPopover) disabling in-flight transitions on the teleported content element during unmount.

Overall Judgement: ✅ Ready to merge — Small, targeted changes with tests that reliably prevent transition callbacks from touching unmounted components and fix DP-185811.

Walkthrough

Adds unmount guards to Collapsible and Popover: components set _isUnmounting during teardown; Popover also cancels in-flight CSS transitions by forcing style.transition = 'none'; transition-complete handlers return early when unmounting to avoid emits and teardown side effects.

Changes

Cohort / File(s) Summary
Collapsible Component
packages/dialtone-vue/components/collapsible/collapsible.vue
Adds beforeUnmount() to set _isUnmounting; onLeaveTransitionComplete and onEnterTransitionComplete now return early when unmounting to prevent opened (and update:open in controlled mode) emissions after teardown.
Popover Component
packages/dialtone-vue/components/popover/popover.vue
Sets _isUnmounting during teardown and forces contentEl.style.transition = 'none' to cancel in-flight CSS transitions; transition completion handlers return early when unmounting to skip focus/scroll cleanup and opened/update:open emissions.
Collapsible tests
packages/dialtone-vue/components/collapsible/collapsible.test.js
Adds tests that simulate _isUnmounting = true and assert onLeaveTransitionComplete/onEnterTransitionComplete do not emit opened (and no update:open in controlled mode).
Popover tests
packages/dialtone-vue/components/popover/popover.test.js
Adds tests verifying teardown forces style.transition = 'none', that transition handlers suppress opened emissions when _isUnmounting = true, and that emits still occur on normal (non-unmounting) paths with correct payloads and update:open behavior.

Sequence Diagram(s)

sequenceDiagram
    participant Parent
    participant Component as DtPopover/DtCollapsible
    participant ContentEl as DOM:contentElement
    participant EventBus as ParentListener

    rect rgba(200,200,255,0.5)
    Parent->>Component: trigger unmount()
    Component->>Component: set _isUnmounting = true
    end

    rect rgba(200,255,200,0.5)
    Component->>ContentEl: force style.transition = 'none' (Popover)
    ContentEl-->>Component: cancel transitionend callbacks
    end

    rect rgba(255,200,200,0.5)
    ContentEl->>Component: onEnter/LeaveTransitionComplete()
    Component--xEventBus: early return when _isUnmounting (no emit)
    end
Loading

Suggested reviewers

  • francisrupert
  • iropolo
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-transitions-on-unmounted

Review rate limit: 9/10 reviews remaining, refill in 6 minutes.

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

@braddialpad Brad Paugh (braddialpad) added the no-visual-test Add this tag when the PR does not need visual testing label Apr 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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/dialtone-vue/components/popover/popover.vue (1)

912-937: ⚠️ Potential issue | 🟠 Major

Add guards after each await to prevent side effects if unmount occurs during async operations.

The transition handlers check _isUnmounting at entry (lines 913, 928) but don't re-check after awaits. If the component unmounts during await this.focusFirstElement(), await this.$nextTick(), or other async operations, the handler continues and emits events or runs side effects on a destroyed component. The beforeUnmount comment explicitly intends to prevent this. Add guards after each await to close the race window.

Suggested hardening
 async onLeaveTransitionComplete () {
   if (this._isUnmounting) return;
   if (this.modal) {
     await this.focusFirstElement(this.$refs.anchor);
+    if (this._isUnmounting) return;
     // await next tick in case the user wants to change focus themselves.
     await this.$nextTick();
+    if (this._isUnmounting) return;
     this.enableScrolling();
   }
+  if (this._isUnmounting) return;
   this.tip?.unmount();
   this.$emit('opened', false);
   if (this.open !== null) {
     this.$emit('update:open', false);
   }
 },

 async onEnterTransitionComplete () {
   if (this._isUnmounting) return;
   this.focusInitialElement();
   // await next tick in case the user wants to change focus themselves.
   await this.$nextTick();
+  if (this._isUnmounting) return;
   this.preventScrolling();
   this.$emit('opened', true, this.$refs.popover__content);
   if (this.open !== null) {
     this.$emit('update:open', true);
   }
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dialtone-vue/components/popover/popover.vue` around lines 912 - 937,
Add post-await unmount guards to both transition handlers: in
onLeaveTransitionComplete and onEnterTransitionComplete, after each await (e.g.,
after await this.focusFirstElement(...), await this.$nextTick(), await
this.focusInitialElement(), etc.) check this._isUnmounting and return early if
true so no further side effects (calls to enableScrolling/preventScrolling,
tip?.unmount, or this.$emit/update:open) run on an unmounted component; update
the sequences around focusFirstElement, focusInitialElement, this.$nextTick,
tip?.unmount, enableScrolling, preventScrolling, and the $emit/update:open
branches to bail out immediately when unmounted.
🤖 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/dialtone-vue/components/popover/popover.vue`:
- Around line 912-937: Add post-await unmount guards to both transition
handlers: in onLeaveTransitionComplete and onEnterTransitionComplete, after each
await (e.g., after await this.focusFirstElement(...), await this.$nextTick(),
await this.focusInitialElement(), etc.) check this._isUnmounting and return
early if true so no further side effects (calls to
enableScrolling/preventScrolling, tip?.unmount, or this.$emit/update:open) run
on an unmounted component; update the sequences around focusFirstElement,
focusInitialElement, this.$nextTick, tip?.unmount, enableScrolling,
preventScrolling, and the $emit/update:open branches to bail out immediately
when unmounted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 9e0bd7d0-bacd-44ad-b97d-70d112083eea

📥 Commits

Reviewing files that changed from the base of the PR and between e43643d and 7ec6c7a.

📒 Files selected for processing (2)
  • packages/dialtone-vue/components/collapsible/collapsible.vue
  • packages/dialtone-vue/components/popover/popover.vue

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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/dialtone-vue/components/collapsible/collapsible.test.js`:
- Around line 148-162: Extend the unmounting tests to also assert suppression of
the controlled-mode event by setting the component into controlled mode (make
open !== null via prop) and then verifying that calling
wrapper.vm.onLeaveTransitionComplete() and
wrapper.vm.onEnterTransitionComplete() do NOT emit 'update:open' in addition to
not emitting 'opened'; use wrapper.vm._isUnmounting = true and the existing
wrapper reference, and target the same methods (onLeaveTransitionComplete,
onEnterTransitionComplete) and emitted events ('opened' and 'update:open').

In `@packages/dialtone-vue/components/popover/popover.test.js`:
- Around line 315-324: The tests only assert the guarded path where
wrapper.vm._isUnmounting = true; add companion assertions that set
wrapper.vm._isUnmounting = false and then call onLeaveTransitionComplete() and
onEnterTransitionComplete() to verify wrapper.emitted('opened') contains the
expected emission(s); update the two test cases around onLeaveTransitionComplete
and onEnterTransitionComplete to first assert no emission when _isUnmounting =
true and then reset _isUnmounting = false, invoke the same handler, and assert
that wrapper.emitted('opened') is defined/contains the expected event to prove
the normal path still emits.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: a0bda81a-c5ac-420c-a4de-b1ec6280c91e

📥 Commits

Reviewing files that changed from the base of the PR and between 7ec6c7a and 1ed29e8.

📒 Files selected for processing (2)
  • packages/dialtone-vue/components/collapsible/collapsible.test.js
  • packages/dialtone-vue/components/popover/popover.test.js

Comment thread packages/dialtone-vue/components/collapsible/collapsible.test.js
Comment thread packages/dialtone-vue/components/popover/popover.test.js

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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/dialtone-vue/components/popover/popover.vue (1)

714-725: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Release scroll lock during unmount cleanup.

If unmount happens while the modal popover is open, preventScrolling() may already have disabled scrolling. With the new _isUnmounting early returns, onLeaveTransitionComplete() no longer guarantees enableScrolling() runs, so scrolling can stay locked after teardown.

Suggested fix
  beforeUnmount () {
    this._isUnmounting = true;
    // Cancel any in-progress CSS transitions so transitionend cannot fire
    // after this component is torn down and call into dead lifecycle methods.
    if (this.popoverContentEl) {
      this.popoverContentEl.style.transition = 'none';
    }
+   // Ensure any modal scroll lock is always released on teardown.
+   if (this.isOpen && this.modal) {
+     this.enableScrolling();
+   }
    this.tip?.destroy();
    this.intersectionObserver?.disconnect();
    this.mutationObserver?.disconnect();
    this.removeReferences();
    this.removeEventListeners();
  },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dialtone-vue/components/popover/popover.vue` around lines 714 - 725,
The beforeUnmount cleanup must release any scroll lock set by preventScrolling
so enableScrolling is always called even if _isUnmounting causes early exits;
update the beforeUnmount method (and/or related teardown path) to call
enableScrolling() (or the component's corresponding scroll-unlock helper)
unconditionally or whenever preventScrolling was previously applied, ensuring
scroll is restored when popoverContentEl is torn down and before/after calling
removeReferences/removeEventListeners, and reference the existing symbols
beforeUnmount, _isUnmounting, preventScrolling, enableScrolling, and
onLeaveTransitionComplete to locate and modify the logic.
🤖 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/dialtone-vue/components/popover/popover.test.js`:
- Around line 315-335: Extend the two transition-complete tests to also assert
controlled-mode behavior: set the component into controlled mode by assigning a
non-null open prop (e.g., set wrapper props so open !== null), call
wrapper.vm.onLeaveTransitionComplete() and
wrapper.vm.onEnterTransitionComplete() as in the tests, and assert that
wrapper.emitted('update:open') is undefined (no update:open emitted) when open
!== null; also verify the opposite (update:open is emitted) when open is
null/uncontrolled to ensure the guard in onLeaveTransitionComplete and
onEnterTransitionComplete behaves correctly.

---

Outside diff comments:
In `@packages/dialtone-vue/components/popover/popover.vue`:
- Around line 714-725: The beforeUnmount cleanup must release any scroll lock
set by preventScrolling so enableScrolling is always called even if
_isUnmounting causes early exits; update the beforeUnmount method (and/or
related teardown path) to call enableScrolling() (or the component's
corresponding scroll-unlock helper) unconditionally or whenever preventScrolling
was previously applied, ensuring scroll is restored when popoverContentEl is
torn down and before/after calling removeReferences/removeEventListeners, and
reference the existing symbols beforeUnmount, _isUnmounting, preventScrolling,
enableScrolling, and onLeaveTransitionComplete to locate and modify the logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 47b68d28-4629-4e11-b1b7-d67b5ec3ab30

📥 Commits

Reviewing files that changed from the base of the PR and between 1ed29e8 and fd8d559.

📒 Files selected for processing (3)
  • packages/dialtone-vue/components/collapsible/collapsible.test.js
  • packages/dialtone-vue/components/popover/popover.test.js
  • packages/dialtone-vue/components/popover/popover.vue

Comment thread packages/dialtone-vue/components/popover/popover.test.js Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

no mayor issues from the ones already flagged.

Comment thread packages/dialtone-vue/components/popover/popover.test.js
Comment thread packages/dialtone-vue/components/popover/popover.test.js Outdated
Comment thread packages/dialtone-vue/components/collapsible/collapsible.vue
Comment thread packages/dialtone-vue/components/popover/popover.vue
@braddialpad

Copy link
Copy Markdown
Contributor Author

Thanks! made the fixes

@github-actions

github-actions Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

✔️ Deploy previews ready!
😎 Dialtone documentation preview: https://dialtone.dialpad.com/deploy-previews/pr-1241/
😎 Dialtone-vue preview: https://dialtone.dialpad.com/vue/deploy-previews/pr-1241/

@braddialpad
Brad Paugh (braddialpad) merged commit 6883a0e into staging May 1, 2026
19 checks passed
@braddialpad
Brad Paugh (braddialpad) deleted the fix-transitions-on-unmounted branch May 1, 2026 01:30
Brad Paugh (braddialpad) pushed a commit that referenced this pull request May 1, 2026
## [3.219.2](dialtone-vue/v3.219.1...dialtone-vue/v3.219.2) (2026-05-01)

### Bug Fixes

* **Popover, Collapsible:** DP-185811 cancel transitions on unmount ([#1241](#1241)) ([6883a0e](6883a0e))
* **Rich Text Editor:** NO-JIRA fix multiple rich text issues ([#1240](#1240)) ([f285a3e](f285a3e))
Brad Paugh (braddialpad) pushed a commit that referenced this pull request May 1, 2026
# [9.182.0](dialtone/v9.181.0...dialtone/v9.182.0) (2026-05-01)

### Bug Fixes

* **Popover, Collapsible:** DP-185811 cancel transitions on unmount ([#1241](#1241)) ([6883a0e](6883a0e))
* **Rich Text Editor:** NO-JIRA fix multiple rich text issues ([#1240](#1240)) ([f285a3e](f285a3e))

### Features

* DLT-3352 refresh GEO standard, publish llms.txt, add freshness check ([#1235](#1235)) ([e43643d](e43643d))
Brad Paugh (braddialpad) pushed a commit that referenced this pull request May 1, 2026
# [3.220.0-next.4](dialtone-vue/v3.220.0-next.3...dialtone-vue/v3.220.0-next.4) (2026-05-01)

### Bug Fixes

* **Popover, Collapsible:** DP-185811 cancel transitions on unmount ([#1241](#1241)) ([6883a0e](6883a0e))
* **Rich Text Editor:** NO-JIRA fix multiple rich text issues ([#1240](#1240)) ([f285a3e](f285a3e))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-visual-test Add this tag when the PR does not need visual testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants