ba36f67371
* 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
81 lines
2.4 KiB
TypeScript
81 lines
2.4 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
||
import { useEscapeKey } from "../hooks/useEscapeKey";
|
||
import { useScrollLock } from "../hooks/useScrollLock";
|
||
|
||
interface ExportModalProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
onExport: (includeImages: boolean) => void;
|
||
exporting: boolean;
|
||
}
|
||
|
||
export default function ExportModal({ isOpen, onClose, onExport, exporting }: ExportModalProps) {
|
||
const { t } = useTranslation();
|
||
|
||
useScrollLock(isOpen);
|
||
useEscapeKey(isOpen, onClose);
|
||
|
||
if (!isOpen) return null;
|
||
|
||
return (
|
||
<div
|
||
className="modal-overlay"
|
||
onClick={onClose}
|
||
onKeyDown={(e) => {
|
||
if (e.key !== "Escape") e.stopPropagation();
|
||
}}
|
||
>
|
||
<div
|
||
className="modal-content"
|
||
onClick={(e) => e.stopPropagation()}
|
||
onKeyDown={(e) => {
|
||
if (e.key !== "Escape") e.stopPropagation();
|
||
}}
|
||
style={{ maxWidth: "450px" }}
|
||
>
|
||
<button className="modal-close" onClick={onClose}>
|
||
×
|
||
</button>
|
||
<h2 style={{ marginBottom: "16px", paddingRight: "2rem" }}>{t("exportImport.exportOptions")}</h2>
|
||
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
|
||
<button
|
||
type="button"
|
||
className="action-card"
|
||
onClick={() => {
|
||
onClose();
|
||
onExport(true);
|
||
}}
|
||
disabled={exporting}
|
||
style={{ textAlign: "left", cursor: "pointer", border: "1px solid var(--border)", borderRadius: "8px" }}
|
||
>
|
||
<div className="action-card-content" style={{ flex: 1 }}>
|
||
<span className="action-card-title">{t("exportImport.exportWithImages")}</span>
|
||
<span className="action-card-desc">{t("exportImport.exportWithImagesDesc")}</span>
|
||
</div>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="action-card"
|
||
onClick={() => {
|
||
onClose();
|
||
onExport(false);
|
||
}}
|
||
disabled={exporting}
|
||
style={{ textAlign: "left", cursor: "pointer", border: "1px solid var(--border)", borderRadius: "8px" }}
|
||
>
|
||
<div className="action-card-content" style={{ flex: 1 }}>
|
||
<span className="action-card-title">{t("exportImport.exportDataOnly")}</span>
|
||
<span className="action-card-desc">{t("exportImport.exportDataOnlyDesc")}</span>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
<div className="modal-footer" style={{ padding: "1rem 0 0 0", borderTop: "none", justifyContent: "flex-end" }}>
|
||
<button type="button" className="ghost" onClick={onClose}>
|
||
{t("common.close")}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|