/** * The tab switcher (milestone 23, ruling 4). * * The hand-rolled version spelled the ARIA attributes by hand but had no * keyboard navigation; React Aria brings arrow-key movement and roving * tabindex with the same roles. State stays inside RAC — no caller needs to * read which tab is open. * * RAC exposes state as render-prop booleans, so every rule below is a * boolean-guarded style object: StyleX cannot express `[data-selected]`. */ import type { ReactNode } from "react"; import * as stylex from "@stylexjs/stylex"; import { Tab, TabList, TabPanel, Tabs as AriaTabs } from "react-aria-components"; import { colors } from "./tokens.stylex"; export interface TabSpec { id: string; label: string; content: ReactNode; } interface Props { /** The tab list's accessible name. */ label: string; tabs: readonly TabSpec[]; } const styles = stylex.create({ list: { display: "flex", gap: "0.5rem", marginTop: "1rem", borderBottomWidth: 1, borderBottomStyle: "solid", borderBottomColor: colors.border, }, tab: { marginBottom: -1, borderBottomWidth: 2, borderBottomStyle: "solid", borderBottomColor: "transparent", paddingInline: "0.75rem", paddingBlock: "0.5rem", fontWeight: 500, color: colors.textMuted, cursor: "pointer", }, tabSelected: { borderBottomColor: colors.primary, color: colors.primaryOnSurface, }, tabHovered: { color: colors.text, }, /** * The milestone-9 focus floor. A Tab is a `div` with a roving tabindex, so * the ring is driven by RAC's `isFocusVisible` rather than `:focus-visible`. */ tabFocusVisible: { outlineWidth: 2, outlineStyle: "solid", outlineColor: colors.focus, outlineOffset: 2, }, panel: { outlineStyle: "none", }, /** Explicit, so RAC's default `react-aria-Tabs` class does not land instead. */ root: { display: "block", }, }); export default function Tabs({ label, tabs }: Props) { return ( stylex.props(styles.root).className ?? ""}> stylex.props(styles.list).className ?? ""}> {tabs.map((tab) => ( stylex.props( styles.tab, isHovered && !isSelected && styles.tabHovered, isSelected && styles.tabSelected, isFocusVisible && styles.tabFocusVisible, ).className ?? "" } > {tab.label} ))} {tabs.map((tab) => ( stylex.props(styles.panel).className ?? ""}> {tab.content} ))} ); }