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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user