Add sticky chapter outline for the reading demo
This commit is contained in:
@@ -0,0 +1,99 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
function OutlineBody({ groups, complete, count, total, active, showTitle }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className="outline-progress">
|
||||||
|
{count}/{total} Try-its complete
|
||||||
|
</p>
|
||||||
|
{showTitle ? <p className="outline-heading">In this chapter</p> : null}
|
||||||
|
{groups.map((g) => (
|
||||||
|
<div key={g.label} className="outline-group">
|
||||||
|
<p className="outline-group-label spec">{g.label}</p>
|
||||||
|
<ul>
|
||||||
|
{g.items.map((item) => {
|
||||||
|
const done = item.recipeId ? !!complete[item.recipeId] : false;
|
||||||
|
const isActive = active === item.id;
|
||||||
|
return (
|
||||||
|
<li key={item.id}>
|
||||||
|
<a
|
||||||
|
href={`#${item.id}`}
|
||||||
|
className={isActive ? "is-active" : undefined}
|
||||||
|
aria-current={isActive ? "location" : undefined}
|
||||||
|
>
|
||||||
|
<span>{item.label}</span>
|
||||||
|
{done ? (
|
||||||
|
<span className="outline-done" aria-label="Try-it complete">
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChapterOutline({
|
||||||
|
groups,
|
||||||
|
complete = {},
|
||||||
|
count = 0,
|
||||||
|
total = 0,
|
||||||
|
variant = "desktop",
|
||||||
|
}) {
|
||||||
|
const ids = useMemo(
|
||||||
|
() => groups.flatMap((g) => g.items.map((item) => item.id)),
|
||||||
|
[groups]
|
||||||
|
);
|
||||||
|
const [active, setActive] = useState(ids[0] || "");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const els = ids.map((id) => document.getElementById(id)).filter(Boolean);
|
||||||
|
if (!els.length || typeof IntersectionObserver === "undefined") return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const visible = entries
|
||||||
|
.filter((e) => e.isIntersecting)
|
||||||
|
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
|
||||||
|
if (visible[0]?.target?.id) setActive(visible[0].target.id);
|
||||||
|
},
|
||||||
|
{ rootMargin: "-88px 0px -55% 0px", threshold: [0, 0.1, 0.25, 0.5, 1] }
|
||||||
|
);
|
||||||
|
els.forEach((el) => observer.observe(el));
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [ids]);
|
||||||
|
|
||||||
|
if (variant === "mobile") {
|
||||||
|
return (
|
||||||
|
<details className="chapter-outline-mobile">
|
||||||
|
<summary>In this chapter</summary>
|
||||||
|
<OutlineBody
|
||||||
|
groups={groups}
|
||||||
|
complete={complete}
|
||||||
|
count={count}
|
||||||
|
total={total}
|
||||||
|
active={active}
|
||||||
|
showTitle={false}
|
||||||
|
/>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="chapter-outline" aria-label="In this chapter">
|
||||||
|
<OutlineBody
|
||||||
|
groups={groups}
|
||||||
|
complete={complete}
|
||||||
|
count={count}
|
||||||
|
total={total}
|
||||||
|
active={active}
|
||||||
|
showTitle
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user