113 lines
3.1 KiB
TypeScript
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>
|
|
);
|
|
}
|