fix: stabilize dashboard modal and image click behavior (#267)

* feat: make medication names clickable in Dashboard dose schedule

Add click handlers to med-name-stack divs in all three dose schedule
sections (past, current/overdue, future) on DashboardPage, opening the
MedDetail modal on click.

Add early-return guards to all four modal openers in AppContext
(openMedDetail, openImageLightbox, openScheduleLightbox, openUserFilter)
to prevent duplicate pushState entries on double-click, which caused
unexpected navigation to the Medications page.

Closes #266

* fix: stabilize dashboard modal and image click handling

* fix: close medication detail on first backdrop click
This commit is contained in:
Daniel Volz
2026-02-22 10:50:58 +01:00
committed by GitHub
parent 088a6c1a05
commit 9a2d42b8b9
7 changed files with 147 additions and 39 deletions
+87 -19
View File
@@ -1,5 +1,5 @@
import type React from "react";
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAuth } from "../components/Auth";
import { useCollapsedDays, useDoses, useMedications, useRefill, useSettings, useShare } from "../hooks";
@@ -253,9 +253,32 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
// Modal state
const [selectedMed, setSelectedMed] = useState<Medication | null>(null);
const selectedMedIdRef = useRef<number | null>(null);
const medDetailOpenedAtRef = useRef(0);
const medDetailCloseInFlightRef = useRef(false);
useEffect(() => {
selectedMedIdRef.current = selectedMed?.id ?? null;
if (!selectedMed) {
medDetailCloseInFlightRef.current = false;
}
}, [selectedMed]);
const [showImageLightbox, setShowImageLightbox] = useState(false);
const imageLightboxOpenedAtRef = useRef(0);
const imageLightboxCloseInFlightRef = useRef(false);
const [scheduleLightboxImage, setScheduleLightboxImage] = useState<string | null>(null);
const scheduleLightboxOpenedAtRef = useRef(0);
const scheduleLightboxCloseInFlightRef = useRef(false);
const [selectedUser, setSelectedUser] = useState<string | null>(null);
useEffect(() => {
if (!showImageLightbox) {
imageLightboxCloseInFlightRef.current = false;
}
}, [showImageLightbox]);
useEffect(() => {
if (!scheduleLightboxImage) {
scheduleLightboxCloseInFlightRef.current = false;
}
}, [scheduleLightboxImage]);
// Export/Import state
const [exporting, setExporting] = useState(false);
@@ -467,6 +490,10 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
// Modal helpers with browser history support
const openMedDetail = useCallback(
(med: Medication) => {
if (selectedMedIdRef.current === med.id) return;
selectedMedIdRef.current = med.id;
medDetailOpenedAtRef.current = Date.now();
medDetailCloseInFlightRef.current = false;
setSelectedMed(med);
refill.setRefillHistoryExpanded(false);
refill.loadRefillHistory(med.id);
@@ -476,37 +503,78 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
);
const closeMedDetail = useCallback(() => {
if (selectedMed) {
window.history.back();
if (!selectedMed || medDetailCloseInFlightRef.current) return;
// Ignore ultra-fast close requests caused by rapid double-click races
if (Date.now() - medDetailOpenedAtRef.current < 320) return;
const currentState = window.history.state as { modal?: string } | null;
if (currentState?.modal !== "medDetail") {
// State already popped by another event: close locally without another back step.
selectedMedIdRef.current = null;
setSelectedMed(null);
return;
}
medDetailCloseInFlightRef.current = true;
window.history.back();
}, [selectedMed]);
const openImageLightbox = useCallback(() => {
if (showImageLightbox) return;
imageLightboxOpenedAtRef.current = Date.now();
imageLightboxCloseInFlightRef.current = false;
setShowImageLightbox(true);
window.history.pushState({ modal: "imageLightbox" }, "");
}, []);
const closeImageLightbox = useCallback(() => {
if (showImageLightbox) {
window.history.back();
}
}, [showImageLightbox]);
const openScheduleLightbox = useCallback((imageUrl: string) => {
setScheduleLightboxImage(imageUrl);
window.history.pushState({ modal: "scheduleLightbox" }, "");
}, []);
const closeImageLightbox = useCallback(() => {
if (!showImageLightbox || imageLightboxCloseInFlightRef.current) return;
if (Date.now() - imageLightboxOpenedAtRef.current < 320) return;
const currentState = window.history.state as { modal?: string } | null;
if (currentState?.modal !== "imageLightbox") {
setShowImageLightbox(false);
return;
}
imageLightboxCloseInFlightRef.current = true;
window.history.back();
}, [showImageLightbox]);
const openScheduleLightbox = useCallback(
(imageUrl: string) => {
if (scheduleLightboxImage) return;
scheduleLightboxOpenedAtRef.current = Date.now();
scheduleLightboxCloseInFlightRef.current = false;
setScheduleLightboxImage(imageUrl);
window.history.pushState({ modal: "scheduleLightbox" }, "");
},
[scheduleLightboxImage]
);
const closeScheduleLightbox = useCallback(() => {
if (scheduleLightboxImage) {
window.history.back();
if (!scheduleLightboxImage || scheduleLightboxCloseInFlightRef.current) return;
if (Date.now() - scheduleLightboxOpenedAtRef.current < 320) return;
const currentState = window.history.state as { modal?: string } | null;
if (currentState?.modal !== "scheduleLightbox") {
setScheduleLightboxImage(null);
return;
}
scheduleLightboxCloseInFlightRef.current = true;
window.history.back();
}, [scheduleLightboxImage]);
const openUserFilter = useCallback((person: string) => {
setSelectedUser(person);
window.history.pushState({ modal: "userFilter", person }, "");
}, []);
const openUserFilter = useCallback(
(person: string) => {
if (selectedUser === person) return;
setSelectedUser(person);
window.history.pushState({ modal: "userFilter", person }, "");
},
[selectedUser]
);
const closeUserFilter = useCallback(() => {
if (selectedUser) {