e65111a7f4
Permisos (superadmin, viewer, busqueda, tareas recurrentes), bootstrap atomico, revocacion de JWT por version de sesion (028), control optimista de edicion (029), logo sin SVG + CSP, ingesta endurecida, correos escapados, borradores, paginacion de proyectos, accesibilidad de dialogos/controles e i18n. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
50 lines
1.9 KiB
React
50 lines
1.9 KiB
React
import { DraftMark } from './NavigationGuard.jsx';
|
|
import { useEffect, useId, useRef } from 'react';
|
|
|
|
export function Tabs({ tabs, active, onChange }) {
|
|
const id = useId();
|
|
const listRef = useRef(null);
|
|
const tabRefs = useRef({});
|
|
useEffect(() => {
|
|
const panel = listRef.current?.nextElementSibling;
|
|
if (!panel) return;
|
|
panel.id = `${id}-panel`;
|
|
panel.setAttribute('role', 'tabpanel');
|
|
panel.setAttribute('aria-labelledby', `${id}-${active}`);
|
|
}, [active, id]);
|
|
function onKeyDown(event, index) {
|
|
const delta = event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0;
|
|
const next = event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : delta ? (index + delta + tabs.length) % tabs.length : null;
|
|
if (next === null) return;
|
|
event.preventDefault();
|
|
onChange(tabs[next].key);
|
|
tabRefs.current[tabs[next].key]?.focus();
|
|
}
|
|
return (
|
|
<div className="tabs" role="tablist" ref={listRef}>
|
|
{tabs.map((tab, index) => (
|
|
<button
|
|
key={tab.key}
|
|
type="button"
|
|
id={`${id}-${tab.key}`}
|
|
ref={(node) => { tabRefs.current[tab.key] = node; }}
|
|
role="tab"
|
|
aria-selected={active === tab.key}
|
|
aria-controls={`${id}-panel`}
|
|
tabIndex={active === tab.key ? 0 : -1}
|
|
onKeyDown={(event) => onKeyDown(event, index)}
|
|
className={`tab-item ${active === tab.key ? 'active' : ''} ${tab.dirty ? 'tab-dirty' : ''}`}
|
|
onClick={() => onChange(tab.key)}
|
|
>
|
|
{tab.label}
|
|
{/* count=0 SI se muestra (badge "0") -- solo se omite si la pestaña ni siquiera manda
|
|
la prop (undefined), para pestañas que no tienen numero que mostrar (Info/Gantt). */}
|
|
{tab.count !== undefined && <span className="tab-badge">{tab.count}</span>}
|
|
{/* Pestaña con cambios sin guardar (borrador pendiente). */}
|
|
{tab.dirty && <DraftMark />}
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|