Skip to content

fix(core/tooltip): guard popover calls against detached dialog#2611

Open
SAY-5 wants to merge 2 commits into
siemens:mainfrom
SAY-5:fix/tooltip-popover-guard
Open

fix(core/tooltip): guard popover calls against detached dialog#2611
SAY-5 wants to merge 2 commits into
siemens:mainfrom
SAY-5:fix/tooltip-popover-guard

Conversation

@SAY-5

@SAY-5 SAY-5 commented Jun 18, 2026

Copy link
Copy Markdown

GitHub Issue Number: #2559

💡 What is the current behavior?

ix-tooltip resolves the <dialog> element with dialogRef.waitForCurrent() and then calls dialog.showPopover() / dialog.hidePopover() inside a deferred setTimeout. If the tooltip is disconnected (or the popover state changes) before the timeout fires, the resolved element is no longer connected, which throws dialog.hidePopover is not a function / showPopover errors. We see these frequently in RUM.

🆕 What is the new behavior?

The deferred callbacks now check the dialog before acting: showTooltip skips when the dialog is not connected or already open, and hideTooltip only calls hidePopover() when the popover is actually open. This matches the existing guards used in ix-dropdown.

🏁 Checklist

A pull request can only be merged if all of these conditions are met (where applicable):

  • 🦮 Accessibility (a11y) features were implemented
  • 🗺️ Internationalization (i18n) - no hard coded strings
  • 📲 Responsiveness - components handle viewport changes and content overflow gracefully
  • 📕 Add or update a Storybook story
  • 📄 Documentation was reviewed/updated siemens/ix-docs
  • 🧪 Unit tests were added/updated and pass (pnpm test)
  • 📸 Visual regression tests were added/updated and pass (Guide)
  • 🧐 Static code analysis passes (pnpm lint)
  • 🏗️ Successful compilation (pnpm build, changes pushed)

👨‍💻 Help & support

Summary by CodeRabbit

Bug Fixes

  • Fixed tooltip behavior when the related element/dialog is removed before the show delay completes.
  • Improved tooltip hiding to avoid calling popover-close actions when the popover isn’t open, while still performing required cleanup.
  • Added automated test coverage for the updated async show/hide edge cases.

Signed-off-by: Sai Asish Y <say.apm35@gmail.com>
@netlify

netlify Bot commented Jun 18, 2026

Copy link
Copy Markdown

Deploy Preview for ix-storybook ready!

Name Link
🔨 Latest commit 68b219b
🔍 Latest deploy log https://app.netlify.com/projects/ix-storybook/deploys/6a35e725e21e5500084d613a
😎 Deploy Preview https://deploy-preview-2611--ix-storybook.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@changeset-bot

changeset-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 68b219b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2430412c-bded-479e-86f2-52978ad07753

📥 Commits

Reviewing files that changed from the base of the PR and between 7107f18 and 68b219b.

📒 Files selected for processing (1)
  • packages/core/src/components/tooltip/tooltip.tsx

📝 Walkthrough

Walkthrough

Guards are added inside the delayed setTimeout of showTooltip to return early if the dialog is disconnected or already open. hideTooltip now calls dialog.hidePopover() only when the dialog matches :popover-open. Two new tests verify these two guarded paths.

Changes

Tooltip Popover Guard Fixes

Layer / File(s) Summary
Popover guard implementation
packages/core/src/components/tooltip/tooltip.tsx
showTooltip exits early when the dialog is disconnected or already open; hideTooltip skips hidePopover when the popover is not open using a new isPopoverOpen helper.
Guard behavior tests
packages/core/src/components/tooltip/test/tooltip.spec.tsx
Two tests assert showPopover and hidePopover are not called in their respective guarded states using shadow DOM spies and a flushTimeout helper.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

  • hidePopover is not a function #2559: The guards added in showTooltip and hideTooltip directly prevent the hidePopover is not a function TypeError and the unsafe popover calls reported in that issue.

Poem

🐇 A tooltip once popped without care,
Calling hide when no popover was there.
Now a guard checks the state,
Before it's too late —
No more errors floating in air! ✨

🚥 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 clearly identifies the main change: adding guards for popover operations in the tooltip component when the dialog element becomes detached.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request adds safety checks to prevent calling showPopover on disconnected dialogs or when a popover is already open, and avoids calling hidePopover when the popover is closed, accompanied by new unit tests. A review comment identifies a bug where the early return check for :popover-open prevents the tooltip from correctly transitioning and updating its position when switching between different anchor elements, and provides a code suggestion to resolve this.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 124 to 128
if (!dialog.isConnected || dialog.matches(':popover-open')) {
return;
}
this.setAnchorElement(anchorElement);
dialog.showPopover();

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.

