diff --git a/e2e/day-doors.spec.ts b/e2e/day-doors.spec.ts index af3b4c2ea..c83cfa352 100644 --- a/e2e/day-doors.spec.ts +++ b/e2e/day-doors.spec.ts @@ -13,11 +13,13 @@ import { expect, test } from "./setup/test"; * * The day view first reached six metric pages; the recovery page beside them * offered no day at all. These journeys walk the surfaces that came after, - * each through the same door: a click on a point (or a calendar cell) opens - * the day beside the page, and on a touch screen a tap does the same as a - * bottom sheet, the tapped value on top. The dashed line (or outline) marks - * the open day, the row of dots and the caption sit under every chart, and - * the data table's dates are the keyboard way to the same days. + * each through the same doors: on a fine pointer a click on a point (or a + * calendar cell) opens the day beside the page; on a touch screen the first + * tap shows the value and the tooltip's "View the whole day" opens it as a + * bottom sheet, the tapped value on top. A date in a list opens in one tap. + * The dashed line (or outline) marks the open day, the row of dots and the + * caption sit under every chart, and the data table's dates are the + * keyboard way to the same days. * * - recovery (the page the beta reported), sleep, a lab marker, a chart * with its comparison period on and a dashboard chart: line charts; @@ -41,8 +43,14 @@ async function openFromPlot(page: Page, plot: Locator, testInfo: TestInfo) { const x = box.x + box.width * 0.8; const y = box.y + box.height / 2; if (isPhone(testInfo)) { - // One tap, the same door as the click: no tooltip step in between. await page.touchscreen.tap(x, y); + const open = page.locator('[data-slot="chart-tooltip-open-day"]:visible'); + await expect(open).toBeVisible(); + // A tap alone never opens a sheet: a point is a small target. + await expect(panel(page)).toHaveCount(0); + // The button is a calm, full 44 px row. + expect((await open.boundingBox())!.height).toBeGreaterThanOrEqual(44); + await open.tap(); await expect(panel(page)).toHaveAttribute("data-shell", "bottom"); } else { await page.mouse.move(x, y); @@ -143,6 +151,12 @@ test.describe("every chart drawn in days opens its days", () => { const day = await cell.getAttribute("data-day"); if (isPhone(testInfo)) { await cell.tap(); + const open = page.locator( + '[data-slot="heatmap-tooltip"] [data-slot="chart-tooltip-open-day"]', + ); + await expect(open).toBeVisible(); + await expect(panel(page)).toHaveCount(0); + await open.tap(); await expect(panel(page)).toHaveAttribute("data-shell", "bottom"); } else { await cell.click(); diff --git a/e2e/day-view.spec.ts b/e2e/day-view.spec.ts index 43806f074..6a04b16f0 100644 --- a/e2e/day-view.spec.ts +++ b/e2e/day-view.spec.ts @@ -15,9 +15,8 @@ import { MOBILE_ROUTES_ANALYTE } from "./setup/mobile-routes-fixture"; * day, what the history does, and where no door is. * * - a chart point opens the day beside the chart (desktop); - * - on a touch screen a tap on a point opens the day at once, as a bottom - * sheet with the tapped value on top (the maintainer's call on the beta: - * the two-step tooltip broke the flow); + * - on a touch screen the first tap shows the value and the tooltip offers + * the day, the second opens it as a bottom sheet; * - a date in a list opens the day while the row keeps its own target; * - `?day=` opens the layer on any page, Back and Escape close it, a date in * the future is dropped; @@ -25,7 +24,7 @@ import { MOBILE_ROUTES_ANALYTE } from "./setup/mobile-routes-fixture"; * - the dashboard's today area offers no door; its charts below do. * * Stable data attributes only: `day-panel` (+ `data-shell`), `day-link`, - * `chart-plot[data-day-links]`, `day-prev` / + * `chart-plot[data-day-links]`, `chart-tooltip-open-day`, `day-prev` / * `day-next` / `day-close`. */ @@ -151,7 +150,7 @@ test.describe("the day view", () => { await expect(page).toHaveURL(/\/mood$/); }); - test("on a phone a tap on a point opens its day at once", async ({ + test("on a phone the first tap shows the value, the second opens the day", async ({ page, }, testInfo) => { test.skip(testInfo.project.name !== "chromium-mobile", "touch only"); @@ -162,11 +161,15 @@ test.describe("the day view", () => { await plot.scrollIntoViewIfNeeded(); await expect(plot).toBeVisible(); const box = (await plot.boundingBox())!; - // One tap, the same door as a click on a desktop: the day opens as a - // bottom sheet with the tapped value on top. await page.touchscreen.tap(box.x + box.width * 0.8, box.y + box.height / 2); + const open = page.locator('[data-slot="chart-tooltip-open-day"]'); + await expect(open).toBeVisible(); + // A tap alone never opens a sheet. + await expect(panel(page)).toHaveCount(0); + await open.tap(); await expect(panel(page)).toBeVisible(); await expect(panel(page)).toHaveAttribute("data-shell", "bottom"); + // The tapped value heads the day. await expect(page.locator('[data-slot="day-focus"]')).toBeVisible(); }); diff --git a/messages/de.json b/messages/de.json index 2aa8f3350..b9b9399de 100644 --- a/messages/de.json +++ b/messages/de.json @@ -12201,11 +12201,11 @@ "captureShort": "Erfassen", "inTimelineShort": "Zeitleiste", "askCoachPrompt": "Was war am {date}?", - "chartHintTouch": "Antippen eines Punkts öffnet den Tag.", + "chartHintTouch": "Antippen zeigt den Wert.", "chartHintBar": "Klick auf einen Balken öffnet den Tag.", - "chartHintTouchBar": "Antippen eines Balkens öffnet den Tag.", + "chartHintTouchBar": "Antippen zeigt die Werte.", "chartHintCell": "Klick auf einen Tag öffnet ihn.", - "chartHintTouchCell": "Antippen eines Tages öffnet ihn.", + "chartHintTouchCell": "Antippen zeigt den Wert des Tages.", "coachChips": "Tage in dieser Antwort", "description": "Alles, was deine Einträge für diesen Tag enthalten.", "openDocument": "Dokument öffnen", diff --git a/messages/en.json b/messages/en.json index f5e55df8b..7efb7a78b 100644 --- a/messages/en.json +++ b/messages/en.json @@ -12201,11 +12201,11 @@ "captureShort": "Log", "inTimelineShort": "Timeline", "askCoachPrompt": "What happened on {date}?", - "chartHintTouch": "Tap a point to open its day.", + "chartHintTouch": "Tap a point to see its value.", "chartHintBar": "Click a bar to open its day.", - "chartHintTouchBar": "Tap a bar to open its day.", + "chartHintTouchBar": "Tap a bar to see its values.", "chartHintCell": "Click a day to open it.", - "chartHintTouchCell": "Tap a day to open it.", + "chartHintTouchCell": "Tap a day to see its value.", "coachChips": "Days in this answer", "description": "Everything your records hold for this day.", "openDocument": "Open document", diff --git a/messages/es.json b/messages/es.json index 2a458aa35..e38b9e1a8 100644 --- a/messages/es.json +++ b/messages/es.json @@ -12201,11 +12201,11 @@ "captureShort": "Registrar", "inTimelineShort": "Cronología", "askCoachPrompt": "¿Qué pasó el {date}?", - "chartHintTouch": "Toca un punto para abrir su día.", + "chartHintTouch": "Toca un punto para ver su valor.", "chartHintBar": "Haz clic en una barra para abrir su día.", - "chartHintTouchBar": "Toca una barra para abrir su día.", + "chartHintTouchBar": "Toca una barra para ver sus valores.", "chartHintCell": "Haz clic en un día para abrirlo.", - "chartHintTouchCell": "Toca un día para abrirlo.", + "chartHintTouchCell": "Toca un día para ver su valor.", "coachChips": "Días en esta respuesta", "description": "Todo lo que tus registros contienen para este día.", "openDocument": "Abrir documento", diff --git a/messages/fr.json b/messages/fr.json index 70d2bd366..9070b5fdc 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -12201,11 +12201,11 @@ "captureShort": "Enregistrer", "inTimelineShort": "Chronologie", "askCoachPrompt": "Que s’est-il passé le {date} ?", - "chartHintTouch": "Touchez un point pour ouvrir son jour.", + "chartHintTouch": "Touchez un point pour voir sa valeur.", "chartHintBar": "Cliquez sur une barre pour ouvrir son jour.", - "chartHintTouchBar": "Touchez une barre pour ouvrir son jour.", + "chartHintTouchBar": "Touchez une barre pour voir ses valeurs.", "chartHintCell": "Cliquez sur un jour pour l’ouvrir.", - "chartHintTouchCell": "Touchez un jour pour l’ouvrir.", + "chartHintTouchCell": "Touchez un jour pour voir sa valeur.", "coachChips": "Jours dans cette réponse", "description": "Tout ce que vos entrées contiennent pour ce jour.", "openDocument": "Ouvrir le document", diff --git a/messages/it.json b/messages/it.json index 9dfef1da6..7da2126a2 100644 --- a/messages/it.json +++ b/messages/it.json @@ -12201,11 +12201,11 @@ "captureShort": "Registra", "inTimelineShort": "Cronologia", "askCoachPrompt": "Cosa è successo il {date}?", - "chartHintTouch": "Tocca un punto per aprire il suo giorno.", + "chartHintTouch": "Tocca un punto per vederne il valore.", "chartHintBar": "Fai clic su una barra per aprire il suo giorno.", - "chartHintTouchBar": "Tocca una barra per aprire il suo giorno.", + "chartHintTouchBar": "Tocca una barra per vederne i valori.", "chartHintCell": "Fai clic su un giorno per aprirlo.", - "chartHintTouchCell": "Tocca un giorno per aprirlo.", + "chartHintTouchCell": "Tocca un giorno per vederne il valore.", "coachChips": "Giorni in questa risposta", "description": "Tutto ciò che le tue voci contengono per questo giorno.", "openDocument": "Apri documento", diff --git a/messages/ko.json b/messages/ko.json index 484effaaa..ccd0eb935 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -12201,11 +12201,11 @@ "captureShort": "기록", "inTimelineShort": "타임라인", "askCoachPrompt": "{date}에 무슨 일이 있었나요?", - "chartHintTouch": "점을 탭하면 그날이 열려요.", + "chartHintTouch": "점을 탭하면 값이 보여요.", "chartHintBar": "막대를 클릭하면 그날이 열려요.", - "chartHintTouchBar": "막대를 탭하면 그날이 열려요.", + "chartHintTouchBar": "막대를 탭하면 값이 보여요.", "chartHintCell": "날짜 칸을 클릭하면 그날이 열려요.", - "chartHintTouchCell": "날짜 칸을 탭하면 그날이 열려요.", + "chartHintTouchCell": "날짜 칸을 탭하면 값이 보여요.", "coachChips": "이 답변에 쓰인 날", "description": "이 날에 대한 모든 기록이에요.", "openDocument": "문서 열기", diff --git a/messages/pl.json b/messages/pl.json index d07ef24d2..032c99b02 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -12201,11 +12201,11 @@ "captureShort": "Zapisz", "inTimelineShort": "Oś czasu", "askCoachPrompt": "Co się działo {date}?", - "chartHintTouch": "Dotknij punktu, aby otworzyć jego dzień.", + "chartHintTouch": "Dotknij punktu, aby zobaczyć wartość.", "chartHintBar": "Kliknij słupek, aby otworzyć jego dzień.", - "chartHintTouchBar": "Dotknij słupka, aby otworzyć jego dzień.", + "chartHintTouchBar": "Dotknij słupka, aby zobaczyć wartości.", "chartHintCell": "Kliknij dzień, aby go otworzyć.", - "chartHintTouchCell": "Dotknij dnia, aby go otworzyć.", + "chartHintTouchCell": "Dotknij dnia, aby zobaczyć jego wartość.", "coachChips": "Dni w tej odpowiedzi", "description": "Wszystko, co Twoje wpisy zawierają dla tego dnia.", "openDocument": "Otwórz dokument", diff --git a/src/components/charts/health-chart.tsx b/src/components/charts/health-chart.tsx index c31fe7eca..3f539c146 100644 --- a/src/components/charts/health-chart.tsx +++ b/src/components/charts/health-chart.tsx @@ -2404,6 +2404,11 @@ export function HealthChart({ : dateLabel } rows={rows} + action={chartDays.tooltipAction( + hoverPoint + ? dayLinkSource?.indexOf(hoverPoint) + : undefined, + )} /> ); }} diff --git a/src/components/charts/medication-compliance-chart.tsx b/src/components/charts/medication-compliance-chart.tsx index 4fb12ca5d..628ce42ce 100644 --- a/src/components/charts/medication-compliance-chart.tsx +++ b/src/components/charts/medication-compliance-chart.tsx @@ -644,8 +644,18 @@ export function MedicationComplianceChart({ delta, }, ]; + const hoverPoint = payload[0]?.payload; return ( - + ); }} /> diff --git a/src/components/charts/mood-chart.tsx b/src/components/charts/mood-chart.tsx index 5f0371278..eeb46aec8 100644 --- a/src/components/charts/mood-chart.tsx +++ b/src/components/charts/mood-chart.tsx @@ -1015,7 +1015,16 @@ export function MoodChart({ } } if (rows.length === 0) return null; - return ; + return ( + + ); }} /> ); }} diff --git a/src/components/custom-metrics/custom-metric-chart.tsx b/src/components/custom-metrics/custom-metric-chart.tsx index af828a9da..e35a77dc8 100644 --- a/src/components/custom-metrics/custom-metric-chart.tsx +++ b/src/components/custom-metrics/custom-metric-chart.tsx @@ -239,6 +239,7 @@ export function CustomMetricChart({ active label={fmt.dateShortSmart(new Date(point.timestamp))} rows={rows} + action={chartDays.tooltipAction(points.indexOf(point))} /> ); }} diff --git a/src/components/cycle/bbt-chart.tsx b/src/components/cycle/bbt-chart.tsx index 9e62520c2..b29454d08 100644 --- a/src/components/cycle/bbt-chart.tsx +++ b/src/components/cycle/bbt-chart.tsx @@ -24,7 +24,7 @@ * it surfaces a "log your morning temperature" hint rather than a flat dot. */ -import { useMemo } from "react"; +import { useMemo, type ReactNode } from "react"; import { Thermometer } from "lucide-react"; import { CartesianGrid, @@ -288,7 +288,20 @@ export function BbtChart({ strokeOpacity: 0.3, strokeDasharray: "3 3", }} - content={} + content={(props) => { + const payload = props.payload as unknown as + { payload: BbtPoint }[] | undefined; + const point = payload?.[0]?.payload; + return ( + + ); + }} /> {ovulationMs != null ? ( ) : null} + {props.action ?? null} ); } diff --git a/src/components/day/__tests__/day-door-coverage-guard.test.ts b/src/components/day/__tests__/day-door-coverage-guard.test.ts index 3d5b534e8..68dd8f2ac 100644 --- a/src/components/day/__tests__/day-door-coverage-guard.test.ts +++ b/src/components/day/__tests__/day-door-coverage-guard.test.ts @@ -9,8 +9,9 @@ import { describe, expect, it } from "vitest"; * The day view arrived on six metric pages and nowhere else, and the recovery * page beside them offered no day at all: nothing noticed, because every check * proved one chart. This guard walks `src/app` and `src/components` and holds - * three kinds of surface to the same doors (a click or a tap opens the day, - * the dashed line through the open day, the row of day dots, the caption): + * three kinds of surface to the same doors (a click opens the day, on touch + * the tooltip's "View the whole day", the dashed line through the open day, + * the row of day dots, the caption): * * A. a mount of a chart whose doors are a prop (`HealthChart`, its dynamic * wrapper, `MoodChart`, the adherence chart, the nutrient bar chart) @@ -20,7 +21,7 @@ import { describe, expect, it } from "vitest"; * B. a file that draws its own Recharts chart over a date axis (an * `` keyed on a date-shaped field, or `scale="time"`) calls * `useChartDayLinks` and renders its parts (`ChartDayFooter`, - * `OPEN_DAY_LINE`, the tooltip's props); + * `OPEN_DAY_LINE`, the tooltip's props and its touch action); * C. a calendar heatmap (anything laid out by `heatmapDays`) calls * `useHeatmapDay`. * @@ -195,14 +196,15 @@ function findMissingDoors(): { /\buseChartDayLinks\(/.test(text) && /\bChartDayFooter\b/.test(text) && /\bOPEN_DAY_LINE\b/.test(text) && - /\.tooltipProps\b/.test(text); + /\.tooltipProps\b/.test(text) && + /\.tooltipAction\(/.test(text); if (wired) { doors.charts += 1; doors.chartFiles.push(file); } else if (!listed) { missing.push({ file, - why: "a date-axis chart without the shared day doors (useChartDayLinks, ChartDayFooter, OPEN_DAY_LINE, tooltip props)", + why: "a date-axis chart without the shared day doors (useChartDayLinks, ChartDayFooter, OPEN_DAY_LINE, tooltip props and touch action)", }); } } diff --git a/src/components/day/chart-day-links.tsx b/src/components/day/chart-day-links.tsx index 5b0438f18..7e645b155 100644 --- a/src/components/day/chart-day-links.tsx +++ b/src/components/day/chart-day-links.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useMemo, useRef } from "react"; +import { useCallback, useMemo, useRef, type ReactNode } from "react"; import type { DateKey } from "@/lib/day/contract"; import { prefersReducedMotion } from "@/lib/charts/reduced-motion"; @@ -8,6 +8,7 @@ import { prefersReducedMotion } from "@/lib/charts/reduced-motion"; import { ChartDayCaption, DayRug, + TooltipDayAction, useCoarsePointer, type ChartDayAxis, type ChartDayMark, @@ -25,11 +26,13 @@ import { useTodayKey } from "./use-today-key"; * button, the dashed line through the open day and the row of dots under the * axis behave the same on every page: * - * - a click, or a tap, on the plot opens the day of the point under it. - * On a fine pointer hovering still shows the tooltip; on a touch screen - * the tap opens the day at once, and the day puts the tapped value at - * its top. A scroll that grazes the chart is a pan, not a tap, and opens - * nothing; + * - fine pointer: a click on the plot opens the day of the point under the + * cursor; hovering shows the tooltip as before; + * - touch: a tap shows the value and pins the tooltip, whose button + * ("View the whole day", a calm 44 px row) opens the day. Two steps on + * purpose (the maintainer's call, after trying one on the beta): a + * point is a small target, and one tap that opened a sheet would open + * days by accident; * - the open day is marked with `OPEN_DAY_LINE` (a dashed vertical line) * at `openIndex`; * - `ChartDayFooter` draws the row of day dots and the one-line caption. @@ -91,12 +94,19 @@ export interface ChartDayLinks { onChartClick: ((state: ChartClickState | null | undefined) => void) | undefined; /** - * Spread on ``. On touch, and under reduced motion, the tooltip - * does not glide: Recharts slides it from where it last was for 400 ms, - * which on a tap drew a box sweeping across the chart while the day was - * already opening. + * Spread on ``: on touch it stays where the tap put it and takes + * taps. It does not glide there either: Recharts slides the tooltip from + * where it last was for 400 ms, which turned "View the whole day" into a + * moving target under the finger. Reduced motion stops the glide on every + * pointer. */ - tooltipProps: { isAnimationActive?: boolean }; + tooltipProps: { + trigger: "click" | "hover"; + wrapperStyle?: React.CSSProperties; + isAnimationActive?: boolean; + }; + /** The tooltip's button for point `index`, on a touch screen only. */ + tooltipAction: (index: number | undefined) => ReactNode | undefined; } export function useChartDayLinks({ @@ -134,16 +144,17 @@ export function useChartDayLinks({ return at === -1 ? undefined : at; }, [enabled, openKey, days]); - const onChartClick = enabled - ? (state: ChartClickState | null | undefined) => { - if (!state) return; - const index = indexOfClick - ? indexOfClick(state) - : Number(state.activeTooltipIndex); - if (index === null || !Number.isInteger(index)) return; - open(index); - } - : undefined; + const onChartClick = + enabled && !coarse + ? (state: ChartClickState | null | undefined) => { + if (!state) return; + const index = indexOfClick + ? indexOfClick(state) + : Number(state.activeTooltipIndex); + if (index === null || !Number.isInteger(index)) return; + open(index); + } + : undefined; return { active: enabled, @@ -159,7 +170,19 @@ export function useChartDayLinks({ plotClassName: enabled && !coarse ? "cursor-pointer" : "", onChartClick, tooltipProps: - coarse || prefersReducedMotion() ? { isAnimationActive: false } : {}, + enabled && coarse + ? { + trigger: "click", + wrapperStyle: { pointerEvents: "auto", zIndex: 20 }, + isAnimationActive: false, + } + : prefersReducedMotion() + ? { trigger: "hover", isAnimationActive: false } + : { trigger: "hover" }, + tooltipAction: (index) => + enabled && coarse && index !== undefined && canOpen(index) ? ( + open(index)} /> + ) : undefined, }; } diff --git a/src/components/day/chart-day.tsx b/src/components/day/chart-day.tsx index 679fb690b..460d3e746 100644 --- a/src/components/day/chart-day.tsx +++ b/src/components/day/chart-day.tsx @@ -1,6 +1,7 @@ "use client"; import { useSyncExternalStore } from "react"; +import { ArrowRight } from "lucide-react"; import type { DateKey } from "@/lib/day/contract"; import { dateOnlyKey } from "@/lib/tz/date-only"; @@ -15,10 +16,12 @@ import { useTodayKey } from "./use-today-key"; /** * The day view's entry points on a value chart. * - * - A click, or a tap, on the plot opens the day of the point under it, - * with that value at the top of the day. Hovering on a fine pointer - * keeps showing the tooltip; a scroll that grazes the chart is a pan, - * not a tap, and opens nothing. + * - On a fine pointer, a click on the plot opens the day of the point under + * the cursor; hovering keeps showing the tooltip as before. + * - On a touch screen a tap shows the value, as it always did, and the + * tooltip carries "View the whole day". Two steps on purpose: a point is + * a small target, and one tap that opened a sheet would open days by + * accident. A date in a list is a bigger target and opens in one tap. * - Under the axis, a quiet row of dots marks the days that hold anything, * a ring the days with a notable observation. Decoration for the eye; the * keyboard and screen-reader way to a day is the chart's data table. @@ -57,6 +60,26 @@ export function chartPointDayKey(timestamp: number): DateKey { return dateOnlyKey(new Date(timestamp)); } +/** The tooltip's way to the day on a touch screen. */ +export function TooltipDayAction({ onOpen }: { onOpen: () => void }) { + const { t } = useTranslations(); + return ( + + ); +} + interface RugPoint { timestamp: number; } diff --git a/src/components/day/heatmap-day.tsx b/src/components/day/heatmap-day.tsx index d71f97fbc..0eb352c51 100644 --- a/src/components/day/heatmap-day.tsx +++ b/src/components/day/heatmap-day.tsx @@ -1,10 +1,10 @@ "use client"; -import { useEffect, useState, type RefObject } from "react"; +import { useEffect, useRef, useState, type RefObject } from "react"; import type { DateKey } from "@/lib/day/contract"; -import { useCoarsePointer } from "./chart-day"; +import { TooltipDayAction, useCoarsePointer } from "./chart-day"; import { openDay, useOpenDay } from "./day-layer-controller"; import { isOpenableDay } from "./day-url"; import { useTodayKey } from "./use-today-key"; @@ -13,10 +13,11 @@ import { useTodayKey } from "./use-today-key"; * The day view's doors on a calendar heatmap (mood, intake), the cell twin of * `useChartDayLinks`: * - * - a click, or a tap, on a cell opens its day; hovering a cell on a fine - * pointer shows its tooltip. A tap on a cell with nothing to open (no - * entry, or a day still ahead) pins its tooltip instead, so the cell - * still says what it is; a tap outside or Escape clears it; + * - fine pointer: hovering a cell shows its tooltip, a click opens its day; + * - touch: a tap pins the tooltip, and the tooltip's button ("View the + * whole day", the same calm 44 px row every chart's tooltip carries) + * opens it. Two steps, as on a chart: a cell is a small target. A tap + * outside or Escape clears the tooltip; * - the open day's cell carries a dashed outline, the heatmap's form of * the dashed line a chart draws through it (`OPEN_DAY_CELL`); * - the keyboard and screen-reader way to a day is the heatmap's data @@ -39,6 +40,8 @@ interface HeatmapTooltip { text: string; /** A touch tooltip stays after the finger lifts. */ pinned?: boolean; + /** The day a pinned tooltip offers to open. */ + day?: DateKey; } export interface HeatmapCellInput { @@ -51,6 +54,9 @@ export interface HeatmapCellInput { export function useHeatmapDay(containerRef: RefObject) { const [tooltip, setTooltip] = useState(null); + // The pointer type of the last press decides whether the click that + // follows opens the day (mouse, pen) or only pinned the tooltip (touch). + const lastPointer = useRef("mouse"); const today = useTodayKey(); const openKey = useOpenDay(); const coarse = useCoarsePointer(); @@ -104,17 +110,18 @@ export function useHeatmapDay(containerRef: RefObject) { setTooltip((prev) => (prev?.pinned ? prev : null)); }, onPointerDown: (e: React.PointerEvent) => { - // A touch on a cell with a day to open opens it (the click below); - // any other cell pins its tooltip so the tap still answers. - if (e.pointerType !== "touch" || openable) return; + lastPointer.current = e.pointerType; + if (e.pointerType !== "touch") return; setTooltip({ x: e.clientX, y: e.clientY, text: cell.describe(), pinned: true, + day: openable ? cell.dateKey : undefined, }); }, onClick: () => { + if (lastPointer.current === "touch") return; if (!openable) return; open(cell.dateKey); }, @@ -129,7 +136,9 @@ export function useHeatmapDay(containerRef: RefObject) { const tooltipNode = tooltip ? (
) { }} > {tooltip.text} + {tooltip.day ? ( + open(tooltip.day as DateKey)} /> + ) : null}
) : null; diff --git a/src/components/insights/mood/mood-dimension-trends.tsx b/src/components/insights/mood/mood-dimension-trends.tsx index bb20d8aff..52a860719 100644 --- a/src/components/insights/mood/mood-dimension-trends.tsx +++ b/src/components/insights/mood/mood-dimension-trends.tsx @@ -216,6 +216,7 @@ export function MoodDimensionTrends({ active label={fmt.dateShortSmartCalendar(String(row.date))} rows={rows} + action={chartDays.tooltipAction(chartData.indexOf(row))} /> ); }} diff --git a/src/components/insights/sleep-stage-stacked-bar.tsx b/src/components/insights/sleep-stage-stacked-bar.tsx index bd09ae618..ccf2e94c0 100644 --- a/src/components/insights/sleep-stage-stacked-bar.tsx +++ b/src/components/insights/sleep-stage-stacked-bar.tsx @@ -469,6 +469,7 @@ export function SleepStageStackedBar({ breakdown }: SleepStageStackedBarProps) { const row = payload[0]?.payload as Record | undefined; const label = row?.label; + const rowIndex = row ? data.indexOf(row) : -1; const napCount = typeof row?.napCount === "number" ? row.napCount : 0; // The footer's first line is the NIGHT. The nap is listed @@ -549,6 +550,9 @@ export function SleepStageStackedBar({ breakdown }: SleepStageStackedBarProps) { )} )} + {chartDays.tooltipAction( + rowIndex === -1 ? undefined : rowIndex, + )} ); }} diff --git a/src/components/labs/lab-biomarker-chart.tsx b/src/components/labs/lab-biomarker-chart.tsx index 97f78dcf8..7a8828340 100644 --- a/src/components/labs/lab-biomarker-chart.tsx +++ b/src/components/labs/lab-biomarker-chart.tsx @@ -406,6 +406,7 @@ export function LabBiomarkerChart({ active label={labShortDate(point.timestamp)} rows={rows} + action={chartDays.tooltipAction(points.indexOf(point))} /> ); }} diff --git a/src/components/medications/detail/efficacy/efficacy-chart.tsx b/src/components/medications/detail/efficacy/efficacy-chart.tsx index 36bab7d32..0d0307570 100644 --- a/src/components/medications/detail/efficacy/efficacy-chart.tsx +++ b/src/components/medications/detail/efficacy/efficacy-chart.tsx @@ -378,11 +378,18 @@ export function EfficacyChart({ color: item.color ?? "var(--chart-1)", }; }); + const index = + typeof x === "number" + ? displaySeries.findIndex((p) => p.t === x) + : -1; return ( ); }} diff --git a/src/components/medications/dose-strength-curve.tsx b/src/components/medications/dose-strength-curve.tsx index 6040fb7c7..4b08d8082 100644 --- a/src/components/medications/dose-strength-curve.tsx +++ b/src/components/medications/dose-strength-curve.tsx @@ -326,6 +326,7 @@ export function DoseStrengthCurve({ active label={fmt.dateShortSmart(new Date(point.t))} rows={rows} + action={chartDays.tooltipAction(chartData.indexOf(point))} /> ); }} diff --git a/src/components/mental-health/assessment-history-chart.tsx b/src/components/mental-health/assessment-history-chart.tsx index 548b2165b..1cdf57adb 100644 --- a/src/components/mental-health/assessment-history-chart.tsx +++ b/src/components/mental-health/assessment-history-chart.tsx @@ -183,6 +183,7 @@ export function AssessmentHistoryChart({ active label={fmt.dateShortSmart(new Date(point.timestamp))} rows={rowsOut} + action={chartDays.tooltipAction(points.indexOf(point))} /> ); }}