fix: smooth mobile edit transition and align modal validation behavior (#286)

* fix: reliable Escape key close for all modals via useEscapeKey hook

- Add useEscapeKey hook (document-level keydown listener)
- Retrofit all 12 modal/overlay components to use it
- Remove redundant overlay onKeyDown Escape handlers
- Simplify modal-content onKeyDown to plain stopPropagation
- Replace MedDetailModal's capture-phase useEffect with 3 useEscapeKey calls
- Replace SharedSchedule's inline useEffect with useEscapeKey
- Add mandatory modal rules to UI Consistency skill
- All 777 frontend + 569 backend tests pass

* fix: smooth mobile edit transition and align modal validation behavior

* fix: keep overlay keydown non-closing for Enter key

* fix: show mobile name error when validation already exists

* fix: restore app-level escape priority handling

* fix: prioritize schedule lightbox on Escape
This commit is contained in:
Daniel Volz
2026-02-23 06:42:06 +01:00
committed by GitHub
parent 2aa6b1f406
commit ba36f67371
21 changed files with 337 additions and 163 deletions
+12 -1
View File
@@ -1,5 +1,5 @@
/* biome-ignore-all lint/style/noNestedTernary: timeline rendering uses explicit UI-state branching */
import { Bell, NotebookPen, Share2 } from "lucide-react";
import { Bell, ClipboardList, NotebookPen, Share2 } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { ConfirmModal, MedicationAvatar } from "../components";
@@ -538,6 +538,17 @@ export function DashboardPage() {
</span>
</>
)}
{med?.prescriptionEnabled && (
<>
{" "}
<span
className="prescription-icon info-tooltip"
data-tooltip={t("tooltips.hasPrescription")}
>
<ClipboardList size={13} aria-hidden="true" />
</span>
</>
)}
</span>
{med?.takenBy && med.takenBy.length > 0 && (
<span className="med-taken-by-line">
+11 -2
View File
@@ -76,12 +76,14 @@ export function MedicationsPage() {
useUnsavedChangesWarning(formChanged);
// View mode: grid (default) or form (edit/new)
const [viewMode, setViewMode] = useState<"grid" | "form">("grid");
// If navigating in with editMedId, suppress rendering until the edit form is ready
const [pendingEditTransition, setPendingEditTransition] = useState(() => searchParams.has("editMedId"));
const [viewMode, setViewMode] = useState<"grid" | "form">(pendingEditTransition ? "form" : "grid");
const [lightboxImage, setLightboxImage] = useState<{ src: string; alt: string } | null>(null);
const [activeTab, setActiveTab] = useState<"general" | "stock" | "prescription" | "schedule">("general");
// Mobile modal state (declared early because it's used in useEffect below)
const [showEditModal, setShowEditModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(pendingEditTransition && window.innerWidth <= 768);
const showEditModalRef = useRef(false);
useEffect(() => {
showEditModalRef.current = showEditModal;
@@ -705,6 +707,8 @@ export function MedicationsPage() {
setActiveTab("general");
startEdit(medicationToEdit, openEditModal);
setViewMode("form");
setPendingEditTransition(false);
window.dispatchEvent(new Event("medassist:edit-transition-ready"));
const nextParams = new URLSearchParams(searchParams);
nextParams.delete("editMedId");
@@ -716,6 +720,11 @@ export function MedicationsPage() {
return allMeds.find((med) => med.id === editingId) ?? null;
}, [allMeds, editingId]);
// While navigating from detail modal to edit, render nothing until form is populated
if (pendingEditTransition) {
return null;
}
return (
<section className={`med-grid-wrapper${viewMode === "form" ? " desktop-edit-open" : ""}`}>
{/* ── Grid View: always visible medication cards ── */}