<tab-group>
<div role="tablist">
<button
type="button"
role="tab"
id="trigger1"
aria-controls="panel1"
aria-selected="true"
tabindex="0"
>
Tab 1
</button>
<button
type="button"
role="tab"
id="trigger2"
aria-controls="panel2"
aria-selected="false"
tabindex="-1"
>
Tab 2
</button>
<button
type="button"
role="tab"
id="trigger3"
aria-controls="panel3"
aria-selected="false"
tabindex="-1"
>
Tab 3
</button>
</div>
<div role="tabpanel" id="panel1" aria-labelledby="trigger1">
Tab 1 content
</div>
<div role="tabpanel" id="panel2" aria-labelledby="trigger2" hidden>
Tab 2 content
</div>
<div role="tabpanel" id="panel3" aria-labelledby="trigger3" hidden>
Tab 3 content
</div>
</tab-group>
tab-group {
display: block;
margin-bottom: var(--space-l);
> [role="tablist"] {
display: flex;
border-bottom: 1px solid var(--color-gray-50);
padding: 0;
margin-bottom: 0;
& button {
border: 0;
border-top: 2px solid transparent;
border-bottom-width: 0;
font-family: var(--font-family-sans);
font-size: var(--font-size-s);
font-weight: var(--font-weight-bold);
padding: var(--space-s) var(--space-m);
color: var(--color-text-soft);
background-color: var(--color-secondary);
cursor: pointer;
transition: all var(--transition-short) var(--easing-inout);
&:hover,
&:focus {
color: var(--color-text);
background-color: var(--color-secondary-hover);
}
&:active {
color: var(--color-text);
background-color: var(--color-secondary-active);
}
&[aria-selected="true"] {
color: var(--color-primary-active);
border-top: 3px solid var(--color-primary);
background-color: var(--color-background);
margin-bottom: -1px;
}
}
}
> [role="tabpanel"] {
font-family: sans-serif;
font-size: var(--font-size-m);
background: var(--color-background);
margin-block: var(--space-l);
}
}
import {
type Component,
all,
component,
first,
on,
setProperty,
} from "../../..";
export type TabGroupProps = {
selected: string;
};
const manageArrowKeyFocus =
(elements: HTMLElement[], index: number) => (e: Event) => {
if (!(e instanceof KeyboardEvent))
throw new TypeError("Event is not a KeyboardEvent");
const handledKeys = [
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"ArrowDown",
"Home",
"End",
];
if (handledKeys.includes(e.key)) {
e.preventDefault();
switch (e.key) {
case "ArrowLeft":
case "ArrowUp":
index = index < 1 ? elements.length - 1 : index - 1;
break;
case "ArrowRight":
case "ArrowDown":
index = index >= elements.length - 1 ? 0 : index + 1;
break;
case "Home":
index = 0;
break;
case "End":
index = elements.length - 1;
break;
}
if (elements[index]) elements[index].focus();
}
};
export default component(
"tab-group",
{
selected: "",
},
(el) => {
el.selected =
el
.querySelector('[role="tab"][aria-selected="true"]')
?.getAttribute("aria-controls") ?? "";
const isSelected = (target: Element) =>
el.selected === target.getAttribute("aria-controls");
const tabs = Array.from(
el.querySelectorAll<HTMLButtonElement>('[role="tab"]'),
);
let focusIndex = 0;
return [
first(
'[role="tablist"]',
on("keydown", manageArrowKeyFocus(tabs, focusIndex)),
),
all<TabGroupProps, HTMLButtonElement>(
'[role="tab"]',
on("click", (e: Event) => {
el.selected =
(e.currentTarget as HTMLElement).getAttribute(
"aria-controls",
) ?? "";
focusIndex = tabs.findIndex((tab) => isSelected(tab));
}),
setProperty("ariaSelected", (target) =>
String(isSelected(target)),
),
setProperty("tabIndex", (target) =>
isSelected(target) ? 0 : -1,
),
),
all<TabGroupProps, HTMLElement>(
'[role="tabpanel"]',
setProperty("hidden", (target) => el.selected !== target.id),
),
];
},
);
declare global {
interface HTMLElementTagNameMap {
"tab-list": Component<TabGroupProps>;
}
}