@redocly/theme
Version:
Shared UI components lib
281 lines • 11.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.useTabs = useTabs;
const react_1 = require("react");
function useTabs({ initialTab, totalTabs, containerRef }) {
const [activeTab, setActiveTab] = (0, react_1.useState)(initialTab);
const [visibleTabs, setVisibleTabs] = (0, react_1.useState)(Array.from({ length: totalTabs }, (_, i) => i));
const [overflowTabs, setOverflowTabs] = (0, react_1.useState)([]);
const [allTabsHidden, setAllTabsHidden] = (0, react_1.useState)(false);
const tabRefs = (0, react_1.useRef)([]);
const tabLabelsRef = (0, react_1.useRef)([]);
const resizeTimeoutRef = (0, react_1.useRef)(undefined);
const [ready, setReady] = (0, react_1.useState)(false);
const hasCalculatedOnce = (0, react_1.useRef)(false);
const lastWidthRef = (0, react_1.useRef)(0);
const originalOrderRef = (0, react_1.useRef)([]);
(0, react_1.useEffect)(() => {
originalOrderRef.current = Array.from({ length: totalTabs }, (_, i) => i);
}, [totalTabs]);
const setTabRef = (0, react_1.useCallback)((element, index) => {
tabRefs.current[index] = element;
if (element) {
const label = element.getAttribute('data-label');
if (label) {
tabLabelsRef.current[index] = label;
}
}
}, []);
const getTabId = (0, react_1.useCallback)((label, index) => {
const cleanLabel = label.replace(/\s+/g, '-').toLowerCase();
return `${cleanLabel}-${index}`;
}, []);
const focusTab = (index) => {
const currentElement = tabRefs.current[index];
if (currentElement) {
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)
setActiveTab(label);
}, []);
const onTabClick = (0, react_1.useCallback)((labelOrIndex) => {
var _a;
let clickedIndex;
if (typeof labelOrIndex === 'string') {
clickedIndex = tabRefs.current.findIndex((ref) => (ref === null || ref === void 0 ? void 0 : ref.getAttribute('data-label')) === labelOrIndex);
if (clickedIndex === -1)
return;
}
else {
clickedIndex = labelOrIndex;
}
if (allTabsHidden) {
const label = tabLabelsRef.current[clickedIndex];
if (label) {
setActiveTab(label);
focusTab(clickedIndex);
}
return;
}
if (overflowTabs.includes(clickedIndex)) {
const newVisibleTabs = [...visibleTabs];
const newOverflowTabs = [...overflowTabs];
const clickedIdxInOverflow = newOverflowTabs.indexOf(clickedIndex);
if (clickedIdxInOverflow !== -1) {
newOverflowTabs.splice(clickedIdxInOverflow, 1);
}
const lastVisible = newVisibleTabs.pop();
if (lastVisible !== undefined) {
newOverflowTabs.unshift(lastVisible);
}
newVisibleTabs.push(clickedIndex);
setVisibleTabs(newVisibleTabs);
setOverflowTabs(newOverflowTabs);
requestAnimationFrame(() => {
var _a;
const label = (_a = tabRefs.current[clickedIndex]) === null || _a === void 0 ? void 0 : _a.getAttribute('data-label');
if (label) {
setActiveTab(label);
focusTab(clickedIndex);
}
});
}
else {
const label = (_a = tabRefs.current[clickedIndex]) === null || _a === void 0 ? void 0 : _a.getAttribute('data-label');
if (label) {
setActiveTab(label);
focusTab(clickedIndex);
}
}
}, [visibleTabs, overflowTabs, allTabsHidden]);
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 calculateVisibleTabs = (0, react_1.useCallback)(() => {
const container = containerRef === null || containerRef === void 0 ? void 0 : containerRef.current;
if (!container)
return;
const contentWrapper = container.closest('div');
if (!contentWrapper) {
setVisibleTabs(Array.from({ length: totalTabs }, (_, i) => i));
setOverflowTabs([]);
setAllTabsHidden(false);
return;
}
const containerWidth = container.offsetWidth - 60;
const tabElements = container.querySelectorAll('[role="tab"]');
const moreButtonWidth = 80;
const safetyMargin = 20;
const tabWidths = Array.from(tabElements).map((el) => el.offsetWidth);
const tabLabels = Array.from(tabElements).map((el) => el.getAttribute('data-label') || '');
const tabTypes = Array.from(tabElements).map((el) => el.getAttribute('data-type') || '');
const hasLongLabels = tabLabels.some((label) => label.length > 30);
const minVisibleTabs = hasLongLabels ? 1 : 2;
const activeTabIndex = tabRefs.current.findIndex((ref) => (ref === null || ref === void 0 ? void 0 : ref.getAttribute('data-label')) === activeTab);
let currentWidth = 0;
const visible = [];
const overflow = [];
let minTabsWidth = 0;
Array.from({ length: minVisibleTabs }).forEach((_, i) => {
if (i < tabWidths.length) {
minTabsWidth += tabWidths[i] + (i > 0 ? moreButtonWidth + safetyMargin : 0);
}
});
if (minTabsWidth > containerWidth) {
setVisibleTabs([]);
setOverflowTabs(Array.from({ length: totalTabs }, (_, i) => i));
setAllTabsHidden(true);
return;
}
const tabsByType = new Map();
Array.from({ length: totalTabs }).forEach((_, i) => {
var _a;
const type = tabTypes[i] || 'default';
if (!tabsByType.has(type)) {
tabsByType.set(type, []);
}
(_a = tabsByType.get(type)) === null || _a === void 0 ? void 0 : _a.push(i);
});
tabsByType.forEach((tabIndices) => {
let typeCurrentWidth = currentWidth;
const typeVisible = [];
const typeOverflow = [];
tabIndices.slice(0, minVisibleTabs).forEach((tabIndex) => {
const tabWidth = tabWidths[tabIndex];
const projectedWidth = typeCurrentWidth +
tabWidth +
(typeVisible.length > 0 ? moreButtonWidth + safetyMargin : 0);
if (projectedWidth <= containerWidth) {
typeVisible.push(tabIndex);
typeCurrentWidth += tabWidth;
}
else {
typeOverflow.push(tabIndex);
}
});
tabIndices.slice(minVisibleTabs).forEach((tabIndex) => {
const tabWidth = tabWidths[tabIndex];
const projectedWidth = typeCurrentWidth + tabWidth + moreButtonWidth + safetyMargin;
if (projectedWidth <= containerWidth) {
typeVisible.push(tabIndex);
typeCurrentWidth += tabWidth;
}
else {
typeOverflow.push(tabIndex);
}
});
visible.push(...typeVisible);
overflow.push(...typeOverflow);
currentWidth = typeCurrentWidth;
});
if (activeTabIndex !== -1 && !visible.includes(activeTabIndex)) {
if (visible.length > 0) {
const removed = visible.pop();
if (removed !== undefined) {
overflow.unshift(removed);
}
}
visible.push(activeTabIndex);
const activeOverflowIndex = overflow.indexOf(activeTabIndex);
if (activeOverflowIndex !== -1)
overflow.splice(activeOverflowIndex, 1);
}
setVisibleTabs(visible);
setOverflowTabs(overflow);
setAllTabsHidden(visible.length === 0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [containerRef, totalTabs]);
(0, react_1.useEffect)(() => {
if (!(containerRef === null || containerRef === void 0 ? void 0 : containerRef.current))
return;
const ensureTabsReady = () => {
const allTabsReady = tabRefs.current.length === totalTabs && tabRefs.current.every((tab) => tab === null || tab === void 0 ? void 0 : tab.offsetWidth);
if (!allTabsReady) {
resizeTimeoutRef.current = requestAnimationFrame(ensureTabsReady);
return;
}
calculateVisibleTabs();
hasCalculatedOnce.current = true;
};
resizeTimeoutRef.current = requestAnimationFrame(ensureTabsReady);
let resizeTimeout;
const handleResize = () => {
if (!hasCalculatedOnce.current)
return;
if (resizeTimeout) {
cancelAnimationFrame(resizeTimeout);
}
resizeTimeout = requestAnimationFrame(() => {
if (resizeTimeoutRef.current) {
cancelAnimationFrame(resizeTimeoutRef.current);
}
resizeTimeoutRef.current = requestAnimationFrame(() => {
const container = containerRef === null || containerRef === void 0 ? void 0 : containerRef.current;
if (!container)
return;
const currentWidth = container.offsetWidth;
if (Math.abs(lastWidthRef.current - currentWidth) > 5) {
lastWidthRef.current = currentWidth;
calculateVisibleTabs();
}
});
});
};
const resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(containerRef.current);
window.addEventListener('resize', handleResize);
return () => {
resizeObserver.disconnect();
window.removeEventListener('resize', handleResize);
if (resizeTimeoutRef.current) {
cancelAnimationFrame(resizeTimeoutRef.current);
}
if (resizeTimeout) {
cancelAnimationFrame(resizeTimeout);
}
};
}, [containerRef, totalTabs, calculateVisibleTabs]);
(0, react_1.useEffect)(() => {
const raf = requestAnimationFrame(() => {
setReady(true);
calculateVisibleTabs();
});
return () => cancelAnimationFrame(raf);
}, [calculateVisibleTabs]);
return {
activeTab,
setActiveTab,
setTabRef,
onTabClick,
handleKeyboard,
getTabId,
visibleTabs,
overflowTabs,
ready,
allTabsHidden,
};
}
//# sourceMappingURL=use-tabs.js.map