Skip to content

fix: anchor the context popup to the visible part of its selection - #169

Merged
Azganoth merged 3 commits into
mainfrom
bug/context-popup-visible-selection-anchor
Aug 2, 2026
Merged

fix: anchor the context popup to the visible part of its selection#169
Azganoth merged 3 commits into
mainfrom
bug/context-popup-visible-selection-anchor

Conversation

@Azganoth

@Azganoth Azganoth commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Summary

The context popup anchored to a position: fixed span measured once when it opened. Three defects followed from that one model, and this replaces the model rather than patching each.

  • The popup now anchors to a virtual reference that measures the live selection, clamped to the document surface's scroll viewport, so it follows the text as the document scrolls. Floating UI re-queries it through a contextElement, which is also what resolves the scroll ancestors it listens on — without it no listener is attached at all and detectOverflow falls back to the document element.
  • A selection with no visible part reports its real off-screen rect, so hideWhenDetached hides the popup instead of closing it. The selection, the open state, and any focus the popup holds all survive, and the popup returns when the selection scrolls back.
  • A popup holding focus — or opened from the keyboard, which pins from the moment it opens rather than from the focus it is about to take — anchors to a rect pinned inside the viewport and freezes it, so scrolling can neither hide it nor move it out from under a keyboard interaction.
  • A selection taller than the visible area, or one that fills it, has no room beside it, so the popup sits inside the selection at its first visible line. Anchoring past an edge only gives collision handling somewhere further out of reach to push it. The anchor also keeps a pixel of height, because Floating UI reads a rect of no height resting on the clipping edge as fully clipped and would hide the popup.

The scroll listener that closed the popup existed only to hide the stale anchor, and goes with it. docs/specification.md and docs/reference.md lose the focus-dependent scroll rule they stated.

Related Issue

Closes #163

Verification

pnpm check:frontend on each of the three commits, green throughout. Final run: 95 files, 1008 tests, coverage 90.73 / 81.38 / 92.22 / 92.10 against the 88 / 78 / 89 / 89 floor. Frontend-only change, so backend checks were not run.

Tests added or reworked:

  • src/features/editor/utils/contextPopupAnchor.test.ts (new) — 13 cases over the pure resolver: full and partial overlap, horizontal clamping, the three ways a selection can be too tall to sit beside, a fully visible selection that fills the viewport, and the live/pinned split for a selection with no visible part.
  • src/features/editor/components/EditorContextPopup.test.tsx — the popup hides while its anchor is off screen and returns when it comes back, without onClose; a popup holding focus stays visible with an off-screen selection; a pointer popup measures live and a keyboard popup pinned; a focused popup reuses one rect across a new request. Both scroll cases now assert the popup stays open.
  • src/features/editor/plugins/contextPopup.test.tsx — the anchor's rect spans the selection's visible ends, re-measures as the selection moves rather than replaying the rect it opened with, and carries the editor as its context element.

Reviewer reproduction, in pnpm tauri dev with corpus/ open on a document longer than the window:

  1. Select a word, right-click it, then scroll. The popup tracks the text instead of drifting or closing.
  2. Keep scrolling until the selection leaves the viewport. The popup disappears; scroll back and it returns, still open and still on the same selection.
  3. Mod+A, then Shift+F10 — a keyboard selection does not open the popup on its own, which is Keyboard selection does not open the context popup that the documents promise #164. The popup lands inside the selection. Repeat with the selection scrolled so only its top, only its bottom, or neither edge is visible.
  4. Shift+F10 on a small selection, then scroll. The focused popup stays put and stays visible.

All four steps were confirmed manually on Windows. Steps 1, 2 and 4 passed on the first two commits; step 3 failed there — the popup landed below the last visible line, above the first, or vanished outright depending on which selection edges were on screen — and the third commit is the response to that. Step 3 was re-confirmed against it.

Automated coverage of visibility is possible at all only because src/test/setup/dom.ts now reports the window size for document.documentElement's clientWidth/clientHeight. happy-dom performs no layout, so the document element measured 0×0 and Floating UI treated every reference as fully clipped. That setup file applies to every DOM test; the full suite passes with it.

Notes

Three findings from manual testing that this pull request deliberately leaves alone. All three predate this branch and none touch anchoring.

One residual case in scope but unfixed: Shift+F10 pressed while the popup is already hidden focuses a control that is still visibility: hidden for that tick, so focus does not land. Pinning makes the popup visible from the next reposition on. The clean fix is scrolling the selection into view on a keyboard request, which changes editor behavior beyond what this issue agreed.

POPUP_CLEARANCE is the one tuned number here. It decides only whether a selection short enough to sit beside leaves room for the popup, and is sized by eye.

The popup positioned against a fixed span measured once when it opened, so it
drifted from its text as the document scrolled and, for a selection taller than
the viewport, landed past the top of the editor. A virtual anchor measuring the
live selection replaces it. Floating UI re-queries that anchor through its
context element, which is also what resolves the scroll ancestors it listens on.

The scroll listener that closed the popup existed only to hide the stale anchor,
so it goes with it.
Scrolling the selection away used to close the popup, discarding a command
surface for a selection that is still live. The anchor now reports the
selection where it actually is once none of it is visible, so Floating UI's
referenceHidden hides the popup and returns it when the selection scrolls back;
open state, focus, and the selection are all untouched.

A popup the user is working in pins to a rect inside the viewport instead, so a
scroll can neither hide it nor move it out from under a keyboard interaction. A
keyboard popup pins from the moment it opens rather than from the focus it is
about to take.

happy-dom performs no layout, so the document element measured 0x0 and every
reference read as fully clipped. The test setup now reports the window size
there, which is what makes any of this assertable.
A selection taller than the visible area has no outside within reach, and one
that fills the visible area leaves no room on either side. Both now anchor the
popup to the first line of the selection the reader can see, so it lands inside
the selection rather than below its last visible line, above its first, or past
an edge that collision handling only pushes it further out of.

The anchor also keeps a pixel of height. Floating UI reads a rect of no height
resting on the clipping edge as fully clipped, so a selection spanning the whole
visible area was hiding the popup outright, and a pinned anchor for a selection
scrolled out of view would have hidden it too.
@Azganoth Azganoth added the Bug Something isn't working label Aug 2, 2026
@Azganoth Azganoth self-assigned this Aug 2, 2026
@Azganoth
Azganoth merged commit 3718b41 into main Aug 2, 2026
2 checks passed
@Azganoth
Azganoth deleted the bug/context-popup-visible-selection-anchor branch August 2, 2026 20:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Context popup anchors to the whole selection and detaches from it on scroll

1 participant