From facfcad1073ddaa16c4e2a734c22476eacea95e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 8 Oct 2026 21:52:18 +0200 Subject: [PATCH] fix(day): keep the pinned heatmap tooltip off the right edge The tooltip shrinks to the room right of its clamped left edge and wraps, so it never ran past the screen, but near the right border it filled that room to the last pixel. Cap its width at the room less the 8 px gutter the clamp was meant to leave. --- .../day/__tests__/heatmap-tooltip-box.test.ts | 27 +++++++++++++++++++ src/components/day/heatmap-day.tsx | 24 ++++++++++++----- 2 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 src/components/day/__tests__/heatmap-tooltip-box.test.ts diff --git a/src/components/day/__tests__/heatmap-tooltip-box.test.ts b/src/components/day/__tests__/heatmap-tooltip-box.test.ts new file mode 100644 index 000000000..8abf4c7fe --- /dev/null +++ b/src/components/day/__tests__/heatmap-tooltip-box.test.ts @@ -0,0 +1,27 @@ +/** + * The pinned heatmap tooltip on a 390 px phone. It shrinks to the room right + * of its left edge and wraps, so it never runs past the screen; measured in + * Chromium, a tap at the right border left it flush against the edge. The + * box keeps the 8 px gutter the clamp meant to leave. + */ +import { describe, expect, it } from "vitest"; + +import { tooltipBox } from "../heatmap-day"; + +describe("tooltipBox", () => { + it("keeps an 8 px gutter at the right edge for a tap near the border", () => { + const { left, maxWidth } = tooltipBox(380, 390); + expect(left).toBe(182); + expect(left + maxWidth).toBe(390 - 8); + }); + + it("sits beside the pointer away from the border", () => { + const { left, maxWidth } = tooltipBox(40, 390); + expect(left).toBe(50); + expect(left + maxWidth).toBe(382); + }); + + it("never starts left of the 8 px gutter", () => { + expect(tooltipBox(-20, 390).left).toBe(8); + }); +}); diff --git a/src/components/day/heatmap-day.tsx b/src/components/day/heatmap-day.tsx index 0eb352c51..2b44459e6 100644 --- a/src/components/day/heatmap-day.tsx +++ b/src/components/day/heatmap-day.tsx @@ -52,6 +52,21 @@ export interface HeatmapCellInput { describe: () => string; } +/** + * Where the heatmap tooltip sits beside the pointer: 10 px to its right, + * clamped so a tap near the right border keeps at least 200 px of room, and + * never wider than the room to the right edge less an 8 px gutter. The box + * shrinks to fit that room and wraps its text, so it cannot run past the + * edge; the width cap keeps the gutter the clamp meant to leave. + */ +export function tooltipBox( + x: number, + viewportWidth: number, +): { left: number; maxWidth: number } { + const left = Math.max(8, Math.min(x + 10, viewportWidth - 200 - 8)); + return { left, maxWidth: viewportWidth - left - 8 }; +} + export function useHeatmapDay(containerRef: RefObject) { const [tooltip, setTooltip] = useState(null); // The pointer type of the last press decides whether the click that @@ -139,13 +154,10 @@ export function useHeatmapDay(containerRef: RefObject) { className={`bg-popover text-popover-foreground border-border fixed z-50 max-w-[calc(100vw-1rem)] rounded-md border px-2.5 py-1.5 text-xs shadow-md ${ tooltip.day ? "pointer-events-auto" : "pointer-events-none" }`} - // Clamp the left edge so a tap near the right border doesn't push - // the pinned label off-screen on a narrow viewport. style={{ - left: - typeof window !== "undefined" - ? Math.max(8, Math.min(tooltip.x + 10, window.innerWidth - 200 - 8)) - : tooltip.x + 10, + ...(typeof window !== "undefined" + ? tooltipBox(tooltip.x, window.innerWidth) + : { left: tooltip.x + 10 }), top: tooltip.y - 30, }} >