49 lines
1.6 KiB
JavaScript
49 lines
1.6 KiB
JavaScript
/* ============================================================
|
|
Tabs.jsx — new primitive.
|
|
|
|
js/ui.js had no tab component, so settings (10 tabs), candidates (8), inbox
|
|
(7) and rbac each hand-rolled one by pre-rendering every pane and toggling
|
|
`.active`. One component replaces four ad-hoc implementations, and only the
|
|
active pane is mounted — which also means a chart in a hidden pane no longer
|
|
draws into a zero-width canvas.
|
|
============================================================ */
|
|
|
|
import { useId, useState } from 'react'
|
|
|
|
export function Tabs({ tabs, value, onChange, className = 'tabs' }) {
|
|
const id = useId()
|
|
return (
|
|
<div className={className} role="tablist">
|
|
{tabs.map((t) => {
|
|
const key = t.key ?? t
|
|
const label = t.label ?? t
|
|
const active = key === value
|
|
return (
|
|
<button
|
|
key={key}
|
|
id={`${id}-${key}`}
|
|
role="tab"
|
|
aria-selected={active}
|
|
className={`tab${active ? ' active' : ''}`}
|
|
onClick={() => onChange(key)}
|
|
>
|
|
{label}{t.count != null && <span className="tab-count">{t.count}</span>}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/** Uncontrolled convenience wrapper: <TabPanel tabs={[{key,label,render}]} /> */
|
|
export default function TabPanel({ tabs, initial, className }) {
|
|
const [value, setValue] = useState(initial ?? tabs[0]?.key)
|
|
const active = tabs.find((t) => t.key === value) ?? tabs[0]
|
|
return (
|
|
<>
|
|
<Tabs tabs={tabs} value={value} onChange={setValue} className={className} />
|
|
<div role="tabpanel">{active?.render?.()}</div>
|
|
</>
|
|
)
|
|
}
|