[SPPM-324] Show timeline tooltip in the top layer - #24893
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the project timeline widget to render hover tooltips as a Primer anchored-position popover in the browser top layer (instead of vis-timeline’s internal tooltip), preventing clipping/stacking issues inside dashboard grid cells.
Changes:
- Disables vis-timeline’s built-in tooltip rendering and implements a custom hover tooltip driven by
itemover/itemoutevents. - Renders the tooltip via lit-html into an
anchored-position popover="manual"and post-processes geometry to place the caret. - Updates tooltip styling to match Primer popover visuals and adds unit tests for tooltip behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| frontend/src/app/shared/components/project-timeline-graph/project-timeline-graph.component.ts | Disables vis tooltips, adds custom popover tooltip rendering and caret alignment logic. |
| frontend/src/app/shared/components/project-timeline-graph/project-timeline-graph.component.spec.ts | Adds unit tests covering tooltip open/close behavior and aria-hidden containment. |
| frontend/src/app/shared/components/project-timeline-graph/project-timeline-graph.component.sass | Replaces vis-tooltip styling with styles for the new Primer popover-based tooltip. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
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. |
a600c95 to
a3d0efe
Compare
HDinger
left a comment
There was a problem hiding this comment.
I am lacking a bit the history of that bug ticket. As far as I can see, there have been many iterations already, so I will not challenge the necessity to create a manual Popover element in JS (although I am not a big fan of it).
Functionalitywise, it looks fine to me. The only thing I'd like to challenge is the code placement. We already have a tooltipBuilder class (and file) for this widget. I guess, the new code belongs there.
a3d0efe to
5c363fa
Compare
|
@HDinger I've moved the tooltip code beside the builder: the popover markup is now I've also dropped the |
There was a problem hiding this comment.
🟡 Changes recommended
The new popover controller currently does not unregister its vis-timeline event listeners on teardown, which can cause stale callbacks and memory retention if the timeline outlives the controller.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 1
- Review effort level: Lite
|
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. |
3fa6439 to
671a919
Compare
vis-timeline renders its hover tooltip inside the timeline root, where the dashboard grid cell clips it and paints it behind the next widget, so long milestone names were cut off. Disables the vis tooltip and renders a Primer anchored-position popover from the itemover/itemout events instead: the top layer escapes both the clipping and the stacking context, and the popover stays inside the aria-hidden container so the sr-only list remains the only accessible version. Primer's anchored-position does not announce which side it settled on, so the caret side and offset are derived from the popover and anchor rectangles once the popover has opened; the tooltip closes on scroll and resize instead of following the repositioned popover. https://community.openproject.org/wp/SPPM-324
The popover relied on the component destroying the timeline right after it, which drops every listener. Removes its own handlers and tolerates a hide after its host left the document, so the class no longer depends on its caller's teardown order.
671a919 to
ff0c18f
Compare
Ticket
https://community.openproject.org/wp/SPPM-324
What are you trying to accomplish?
The project timeline widget renders its hover tooltip via vis-timeline, which appends it inside
.vis-timeline. That element sits in a dashboard grid cell (.grid--area) that both clips withoverflow: hiddenand owns az-indexstacking context, so a long milestone name was cut off and could be painted behind the widget below.The tooltip now renders as a popover in the browser's top layer, which is immune to both ancestor clipping and ancestor stacking contexts. It stays fully visible regardless of the milestone name length or position, flips below the item near the top of the viewport, and shifts sideways at the viewport edges while keeping its caret over the hovered item.
Screenshots
Screen recording: different screen sizes
Screen.Recording.2026-08-23.at.15.41.08.mov
What approach did you choose and why?
vis-timeline hard-wires its tooltip to the timeline root (
new Popup(this.body.dom.root, …)) and offers no container or callback option, so there is no library-native way to render it elsewhere. Instead of relocating vis's private tooltip element into aposition: fixedbody-level layer and reimplementing its geometry (#24700), this PR uses the library's documented off-switch (showTooltips: false) and renders the tooltip itself from theitemover/itemoutevents.The tooltip is a Primer
<anchored-position popover="manual">wrapping a.Popover-message(for the caret), rendered with lit-html — the same pattern the time-entry calendar uses for its hover popovers (te-calendar.component.ts). The existingProjectTimelineTooltipBuilderkeeps building the content (vis still calls it throughItem#getTitle(), which also covers clustered gates) and now also owns the popover markup (popoverTemplate), while the popover lifecycle (host, hover delay, caret, teardown) lives in a sibling class,ProjectTimelineTooltipPopover. The component only wires the two to the timeline.Consequences of this choice:
z-indexescalation and no scrollbar-gutter edge case: the top layer handles clipping and stacking,anchored-positionhandles flip/shift.aria-hiddentimeline- Primer'sanchored-positiondoes not expose which side it settled on (it can fall back to below, right or left of the anchor) and positions the popover in an animation frame it requests on open. The caret is derived once, one frame after opening, from the popover and anchor rectangles (caretPlacement()inproject-timeline-tooltip-caret.ts): the side facing the anchor, and the offset along that edge clamped clear of the rounded corners. The tooltip closes on scroll and resize rather than following a repositioned popover; it also hides on timeline pan/zoom and before the data is replaced. One popover element lives for the component lifetime.t.ts`): the side facing the anchor, and the offset along that edge clamped clear of the rounded corners. One popover element lives for the component lifetime; the tooltip hides on timeline pan/zoom and before the data is replaced.timeline.itemSet.getItemById(id).getTitle()(typed via a small local interface), needed because theitemoverevent only carries the item id and cluster items exist solely inside vis.Relaxing
.grid--area { overflow: hidden }was considered and rejected: every dashboard widget relies on it.Important
On viewports narrower than 768px the tooltip has no caret. Primer's
.Popover-messagemobile styles (@media (max-width: 767.98px)) hide the caret and lay the popover out as a sheet. This is left as is: hover tooltips are of little use on touch devices, and overriding Primer's mobile rules for a hover-only element is not worth the coupling.Merge checklist