From 288e075786fcf85a574ffeea2309733c71ef7dfe Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Sat, 17 Jan 2026 23:00:39 +0100 Subject: [PATCH] fix: browser back gesture closes modal instead of navigating (#36) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: browser back gesture closes modal instead of navigating - Push history state when opening medication detail modal - Handle popstate event to close modal on browser back - Replace direct setSelectedMed(null) with closeMedDetail() helper - Improves mobile UX: swiping back closes modal instead of leaving page * feat: add back-swipe support for all modals - Add history.pushState/popstate handling for all modal types - Profile, ShareDialog, EditModal, RefillModal, ImageLightbox, ScheduleLightbox, UserFilter now all support browser back button - Mobile users can now swipe back to close any modal instead of navigating away from the app - ESC key also triggers proper history-based close for all modals - Fix duplicate openShareDialog function - Fix recursive call bug in openUserFilter * fix: prevent past days count from wrapping to new line - Add flex-wrap: nowrap to .past-days-toggle - Add white-space: nowrap and flex-shrink: 0 to .past-days-count - Ensures (7 Tage), (14 Tage) etc. stays on same line as label * fix: improve schedule row layout for mobile screens - Stack schedule label and value vertically on small screens (<400px) - Add word-break for long text values - Prevents 'Einnahmeprüfung' and '15 Min. vor geplanter Zeit' from overlapping * feat: add back-swipe support for image lightbox on share page - Add history.pushState/popstate handling for lightbox in SharedSchedule - Mobile users can now swipe back to close image instead of navigating away --- frontend/src/App.tsx | 238 +++++++++++++++++++++++++++++++--------- frontend/src/styles.css | 23 +++- 2 files changed, 209 insertions(+), 52 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9b8c134..f110f5a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -535,26 +535,57 @@ function AppContent() { if (userDropdownOpen) { setUserDropdownOpen(false); } else if (scheduleLightboxImage) { - setScheduleLightboxImage(null); + closeScheduleLightbox(); } else if (showImageLightbox) { - setShowImageLightbox(false); + closeImageLightbox(); + } else if (showRefillModal) { + closeRefillModal(); } else if (showEditModal) { - setShowEditModal(false); + closeEditModal(); resetForm(); } else if (showShareDialog) { - setShowShareDialog(false); + closeShareDialog(); } else if (showProfile) { - setShowProfile(false); + closeProfile(); } else if (selectedUser) { - setSelectedUser(null); + closeUserFilter(); } else if (selectedMed) { - setSelectedMed(null); + closeMedDetail(); } } }; document.addEventListener("keydown", handleEscape); return () => document.removeEventListener("keydown", handleEscape); - }, [selectedMed, showImageLightbox, scheduleLightboxImage, selectedUser, showProfile, showShareDialog, showEditModal, userDropdownOpen]); + }, [selectedMed, showImageLightbox, scheduleLightboxImage, selectedUser, showProfile, showShareDialog, showEditModal, showRefillModal, userDropdownOpen]); + + // Handle browser back button to close modals (in priority order) + useEffect(() => { + const handlePopState = () => { + // Close modals in order of priority (topmost first) + // NOTE: This handler MUST NOT call history.back() or it will cause infinite loops + // Only use direct state setters here + if (showImageLightbox) { + setShowImageLightbox(false); + } else if (scheduleLightboxImage) { + setScheduleLightboxImage(null); + } else if (showRefillModal) { + setShowRefillModal(false); + } else if (showEditModal) { + setShowEditModal(false); + resetForm(); + } else if (showShareDialog) { + resetShareDialogState(); + } else if (showProfile) { + setShowProfile(false); + } else if (selectedUser) { + setSelectedUser(null); + } else if (selectedMed) { + setSelectedMed(null); + } + }; + window.addEventListener('popstate', handlePopState); + return () => window.removeEventListener('popstate', handlePopState); + }, [selectedMed, showImageLightbox, scheduleLightboxImage, selectedUser, showProfile, showShareDialog, showEditModal, showRefillModal]); // Close user dropdown when clicking outside useEffect(() => { @@ -860,7 +891,10 @@ function AppContent() { // Reset refill form setRefillPacks(1); setRefillLoose(0); - setShowRefillModal(false); + // Close refill modal via history back for proper back-button support + if (showRefillModal) { + window.history.back(); + } // Reload medications to get updated stock loadMeds(); // Reload refill history @@ -878,6 +912,76 @@ function AppContent() { setRefillHistory([]); setRefillHistoryExpanded(false); loadRefillHistory(med.id); + // Push history state so browser back closes modal instead of navigating + window.history.pushState({ modal: 'medDetail', medId: med.id }, ''); + } + + // Helper to close medication detail modal via history back + function closeMedDetail() { + if (selectedMed) { + window.history.back(); + } + } + + // Modal helper functions for browser back button support + function openImageLightbox() { + setShowImageLightbox(true); + window.history.pushState({ modal: 'imageLightbox' }, ''); + } + function closeImageLightbox() { + if (showImageLightbox) { + window.history.back(); + } + } + + function openScheduleLightbox(imageUrl: string) { + setScheduleLightboxImage(imageUrl); + window.history.pushState({ modal: 'scheduleLightbox' }, ''); + } + function closeScheduleLightbox() { + if (scheduleLightboxImage) { + window.history.back(); + } + } + + function openRefillModal() { + setShowRefillModal(true); + window.history.pushState({ modal: 'refill' }, ''); + } + function closeRefillModal() { + if (showRefillModal) { + window.history.back(); + } + } + + function openEditModal() { + setShowEditModal(true); + window.history.pushState({ modal: 'edit' }, ''); + } + function closeEditModal() { + if (showEditModal) { + window.history.back(); + } + } + + function openProfile() { + setShowProfile(true); + window.history.pushState({ modal: 'profile' }, ''); + } + function closeProfile() { + if (showProfile) { + window.history.back(); + } + } + + function openUserFilter(person: string) { + setSelectedUser(person); + window.history.pushState({ modal: 'userFilter', person }, ''); + } + function closeUserFilter() { + if (selectedUser) { + window.history.back(); + } } async function testEmail() { @@ -1159,7 +1263,7 @@ function AppContent() { setOriginalForm(editForm); // Show modal on mobile if (window.innerWidth <= 768) { - setShowEditModal(true); + openEditModal(); } } @@ -1256,8 +1360,10 @@ function AppContent() { if (!wasEditing) { resetForm(); } else { - // Close modal on mobile after edit - setShowEditModal(false); + // Close modal on mobile after edit (via history back for proper back-button support) + if (showEditModal) { + window.history.back(); + } } loadMeds(); @@ -1291,6 +1397,7 @@ function AppContent() { // Share dialog functions async function openShareDialog() { setShowShareDialog(true); + window.history.pushState({ modal: 'share' }, ''); setShareLink(null); setShareCopied(false); setShareSelectedPerson(""); @@ -1344,6 +1451,13 @@ function AppContent() { } function closeShareDialog() { + if (showShareDialog) { + window.history.back(); + } + } + + // Internal function to reset share dialog state (called by popstate handler) + function resetShareDialogState() { setShowShareDialog(false); setShareLink(null); setShareCopied(false); @@ -1445,7 +1559,7 @@ function AppContent() { {user.username}
- @@ -1466,10 +1580,10 @@ function AppContent() { {/* Profile Modal */} {showProfile && ( -
setShowProfile(false)}> +
closeProfile()}>
e.stopPropagation()}> - - setShowProfile(false)} /> + + closeProfile()} />
)} @@ -1544,7 +1658,7 @@ function AppContent() { {row.name} {med?.takenBy && med.takenBy.length > 0 && med.takenBy.map((person) => ( - { e.stopPropagation(); setSelectedUser(person); }}>{person} + { e.stopPropagation(); openUserFilter(person); }}>{person} ))} {(med?.intakeRemindersEnabled || med?.notes) && ( @@ -1614,7 +1728,7 @@ function AppContent() { {row.name} {med?.takenBy && med.takenBy.length > 0 && med.takenBy.map((person) => ( - { e.stopPropagation(); setSelectedUser(person); }}>{person} + { e.stopPropagation(); openUserFilter(person); }}>{person} ))} {(med?.intakeRemindersEnabled || med?.notes) && ( @@ -1739,7 +1853,7 @@ function AppContent() {
med?.imageUrl && setScheduleLightboxImage(`/api/images/${med.imageUrl}`)} + onClick={() => med?.imageUrl && openScheduleLightbox(`/api/images/${med.imageUrl}`)} >
@@ -1763,7 +1877,7 @@ function AppContent() { const isTaken = takenDoses.has(doseId); return (
- {person && setSelectedUser(person)}>{person}} + {person && openUserFilter(person)}>{person}} {isTaken ? ( ) : ( @@ -1850,7 +1964,7 @@ function AppContent() {
med?.imageUrl && setScheduleLightboxImage(`/api/images/${med.imageUrl}`)} + onClick={() => med?.imageUrl && openScheduleLightbox(`/api/images/${med.imageUrl}`)} >
@@ -1885,7 +1999,7 @@ function AppContent() { const isTaken = takenDoses.has(doseId); return (
- {person && setSelectedUser(person)}>{person}} + {person && openUserFilter(person)}>{person}} {isTaken ? ( ) : ( @@ -1952,7 +2066,7 @@ function AppContent() { resetForm(); // On mobile, open the edit modal if (window.innerWidth <= 768) { - setShowEditModal(true); + openEditModal(); } }} > @@ -2900,7 +3014,7 @@ function AppContent() { const isTaken = takenDoses.has(doseId); return (
- {person && setSelectedUser(person)}>{person}} + {person && openUserFilter(person)}>{person}} {isTaken ? ( ) : ( @@ -2970,7 +3084,7 @@ function AppContent() { const isOverdue = !isTaken && dose.when < now && !isPastDay; return (
- {person && setSelectedUser(person)}>{person}} + {person && openUserFilter(person)}>{person}} {isTaken ? ( ) : ( @@ -2999,15 +3113,15 @@ function AppContent() { {/* Medication Detail Modal */} {selectedMed && ( -
setSelectedMed(null)}> +
e.stopPropagation()}> - +
selectedMed.imageUrl && setShowImageLightbox(true)} + onClick={() => selectedMed.imageUrl && openImageLightbox()} > {selectedMed.imageUrl && 🔍} @@ -3164,14 +3278,14 @@ function AppContent() {
-
- - {selectedMed.blisters.length > 0 && ( @@ -3185,8 +3299,8 @@ function AppContent() { {/* Image Lightbox */} {showImageLightbox && selectedMed.imageUrl && ( -
{ e.stopPropagation(); setShowImageLightbox(false); }}> - +
{ e.stopPropagation(); closeImageLightbox(); }}> + {selectedMed.name} { e.stopPropagation(); setShowRefillModal(false); }}> +
{ e.stopPropagation(); closeRefillModal(); }}>
e.stopPropagation()}> - +

{t('refill.title')}

{selectedMed.name}

@@ -3226,7 +3340,7 @@ function AppContent() {
-
@@ -3250,9 +3364,9 @@ function AppContent() { {/* User Medications Modal */} {selectedUser && ( -
setSelectedUser(null)}> +
closeUserFilter()}>
e.stopPropagation()}> - +
{selectedUser.charAt(0).toUpperCase()}
@@ -3269,7 +3383,7 @@ function AppContent() {
{ setSelectedUser(null); openMedDetail(med); }} + onClick={() => { closeUserFilter(); openMedDetail(med); }} >
@@ -3289,7 +3403,7 @@ function AppContent() {
- +
@@ -3373,8 +3487,8 @@ function AppContent() { {/* Schedule Lightbox - for clicking medication images in schedule */} {scheduleLightboxImage && ( -
setScheduleLightboxImage(null)}> - +
+ Medication setShowEditModal(false)}> +
closeEditModal()}>
e.stopPropagation()}> - +

{editingId ? t('form.editEntry') : t('form.newEntry')}

-
{ saveMedication(e); setShowEditModal(false); }}> + { saveMedication(e); }}>