[OP-20110] Anchor the popover on the visible entry - #25124
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Popover repositioning state (placeOpenPopover) can get out of sync when the popover="hint" element toggles via native interactions (e.g., Escape/light-dismiss), so it should be synchronized via the popover’s toggle event.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR improves the “My spent time” calendar entry popover positioning by anchoring it to the visible portion of a horizontally clipped entry and keeping the popover correctly positioned while scrolling.
Changes:
- Added
visibleRect()to compute the visible slice of an element by clipping its bounding rect to overflow ancestors’ padding boxes. - Added
liveRect()to provide aDOMRect-compatible, live-updating rect source for anchoring. - Updated the time entry calendar popover to anchor to
liveRect(() => visibleRect(entryEl))and to re-place an open popover on scroll; added unit tests for the new helpers and calendar behavior.
File summaries
| File | Description |
|---|---|
| frontend/src/app/shared/components/anchored-popover/visible-rect.ts | New helper computing the visible (clipped) rectangle of an element. |
| frontend/src/app/shared/components/anchored-popover/visible-rect.spec.ts | Unit tests validating clipping behavior across scroll/border/scrollbar cases. |
| frontend/src/app/shared/components/anchored-popover/live-rect.ts | New helper providing a live-updating DOMRect proxy for anchoring. |
| frontend/src/app/shared/components/anchored-popover/live-rect.spec.ts | Unit tests verifying live reads and integration with popover placement. |
| frontend/src/app/features/calendar/te-calendar/te-calendar.component.ts | Uses visibleRect+liveRect for anchoring and repositions an open popover on scroll. |
| frontend/src/app/features/calendar/te-calendar/te-calendar.component.spec.ts | Integration-style tests covering clipped-entry anchoring and scroll-follow behavior. |
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
5d9314d to
e69d2d5
Compare
The widget calendar scrolls horizontally, but the popover was anchored on the entry's full box, so for an entry half hidden behind the scroller edge it opened beside the hidden part with its caret pointing into it, and it stayed there when the calendar scrolled. Anchors on the entry's rect clipped by its overflow ancestors' padding boxes, handed to Primer as a live rect so its own updates never read a stale box, and re-places the open popover on any scroll, which also keeps it beside an entry that focus scrolls into view. https://community.openproject.org/wp/OP-20110
vis-timeline hides items that scroll past its centre panel, so a tooltip on a half-hidden item opened beside the hidden part. Uses the calendar's live visible rect for the anchor as well.
a6ac69c to
cf0678a
Compare
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer. |
Note
Stacked on #25123 (OP-19358). Only the last two commits belong to this PR.
Ticket
https://community.openproject.org/wp/OP-20110
What are you trying to accomplish?
The "My spent time" calendar scrolls horizontally, but its entry popover was anchored on the entry's full box. For an entry partly hidden behind the widget's right edge the popover opened beside the hidden part, floating over the neighbouring widget with its caret pointing into the clipped area, and it stayed there when the calendar scrolled.
Screenshots
My spent time widget
Project timeline widget
What approach did you choose and why?
Two small helpers next to the shared placement in
anchored-popover/:visibleRect(element)clips the element's rect by the padding boxes of its overflow ancestors, so borders and scrollbars do not count as visible. Transforms,clip-pathand shadow boundaries are out of scope; both intended callers are plain light-DOM descendants of their scrollers.liveRect(source)wraps that in aDOMRectproxy that re-reads the source on every access. Primer'sgetAnchoredPositionreads a non-element anchor as the rect itself, so its own updates (window scroll, resize, itsResizeObserver) never see a stale box.The calendar anchors the popover on
liveRect(() => visibleRect(entry))and re-places the open popover on any scroll (one capture-phase listener per component, so the calendar's own scroller is covered as well as the page). Repositioning rather than closing matters for keyboard users: focusing a clipped entry scrolls it into view, and the popover follows instead of vanishing. This also removes the "caret stale after page scroll" limitation noted in #25112.Scope, as agreed on the work package: the caret and anchor target the visible slice of the entry. The popover itself may still extend past the widget edge, as any popover on the last visible column does; flipping it into the widget (option 2 on the work package) was left out.
The project timeline widget's tooltip had the same exposure for items half hidden by the timeline's centre panel, so its second commit anchors that tooltip on the same live visible rect. It keeps closing on scroll, as it is hover-only.
Merge checklist
visible-rect.spec.ts,te-calendar.component.spec.tsandproject-timeline-tooltip.popover.spec.tsalso in Firefox and WebKit)