OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
 
master @ 62 LINES
 
[ HISTORY ]  [ UP ]
 

---
// 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>

---
// 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