master @ 62 LINES
[ HISTORY ] [ UP ]
┌─ ASTRO ────────────────────────────────────────────────────────────────────┐
│ --- │
│ // Tab bar over a full-width rule. The bar and panels render once (both │
│ // are width-independent; panel content dual-renders itself), so active │
│ // state lives in one place. Panels fill via named slots: tab-0, tab-1... │
│ import Pre from "../Pre/Pre.astro"; │
│ import Frame from "../Frame/Frame.astro"; │
│ import { rule } from "../../lib/ascii"; │
│ import type { Props } from "./Tabs"; │
│ import "./Tabs.scss"; │
│ │
│ const { labels } = Astro.props as Props; │
│ // slot[name] must be static, so dynamic panel slots render here. │
│ const panels = await Promise.all(labels.map((_, i) => Astro.slots.render(`tab-${i} │
│ `))); │
│ --- │
│ <div class="tabs"> │
│ <Pre class="tabs-bar"><span role="tablist">{labels.map((l, i) => ( │
│ <Fragment>{i > 0 ? " " : ""}<button type="button" role="tab" class="tab-btn" │
│ aria-selected={i === 0 ? "true" : "false"}>[ {l} ]</button></Fragment> │
│ ))}</span></Pre> │
│ <Frame build={(w) => rule(w)} /> │
│ {panels.map((html, i) => ( │
│ <div class="tabs-panel" role="tabpanel" hidden={i !== 0} set:html={html} /> │
│ ))} │
│ </div> │
│ │
│ <script> │
│ function activate(tabs: HTMLElement, idx: number) { │
│ const btns = Array.from(tabs.querySelectorAll<HTMLButtonElement>(":scope > .ta │
│ bs-bar .tab-btn")); │
│ const panels = Array.from(tabs.querySelectorAll<HTMLElement>(":scope > .tabs-p │
│ anel")); │
│ btns.forEach((b, i) => b.setAttribute("aria-selected", String(i === idx))); │
│ panels.forEach((p, i) => { p.hidden = i !== idx; }); │
│ } │
│ │
│ function ownIndex(btn: HTMLButtonElement): { tabs: HTMLElement; idx: number } | │
│ null { │
│ const tabs = btn.closest<HTMLElement>(".tabs"); │
│ if (!tabs) return null; │
│ const btns = Array.from(tabs.querySelectorAll(":scope > .tabs-bar .tab-btn")); │
│ return { tabs, idx: btns.indexOf(btn) }; │
│ } │
│ │
│ document.addEventListener("click", (e) => { │
│ const btn = (e.target as HTMLElement).closest<HTMLButtonElement>(".tab-btn"); │
│ if (!btn) return; │
│ const own = ownIndex(btn); │
│ if (own) activate(own.tabs, own.idx); │
│ }); │
│ │
│ document.addEventListener("keydown", (e) => { │
│ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return; │
│ const btn = (e.target as HTMLElement).closest?.<HTMLButtonElement>(".tab-btn") │
│ ; │
│ if (!btn) return; │
│ const own = ownIndex(btn); │
│ if (!own) return; │
│ const btns = Array.from(own.tabs.querySelectorAll<HTMLButtonElement>(":scope > │
│ .tabs-bar .tab-btn")); │
│ const next = e.key === "ArrowRight" │
│ ? (own.idx + 1) % btns.length │
│ : (own.idx - 1 + btns.length) % btns.length; │
│ e.preventDefault(); │
│ btns[next].focus(); │
│ activate(own.tabs, next); │
│ }); │
│ </script> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ASTRO ──────────────────────────────┐
│ --- │
│ // Tab bar over a full-width rule. The bar a │
│ nd panels render once (both │
│ // are width-independent; panel content dual │
│ -renders itself), so active │
│ // state lives in one place. Panels fill via │
│ named slots: tab-0, tab-1... │
│ import Pre from "../Pre/Pre.astro"; │
│ import Frame from "../Frame/Frame.astro"; │
│ import { rule } from "../../lib/ascii"; │
│ import type { Props } from "./Tabs"; │
│ import "./Tabs.scss"; │
│ │
│ const { labels } = Astro.props as Props; │
│ // slot[name] must be static, so dynamic pan │
│ el slots render here. │
│ const panels = await Promise.all(labels.map( │
│ (_, i) => Astro.slots.render(`tab-${i}`))); │
│ --- │
│ <div class="tabs"> │
│ <Pre class="tabs-bar"><span role="tablist" │
│ >{labels.map((l, i) => ( │
│ <Fragment>{i > 0 ? " " : ""}<button typ │
│ e="button" role="tab" class="tab-btn" aria-s │
│ elected={i === 0 ? "true" : "false"}>[ {l} ] │
│ </button></Fragment> │
│ ))}</span></Pre> │
│ <Frame build={(w) => rule(w)} /> │
│ {panels.map((html, i) => ( │
│ <div class="tabs-panel" role="tabpanel" │
│ hidden={i !== 0} set:html={html} /> │
│ ))} │
│ </div> │
│ │
│ <script> │
│ function activate(tabs: HTMLElement, idx: │
│ number) { │
│ const btns = Array.from(tabs.querySelect │
│ orAll<HTMLButtonElement>(":scope > .tabs-bar │
│ .tab-btn")); │
│ const panels = Array.from(tabs.querySele │
│ ctorAll<HTMLElement>(":scope > .tabs-panel") │
│ ); │
│ btns.forEach((b, i) => b.setAttribute("a │
│ ria-selected", String(i === idx))); │
│ panels.forEach((p, i) => { p.hidden = i │
│ !== idx; }); │
│ } │
│ │
│ function ownIndex(btn: HTMLButtonElement): │
│ { tabs: HTMLElement; idx: number } | null { │
│ const tabs = btn.closest<HTMLElement>(". │
│ tabs"); │
│ if (!tabs) return null; │
│ const btns = Array.from(tabs.querySelect │
│ orAll(":scope > .tabs-bar .tab-btn")); │
│ return { tabs, idx: btns.indexOf(btn) }; │
│ } │
│ │
│ document.addEventListener("click", (e) => │
│ { │
│ const btn = (e.target as HTMLElement).cl │
│ osest<HTMLButtonElement>(".tab-btn"); │
│ if (!btn) return; │
│ const own = ownIndex(btn); │
│ if (own) activate(own.tabs, own.idx); │
│ }); │
│ │
│ document.addEventListener("keydown", (e) = │
│ > { │
│ if (e.key !== "ArrowLeft" && e.key !== " │
│ ArrowRight") return; │
│ const btn = (e.target as HTMLElement).cl │
│ osest?.<HTMLButtonElement>(".tab-btn"); │
│ if (!btn) return; │
│ const own = ownIndex(btn); │
│ if (!own) return; │
│ const btns = Array.from(own.tabs.querySe │
│ lectorAll<HTMLButtonElement>(":scope > .tabs │
│ -bar .tab-btn")); │
│ const next = e.key === "ArrowRight" │
│ ? (own.idx + 1) % btns.length │
│ : (own.idx - 1 + btns.length) % btns.l │
│ ength; │
│ e.preventDefault(); │
│ btns[next].focus(); │
│ activate(own.tabs, next); │
│ }); │
│ </script> │
└──────────────────────────────────────────────┘
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET