Merge pull request #19 from every-app/split-modal-dismissal

fix: exit closes modal + accessible labels
This commit is contained in:
Ben Senescu 2026-05-06 10:46:45 -04:00 committed by GitHub
parent 6f50850b5a
commit 7f892ca433
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 62 additions and 14 deletions

View File

@ -1,15 +1,35 @@
import type { ReactNode } from "react";
import { useEffect, type ReactNode } from "react";
export function Modal({
maxWidth = "max-w-sm",
children,
onClose,
labelledBy,
}: {
maxWidth?: string;
children: ReactNode;
onClose?: () => void;
labelledBy?: string;
}) {
useEffect(() => {
if (!onClose) return;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== "Escape" || event.defaultPrevented) return;
event.preventDefault();
onClose();
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [onClose]);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div
role="dialog"
aria-modal="true"
aria-labelledby={labelledBy}
className={`card bg-base-100 border border-base-300 w-full ${maxWidth} shadow-xl`}
>
<div className="card-body gap-4">{children}</div>

View File

@ -28,13 +28,17 @@ export function ExportToSheetsModal() {
};
return (
<Modal maxWidth="max-w-md">
<Modal
maxWidth="max-w-md"
onClose={closeExportToSheetsModal}
labelledBy="export-to-sheets-title"
>
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-2">
<span className="inline-flex size-8 items-center justify-center rounded-full bg-success/15 text-success">
<Check className="size-4" />
</span>
<h3 className="text-base font-semibold">
<h3 id="export-to-sheets-title" className="text-base font-semibold">
Copied {rowCount} row{rowCount === 1 ? "" : "s"} to your clipboard
</h3>
</div>

View File

@ -34,9 +34,13 @@ export function CheckConfirmModal({
Math.ceil(totalChecks / KEYWORDS_PER_BATCH) * SECONDS_PER_BATCH;
return (
<Modal maxWidth="max-w-md">
<Modal
maxWidth="max-w-md"
onClose={onCancel}
labelledBy="rank-check-confirm-title"
>
<div>
<h3 className="text-lg font-semibold">
<h3 id="rank-check-confirm-title" className="text-lg font-semibold">
Check {keywordCount} keyword
{keywordCount !== 1 ? "s" : ""}
</h3>

View File

@ -255,7 +255,9 @@ export function KeywordSuggestionStep({
const sectionHeader = (title: string) => (
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">{title}</h2>
<h2 id="keyword-suggestions-title" className="text-lg font-semibold">
{title}
</h2>
<button className="btn btn-ghost btn-sm btn-square" onClick={onClose}>
<X className="size-4" />
</button>

View File

@ -127,8 +127,14 @@ export function RankTrackingConfigModal({
const isPending = createMutation.isPending || updateMutation.isPending;
if (step === "keywords" && createdConfigId) {
const closeKeywordStep = () => onSaved(createdConfigId);
return (
<Modal maxWidth="max-w-3xl">
<Modal
maxWidth="max-w-3xl"
onClose={closeKeywordStep}
labelledBy="keyword-suggestions-title"
>
<KeywordSuggestionStep
configId={createdConfigId}
projectId={projectId}
@ -136,16 +142,20 @@ export function RankTrackingConfigModal({
locationCode={locationCode}
languageCode={getLanguageCode(locationCode)}
onDone={(id) => onSaved(id)}
onClose={() => onSaved(createdConfigId ?? undefined)}
onClose={closeKeywordStep}
/>
</Modal>
);
}
return (
<Modal maxWidth="max-w-lg">
<Modal
maxWidth="max-w-lg"
onClose={onClose}
labelledBy="rank-config-modal-title"
>
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">
<h2 id="rank-config-modal-title" className="text-lg font-semibold">
{isEdit ? "Edit Domain Config" : "Add Domain"}
</h2>
<button className="btn btn-ghost btn-sm btn-square" onClick={onClose}>

View File

@ -97,8 +97,11 @@ export function RankTrackingDomainList({
</div>
{archiveTarget && (
<Modal>
<h3 className="text-lg font-semibold">
<Modal
onClose={() => setArchiveTarget(null)}
labelledBy="archive-domain-title"
>
<h3 id="archive-domain-title" className="text-lg font-semibold">
Archive {archiveTarget.domain}?
</h3>
<p className="text-sm text-base-content/70">

View File

@ -129,8 +129,13 @@ export function RankTrackingTable({
{/* Confirm modal */}
{showConfirm && (
<Modal>
<h3 className="text-lg font-semibold">Remove keywords?</h3>
<Modal
onClose={() => setShowConfirm(false)}
labelledBy="remove-keywords-title"
>
<h3 id="remove-keywords-title" className="text-lg font-semibold">
Remove keywords?
</h3>
<p className="text-sm text-base-content/70">
This will stop tracking {selectedCount} keyword
{selectedCount !== 1 ? "s" : ""}. Historical ranking data is