| 1 | import { type JSX, onCleanup, onMount } from "solid-js"; |
| 2 | |
| 3 | /** |
| 4 | * A `.segmented` group of `aria-pressed` buttons, or links marked `aria-current="page"`, whose highlight slides to |
| 5 | * the chosen one. A copy of the labels in the highlight's text color is clipped to the highlight as it moves, so |
| 6 | * each letter takes its color from whatever is under it at that moment. |
| 7 | */ |
| 8 | export function TabBar(props: { label: string; children: JSX.Element }) { |
| 9 | let root!: HTMLDivElement; |
| 10 | let pill!: HTMLSpanElement; |
| 11 | let ink!: HTMLDivElement; |
| 12 | const place = () => { |
| 13 | ink.replaceChildren(...[...root.children].filter((el) => el !== pill && el !== ink).map((el) => el.cloneNode(true))); |
| 14 | const chosen = root.querySelector<HTMLElement>(':scope > :is([aria-pressed="true"], [aria-current="page"])'); |
| 15 | pill.hidden = ink.hidden = !chosen; |
| 16 | if (!chosen) return; |
| 17 | pill.style.left = `${chosen.offsetLeft}px`; |
| 18 | pill.style.width = `${chosen.offsetWidth}px`; |
| 19 | const right = root.clientWidth - chosen.offsetLeft - chosen.offsetWidth; |
| 20 | ink.style.clipPath = `inset(2px ${right}px 2px ${chosen.offsetLeft}px round 7px)`; |
| 21 | }; |
| 22 | onMount(() => { |
| 23 | place(); |
| 24 | const changes = new MutationObserver((records) => records.some((record) => !ink.contains(record.target)) && place()); |
| 25 | changes.observe(root, { subtree: true, childList: true, characterData: true, attributeFilter: ["aria-pressed", "aria-current"] }); |
| 26 | const resizes = new ResizeObserver(place); |
| 27 | resizes.observe(root); |
| 28 | onCleanup(() => { |
| 29 | changes.disconnect(); |
| 30 | resizes.disconnect(); |
| 31 | }); |
| 32 | }); |
| 33 | return ( |
| 34 | <div ref={root} class="segmented" role="group" aria-label={props.label}> |
| 35 | <span ref={pill} class="pill" aria-hidden="true" /> |
| 36 | {props.children} |
| 37 | <div ref={ink} class="ink" aria-hidden="true" inert /> |
| 38 | </div> |
| 39 | ); |
| 40 | } |