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, }} >