UNPKG

@redocly/theme

Version:

Shared UI components lib

262 lines 11.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useActiveTab = void 0; exports.useTabs = useTabs; const react_1 = require("react"); const react_router_dom_1 = require("react-router-dom"); const MORE_BUTTON_WIDTH = 80; const TABS_GAP = 8; function useTabs({ activeTab, onTabChange, totalTabs, containerRef, }) { const [tabs, setTabs] = (0, react_1.useState)({ visible: Array.from({ length: totalTabs }, (_, i) => i), overflow: [], }); const [isReady, setIsReady] = (0, react_1.useState)(false); const isFirstCalculation = (0, react_1.useRef)(true); const tabRefs = (0, react_1.useRef)([]); const tabWidthsRef = (0, react_1.useRef)([]); const tabLabelsRef = (0, react_1.useRef)([]); const activeTabRef = (0, react_1.useRef)(activeTab); const calculateVisibleTabsRef = (0, react_1.useRef)(null); // Synchronously update ref before any callbacks or effects run activeTabRef.current = activeTab; const setTabRef = (0, react_1.useCallback)((element, index) => { tabRefs.current[index] = element; const width = element === null || element === void 0 ? void 0 : element.offsetWidth; if (width) { tabWidthsRef.current[index] = width; } const label = element === null || element === void 0 ? void 0 : element.getAttribute('data-label'); if (label) { tabLabelsRef.current[index] = label; } // Trigger calculation once all tabs are registered if (isFirstCalculation.current && tabWidthsRef.current.length >= totalTabs && tabLabelsRef.current.length >= totalTabs && calculateVisibleTabsRef.current) { requestAnimationFrame(calculateVisibleTabsRef.current); } }, [totalTabs]); const focusTab = (0, react_1.useCallback)((index) => { const currentElement = tabRefs.current[index]; currentElement === null || currentElement === void 0 ? void 0 : currentElement.focus(); }, []); const onTabSelect = (0, react_1.useCallback)((index) => { var _a; focusTab(index); const label = (_a = tabRefs.current[index]) === null || _a === void 0 ? void 0 : _a.getAttribute('data-label'); if (label) onTabChange(label); }, [onTabChange, focusTab]); const handleKeyboard = (0, react_1.useCallback)((event, index) => { let newIndex = index; if (event.key === 'ArrowRight') { newIndex = (index + 1) % totalTabs; } else if (event.key === 'ArrowLeft') { newIndex = (index - 1 + totalTabs) % totalTabs; } else if (event.key === 'Home') { event.preventDefault(); newIndex = 0; } else if (event.key === 'End') { event.preventDefault(); newIndex = totalTabs - 1; } else { return; } onTabSelect(newIndex); }, [totalTabs, onTabSelect]); const replaceLastVisibleTabWithClickedOverflowTab = (0, react_1.useCallback)((clickedIndex) => { setTabs((prevTabs) => { const { visible: visibleTabs, overflow: overflowTabs } = prevTabs; const sortedVisible = [...visibleTabs].sort((a, b) => a - b); const lastVisible = sortedVisible[sortedVisible.length - 1]; return { visible: visibleTabs.map((idx) => (idx === lastVisible ? clickedIndex : idx)), overflow: overflowTabs.map((idx) => (idx === clickedIndex ? lastVisible : idx)), }; }); }, []); const onTabClick = (0, react_1.useCallback)((labelOrIndex) => { const clickedIndex = typeof labelOrIndex === 'string' ? tabRefs.current.findIndex((ref) => (ref === null || ref === void 0 ? void 0 : ref.getAttribute('data-label')) === labelOrIndex) : labelOrIndex; if (clickedIndex === -1) return; const label = tabLabelsRef.current[clickedIndex]; if (!label) return; // If this is an overflow tab, replace it with a visible one if (tabs.overflow.includes(clickedIndex)) { replaceLastVisibleTabWithClickedOverflowTab(clickedIndex); } onTabChange(label); focusTab(clickedIndex); }, [tabs.overflow, onTabChange, replaceLastVisibleTabWithClickedOverflowTab, focusTab]); const calculateVisibleTabs = (0, react_1.useCallback)(() => { const container = containerRef === null || containerRef === void 0 ? void 0 : containerRef.current; if (!container) return; const containerWidth = container.offsetWidth; const tabWidths = tabWidthsRef.current; const tabLabels = tabLabelsRef.current; // Wait until all tabs are registered before calculating if (tabWidths.length < totalTabs || tabLabels.length < totalTabs) { return; } // Check if container has proper width (not zero) if (containerWidth === 0) { return; } // Find active tab index by label in tabLabelsRef, not by DOM element // because tab might not be rendered if it's in overflow const activeTabIndex = tabLabels.findIndex((label) => label === activeTabRef.current); let tabsWidth = activeTabIndex !== -1 ? tabWidths[activeTabIndex] : 0; const visibleTabs = activeTabIndex !== -1 ? [activeTabIndex] : []; const overflowTabs = []; for (let i = 0; i < tabWidths.length; i++) { if (i === activeTabIndex) continue; const tabWidthWithGap = tabWidths[i] + TABS_GAP; const projectedWidth = tabsWidth + tabWidthWithGap; if (projectedWidth <= containerWidth) { visibleTabs.push(i); tabsWidth += tabWidthWithGap; } else { overflowTabs.push(i); } } if (overflowTabs.length > 0) { tabsWidth += MORE_BUTTON_WIDTH; while (tabsWidth > containerWidth && visibleTabs.length > 1) { const removed = visibleTabs.pop(); // Never remove the active tab - it should always stay visible or be the last one if (removed !== undefined && removed !== activeTabIndex) { overflowTabs.unshift(removed); tabsWidth -= tabWidths[removed]; } else if (removed === activeTabIndex) { // Put it back if we accidentally removed the active tab visibleTabs.push(removed); break; } } // If even with only the active tab visible, it doesn't fit with More button, // move all tabs to overflow (show only dropdown) if (tabsWidth > containerWidth && visibleTabs.length === 1) { overflowTabs.unshift(...visibleTabs); visibleTabs.length = 0; } } setTabs({ visible: visibleTabs, overflow: overflowTabs, }); // Set ready state on first calculation if (isFirstCalculation.current) { isFirstCalculation.current = false; setIsReady(true); } }, [containerRef, totalTabs]); // Store calculateVisibleTabs in ref for use in setTabRef calculateVisibleTabsRef.current = calculateVisibleTabs; // Reset isFirstCalculation when totalTabs changes (new page/tabs) (0, react_1.useEffect)(() => { isFirstCalculation.current = true; setIsReady(false); // Clear refs so we wait for new tabs to register tabWidthsRef.current = []; tabLabelsRef.current = []; }, [totalTabs]); // Call calculateVisibleTabs on first render and resize (0, react_1.useEffect)(() => { const container = containerRef === null || containerRef === void 0 ? void 0 : containerRef.current; if (!container) return; let resizeTimeout = null; // Use ResizeObserver to wait until container has proper size const resizeObserver = new ResizeObserver(() => { if (resizeTimeout) cancelAnimationFrame(resizeTimeout); resizeTimeout = requestAnimationFrame(calculateVisibleTabs); }); resizeObserver.observe(container); const handleResize = () => { if (resizeTimeout) cancelAnimationFrame(resizeTimeout); resizeTimeout = requestAnimationFrame(calculateVisibleTabs); }; window.addEventListener('resize', handleResize); return () => { resizeObserver.disconnect(); window.removeEventListener('resize', handleResize); if (resizeTimeout) cancelAnimationFrame(resizeTimeout); }; }, [containerRef, totalTabs, calculateVisibleTabs]); // Recalculate when activeTab changes to ensure it's visible (0, react_1.useEffect)(() => { if (!(containerRef === null || containerRef === void 0 ? void 0 : containerRef.current) || isFirstCalculation.current) return; requestAnimationFrame(calculateVisibleTabs); }, [activeTab, containerRef, calculateVisibleTabs]); return { setTabRef, onTabClick, handleKeyboard, visibleTabs: tabs.visible, overflowTabs: tabs.overflow, isReady, }; } const useActiveTab = ({ initialTab, tabsId }) => { const location = (0, react_router_dom_1.useLocation)(); const navigate = (0, react_router_dom_1.useNavigate)(); const [activeTab, setActiveTab] = (0, react_1.useState)(undefined); const resolvedActiveTab = activeTab !== null && activeTab !== void 0 ? activeTab : initialTab; const prevActiveTabRef = (0, react_1.useRef)(resolvedActiveTab); (0, react_1.useEffect)(() => { if (activeTab !== undefined) { return; } setActiveTab(getInitialTab({ initialTab, hash: location.hash, tabsId })); }, [activeTab, initialTab, location.hash, tabsId]); (0, react_1.useEffect)(() => { const hasActiveTabChanged = prevActiveTabRef.current !== resolvedActiveTab; if (!tabsId || !hasActiveTabChanged) { return; } prevActiveTabRef.current = resolvedActiveTab; const nextHashParams = new URLSearchParams(location.hash.startsWith('#') ? location.hash.slice(1) : location.hash); nextHashParams.set(tabsId, resolvedActiveTab); const nextHash = `#${nextHashParams.toString()}`; if (nextHash === location.hash) { return; } navigate({ pathname: location.pathname, search: location.search, hash: nextHash, }, { replace: true }); }, [resolvedActiveTab, navigate, location.pathname, location.search, location.hash, tabsId]); return (0, react_1.useMemo)(() => ({ activeTab: resolvedActiveTab, setActiveTab, }), [resolvedActiveTab]); }; exports.useActiveTab = useActiveTab; const getInitialTab = ({ initialTab, hash, tabsId }) => { const hashParams = new URLSearchParams(hash.startsWith('#') ? hash.slice(1) : hash); let resultTab = initialTab; if (tabsId) { const tabFromUrl = hashParams.get(tabsId); resultTab = tabFromUrl ? tabFromUrl : resultTab; } return resultTab; }; //# sourceMappingURL=use-tabs.js.map