import React from 'react'; import classNames from 'classnames'; import { Tabs as CoreTabs } from '@shoptet/ui-core-web'; import { useTranslations } from '@shoptet/ui-core-web'; import { useOutsideElementClick } from '../../hooks/useOutsideElementClick'; import { useTabsOverflow } from '../../hooks/useTabsOverflow'; import { dictionary } from './dictionary'; import { TabPanel } from './TabPanel'; export type TabType = { id: T; title: string; UNSAFE_error?: boolean; content?: React.ReactNode; url?: string; adminOnly?: boolean; }; export interface TabsProps { /** * The orientation of the tab list. * Vertical tabs use Up/Down arrow keys for navigation and set aria-orientation="vertical". * Defaults to 'horizontal'. */ orientation?: 'horizontal' | 'vertical'; /** * Array of tabs to render. * Each tab should have a unique id and title. * Optionally, it can have content and a URL. * @param url The URL to navigate to when the tab is changed. * @param id The unique identifier for the tab. * @param content The content to display when the tab is active. * @param UNSAFE_error If true, indicates that the tab has an error state. * @param title The title of the tab. */ tabs: TabType[]; /** * The id of the currently active tab. */ activeTabId: T; /** * Callback triggered when tab changed. * @param tabId The id of the changed tab. * @param event The mouse or keyboard event that triggered the change. */ onTabChange: ( tabId: T, event: React.MouseEvent | React.KeyboardEvent ) => void; /** * When true, moving focus with arrow keys immediately activates the tab without requiring Enter/Space. * Defaults to false (manual activation). */ autoActivate?: boolean; /** * If true, all tabs content is rendered, otherwise only the active tab's content is rendered. * The non-active tabs are hidden with CSS though they are still in the DOM. */ mountAllTabs?: boolean; } export const Tabs = ({ tabs, activeTabId, onTabChange, mountAllTabs = false, orientation = 'horizontal', autoActivate = false, ...rest }: TabsProps) => { const translations = useTranslations(dictionary); const [showMore, setShowMore] = React.useState(false); const tabsRef = React.useRef(null); const tabsPanelRef = React.useRef(null); const moreItemsRef = React.useRef(null); useOutsideElementClick(moreItemsRef, () => setShowMore(false)); const overflowedTabs = useTabsOverflow(tabs, tabsRef, tabsPanelRef); const errorInMoreItems = React.useMemo(() => { return tabs.some(tab => tab.UNSAFE_error); }, [tabs]); const handleDropdownTabChange = ( tabId: T, event: React.MouseEvent | React.KeyboardEvent ) => { event.preventDefault(); onTabChange(tabId, event); }; return ( { event.preventDefault(); onTabChange(tabId as T, event); }} orientation={orientation} autoActivate={autoActivate} >
{tabs.map(tab => { const overflowed = overflowedTabs.includes(tab); return ( classNames({ active, 'ui-tabs-validation-error': tab.UNSAFE_error, 'v2tabs__tab--overflowed': overflowed, 'element-admin-only': tab.adminOnly, }) } aria-hidden={overflowed ? true : undefined} > {tab.title} ); })}
  • item.id === activeTabId), 'v2tabs__tab--overflowed': overflowedTabs.length === 0, })} aria-hidden={overflowedTabs.length === 0 ? true : undefined} > {showMore && ( )}
  • {tabs.some(tab => tab.content) && tabs.map(tab => { const isActiveTab = tab.id === activeTabId; return ( ); })}
    ); };