fix(#11242): keep mobile date tooltip on-screen#11274
Draft
binokaryg wants to merge 14 commits into
Draft
Conversation
MobileTooltipDirective renders a focused element's `title` in a CDK overlay, positioned with a flexible strategy (withPush) so it stays within the viewport regardless of the trigger's position, the text length, or the direction. It runs only on touch / no-hover devices. Not wired in yet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The touch tooltip was a CSS pseudo-element (`[title]:focus::before`) anchored to a fixed edge with `width: max-content`, so it grew off the viewport and was clipped in several reachable contexts — the report detail summary, the reports list on tablet landscape, and the sync status on narrow phones — in both LTR and RTL. No fixed CSS anchor satisfies all of them: the trigger can sit at either edge and the tooltip can be wider than the space beside it. Render it through MobileTooltipDirective (on the app root) instead: a CDK overlay at the document level (escaping ancestor `overflow`) that flips/shifts to stay within the viewport. This drops the old pseudo-element and its per-context band-aids (min-width, first-row flip, sync-status overflow). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review findings on the overlay directive: - Register the focus/scroll listeners outside the Angular zone so they don't run app-wide change detection on every focus (the overlay content is rendered with a manual detectChanges). - Dismiss reliably: clear any existing tooltip before the title check; dismiss on scroll (capture phase, so plain CSS-overflow panes are covered); and drop the tooltip when its trigger is detached from the DOM (focusout doesn't fire then) via a MutationObserver. - Pass the trigger's direction to the overlay so start/end placement and bidi text are correct in RTL (the overlay lives outside the app's [dir] subtree). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cht-form compiles inbox.less (and thus the tooltip styles) but never applied the directive, so its Enketo form tooltips were lost on touch. Apply the directive on the cht-form root and add the @mm-directives path alias to its tsconfigs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tooltip is focus-based, so a long-press on a date shows it without navigating (the way to peek a full date on touch). But a short tap on a list-row date focuses it (showing the tooltip) and also opens the report; the date keeps focus, so the tooltip lingered over the loaded report. Dismiss on router NavigationEnd so it clears once the report has opened. NavigationEnd, not NavigationStart: the row's routerLink emits NavigationStart synchronously while the click is still bubbling, before the focusin handler shows the tooltip, so only a post-show event can dismiss it. A long-press focuses without navigating, so its tooltip stays. Router is injected optionally so cht-form (which has no router) still works. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tapping the date of the already-open report re-navigates to the current URL, which the router skips (NavigationSkipped, not NavigationEnd) — so the previous NavigationEnd-only dismissal left the tooltip lingering in exactly that case. Handle NavigationSkipped too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ation
Dismissing on router events (NavigationEnd/NavigationSkipped) broke the tooltip
on two-pane layouts (e.g. 951px): every tap on a list-row date navigates — End
for a new report, Skipped for the already-open one — so the tooltip was killed
instantly even though the list, and the still-focused trigger, remain visible.
What the navigation dismissal actually wanted to know is "did the trigger stop
being visible?" — on single-pane mobile, opening a report slides the
still-focused list off-screen (.show-content .left-pane { left: -100% }) with
no focusout, no scroll, and no DOM removal. Watch exactly that with an
IntersectionObserver on the trigger: off-viewport → dismiss (the original
lingering bug stays fixed); visible in two panes → the tooltip stays, like the
old CSS tooltip did. The router subscription (and the directive's only Router
dependency) goes away entirely; same-URL taps need no special case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…haviors Three behaviors the branch fixed had no regression guard: show() clearing an existing tooltip before the title check (a focusin alone, with no focusout, must not strand the old tooltip), only one overlay existing when focus moves between titled elements, and the overlay resolving its direction from the trigger so RTL anchoring and bidi text survive a refactor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
readonly on the never-reassigned constructor-injected members (S2933), and dedicated chai matchers (.to.be.null / .to.have.lengthOf) over generic equality assertions in the spec (S5906). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gating on `(hover: none), (pointer: coarse)` showed two tooltips on devices that match `pointer: coarse` but can still hover (e.g. touchscreen laptops): click focused the trigger (overlay tooltip) while the mouse hover revealed the native `title`. The overlay is only warranted where hovering is impossible, so gate on `hover: none` alone — where hover works, the native tooltip does too, and the directive stays out of the way. Also evaluate the query per focus instead of once at startup, so input-capability changes (DevTools device emulation toggled, a convertible docking/undocking) apply without a reload. The document listeners are now always registered, but they run outside the Angular zone and no-op after a boolean check, so hover-capable devices pay nothing meaningful. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Description
On touch devices the date/time tooltip — the accessible way to reveal a relative date's full value — was clipped off-screen: only the tail of the string was readable (e.g.
5:30 AMinstead ofTue, Jul 7th, 2026 at 5:30 AM). Most visible on the report detail "Submitted by …" line. Not language-specific — reproduced in English, Nepali, and Arabic (RTL).Fixes #11242
Root cause. The touch tooltip was a CSS pseudo-element in
webapp/src/css/tooltip.less([title]:focus::before, withcontent: attr(title); desktop keeps the nativetitle). It was anchored to a fixed edge (right: -10px,width: max-content), so it grew off the viewport, and it was also cropped by ancestoroverflow: hiddenscroll containers. No fixed CSS anchor can satisfy every case — the trigger can sit at either edge, the text can be wider than the space beside it, and RTL flips which edge is which. (A series of scoped CSS overrides was prototyped and each broke another context; see the issue thread.)Fix. Replace the CSS pseudo-element with
MobileTooltipDirective, which renders thetitlein a CDK overlay at the document level:withPush) so it stays inside the viewport regardless of the trigger's position, the text length, or the direction;overflowclipping;start/endplacement and bidirectional text are correct in RTL.It runs only on touch / no-hover devices (desktop is untouched — native
titleon hover). It's hosted on the webapp app root and on the cht-form root, and the old CSS mechanism (plus its per-contextmin-width/overflow: visible/ first-row band-aids) is removed.Why a document-level directive and not
matTooltip? The date tooltips aren't real elements a directive can bind to — the date pipes (relativeDate,taskDueDate, …) build an HTML string with thetitleinside it, injected via[innerHTML]in 9 places, and Angular directives can't attach to innerHTML-injected DOM. UsingmatTooltipwould mean converting that shared date rendering into a component, reworking the interval-based relative-date live-update (RelativeDateService.updateRelativeDates), updating the 9 call sites, and rewriting the pipe tests — a broad, higher-risk refactor of a path used everywhere dates appear. This directive instead reads thetitlealready in the DOM via one listener, leaving the pipes, templates, and live-update untouched. CDK itself is already a first-class dependency (@angular/cdkinwebapp/package.json; every modal usesMatDialog, which is CDK Overlay) — this is just the first place we use the Overlay API directly rather than through Material.Behavior on touch:
IntersectionObserver— when the trigger leaves the viewport. On a single-pane layout, opening a report from the list slides the still-focused list off-screen, so the tooltip clears instead of hovering over the report; on a two-pane layout the list (and the tooltip) stay put. Watching visibility rather than router navigation is deliberate: every list-row tap fires a navigation event whether the trigger stays on screen or not, so navigation was the wrong signal.Code review checklist
can_view_old_navigationpermission to see the old design. Test it has appropriate design for RTL languages.AI disclosure
Per the AI contribution guidelines:
License
The software is provided under AGPL-3.0. Contributions to this project are accepted under the same license.