Content
View differences
Updated by Alexander Coles 5 days ago
### Context
Three components hand-build a Primer `.Popover-message` tooltip with a caret: the project timeline widget (`project-timeline-graph`, via https://github.com/opf/openproject/pull/24893), the time-entry calendar popover (`te-calendar.component.ts`) and the budget chart tooltips (`budget-graphs/chart.config.ts`).
Primer's `anchored-position` does not expose which side it settled on and repositions on scroll and resize, so a static caret class ends up pointing the wrong way when the popover flips. The timeline widget solves this with a pure `caretPlacement()` function and an observer that re-derives the caret after every reposition; the calendar and the charts still use a fixed `Popover-message--left-top` caret. The chart tooltips additionally position themselves by hand (`position: fixed` + `transform` from Chart.js `caretX`/`caretY`) and render into `document.body`.
### Goal
* Extract the generic parts into `frontend/src/app/shared/components/anchored-popover/`: `caretPlacement()`, a `popoverMessage()` lit partial for the `.Popover-message` wrapper, and a `placePopover()` observer helper, each with unit specs.
* Migrate all three consumers to it in one PR. Timeline: no behaviour change. Calendar: caret follows Primer's flip. Charts: replace the hand-rolled positioning with `anchored-position` anchored on a `DOMRect` at the Chart.js caret point, render into a component-owned host instead of `document.body`.
* Hover/trigger lifecycle, popover type and ARIA roles stay per consumer; no generic hover-popover abstraction.
### Notes
* Stacks on https://github.com/opf/openproject/pull/24893.
* Design: `openproject-agent-plans/2026-08-24-anchored-popover-caret-design.md`.
* Primer's mobile CSS (below 768px) hides `.Popover-message` carets; unchanged, same note as #SPPM-324.
### Hints for QA
Test on a desktop viewport (≥ 768px): below that Primer hides popover carets and forces the width to auto, and the calendar popover is disabled on mobile. Tooltip behaviour **should not be significantly changed** by this code maintenance work. However there are manual steps that can be performed to verify, if time/priorities allow.
* **Budget charts** (project overview, "Actual costs by month" and "Budget by cost type"):
* hover a bar. The popover sits beside the bar, vertically centred on the hovered segment, with the caret pointing at the segment's middle. Hover a pie slice: the caret points at the slice.
* Move the mouse from bar to bar: the tooltip must not flash anywhere else on the page (it used to paint one frame over the neighbouring widgets).
* Hover a bar close to the right edge of the viewport: the popover flips to the left of the bar and the caret moves to its right edge.
* Leave the canvas, then hover the same bar again: the caret is present both times. Scroll the page or resize the window while a chart tooltip is open: it closes.
* **Time entry calendar** (My page → "My spent time"):
* hover an entry. The popover opens right of the entry with the caret on the entry; near the right viewport edge it flips to the left and the caret follows. Tab focus opens it, leaving or blurring closes it.
* Resize the window while a calendar popover is open: it closes. Scrolling the page while it is open leaves it where it is (accepted, it closes on leave).
* Entries partly hidden by the widget's horizontal scroller still anchor on their full box: known, tracked in #OP-20110.
* Scrolled page: scroll so a chart or the calendar sits mid-viewport, then hover. The caret must face the anchor (an earlier draft flipped it on scrolled pages).
* Timeline widget: unchanged from #SPPM-324, caret on the milestone or phase, closes on scroll and resize.
* (for devs to check) Nothing is rendered directly under `document.body` any more: the chart tooltip lives in a host next to its canvas.
Three components hand-build a Primer `.Popover-message` tooltip with a caret: the project timeline widget (`project-timeline-graph`, via https://github.com/opf/openproject/pull/24893), the time-entry calendar popover (`te-calendar.component.ts`) and the budget chart tooltips (`budget-graphs/chart.config.ts`).
Primer's `anchored-position` does not expose which side it settled on and repositions on scroll and resize, so a static caret class ends up pointing the wrong way when the popover flips. The timeline widget solves this with a pure `caretPlacement()` function and an observer that re-derives the caret after every reposition; the calendar and the charts still use a fixed `Popover-message--left-top` caret. The chart tooltips additionally position themselves by hand (`position: fixed` + `transform` from Chart.js `caretX`/`caretY`) and render into `document.body`.
### Goal
* Extract the generic parts into `frontend/src/app/shared/components/anchored-popover/`: `caretPlacement()`, a `popoverMessage()` lit partial for the `.Popover-message` wrapper, and a `placePopover()` observer helper, each with unit specs.
* Migrate all three consumers to it in one PR. Timeline: no behaviour change. Calendar: caret follows Primer's flip. Charts: replace the hand-rolled positioning with `anchored-position` anchored on a `DOMRect` at the Chart.js caret point, render into a component-owned host instead of `document.body`.
* Hover/trigger lifecycle, popover type and ARIA roles stay per consumer; no generic hover-popover abstraction.
### Notes
* Stacks on https://github.com/opf/openproject/pull/24893.
* Design: `openproject-agent-plans/2026-08-24-anchored-popover-caret-design.md`.
* Primer's mobile CSS (below 768px) hides `.Popover-message` carets; unchanged, same note as #SPPM-324.
### Hints for QA
Test on a desktop viewport (≥ 768px): below that Primer hides popover carets and forces the width to auto, and the calendar popover is disabled on mobile. Tooltip behaviour **should not be significantly changed** by this code maintenance work. However there are manual steps that can be performed to verify, if time/priorities allow.
* **Budget charts** (project overview, "Actual costs by month" and "Budget by cost type"):
* hover a bar. The popover sits beside the bar, vertically centred on the hovered segment, with the caret pointing at the segment's middle. Hover a pie slice: the caret points at the slice.
* Move the mouse from bar to bar: the tooltip must not flash anywhere else on the page (it used to paint one frame over the neighbouring widgets).
* Hover a bar close to the right edge of the viewport: the popover flips to the left of the bar and the caret moves to its right edge.
* Leave the canvas, then hover the same bar again: the caret is present both times. Scroll the page or resize the window while a chart tooltip is open: it closes.
* **Time entry calendar** (My page → "My spent time"):
* hover an entry. The popover opens right of the entry with the caret on the entry; near the right viewport edge it flips to the left and the caret follows. Tab focus opens it, leaving or blurring closes it.
* Resize the window while a calendar popover is open: it closes. Scrolling the page while it is open leaves it where it is (accepted, it closes on leave).
* Entries partly hidden by the widget's horizontal scroller still anchor on their full box: known, tracked in #OP-20110.
* Scrolled page: scroll so a chart or the calendar sits mid-viewport, then hover. The caret must face the anchor (an earlier draft flipped it on scrolled pages).
* Timeline widget: unchanged from #SPPM-324, caret on the milestone or phase, closes on scroll and resize.
* (for devs to check) Nothing is rendered directly under `document.body` any more: the chart tooltip lives in a host next to its canvas.