diff --git a/frontend/__tests__/elements/test-activity.spec.ts b/frontend/__tests__/elements/test-activity.spec.ts new file mode 100644 index 000000000000..96801c51f56c --- /dev/null +++ b/frontend/__tests__/elements/test-activity.spec.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; + +import { getActivityBalloonPos } from "../../src/ts/elements/test-activity"; + +describe("test-activity.ts", () => { + describe("getActivityBalloonPos", () => { + // ~53 weeks of activity cells (7 days each) + const dayCount = 53 * 7; + + it("uses up-right for days in the last 10 weeks (right edge)", () => { + // last day of the calendar (today / rightmost column) + expect(getActivityBalloonPos(dayCount - 1, dayCount)).toBe("up-right"); + // first day of the 10th week from the end + expect(getActivityBalloonPos((53 - 10) * 7, dayCount)).toBe("up-right"); + }); + + it("uses up-left for days in the first 10 weeks (left edge)", () => { + expect(getActivityBalloonPos(0, dayCount)).toBe("up-left"); + expect(getActivityBalloonPos(9 * 7 + 6, dayCount)).toBe("up-left"); + }); + + it("uses centered up for days in the middle", () => { + expect(getActivityBalloonPos(26 * 7, dayCount)).toBe("up"); + expect(getActivityBalloonPos(10 * 7, dayCount)).toBe("up"); + expect(getActivityBalloonPos((53 - 11) * 7 + 6, dayCount)).toBe("up"); + }); + }); +}); diff --git a/frontend/src/ts/elements/test-activity.ts b/frontend/src/ts/elements/test-activity.ts index 7d2bae4b95de..e8170af59572 100644 --- a/frontend/src/ts/elements/test-activity.ts +++ b/frontend/src/ts/elements/test-activity.ts @@ -3,6 +3,27 @@ import { TestActivityMonth, } from "./test-activity-calendar"; +// Non-centered balloons on edge weeks so tooltips aren't clipped by viewport. +const BALLOON_EDGE_WEEKS = 10; + +export type ActivityBalloonPos = "up" | "up-left" | "up-right"; + +/** Right-edge cells → `up-right` (extends left); left-edge → `up-left`. */ +export function getActivityBalloonPos( + dayIndex: number, + dayCount: number, +): ActivityBalloonPos { + const weekCount = Math.ceil(dayCount / 7); + const week = Math.floor(dayIndex / 7); + if (week >= weekCount - BALLOON_EDGE_WEEKS) { + return "up-right"; + } + if (week < BALLOON_EDGE_WEEKS) { + return "up-left"; + } + return "up"; +} + export function init( element: HTMLElement, calendar?: TestActivityCalendar, @@ -51,12 +72,18 @@ export function update( } } - for (const day of calendar.getDays()) { + const days = calendar.getDays(); + for (let i = 0; i < days.length; i++) { + const day = days[i]; + if (day === undefined) continue; const elem = document.createElement("div"); elem.setAttribute("data-level", day.level); if (day.label !== undefined) { elem.setAttribute("aria-label", day.label); - elem.setAttribute("data-balloon-pos", "up"); + elem.setAttribute( + "data-balloon-pos", + getActivityBalloonPos(i, days.length), + ); } container.appendChild(elem); }