| 1 | import { useEffect, useRef, useState } from "react"; |
| 2 | import { highlightTS } from "../marketing/highlightTS"; |
| 3 | |
| 4 | interface Panel { |
| 5 | filename: string; |
| 6 | code: string; |
| 7 | caption: string; |
| 8 | } |
| 9 | |
| 10 | const DWELL_MS = 5000; |
| 11 | |
| 12 | export default function AsyncTabs({ panels }: { panels: Panel[] }) { |
| 13 | const [active, setActive] = useState(0); |
| 14 | const wrapRef = useRef<HTMLDivElement>(null); |
| 15 | |
| 16 | useEffect(() => { |
| 17 | let cancelled = false; |
| 18 | let timer: ReturnType<typeof setTimeout> | null = null; |
| 19 | |
| 20 | const schedule = () => { |
| 21 | timer = setTimeout(() => { |
| 22 | if (cancelled) return; |
| 23 | setActive((i) => (i + 1) % panels.length); |
| 24 | }, DWELL_MS); |
| 25 | }; |
| 26 | |
| 27 | const obs = new IntersectionObserver( |
| 28 | (entries) => { |
| 29 | for (const e of entries) { |
| 30 | if (e.isIntersecting) { |
| 31 | schedule(); |
| 32 | } else if (timer) { |
| 33 | clearTimeout(timer); |
| 34 | timer = null; |
| 35 | } |
| 36 | } |
| 37 | }, |
| 38 | { threshold: 0.3 }, |
| 39 | ); |
| 40 | if (wrapRef.current) obs.observe(wrapRef.current); |
| 41 | |
| 42 | return () => { |
| 43 | cancelled = true; |
| 44 | if (timer) clearTimeout(timer); |
| 45 | obs.disconnect(); |
| 46 | }; |
| 47 | }, [active, panels.length]); |
| 48 | |
| 49 | const html = panels.map((p) => highlightTS(p.code)); |
| 50 | |
| 51 | return ( |
| 52 | <div ref={wrapRef} className="async-tabs" data-nosnippet=""> |
| 53 | <div className="async-tabs__chrome"> |
| 54 | <div className="async-tabs__header"> |
| 55 | <span |
| 56 | className="alc-code-block__dot" |
| 57 | style={{ background: "var(--alc-danger)" }} |
| 58 | /> |
| 59 | <span |
| 60 | className="alc-code-block__dot" |
| 61 | style={{ background: "var(--alc-warn)" }} |
| 62 | /> |
| 63 | <span |
| 64 | className="alc-code-block__dot" |
| 65 | style={{ background: "var(--alc-accent-bright)" }} |
| 66 | /> |
| 67 | <div className="async-tabs__tabs" role="tablist"> |
| 68 | {panels.map((p, i) => ( |
| 69 | <button |
| 70 | key={p.filename} |
| 71 | type="button" |
| 72 | role="tab" |
| 73 | aria-selected={active === i} |
| 74 | data-active={active === i} |
| 75 | className="async-tabs__tab" |
| 76 | onClick={() => setActive(i)} |
| 77 | > |
| 78 | {p.filename} |
| 79 | </button> |
| 80 | ))} |
| 81 | </div> |
| 82 | </div> |
| 83 | <div className="async-tabs__body"> |
| 84 | {panels.map((p, i) => ( |
| 85 | <div |
| 86 | key={p.filename} |
| 87 | className="async-tabs__panel" |
| 88 | hidden={active !== i} |
| 89 | > |
| 90 | <pre |
| 91 | className="alc-code-block__pre async-tabs__pre" |
| 92 | dangerouslySetInnerHTML={{ __html: html[i] }} |
| 93 | /> |
| 94 | </div> |
| 95 | ))} |
| 96 | </div> |
| 97 | </div> |
| 98 | <div key={active} className="alc-code-block__caption" aria-live="polite"> |
| 99 | {panels[active].caption} |
| 100 | </div> |
| 101 | </div> |
| 102 | ); |
| 103 | } |