Skip to content

fix(viewer): keep Add-comment popover attached to highlighted text on scroll#326

Open
yumike wants to merge 8 commits into
mainfrom
fix/comment-popover-scroll-tracking
Open

fix(viewer): keep Add-comment popover attached to highlighted text on scroll#326
yumike wants to merge 8 commits into
mainfrom
fix/comment-popover-scroll-tracking

Conversation

@yumike
Copy link
Copy Markdown
Collaborator

@yumike yumike commented Apr 29, 2026

Summary

  • The "Add comment" popover captured a viewport-fixed {x, y} snapshot at mouseup and the Popover primitive doesn't subscribe to scroll in free mode, so any scroll detached the popover from the selection.
  • Track the Range itself (cloned to decouple from the live Selection) and re-measure on scroll (capture phase, so embedded ancestor scroll containers count) + resize — same listener pattern observeElement already uses for element anchors.

Test plan

  • npm -w @rwdocs/viewer run check — 0 errors, 0 warnings
  • npm -w @rwdocs/viewer run test — 361/361 unit tests pass
  • npx playwright test --project=chromium e2e/comments.spec.ts — 19/19 e2e tests pass, including new popover follows the highlighted text on scroll
  • make format, make lint clean

🤖 Generated with Claude Code

yumike and others added 8 commits April 29, 2026 19:40
… scroll

Previously the popover captured a viewport-fixed rect at mouseup, so any
scroll detached it from the selection. Track the Range and re-measure on
scroll/resize, mirroring how `observeElement` already does for element
anchors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If live reload or navigation swapped the article DOM while a selection
was active, the cached Range's start/end nodes got detached and
getBoundingClientRect() returned zeros, briefly jumping the
Add-comment popover to the top-left corner. Clear selectionRange when
page.data identity changes so the popover dismisses cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The scroll/resize handler reassigned `selectionRect` to a new object on
every tick, even when x/y were identical — triggering Svelte to re-run
the Popover's reactive bindings during scrolls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the bespoke scroll/resize listener wiring and equality guard in
PageContent with a sibling to useAnchorOffset that tracks a Range's
viewport rect. Per-field $state writes give the no-op-skip behavior
implicitly, so the equality guard added in dce6249 is gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nd observeElement

Generalize observeElement to accept Element | Range — ResizeObserver
attaches only when the target is an Element. useRangeRect now delegates
its scroll/resize listeners through observeElement instead of
re-implementing the capture-phase scroll + resize pair, removing a
near-duplicate of useAnchorOffset's wiring.

Also consolidate the makeRange test fixture into the shared
__fixtures__/resize-observer-mock module alongside makeAnchor (sharing
a fakeRect builder), trim a few change-narration comments, and fix the
cloneRange comment in PageContent — the real reason to clone is that
Selection.getRangeAt(0) returns a live Range, not anything about
\$state.raw identity tracking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
useRangeRect and useAnchorOffset had identical state shape, identical
observeElement wiring, and identical getter return — only the target
type differed. Generalize useAnchorOffset over `Element | Range` and
delete the duplicate hook.

Push the "measured = false when target becomes null" reset into
useAnchorOffset itself, so popovers anchored to elements also hide
cleanly when their anchor disappears.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`observeElement` now accepts `Element | Range`, so rename it to
`observeTarget` to match the contract. Export `fakeRect` from the
shared resize-observer mock and consume it in `overlay-testing`'s
`mockRect`, removing the inlined DOMRect literal. Extend `makeAnchor`
to accept a getter (mirroring `makeRange`) and collapse four
hand-rolled `getBoundingClientRect` blocks in the hook tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces a duplicate $effect in useAnchorOffset that re-subscribed to
the same getter just to flip `measured` to false. Drops `as unknown as
Range` in the test fixture by using a real `document.createRange()`,
and converts the remaining inline DOMRect literals in tests to
fakeRect / makeAnchor's getter form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant