feat: complete TechLog Studio publication flow
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user