1import { 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 */
8export 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}