Files
Ely-GPUI-Components/frontend/src/Live.tsx
T

97 lines
3.5 KiB
TypeScript

import { useEffect, useMemo, useRef, useState } from "react";
import { Icon } from "./icons";
import { mode, useMode } from "./theme";
const SPIN_AFTER = 400;
const GIVE_UP = 20000;
type State = "starting" | "slow" | "ready" | "failed";
/** One story live from the gallery, in the site's mode. */
export function Live({ page, story, title }: { page: string; story: string; title: string }) {
const theme = useMode();
const frame = useRef<HTMLIFrameElement>(null);
const [state, setState] = useState<State>("starting");
const [full, setFull] = useState(false);
const [run, setRun] = useState(0);
const after = useRef<HTMLSpanElement>(null);
// Each new frame opens in the current mode.
const start = useMemo(() => mode(), [run]);
const src = (t: string) => `/gallery/?page=${page}&story=${story}&theme=${t}`;
useEffect(() => {
setState("starting");
const slow = window.setTimeout(() => setState((s) => (s === "starting" ? "slow" : s)), SPIN_AFTER);
const failed = window.setTimeout(() => {
setState((s) => (s === "ready" ? s : "failed"));
console.error(`live: ${page}/${story} sent no ready in ${GIVE_UP} ms`);
}, GIVE_UP);
const ready = (event: MessageEvent) => {
if (event.source !== frame.current?.contentWindow || event.data?.ely !== "ready") return;
clearTimeout(slow);
clearTimeout(failed);
setState("ready");
frame.current?.contentWindow?.postMessage({ ely: "theme", theme: mode() }, location.origin);
};
addEventListener("message", ready);
return () => {
clearTimeout(slow);
clearTimeout(failed);
removeEventListener("message", ready);
};
}, [page, story, run]);
useEffect(() => {
frame.current?.contentWindow?.postMessage({ ely: "theme", theme }, location.origin);
}, [theme]);
useEffect(() => {
if (!full) return;
document.documentElement.style.overflow = "hidden";
const escape = (event: KeyboardEvent) => event.key === "Escape" && setFull(false);
addEventListener("keydown", escape);
return () => {
document.documentElement.style.overflow = "";
removeEventListener("keydown", escape);
};
}, [full]);
return (
<>
<button className="skip-live" onClick={() => after.current?.focus()}>
Skip the live example
</button>
<div className={full ? "live full" : "live"}>
<iframe key={run} ref={frame} title={`${title}, live`} src={src(start)} />
{state !== "ready" && (
<div className="live-state" role="status">
{state === "slow" && <span className="spin" />}
{state === "failed" ? (
<>
Couldn't start the example.
<button className="text" onClick={() => setRun((n) => n + 1)}>
Reload
</button>
</>
) : (
"Starting…"
)}
</div>
)}
<div className="live-tools">
<button className="tool" aria-label="Reload the example" onClick={() => setRun((n) => n + 1)}>
<Icon name="Refresh" />
</button>
<a className="tool" href={src(theme)} target="_blank" rel="noopener" aria-label="Open the example alone">
<Icon name="ArrowRightUp" />
</a>
<button className="tool" aria-label={full ? "Leave full screen" : "Full screen"} onClick={() => setFull(!full)}>
<Icon name={full ? "Minimize" : "Maximize"} />
</button>
</div>
</div>
<span ref={after} tabIndex={-1} />
</>
);
}