Use the central Dialog for Campaign previews

This commit is contained in:
2026-07-21 03:17:11 +02:00
parent 35b3cc151d
commit 04c214b149
4 changed files with 33 additions and 24 deletions

View File

@@ -1,6 +1,6 @@
import { useEffect, type ReactNode } from "react"; import { useEffect, useRef, type ReactNode } from "react";
import { ArrowBigLeft, ArrowBigLeftDash, ArrowBigRight, ArrowBigRightDash } from "lucide-react"; import { ArrowBigLeft, ArrowBigLeftDash, ArrowBigRight, ArrowBigRightDash } from "lucide-react";
import { Button, MessageDisplayPanel, type MessageDisplayAttachment } from "@govoplan/core-webui"; import { Button, Dialog, MessageDisplayPanel, type MessageDisplayAttachment } from "@govoplan/core-webui";
// Campaign review/template/mock previews need recipient navigation, review notes, // Campaign review/template/mock previews need recipient navigation, review notes,
// raw MIME inspection and attachment grouping. Generic mailbox reading uses // raw MIME inspection and attachment grouping. Generic mailbox reading uses
@@ -60,15 +60,13 @@ export default function CampaignMessagePreviewOverlay({
}: CampaignMessagePreviewOverlayProps) { }: CampaignMessagePreviewOverlayProps) {
const shownSubject = subject?.trim() || "i18n:govoplan-campaign.no_subject.7b4e8035"; const shownSubject = subject?.trim() || "i18n:govoplan-campaign.no_subject.7b4e8035";
const fields = metaItems.map((item) => ({ label: item.label, value: item.value })); const fields = metaItems.map((item) => ({ label: item.label, value: item.value }));
const contentRef = useRef<HTMLDivElement | null>(null);
useEffect(() => { useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
const dialogPanel = contentRef.current?.closest<HTMLElement>("[data-dialog-stack-state]");
if (dialogPanel?.dataset.dialogStackState !== "topmost") return;
if (isEditableTarget(event.target)) return; if (isEditableTarget(event.target)) return;
if (event.key === "Escape") {
event.preventDefault();
onClose();
return;
}
if (!navigation) return; if (!navigation) return;
if (event.key === "ArrowLeft") { if (event.key === "ArrowLeft") {
event.preventDefault(); event.preventDefault();
@@ -87,16 +85,26 @@ export default function CampaignMessagePreviewOverlay({
window.addEventListener("keydown", handleKeyDown); window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown);
}, [navigation, onClose]); }, [navigation]);
return ( return (
<div className="overlay-backdrop message-preview-backdrop" role="dialog" aria-modal="true" aria-labelledby="message-preview-title"> <Dialog
<div className="modal-panel template-preview-modal message-preview-modal"> open
<header className="modal-header"> title={title}
<h2 id="message-preview-title">{title}</h2> onClose={onClose}
<button type="button" className="modal-close" aria-label={closeLabel} title={closeLabel} onClick={onClose}>×</button> closeLabel={closeLabel}
</header> closeOnBackdrop={false}
<div className="modal-body"> backdropClassName="overlay-backdrop message-preview-backdrop"
className="modal-panel template-preview-modal message-preview-modal"
headerClassName="modal-header"
bodyClassName="modal-body"
footerClassName="modal-footer"
footer={<>
{actions && <div className="button-row compact-actions">{actions}</div>}
<Button variant="primary" onClick={onClose}>{closeLabel}</Button>
</>}
>
<div ref={contentRef} className="message-preview-content">
{(recipientLabel || recipientNote || navigation) && {(recipientLabel || recipientNote || navigation) &&
<div className="template-preview-toolbar"> <div className="template-preview-toolbar">
<div> <div>
@@ -132,13 +140,8 @@ export default function CampaignMessagePreviewOverlay({
<pre className="mock-message-raw">{raw}</pre> <pre className="mock-message-raw">{raw}</pre>
</details> </details>
} }
</div>
<footer className="modal-footer">
{actions && <div className="button-row compact-actions">{actions}</div>}
<Button variant="primary" onClick={onClose}>{closeLabel}</Button>
</footer>
</div> </div>
</div>); </Dialog>);
} }

View File

@@ -1335,6 +1335,10 @@
scrollbar-gutter: stable; scrollbar-gutter: stable;
} }
.message-preview-content {
display: contents;
}
.message-preview-modal .modal-footer { .message-preview-modal .modal-footer {
align-items: center; align-items: center;
min-height: 68px; min-height: 68px;

View File

@@ -15,8 +15,10 @@ assert(
"message previews expose their responsive backdrop hook" "message previews expose their responsive backdrop hook"
); );
assert( assert(
overlaySource.includes('type="button" className="modal-close" aria-label={closeLabel} title={closeLabel}'), overlaySource.includes("<Dialog") &&
"the close control has stable button semantics, an accessible name, and a tooltip" overlaySource.includes("closeLabel={closeLabel}") &&
overlaySource.includes('dataset.dialogStackState !== "topmost"'),
"message previews use the central stacked Dialog with an accessible close label"
); );
assert( assert(
styles.includes("height: min(780px, calc(100dvh - 48px));"), styles.includes("height: min(780px, calc(100dvh - 48px));"),

View File

@@ -103,7 +103,7 @@ for (const [key, english, german] of newTranslations) {
const overlaySource = readFileSync("src/features/campaigns/components/MessagePreviewOverlay.tsx", "utf8"); const overlaySource = readFileSync("src/features/campaigns/components/MessagePreviewOverlay.tsx", "utf8");
assert(overlaySource.includes("message-preview-backdrop"), "message previews expose a layout-specific responsive backdrop hook"); assert(overlaySource.includes("message-preview-backdrop"), "message previews expose a layout-specific responsive backdrop hook");
assert(overlaySource.includes("{actions && <div"), "built-message footer actions remain in the stable shared preview footer"); assert(overlaySource.includes("{actions && <div"), "built-message footer actions remain in the stable shared preview footer");
assert(overlaySource.includes('aria-label={closeLabel}'), "the preview close control has an accessible label"); assert(overlaySource.includes("<Dialog") && overlaySource.includes("closeLabel={closeLabel}"), "the preview uses the central Dialog and its accessible close label");
const styles = readFileSync("src/styles/campaign-workspace.css", "utf8"); const styles = readFileSync("src/styles/campaign-workspace.css", "utf8");
assert(styles.includes("height: min(780px, calc(100dvh - 48px));"), "desktop previews use a stable responsive height"); assert(styles.includes("height: min(780px, calc(100dvh - 48px));"), "desktop previews use a stable responsive height");