Skip to content

fix(super-editor): flip toolbar tooltip below trigger when clipped by viewport top#3832

Open
IvanRodriCalleja wants to merge 2 commits into
superdoc-dev:mainfrom
IvanRodriCalleja:fix/gh-3786-toolbar-tooltip-viewport-flip
Open

fix(super-editor): flip toolbar tooltip below trigger when clipped by viewport top#3832
IvanRodriCalleja wants to merge 2 commits into
superdoc-dev:mainfrom
IvanRodriCalleja:fix/gh-3786-toolbar-tooltip-viewport-flip

Conversation

@IvanRodriCalleja

Copy link
Copy Markdown

Summary

  • Fixes Toolbar tooltips are cut off by edge of the window #3786
  • SdTooltip.vue's updatePosition() clamped the tooltip horizontally against the viewport but always placed it above the trigger (triggerRect.top - contentHeight - 10), with no vertical bounds check.
  • Fix: when the tooltip would not fit above the trigger (within the existing 8px viewport gutter), place it below (triggerRect.bottom + offset) instead. Placement is exposed as data-placement="top" | "bottom" on .sd-tooltip-content, which the component's own CSS uses to flip the arrow anchor and the transition's transform-origin so the tooltip still points at and grows from the trigger.
  • Note: this introduces data-placement as a small styling contract on the tooltip content element (same pattern as
    floating-ui/Radix data-side). Pre-existing and untouched: the arrow is centered on the tooltip, so it can miss the trigger when the tooltip is horizontally clamped at the far left/right viewport edges.

Before / After

Toolbar flush with the top of the viewport, hovering the first toolbar button:

Before After
image image

Test plan

  • New unit tests in SdTooltip.test.js (positioning block): tooltip stays fully inside the viewport and clear of the trigger
    when the trigger is flush with the top; still renders fully above when there is room; data-placement marks the side so the arrow points at the trigger. Verified the flush-top test fails without the fix (tooltip at top: -44px) and passes with it.
  • Existing SdTooltip auto-hide test unchanged and passing.
  • Full @superdoc/super-editor test suite: 1218 files / 17210 tests passed.
  • Manual check on the dev server with the toolbar at the top of the page: tooltip flips below the toolbar and the arrow points up at the button; with room above, behavior is unchanged.

@qodo-code-review

qodo-code-review Bot commented Jul 15, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used

Grey Divider


Remediation recommended

1. Tooltip tests leak listeners ✓ Resolved 🐞 Bug ☼ Reliability
Description
The new positioning tests open SdTooltip instances but never close or unmount them, so the
component’s global resize/scroll/keydown listeners remain registered after the test finishes. This
can create cross-test contamination/flakiness and accumulate listeners across the suite.
Code

packages/super-editor/src/editors/v1/components/toolbar/SdTooltip.test.js[R74-93]

+    const mountAndOpen = async (triggerRect) => {
+      stubLayout(triggerRect);
+
+      const wrapper = mount(SdTooltip, {
+        attachTo: document.body,
+        props: {
+          delay: 0,
+          duration: 0,
+        },
+        slots: {
+          trigger: '<button type="button">Undo</button>',
+          default: 'Undo',
+        },
+      });
+
+      await wrapper.find('.sd-tooltip-trigger').trigger('mouseenter');
+      await nextTick();
+      await nextTick();
+      return document.body.querySelector('.sd-tooltip-content');
+    };
Relevance

⭐⭐⭐ High

