From 70f2392a7174f045ac930452eb710dc0f17e00fb Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Sun, 10 May 2026 15:03:23 +0200 Subject: [PATCH] fix: harden dashboard notification focus rendering --- frontend/src/pages/DashboardPage.tsx | 53 +++++++++++++------ .../src/test/pages/DashboardPage.test.tsx | 47 ++++++++++++++++ 2 files changed, 84 insertions(+), 16 deletions(-) diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index 4e3a666..b482dd4 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -46,6 +46,10 @@ function getMedicationIdFromNotificationDoseId(doseId: string | null): string | } function findFocusTargetElement(doseId: string | null, medId: string | null): HTMLElement | null { + if (typeof document === "undefined") { + return null; + } + if (doseId) { const elements = Array.from(document.querySelectorAll("[data-dose-id]")); const doseElement = elements.find((element) => element.dataset.doseId === doseId); @@ -67,6 +71,8 @@ function getDosePeople(takenBy: unknown): Array { return takenByArray.length > 0 ? takenByArray : [null]; } +const EMPTY_DOSE_SET = new Set(); + export function DashboardPage() { const { t, i18n } = useTranslation(); const { user } = useAuth(); @@ -115,6 +121,9 @@ export function DashboardPage() { const [showObsoleteConfirm, setShowObsoleteConfirm] = useState(false); const [obsoleteCandidate, setObsoleteCandidate] = useState<{ id: number; name: string } | null>(null); const notificationFocusAppliedRef = useRef(null); + const effectiveSkippedDoses = + skippedDoses instanceof Set ? skippedDoses : dismissedDoses instanceof Set ? dismissedDoses : EMPTY_DOSE_SET; + const canManageSkippedDoses = typeof markDoseSkipped === "function" && typeof undoDoseSkipped === "function"; const isDoseTakenForDisplay = useCallback((doseId: string) => takenDoses.has(doseId), [takenDoses]); @@ -182,16 +191,20 @@ export function DashboardPage() { return; } - if (targetDayState.section === "past" && !showPastDays) { - setShowPastDays(true); - } + try { + if (targetDayState.section === "past" && !showPastDays) { + setShowPastDays(true); + } - if (targetDayState.section === "future" && !showFutureDays) { - setShowFutureDays(true); - } + if (targetDayState.section === "future" && !showFutureDays) { + setShowFutureDays(true); + } - if (targetDayState.isCollapsed) { - toggleDayCollapse(targetDayState.day.dateStr, targetDayState.isAutoCollapsed); + if (targetDayState.isCollapsed) { + toggleDayCollapse(targetDayState.day.dateStr, targetDayState.isAutoCollapsed); + } + } catch { + notificationFocusAppliedRef.current = null; } }, [ notificationTarget, @@ -224,14 +237,18 @@ export function DashboardPage() { let correctionTimerId: number | null = null; const scrollTargetIntoView = () => { - const targetElement = findFocusTargetElement(notificationTarget.doseId, notificationTarget.medId); + try { + const targetElement = findFocusTargetElement(notificationTarget.doseId, notificationTarget.medId); - if (!targetElement) { + if (!targetElement || typeof targetElement.scrollIntoView !== "function") { + return false; + } + + targetElement.scrollIntoView({ behavior: "smooth", block: "start" }); + return true; + } catch { return false; } - - targetElement.scrollIntoView({ behavior: "smooth", block: "start" }); - return true; }; const frameId = requestAnimationFrame(() => { @@ -364,6 +381,10 @@ export function DashboardPage() { ); + if (!canManageSkippedDoses) { + return takeButton; + } + const skipButton = options.isSkipped ? (