T45c Site · Home: full screen stops the glide at once, a starting gallery leaves the host's focus alone, a focused frame stays mounted
This commit is contained in:
7 files changed
+55
-17
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { Icon } from "./icons";
|
||||
import { hold } from "./scroll";
|
||||
import { mode, useMode } from "./theme";
|
||||
|
||||
const SPIN_AFTER = 400;
|
||||
@@ -45,13 +46,16 @@ export function Live({ page, story, title }: { page: string; story: string; titl
|
||||
frame.current?.contentWindow?.postMessage({ ely: "theme", theme }, location.origin);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
// Before paint, so no glide frame lands after the press.
|
||||
useLayoutEffect(() => {
|
||||
if (!full) return;
|
||||
document.documentElement.style.overflow = "hidden";
|
||||
hold(true);
|
||||
const escape = (event: KeyboardEvent) => event.key === "Escape" && setFull(false);
|
||||
addEventListener("keydown", escape);
|
||||
return () => {
|
||||
document.documentElement.style.overflow = "";
|
||||
hold(false);
|
||||
removeEventListener("keydown", escape);
|
||||
};
|
||||
}, [full]);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Link, Outlet, useRouter } from "@tanstack/react-router";
|
||||
import Lenis from "lenis";
|
||||
import "lenis/dist/lenis.css";
|
||||
import { useEffect } from "react";
|
||||
import { REPO } from "./data";
|
||||
import { Icon } from "./icons";
|
||||
import { useStill } from "./iso/Iso";
|
||||
import { smooth } from "./scroll";
|
||||
import { setSound, useSound } from "./iso/marimba";
|
||||
import { setMode, useMode } from "./theme";
|
||||
|
||||
@@ -61,13 +60,12 @@ export function Shell() {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
if (still) return;
|
||||
// It halts while the page's overflow is hidden, as full screen sets.
|
||||
const lenis = new Lenis({ autoRaf: true, autoToggle: true });
|
||||
const { lenis, end } = smooth();
|
||||
// A glide in flight must not outlive its page.
|
||||
const off = router.subscribe("onBeforeNavigate", () => lenis.scrollTo(lenis.animatedScroll, { immediate: true, force: true }));
|
||||
return () => {
|
||||
off();
|
||||
lenis.destroy();
|
||||
end();
|
||||
};
|
||||
}, [still, router]);
|
||||
return (
|
||||
|
||||
@@ -21,14 +21,27 @@ const FEATURES = [
|
||||
return { ...f, name: story.title };
|
||||
});
|
||||
|
||||
/** Mounts its children only while near the view. */
|
||||
/** Mounts its children while near the view or holding focus. */
|
||||
function Near({ className, children }: { className: string; children: ReactNode }) {
|
||||
const box = useRef<HTMLDivElement>(null);
|
||||
const [near, setNear] = useState(false);
|
||||
useEffect(() => {
|
||||
const seen = new IntersectionObserver(([e]) => setNear(e.isIntersecting), { rootMargin: "100% 0px" });
|
||||
seen.observe(box.current!);
|
||||
return () => seen.disconnect();
|
||||
const el = box.current!;
|
||||
let seen = false;
|
||||
const view = new IntersectionObserver(
|
||||
([e]) => {
|
||||
seen = e.isIntersecting;
|
||||
setNear(seen || el.contains(document.activeElement));
|
||||
},
|
||||
{ rootMargin: "100% 0px" },
|
||||
);
|
||||
const left = (e: FocusEvent) => setNear(seen || el.contains(e.relatedTarget as Node | null));
|
||||
view.observe(el);
|
||||
el.addEventListener("focusout", left);
|
||||
return () => {
|
||||
view.disconnect();
|
||||
el.removeEventListener("focusout", left);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div ref={box} className={className}>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import Lenis from "lenis";
|
||||
import "lenis/dist/lenis.css";
|
||||
|
||||
let lenis: Lenis | null = null;
|
||||
|
||||
/** Smooth scroll for the page; the returned call ends it. */
|
||||
export function smooth(): { lenis: Lenis; end: () => void } {
|
||||
if (lenis) throw new Error("scroll: smooth scroll already runs");
|
||||
const own = new Lenis({ autoRaf: true });
|
||||
lenis = own;
|
||||
return { lenis: own, end: () => (own.destroy(), (lenis = null)) };
|
||||
}
|
||||
|
||||
/** Holds the page still at once, a glide in flight included. */
|
||||
export function hold(still: boolean): void {
|
||||
if (still) lenis?.stop();
|
||||
else lenis?.start();
|
||||
}
|
||||
Reference in new issue
Block a user