38 lines
1.8 KiB
TypeScript
38 lines
1.8 KiB
TypeScript
import { useId, useState, type ReactNode, type KeyboardEvent } from 'react';
|
|
|
|
export interface ClientInfoTab {
|
|
id: string;
|
|
label: string;
|
|
content?: ReactNode;
|
|
}
|
|
|
|
/** Shared attached-panel presentation for every Client Info tab. */
|
|
export function ClientInfoTabs({ tabs }: { tabs: readonly ClientInfoTab[] }) {
|
|
const prefix = useId();
|
|
const [selected, setSelected] = useState(tabs[0]?.id);
|
|
const active = tabs.some(tab => tab.id === selected) ? selected : tabs[0]?.id;
|
|
const navigate = (event: KeyboardEvent<HTMLButtonElement>, index: number) => {
|
|
let next: number;
|
|
if (event.key === 'ArrowRight') next = (index + 1) % tabs.length;
|
|
else if (event.key === 'ArrowLeft') next = (index + tabs.length - 1) % tabs.length;
|
|
else if (event.key === 'Home') next = 0;
|
|
else if (event.key === 'End') next = tabs.length - 1;
|
|
else return;
|
|
event.preventDefault();
|
|
setSelected(tabs[next].id);
|
|
document.getElementById(`${prefix}-tab-${tabs[next].id}`)?.focus();
|
|
};
|
|
return <div className="client-info-tabs">
|
|
<div className="client-info-tablist" role="tablist" aria-label="Client Info">
|
|
{tabs.map((tab, index) => <button key={tab.id} type="button" role="tab"
|
|
className="client-info-tab" id={`${prefix}-tab-${tab.id}`}
|
|
aria-controls={`${prefix}-panel-${tab.id}`} aria-selected={tab.id === active}
|
|
tabIndex={tab.id === active ? 0 : -1} onClick={() => setSelected(tab.id)}
|
|
onKeyDown={event => navigate(event, index)}>{tab.label}</button>)}
|
|
</div>
|
|
{tabs.map(tab => <section key={tab.id} className="client-info-tabpanel" role="tabpanel"
|
|
id={`${prefix}-panel-${tab.id}`} aria-labelledby={`${prefix}-tab-${tab.id}`}
|
|
hidden={tab.id !== active} data-group-scroll={`client-info-${tab.id}`} tabIndex={0}>{tab.content}</section>)}
|
|
</div>;
|
|
}
|