high

If the tooltip is currently visible for Anchor A, and the user hovers over Anchor B before the hide delay of Anchor A completes, the hide timeout is cleared, meaning the popover remains open (:popover-open is true).

When the show timeout for Anchor B fires, dialog.matches(':popover-open') will evaluate to true. With the current implementation, this causes an early return, which skips updating the anchor element (setAnchorElement) and repositioning the tooltip (applyTooltipPosition). As a result, the tooltip will remain stuck at Anchor A instead of transitioning to Anchor B.

To fix this, we should only guard the dialog.showPopover() call itself with !dialog.matches(':popover-open'), while still allowing the anchor update and repositioning logic to execute.

Suggested change
if (!dialog.isConnected || dialog.matches(':popover-open')) {
return;
}
this.setAnchorElement(anchorElement);
dialog.showPopover();
if (!dialog.isConnected) {
return;
}
this.setAnchorElement(anchorElement);
if (!dialog.matches(':popover-open')) {
dialog.showPopover();
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/components/tooltip/test/tooltip.spec.tsx`:
- Around line 16-32: The test suite for the showTooltip method currently only
covers the disconnected branch guard path, but there are two branches that need
testing: disconnected and popover-open. Add a new test case (similar in
structure to the existing "does not call showPopover on a dialog that was
removed before the delay" test) that specifically tests the popover-open guard
path by setting up a scenario where the dialog is already open with the
:popover-open pseudo-class before calling showTooltip, and verify that the
appropriate guard logic prevents duplicate popover calls.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: adeec3fc-4051-45a6-8a65-3faa90eb041e

📥 Commits

Reviewing files that changed from the base of the PR and between 7c34939 and 7107f18.

📒 Files selected for processing (2)
  • packages/core/src/components/tooltip/test/tooltip.spec.tsx
  • packages/core/src/components/tooltip/tooltip.tsx

Comment on lines +16 to +32
it('does not call showPopover on a dialog that was removed before the delay', async () => {
const { root, waitForChanges } = await render(
<ix-tooltip show-delay={0}></ix-tooltip>
);
const tooltip = root as HTMLIxTooltipElement;
const dialog = tooltip.shadowRoot!.querySelector('dialog')!;
const showPopover = vi.fn();
dialog.showPopover = showPopover;

const anchor = document.createElement('div');
await tooltip.showTooltip(anchor);
dialog.remove();
await flushTimeout();
await waitForChanges();

expect(showPopover).not.toHaveBeenCalled();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a test for the showTooltip “already open” guard path.

The new showTooltip guard has two branches (disconnected and :popover-open), but only the disconnected branch is covered here.

Proposed test addition
 describe('ix-tooltip', () => {
+  it('does not call showPopover when the popover is already open', async () => {
+    const { root, waitForChanges } = await render(
+      <ix-tooltip show-delay={0}></ix-tooltip>
+    );
+    const tooltip = root as HTMLIxTooltipElement;
+    const dialog = tooltip.shadowRoot!.querySelector('dialog')!;
+
+    const showPopover = vi.fn();
+    dialog.showPopover = showPopover;
+    vi.spyOn(dialog, 'matches').mockImplementation((selector: string) => {
+      if (selector === ':popover-open') return true;
+      return false;
+    });
+
+    const anchor = document.createElement('div');
+    await tooltip.showTooltip(anchor);
+    await flushTimeout();
+    await waitForChanges();
+
+    expect(showPopover).not.toHaveBeenCalled();
+  });

Also applies to: 34-54

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/components/tooltip/test/tooltip.spec.tsx` around lines 16 -
32, The test suite for the showTooltip method currently only covers the
disconnected branch guard path, but there are two branches that need testing:
disconnected and popover-open. Add a new test case (similar in structure to the
existing "does not call showPopover on a dialog that was removed before the
delay" test) that specifically tests the popover-open guard path by setting up a
scenario where the dialog is already open with the :popover-open pseudo-class
before calling showTooltip, and verify that the appropriate guard logic prevents
duplicate popover calls.

Signed-off-by: Sai Asish Y <say.apm35@gmail.com>
@sonarqubecloud

Copy link
Copy Markdown

Comment on lines +159 to +168
private isPopoverOpen(dialog: HTMLElement) {
try {
return dialog.matches(':popover-open');
} catch {
// Engines without :popover-open support (e.g. jsdom) throw on the
// selector; treat the popover as not open in that case.
return false;
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is the css selector really necessary here? DId you consider to use the open attribute of the instance? https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/open

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.

2 participants