"use client"; import { useEffect, useId, useState } from "react"; import { createPortal } from "react-dom"; import { useTranslations } from "next-intl"; import { useFocusTrap } from "@/hooks/use-focus-trap"; import { Button } from "@/components/ui/button"; import { Clock, Loader2 } from "@/components/icons"; import type { SessionEndKind } from "@/lib/session-policy/session-timer"; interface SessionTimeoutDialogProps { /** 'ending': the session is over, drafts are being saved. */ mode: SessionEndKind | 'ending'; deadline: number | null; /** Hours of the maximum session length, for its message. */ maxHours?: number; onStay: () => void; onDismiss: () => void; onSignOut: () => void; } function secondsLeft(deadline: number | null): number { return deadline === null ? 0 : Math.max(0, Math.ceil((deadline - Date.now()) / 1000)); } /** Screen readers hear the dialog when it opens, then only at these marks, not every second. */ const ANNOUNCE_AT = [30, 10]; /** * The warning before an organisation session limit signs the user out. For * the idle timeout it asks whether to stay; the maximum session length * can't be extended, so that one only says when it ends. */ export function SessionTimeoutDialog({ mode, deadline, maxHours, onStay, onDismiss, onSignOut }: SessionTimeoutDialogProps) { const t = useTranslations("session_limits"); const id = useId(); const [seconds, setSeconds] = useState(() => secondsLeft(deadline)); const [announced, setAnnounced] = useState(null); const dialogRef = useFocusTrap({ isActive: true, onEscape: mode === "idle" ? onStay : mode === "max" ? onDismiss : undefined, restoreFocus: true, }); useEffect(() => { setSeconds(secondsLeft(deadline)); if (mode === "ending") return; const interval = setInterval(() => setSeconds(secondsLeft(deadline)), 250); return () => clearInterval(interval); }, [deadline, mode]); useEffect(() => { const mark = ANNOUNCE_AT.find((at) => seconds === at); if (mark !== undefined) setAnnounced(mark); }, [seconds]); if (typeof document === "undefined") return null; const title = mode === "max" ? t("max_title") : mode === "idle" ? t("idle_title") : t("signing_out"); // Portalled to the end of the body above everything else, the office // editor (z-100) and plugin dialogs (z-100000) included. return createPortal(
{mode === "ending" ? : }

{title}

{mode !== "ending" && (

{mode === "max" ? t("max_message", { hours: maxHours ?? 0, seconds }) : t("idle_message", { seconds })}

)} {mode !== "ending" && announced !== null ? t("seconds_left", { seconds: announced }) : ""}
{mode !== "ending" && ( // The safe answer comes first, so the focus trap starts on it and // Enter keeps the user signed in; row-reverse shows it on the right.
{mode === "idle" ? : }
)}
, document.body, ); }