Files
tech-log-frontend/src/features/tech-log/presentation/studio/components/unpublish-dialog.tsx
T

113 lines
3.1 KiB
TypeScript

import { useEffect, useId, useRef, type KeyboardEvent } from "react";
import type { PublicationListItem } from "../../../contracts/studio/contract.ts";
import {
defaultPublicationFlowClasses,
type PublicationFlowClasses,
} from "./publication-flow-classes.ts";
export function UnpublishDialog({
item,
pending,
error,
onDismiss,
onConfirm,
classes: styles = defaultPublicationFlowClasses,
}: {
item: PublicationListItem | null;
pending: boolean;
error: string;
onDismiss(): void;
onConfirm(): void;
classes?: PublicationFlowClasses;
}) {
const dialogRef = useRef<HTMLDialogElement>(null);
const cancelRef = useRef<HTMLButtonElement>(null);
const titleId = useId();
const descriptionId = useId();
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (item && !dialog.open) {
dialog.showModal();
cancelRef.current?.focus();
} else if (!item && dialog.open) {
dialog.close();
}
}, [item]);
useEffect(
() => () => {
if (dialogRef.current?.open) dialogRef.current.close();
},
[],
);
const trapFocus = (event: KeyboardEvent<HTMLDialogElement>) => {
if (event.key !== "Tab") return;
const controls = Array.from(
event.currentTarget.querySelectorAll<HTMLElement>(
"button:not([disabled])",
),
);
if (!controls.length) return;
const first = controls[0];
const last = controls.at(-1)!;
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
};
return (
<dialog
ref={dialogRef}
className={styles.dialog}
aria-labelledby={titleId}
aria-describedby={descriptionId}
onCancel={(event) => {
event.preventDefault();
if (!pending) onDismiss();
}}
onKeyDown={trapFocus}
>
<div className={styles.dialogBody}>
<p className={styles.eyebrow}>UNPUBLISH</p>
<h2 id={titleId}>게시를 취소할까요?</h2>
<p id={descriptionId}>
{item?.document.title ?? "선택한 기록"} Studio 게시 상태를 중단하고
게시 취소 이벤트를 남깁니다.
</p>
<div className={styles.preservedNote}>
<strong>작업본과 이전 Snapshot은 보존됩니다.</strong>
<p>
현재 Mock에서는 기존 Public 사이트와 검색 결과를 변경하지 않습니다.
</p>
</div>
{error ? (
<p className={styles.error} role="alert">
{error}
</p>
) : null}
<div className={styles.dialogActions}>
<button
ref={cancelRef}
type="button"
disabled={pending}
onClick={onDismiss}
>
계속 게시
</button>
<button type="button" disabled={pending} onClick={onConfirm}>
{pending ? "취소 처리 중…" : "게시 취소 확인"}
</button>
</div>
</div>
</dialog>
);
}