Team previously accepted preventing cross-test pollution by restoring globals/listeners in tests (PR
#3181) and listener teardown fixes (PR #2783).

PR-#3181
PR-#2783

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
mountAndOpen() mounts and opens the tooltip but returns only the DOM node, leaving no unmount
path; meanwhile SdTooltip adds global listeners when isOpen is true and only removes them on
close/unmount. Since the positioning tests don’t close or unmount, the listeners persist beyond the
test cleanup (which only clears the DOM and restores mocks).

packages/super-editor/src/editors/v1/components/toolbar/SdTooltip.test.js[6-11]
packages/super-editor/src/editors/v1/components/toolbar/SdTooltip.test.js[74-93]
packages/super-editor/src/editors/v1/components/toolbar/SdTooltip.vue[185-195]
packages/super-editor/src/editors/v1/components/toolbar/SdTooltip.vue[197-204]
PR-#3181

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`mountAndOpen()` mounts `SdTooltip` and opens it, but the tests never call `wrapper.unmount()` (nor do they close the tooltip). `SdTooltip` registers global `window`/`document` listeners while open, so these listeners can survive past the test and interfere with later tests.

### Issue Context
Clearing `document.body.innerHTML` does **not** unmount Vue components, so `onBeforeUnmount` cleanup never runs.

### Fix Focus Areas
- packages/super-editor/src/editors/v1/components/toolbar/SdTooltip.test.js[6-11]
- packages/super-editor/src/editors/v1/components/toolbar/SdTooltip.test.js[74-93]

### Suggested fix
- Track mounted wrappers and unmount them in `afterEach` (e.g., push each `wrapper` returned by `mount()` into an array and `unmount()` them during cleanup).
- Alternatively, change `mountAndOpen()` to return `{ wrapper, content }` and have each test call `wrapper.unmount()` in a `finally` block.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit 5466bfc

Results up to commit f468bd4


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Tooltip tests leak listeners ✓ Resolved 🐞 Bug ☼ Reliability
Description
The new positioning tests open SdTooltip instances but never close or unmount them, so the
component’s global resize/scroll/keydown listeners remain registered after the test finishes. This
can create cross-test contamination/flakiness and accumulate listeners across the suite.
Code

packages/super-editor/src/editors/v1/components/toolbar/SdTooltip.test.js[R74-93]

+    const mountAndOpen = async (triggerRect) => {
+      stubLayout(triggerRect);
+
+      const wrapper = mount(SdTooltip, {
+        attachTo: document.body,
+        props: {
+          delay: 0,
+          duration: 0,
+        },
+        slots: {
+          trigger: '<button type="button">Undo</button>',
+          default: 'Undo',
+        },
+      });
+
+      await wrapper.find('.sd-tooltip-trigger').trigger('mouseenter');
+      await nextTick();
+      await nextTick();
+      return document.body.querySelector('.sd-tooltip-content');
+    };
Relevance

⭐⭐⭐ High

Team previously accepted preventing cross-test pollution by restoring globals/listeners in tests (PR
#3181) and listener teardown fixes (PR #2783).

PR-#3181
PR-#2783

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
mountAndOpen() mounts and opens the tooltip but returns only the DOM node, leaving no unmount
path; meanwhile SdTooltip adds global listeners when isOpen is true and only removes them on
close/unmount. Since the positioning tests don’t close or unmount, the listeners persist beyond the
test cleanup (which only clears the DOM and restores mocks).

packages/super-editor/src/editors/v1/components/toolbar/SdTooltip.test.js[6-11]
packages/super-editor/src/editors/v1/components/toolbar/SdTooltip.test.js[74-93]
packages/super-editor/src/editors/v1/components/toolbar/SdTooltip.vue[185-195]
packages/super-editor/src/editors/v1/components/toolbar/SdTooltip.vue[197-204]
PR-#3181

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`mountAndOpen()` mounts `SdTooltip` and opens it, but the tests never call `wrapper.unmount()` (nor do they close the tooltip). `SdTooltip` registers global `window`/`document` listeners while open, so these listeners can survive past the test and interfere with later tests.

### Issue Context
Clearing `document.body.innerHTML` does **not** unmount Vue components, so `onBeforeUnmount` cleanup never runs.

### Fix Focus Areas
- packages/super-editor/src/editors/v1/components/toolbar/SdTooltip.test.js[6-11]
- packages/super-editor/src/editors/v1/components/toolbar/SdTooltip.test.js[74-93]

### Suggested fix
- Track mounted wrappers and unmount them in `afterEach` (e.g., push each `wrapper` returned by `mount()` into an array and `unmount()` them during cleanup).
- Alternatively, change `mountAndOpen()` to return `{ wrapper, content }` and have each test call `wrapper.unmount()` in a `finally` block.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Qodo Logo

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 5466bfc

@caio-pizzol caio-pizzol self-assigned this Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Toolbar tooltips are cut off by edge of the window

2 participants