diff --git a/frontend/src/styles/test-activity.scss b/frontend/src/styles/test-activity.scss index 84d852c3b9c6..2bf70c4d44e6 100644 --- a/frontend/src/styles/test-activity.scss +++ b/frontend/src/styles/test-activity.scss @@ -115,6 +115,8 @@ } .activity { + --activity-tooltip-offset: 0px; + grid-area: chart; display: grid; grid-auto-flow: column; @@ -131,6 +133,27 @@ &[data-level="filler"]:hover { border: none; } + + &[aria-label][data-balloon-pos="up"]::after { + font-size: clamp(0.75rem, calc(0.55rem + 1vw), 1rem); + line-height: 1.2; + max-width: calc(100vw - 1rem); + text-align: center; + transform: translate( + calc(-50% + var(--activity-tooltip-offset)), + var(--balloon-move) + ); + white-space: normal; + width: max-content; + } + + &[aria-label][data-balloon-pos="up"]:hover::after, + &[aria-label][data-balloon-pos="up"][data-balloon-visible]::after, + &[aria-label][data-balloon-pos="up"]:not( + [data-balloon-nofocus] + ):focus::after { + transform: translate(calc(-50% + var(--activity-tooltip-offset)), 0); + } } } .legend { diff --git a/frontend/src/ts/elements/test-activity.ts b/frontend/src/ts/elements/test-activity.ts index 7d2bae4b95de..f3e52eaf69c5 100644 --- a/frontend/src/ts/elements/test-activity.ts +++ b/frontend/src/ts/elements/test-activity.ts @@ -3,6 +3,74 @@ import { TestActivityMonth, } from "./test-activity-calendar"; +const tooltipViewportPadding = 8; + +function getTooltipHorizontalOffset( + triggerLeft: number, + triggerWidth: number, + tooltipWidth: number, + viewportWidth: number, +): number { + const availableWidth = Math.max( + 0, + viewportWidth - tooltipViewportPadding * 2, + ); + const renderedTooltipWidth = Math.min(tooltipWidth, availableWidth); + const triggerCenter = triggerLeft + triggerWidth / 2; + const tooltipLeft = triggerCenter - renderedTooltipWidth / 2; + const tooltipRight = triggerCenter + renderedTooltipWidth / 2; + + if (tooltipLeft < tooltipViewportPadding) { + return tooltipViewportPadding - tooltipLeft; + } + + if (tooltipRight > viewportWidth - tooltipViewportPadding) { + return viewportWidth - tooltipViewportPadding - tooltipRight; + } + + return 0; +} + +function measureTooltipWidth(element: HTMLElement): number { + const label = element.getAttribute("aria-label"); + if (label === null) { + return 0; + } + + const tooltipStyle = window.getComputedStyle(element, "::after"); + const measure = document.createElement("span"); + measure.textContent = label; + Object.assign(measure.style, { + fontFamily: tooltipStyle.fontFamily, + fontSize: tooltipStyle.fontSize, + fontStyle: tooltipStyle.fontStyle, + fontWeight: tooltipStyle.fontWeight, + letterSpacing: tooltipStyle.letterSpacing, + paddingLeft: tooltipStyle.paddingLeft, + paddingRight: tooltipStyle.paddingRight, + position: "fixed", + visibility: "hidden", + whiteSpace: "nowrap", + }); + document.body.appendChild(measure); + const width = measure.getBoundingClientRect().width; + measure.remove(); + + return width; +} + +function positionTooltip(element: HTMLElement): void { + const triggerRect = element.getBoundingClientRect(); + const offset = getTooltipHorizontalOffset( + triggerRect.left, + triggerRect.width, + measureTooltipWidth(element), + document.documentElement.clientWidth, + ); + + element.style.setProperty("--activity-tooltip-offset", `${offset}px`); +} + export function init( element: HTMLElement, calendar?: TestActivityCalendar, @@ -57,6 +125,7 @@ export function update( if (day.label !== undefined) { elem.setAttribute("aria-label", day.label); elem.setAttribute("data-balloon-pos", "up"); + elem.addEventListener("pointerenter", () => positionTooltip(elem)); } container.appendChild(elem); }