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