Merge pull request #19 from every-app/split-modal-dismissal
fix: exit closes modal + accessible labels
This commit is contained in:
parent
6f50850b5a
commit
7f892ca433
@ -1,15 +1,35 @@
|
|||||||
import type { ReactNode } from "react";
|
import { useEffect, type ReactNode } from "react";
|
||||||
|
|
||||||
export function Modal({
|
export function Modal({
|
||||||
maxWidth = "max-w-sm",
|
maxWidth = "max-w-sm",
|
||||||
children,
|
children,
|
||||||
|
onClose,
|
||||||
|
labelledBy,
|
||||||
}: {
|
}: {
|
||||||
maxWidth?: string;
|
maxWidth?: string;
|
||||||
children: ReactNode;
|
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 (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||||
<div
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby={labelledBy}
|
||||||
className={`card bg-base-100 border border-base-300 w-full ${maxWidth} shadow-xl`}
|
className={`card bg-base-100 border border-base-300 w-full ${maxWidth} shadow-xl`}
|
||||||
>
|
>
|
||||||
<div className="card-body gap-4">{children}</div>
|
<div className="card-body gap-4">{children}</div>
|
||||||
|
|||||||
@ -28,13 +28,17 @@ export function ExportToSheetsModal() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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-start justify-between gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="inline-flex size-8 items-center justify-center rounded-full bg-success/15 text-success">
|
<span className="inline-flex size-8 items-center justify-center rounded-full bg-success/15 text-success">
|
||||||
<Check className="size-4" />
|
<Check className="size-4" />
|
||||||
</span>
|
</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
|
Copied {rowCount} row{rowCount === 1 ? "" : "s"} to your clipboard
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -34,9 +34,13 @@ export function CheckConfirmModal({
|
|||||||
Math.ceil(totalChecks / KEYWORDS_PER_BATCH) * SECONDS_PER_BATCH;
|
Math.ceil(totalChecks / KEYWORDS_PER_BATCH) * SECONDS_PER_BATCH;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal maxWidth="max-w-md">
|
<Modal
|
||||||
|
maxWidth="max-w-md"
|
||||||
|
onClose={onCancel}
|
||||||
|
labelledBy="rank-check-confirm-title"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold">
|
<h3 id="rank-check-confirm-title" className="text-lg font-semibold">
|
||||||
Check {keywordCount} keyword
|
Check {keywordCount} keyword
|
||||||
{keywordCount !== 1 ? "s" : ""}
|
{keywordCount !== 1 ? "s" : ""}
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@ -255,7 +255,9 @@ export function KeywordSuggestionStep({
|
|||||||
|
|
||||||
const sectionHeader = (title: string) => (
|
const sectionHeader = (title: string) => (
|
||||||
<div className="flex items-center justify-between">
|
<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}>
|
<button className="btn btn-ghost btn-sm btn-square" onClick={onClose}>
|
||||||
<X className="size-4" />
|
<X className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -127,8 +127,14 @@ export function RankTrackingConfigModal({
|
|||||||
const isPending = createMutation.isPending || updateMutation.isPending;
|
const isPending = createMutation.isPending || updateMutation.isPending;
|
||||||
|
|
||||||
if (step === "keywords" && createdConfigId) {
|
if (step === "keywords" && createdConfigId) {
|
||||||
|
const closeKeywordStep = () => onSaved(createdConfigId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal maxWidth="max-w-3xl">
|
<Modal
|
||||||
|
maxWidth="max-w-3xl"
|
||||||
|
onClose={closeKeywordStep}
|
||||||
|
labelledBy="keyword-suggestions-title"
|
||||||
|
>
|
||||||
<KeywordSuggestionStep
|
<KeywordSuggestionStep
|
||||||
configId={createdConfigId}
|
configId={createdConfigId}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
@ -136,16 +142,20 @@ export function RankTrackingConfigModal({
|
|||||||
locationCode={locationCode}
|
locationCode={locationCode}
|
||||||
languageCode={getLanguageCode(locationCode)}
|
languageCode={getLanguageCode(locationCode)}
|
||||||
onDone={(id) => onSaved(id)}
|
onDone={(id) => onSaved(id)}
|
||||||
onClose={() => onSaved(createdConfigId ?? undefined)}
|
onClose={closeKeywordStep}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<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"}
|
{isEdit ? "Edit Domain Config" : "Add Domain"}
|
||||||
</h2>
|
</h2>
|
||||||
<button className="btn btn-ghost btn-sm btn-square" onClick={onClose}>
|
<button className="btn btn-ghost btn-sm btn-square" onClick={onClose}>
|
||||||
|
|||||||
@ -97,8 +97,11 @@ export function RankTrackingDomainList({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{archiveTarget && (
|
{archiveTarget && (
|
||||||
<Modal>
|
<Modal
|
||||||
<h3 className="text-lg font-semibold">
|
onClose={() => setArchiveTarget(null)}
|
||||||
|
labelledBy="archive-domain-title"
|
||||||
|
>
|
||||||
|
<h3 id="archive-domain-title" className="text-lg font-semibold">
|
||||||
Archive {archiveTarget.domain}?
|
Archive {archiveTarget.domain}?
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-base-content/70">
|
<p className="text-sm text-base-content/70">
|
||||||
|
|||||||
@ -129,8 +129,13 @@ export function RankTrackingTable({
|
|||||||
|
|
||||||
{/* Confirm modal */}
|
{/* Confirm modal */}
|
||||||
{showConfirm && (
|
{showConfirm && (
|
||||||
<Modal>
|
<Modal
|
||||||
<h3 className="text-lg font-semibold">Remove keywords?</h3>
|
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">
|
<p className="text-sm text-base-content/70">
|
||||||
This will stop tracking {selectedCount} keyword
|
This will stop tracking {selectedCount} keyword
|
||||||
{selectedCount !== 1 ? "s" : ""}. Historical ranking data is
|
{selectedCount !== 1 ? "s" : ""}. Historical ranking data is
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user