[DREAM-813] Share anchored popover caret placement - #24928
Draft
myabc wants to merge 9 commits into
Draft
Conversation
The timeline tooltip's caret geometry is generic: it only needs the popover and anchor rectangles. Moves it under a shared anchored-popover directory so the calendar and budget chart tooltips can use it too. https://community.openproject.org/wp/DREAM-813
Renders the Popover-message wrapper with the caret modifier class and offset custom property from a caret placement, so consumers stop hand-writing the same markup.
Watches the popover's inline style, which is where anchored-position writes its position, and recomputes the caret placement after every reposition so flips and shifts never leave the caret pointing at nothing.
Moves the caret offset wiring out of the timeline widget so every consumer of the popover message partial gets the same caret behaviour.
Replaces the inline message template and observer with the shared partial and synchroniser. No behaviour change.
The time-entry popover carried a fixed left caret that pointed the wrong way whenever anchored-position flipped it. Renders it through the shared message partial and keeps the caret in sync on reposition. The popover now uses the Primer message padding and body font size and sits eight pixels further from the entry to make room for the caret.
The chart tooltips positioned themselves with a fixed transform from the Chart.js caret point, rendered their lit root straight into document.body and carried a caret that never flipped. Anchors them on a DOMRect at the caret point through Primer's anchored-position, renders into a host owned by the chart component and shares the caret handling with the other popovers.
The synchroniser kept the last applied placement for the element's lifetime, so reopening a tooltip on the same item with identical geometry never re-applied the caret. Clears it whenever the popover is observed closed.
Re-creates the chart tooltip when Angular swaps its host element, disconnects its caret synchroniser on destroy, and moves the pointer-events reset out of the shared host style so the calendar's dialog popover keeps selectable text.
|
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. |
Contributor
There was a problem hiding this comment.
Pull request overview
Refactors timeline, calendar, and budget-chart tooltips to share anchored-popover caret handling and positioning.
Changes:
- Adds shared caret geometry, rendering, synchronization, and styles.
- Migrates tooltip consumers to anchored positioning.
- Adds coverage for caret behavior and tooltip rendering.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Change | Final review comment |
|---|---|---|
frontend/src/global_styles/common/openproject-common.module.sass |
Imports shared popover and chart styles. | — |
frontend/src/app/shared/components/project-timeline-graph/project-timeline-graph.component.ts |
Uses shared caret synchronization. | — |
frontend/src/app/shared/components/project-timeline-graph/project-timeline-graph.component.spec.ts |
Tests caret reuse after reopening. | — |
frontend/src/app/shared/components/project-timeline-graph/project-timeline-graph.component.sass |
Applies shared tooltip styling. | — |
frontend/src/app/shared/components/budget-graphs/overview/budget-by-cost-type.component.ts |
Hosts pie-chart tooltips. | — |
frontend/src/app/shared/components/budget-graphs/overview/budget-by-cost-type.component.html |
Adds the pie tooltip host. | — |
frontend/src/app/shared/components/budget-graphs/overview/actual-costs.component.ts |
Hosts bar-chart tooltips. | — |
frontend/src/app/shared/components/budget-graphs/overview/actual-costs.component.html |
Adds the bar tooltip host. | — |
frontend/src/app/shared/components/budget-graphs/chart.config.ts |
Implements anchored chart tooltips. | Moderate (3 votes, L88): The synthetic DOMRect can become stale after scrolling or resizing. Nit (2 votes, L135): Add bar-renderer coverage for date/value output and lifecycle. |
frontend/src/app/shared/components/budget-graphs/chart.config.spec.ts |
Tests chart tooltip behavior. | — |
frontend/src/app/shared/components/budget-graphs/budget-graphs.sass |
Styles chart popovers. | — |
frontend/src/app/shared/components/anchored-popover/popover-message.ts |
Provides the shared popover message partial. | — |
frontend/src/app/shared/components/anchored-popover/popover-message.spec.ts |
Tests message and caret rendering. | — |
frontend/src/app/shared/components/anchored-popover/caret-sync.ts |
Synchronizes caret placement. | — |
frontend/src/app/shared/components/anchored-popover/caret-sync.spec.ts |
Tests synchronization lifecycle. | — |
frontend/src/app/shared/components/anchored-popover/caret-placement.ts |
Provides caret geometry. | — |
frontend/src/app/shared/components/anchored-popover/caret-placement.spec.ts |
Tests caret geometry. | — |
frontend/src/app/shared/components/anchored-popover/anchored-popover.sass |
Defines shared popover styles. | — |
frontend/src/app/features/calendar/te-calendar/te-calendar.component.ts |
Adopts shared popover rendering and synchronization. | Moderate (2 votes, L606): After the schema await, the event or component may be gone; verify the anchor and popover remain connected before observing, and clean up the map on teardown. |
frontend/src/app/features/calendar/te-calendar/te-calendar-popover.spec.ts |
Tests calendar popover markup and caret classes. | — |
Suppressed comments (2)
frontend/src/app/features/calendar/te-calendar/te-calendar.component.ts:148
- The old calendar message applied
p-2 text-small, but this replacement passes only the shared wrapper and the calendar has no replacement rule for those values (the chart consumer adds one explicitly). The message therefore falls back to Primer's default padding and font size instead of retaining the compact calendar presentation. Preserve the calendar-specific padding and typography on the new message.
${popoverMessage(list, caret)}
frontend/src/app/shared/components/budget-graphs/chart.config.ts:90
- This clears the rendered caret on every external tooltip update, but
syncCaretretains its internallastplacement. When Chart.js invokes the renderer again for the same active point, the placement can equallast, so the observer suppresses the callback and the tooltip remains without a caret afterdraw()removes it. Reset the synchronizer cache when clearing the caret, or keep the existing caret until a new placement is applied.
this.caret = null;
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| anchorEl.setAttribute('popovertarget', popoverId); | ||
|
|
||
| const popoverEl = document.getElementById(popoverId)!; | ||
| const stopCaretSync = syncCaret(popoverEl, () => anchorEl.getBoundingClientRect(), draw); |
| } | ||
|
|
||
| const { left, top } = context.chart.canvas.getBoundingClientRect(); | ||
| this.anchor = new DOMRect(Math.round(left + context.tooltip.caretX), Math.round(top + context.tooltip.caretY), 0, 0); |
| return function(context:TooltipContext<'bar'>) { | ||
| const { tooltip } = context; | ||
| const items = tooltip.dataPoints.map((dp, i) => { | ||
| export function createBarTooltipRenderer(host:HTMLElement, formatCurrency:FormatCurrency) { |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Stacked on #24893 — review only the commits after its head.
Ticket
https://community.openproject.org/wp/DREAM-813
What are you trying to accomplish?
Three tooltips hand-built a Primer
.Popover-messagewith a caret: the project timeline widget (#24893), the time-entry calendar popover and the budget chart tooltips. Primer'sanchored-positiondoes not expose which side it settled on and repositions on scroll and resize, so the calendar's and the charts' fixedPopover-message--left-topcaret pointed the wrong way whenever the popover flipped. The chart tooltips also positioned themselves by hand and rendered their lit root straight intodocument.body.This PR extracts the timeline's caret handling into a small shared primitive and moves all three consumers onto it.
Screenshots
What approach did you choose and why?
frontend/src/app/shared/components/anchored-popover/holds three pieces:caretPlacement()(pure geometry, moved from the timeline),popoverMessage()(a lit partial for the.Popover-messagewrapper carrying the caret class and offset) andsyncCaret()(aMutationObserveron the popover's inline style, which is whereanchored-positionwrites its position). Consumers keep their own trigger lifecycle, popover type and ARIA roles — a generic hover-popover abstraction was deliberately not built, the three behave too differently.The budget charts go one step further: instead of a fixed transform from the Chart.js caret point they use
anchored-positionanchored on aDOMRectat that point (Primer'sgetAnchoredPositionacceptsElement | DOMRect; the custom element's setter only narrows the type). That removes the hand-rolled positioning and fade, and the tooltip now renders into a host element owned by the chart component.The caret synchroniser clears its cached placement whenever the popover is observed closed, so reopening a tooltip on the same item re-applies the caret; each consumer disconnects it on teardown.
Important
Primer's
.Popover-messagemobile styles (< 768px) hide the caret for every consumer; unchanged, same note as #24893.Merge checklist
anchored-positionleft underdocument.body