/** * Tabs - Horizontal tab navigation component. * * Renders a row of tab buttons and the content panel for * the active tab. Supports controlled and uncontrolled modes. * Keyboard accessible (arrow keys navigate between tabs). */ import { useState, useRef, useCallback } from "react"; import PropTypes from "prop-types"; function Tabs({ tabs, activeTab: controlledActive, onChange, className = "", }) { const [internalActive, setInternalActive] = useState( tabs.length > 0 ? tabs[0].id : null, ); const tabListRef = useRef(null); const isControlled = controlledActive !== undefined; const activeId = isControlled ? controlledActive : internalActive; const handleTabClick = useCallback( (tabId) => { if (!isControlled) { setInternalActive(tabId); } if (onChange) { onChange(tabId); } }, [isControlled, onChange], ); const handleKeyDown = useCallback( (event) => { const enabledTabs = tabs.filter((t) => !t.disabled); const currentIndex = enabledTabs.findIndex((t) => t.id === activeId); let nextIndex = currentIndex; if (event.key === "ArrowRight") { nextIndex = (currentIndex + 1) % enabledTabs.length; } else if (event.key === "ArrowLeft") { nextIndex = (currentIndex - 1 + enabledTabs.length) % enabledTabs.length; } else if (event.key === "Home") { nextIndex = 0; } else if (event.key === "End") { nextIndex = enabledTabs.length - 1; } else { return; } event.preventDefault(); const nextTab = enabledTabs[nextIndex]; handleTabClick(nextTab.id); // Focus the next tab button const buttons = tabListRef.current?.querySelectorAll('[role="tab"]'); const targetIndex = tabs.findIndex((t) => t.id === nextTab.id); buttons?.[targetIndex]?.focus(); }, [tabs, activeId, handleTabClick], ); const activeTabData = tabs.find((t) => t.id === activeId); return (
{/* Tab list */}
{tabs.map((tab) => { const isActive = tab.id === activeId; return ( ); })}
{/* Tab panel */} {activeTabData && (
{activeTabData.content}
)}
); } Tabs.propTypes = { tabs: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.string.isRequired, label: PropTypes.string.isRequired, content: PropTypes.node.isRequired, icon: PropTypes.node, badge: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), disabled: PropTypes.bool, }), ).isRequired, activeTab: PropTypes.string, onChange: PropTypes.func, className: PropTypes.string, }; export default Tabs;