Files
carmanagement/homepage/src/components/controls/Tabs.tsx
T
root d7fb7b7a7b
Build & Deploy / Build & Push Docker Image (push) Failing after 47s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / API Unit Tests (push) Failing after 5m4s
Test / Marketplace Unit Tests (push) Failing after 4m55s
Test / Admin Unit Tests (push) Successful in 9m37s
Test / Dashboard Unit Tests (push) Successful in 9m37s
Test / API Integration Tests (push) Successful in 9m54s
redesign the homepage
2026-06-26 16:27:21 -04:00

117 lines
3.6 KiB
TypeScript

'use client';
import { classNames } from '@/lib/components/classNames';
import { useId, useRef, useState, type KeyboardEvent, type ReactNode } from 'react';
import styles from './Tabs.module.css';
export interface TabItem {
id: string;
label: string;
content: ReactNode;
disabled?: boolean;
}
interface TabsProps {
items: TabItem[];
label: string;
defaultActiveId?: string;
activation?: 'automatic' | 'manual';
className?: string;
}
export function Tabs({
items,
label,
defaultActiveId,
activation = 'automatic',
className,
}: TabsProps) {
const available = items.filter((item) => !item.disabled);
const initial =
defaultActiveId && available.some((item) => item.id === defaultActiveId)
? defaultActiveId
: (available[0]?.id ?? '');
const [activeId, setActiveId] = useState(initial);
const [focusedId, setFocusedId] = useState(initial);
const refs = useRef(new Map<string, HTMLButtonElement>());
const prefix = useId().replaceAll(':', '');
const focusByOffset = (currentId: string, offset: number) => {
const currentIndex = available.findIndex((item) => item.id === currentId);
if (currentIndex < 0 || available.length === 0) return;
const next = available[(currentIndex + offset + available.length) % available.length];
if (!next) return;
setFocusedId(next.id);
if (activation === 'automatic') setActiveId(next.id);
refs.current.get(next.id)?.focus();
};
const handleKeyDown = (event: KeyboardEvent<HTMLButtonElement>, id: string) => {
if (event.key === 'ArrowRight') {
event.preventDefault();
focusByOffset(id, document.documentElement.dir === 'rtl' ? -1 : 1);
} else if (event.key === 'ArrowLeft') {
event.preventDefault();
focusByOffset(id, document.documentElement.dir === 'rtl' ? 1 : -1);
} else if (event.key === 'Home') {
event.preventDefault();
const first = available[0];
if (first) focusByOffset(first.id, 0);
} else if (event.key === 'End') {
event.preventDefault();
const last = available.at(-1);
if (last) focusByOffset(last.id, 0);
} else if ((event.key === 'Enter' || event.key === ' ') && activation === 'manual') {
event.preventDefault();
setActiveId(id);
}
};
return (
<div className={classNames(styles.tabs, className)}>
<div className={styles.list} role="tablist" aria-label={label}>
{items.map((item) => {
const selected = item.id === activeId;
return (
<button
key={item.id}
ref={(node) => {
if (node) refs.current.set(item.id, node);
else refs.current.delete(item.id);
}}
type="button"
role="tab"
id={`${prefix}-tab-${item.id}`}
aria-controls={`${prefix}-panel-${item.id}`}
aria-selected={selected}
tabIndex={item.id === focusedId ? 0 : -1}
disabled={item.disabled}
className={styles.tab}
onClick={() => {
setFocusedId(item.id);
setActiveId(item.id);
}}
onKeyDown={(event) => handleKeyDown(event, item.id)}
>
{item.label}
</button>
);
})}
</div>
{items.map((item) => (
<div
key={item.id}
role="tabpanel"
id={`${prefix}-panel-${item.id}`}
aria-labelledby={`${prefix}-tab-${item.id}`}
hidden={item.id !== activeId}
tabIndex={0}
className={styles.panel}
>
{item.content}
</div>
))}
</div>
);
}