@redocly/theme
Version:
Shared UI components lib
431 lines (412 loc) • 17.3 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.TabButtonLink = exports.TabItem = exports.TabListContainer = void 0;
exports.TabList = TabList;
const react_1 = __importStar(require("react"));
const styled_components_1 = __importStar(require("styled-components"));
const Tab_1 = require("../../../markdoc/components/Tabs/Tab");
const Dropdown_1 = require("../../../components/Dropdown/Dropdown");
const DropdownMenu_1 = require("../../../components/Dropdown/DropdownMenu");
const DropdownMenuItem_1 = require("../../../components/Dropdown/DropdownMenuItem");
const Button_1 = require("../../../components/Button/Button");
const hooks_1 = require("../../../core/hooks");
const utils_1 = require("../../../core/utils");
/**
* Calculates optimal dropdown position relative to viewport to ensure visibility.
* Positions below the button by default, but moves above if insufficient space.
* Adjusts horizontal position to prevent overflow off screen edges.
*/
const calculateDropdownPosition = (buttonRect, dropdownRect) => {
const gap = 4;
const margin = 16;
const spaceBelow = window.innerHeight - buttonRect.bottom;
const spaceAbove = buttonRect.top;
// Position below button, or above if dropdown doesn't fit below
const top = spaceBelow < dropdownRect.height + gap && spaceAbove > spaceBelow
? buttonRect.top - gap
: buttonRect.bottom + gap;
// Align with button left edge, adjust if overflows screen
const idealLeft = buttonRect.left;
const rightEdge = idealLeft + dropdownRect.width;
const overflowsRight = rightEdge > window.innerWidth - margin;
const left = overflowsRight
? window.innerWidth - dropdownRect.width - margin
: Math.max(margin, idealLeft);
return { top, left };
};
/**
* Manages dropdown positioning and updates on scroll/resize events for TabList.
*/
const useDropdownPosition = (hasOverflow, dropdownRef) => {
const [dropdownPosition, setDropdownPosition] = (0, react_1.useState)({});
const [isDropdownOpen, setIsDropdownOpen] = (0, react_1.useState)(false);
const updateDropdownPosition = (0, react_1.useCallback)(() => {
if (!dropdownRef.current)
return;
const button = dropdownRef.current.querySelector('button');
const dropdownMenu = dropdownRef.current.querySelector('div:last-child');
if (!button || !dropdownMenu)
return;
const buttonRect = button.getBoundingClientRect();
const dropdownRect = dropdownMenu.getBoundingClientRect();
const position = calculateDropdownPosition(buttonRect, dropdownRect);
setDropdownPosition(position);
}, [dropdownRef]);
// Track when dropdown menu appears and recalculate position
(0, react_1.useEffect)(() => {
if (!hasOverflow || !isDropdownOpen || !dropdownRef.current)
return;
const dropdownMenu = dropdownRef.current.querySelector('div:last-child');
if (!dropdownMenu)
return;
// ResizeObserver tracks both initial render and size changes
const resizeObserver = new ResizeObserver(() => {
updateDropdownPosition();
});
resizeObserver.observe(dropdownMenu);
return () => resizeObserver.disconnect();
}, [hasOverflow, isDropdownOpen, dropdownRef, updateDropdownPosition]);
// Update position on scroll/resize
(0, react_1.useEffect)(() => {
if (!hasOverflow || !isDropdownOpen)
return;
window.addEventListener('scroll', updateDropdownPosition, true);
window.addEventListener('resize', updateDropdownPosition);
return () => {
window.removeEventListener('scroll', updateDropdownPosition, true);
window.removeEventListener('resize', updateDropdownPosition);
};
}, [hasOverflow, isDropdownOpen, updateDropdownPosition]);
return {
dropdownPosition,
isDropdownOpen,
setIsDropdownOpen,
setDropdownPosition,
updateDropdownPosition,
};
};
const renderTab = (child, index, size, setTabRef, handleKeyboard, onTabClick) => {
const { label, icon } = child.props;
const tabId = (0, utils_1.getTabId)(label, index);
return (react_1.default.createElement(Tab_1.Tab, { key: `key-${tabId}`, tabId: tabId, label: label, icon: icon, size: size, disabled: child.props.disable, setRef: (el) => setTabRef(el, index), onKeyDown: (event) => handleKeyboard(event, index), onClick: () => {
var _a, _b;
(_b = (_a = child.props).onClick) === null || _b === void 0 ? void 0 : _b.call(_a);
onTabClick(label);
} }));
};
function TabList({ childrenArray, size, activeTab, onTabChange, containerRef, onReadyChange, }) {
const dropdownRef = (0, react_1.useRef)(null);
const totalTabs = childrenArray.length;
const { overflowTabs, visibleTabs, handleKeyboard, onTabClick, setTabRef, isReady } = (0, hooks_1.useTabs)({
activeTab,
onTabChange,
containerRef,
totalTabs,
});
(0, react_1.useEffect)(() => {
onReadyChange === null || onReadyChange === void 0 ? void 0 : onReadyChange(isReady);
}, [isReady, onReadyChange]);
const { highlightStyle } = useHighlightBarAnimation({
activeTab,
childrenArray,
overflowTabs,
tabsContainerRef: containerRef,
visibleTabs,
});
const hasOverflow = overflowTabs.length > 0;
const isMoreActive = hasOverflow &&
overflowTabs.some((i) => childrenArray[i] && activeTab === childrenArray[i].props.label);
// Show as selector when no visible tabs (all tabs in dropdown)
const showAsSelector = visibleTabs.length === 0 && hasOverflow;
const { dropdownPosition, setIsDropdownOpen, setDropdownPosition } = useDropdownPosition(hasOverflow, dropdownRef);
return (react_1.default.createElement(exports.TabListContainer, { role: "tablist", ref: containerRef },
react_1.default.createElement(HighlightBar, { size: size, style: highlightStyle },
react_1.default.createElement("div", null)),
childrenArray.map((child, index) => {
// Show all tabs before ready (for measurement), then only visible ones
const shouldRender = !isReady || visibleTabs.includes(index);
if (!shouldRender)
return null;
return renderTab(child, index, size, setTabRef, handleKeyboard, onTabClick);
}),
hasOverflow && (react_1.default.createElement(exports.TabItem, { size: size, active: isMoreActive || showAsSelector, tabIndex: 0, className: "dropdown-tab" },
react_1.default.createElement(DropdownWrapper, { "$top": dropdownPosition.top, "$left": dropdownPosition.left, onClickCapture: () => {
setIsDropdownOpen(true);
} },
react_1.default.createElement(FixedPositionDropdown, { ref: dropdownRef, trigger: react_1.default.createElement(exports.TabButtonLink, { size: size, className: isMoreActive || showAsSelector ? 'active' : undefined }, showAsSelector ? react_1.default.createElement(TabButtonText, null, activeTab) : 'More'), alignment: "start", withArrow: true, onClose: () => {
setIsDropdownOpen(false);
setDropdownPosition({});
} },
react_1.default.createElement(DropdownMenu_1.DropdownMenu, null, overflowTabs.map((index) => {
const child = childrenArray[index];
if (!child)
return null;
const { label } = child.props;
const tabId = (0, utils_1.getTabId)(label, index);
return (react_1.default.createElement(DropdownMenuItem_1.DropdownMenuItem, { key: `more-${tabId}`, active: activeTab === label, onAction: () => {
var _a, _b;
(_b = (_a = child.props).onClick) === null || _b === void 0 ? void 0 : _b.call(_a);
onTabClick(index);
}, disabled: child.props.disable }, label));
}))))))));
}
const useHighlightBarAnimation = (props) => {
const { childrenArray, activeTab, tabsContainerRef, visibleTabs, overflowTabs } = props;
const [highlightStyle, setHighlightStyle] = react_1.default.useState({
left: 0,
width: 0,
});
(0, react_1.useEffect)(() => {
const activeIndex = childrenArray.findIndex((child) => child.props.label === activeTab);
const container = tabsContainerRef.current;
if (!container || activeIndex === -1) {
setHighlightStyle({ left: 0, width: 0 });
return;
}
// Remove active class from all tabs first
container.querySelectorAll('[data-label]').forEach((el) => {
el.classList.remove('active');
});
// Check if active tab is in overflow first
if (overflowTabs.includes(activeIndex)) {
const moreButton = container.querySelector('button');
if (!moreButton)
return;
const moreButtonRect = moreButton.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
setHighlightStyle({
left: moreButtonRect.left - containerRect.left,
width: moreButtonRect.width,
});
return;
}
// Active tab is visible, find its element
const activeTabElement = container.querySelector(`[data-label="${activeTab}"]`);
if (!activeTabElement)
return;
const { offsetLeft, offsetWidth } = activeTabElement;
if (visibleTabs.includes(activeIndex)) {
activeTabElement.classList.add('active');
setHighlightStyle({ left: offsetLeft, width: offsetWidth });
return;
}
}, [activeTab, childrenArray, visibleTabs, overflowTabs, tabsContainerRef]);
return { highlightStyle };
};
exports.TabListContainer = styled_components_1.default.ul `
position: relative;
display: flex;
gap: var(--md-tabs-gap);
width: 100%;
min-width: 0;
&::before {
content: '';
position: absolute;
inset: 0;
border: var(--md-tabs-border);
border-width: var(--md-tabs-border-width);
pointer-events: none;
}
&& {
padding: var(--md-tabs-padding);
margin: 0;
& > li {
margin-bottom: 0;
flex-shrink: 0;
&.dropdown-tab {
flex-shrink: 1;
min-width: 0;
max-width: 100%;
}
}
}
`;
exports.TabItem = styled_components_1.default.li `
display: inline-flex;
list-style: none;
cursor: pointer;
align-items: center;
padding: var(--md-tabs-tab-wrapper-padding);
z-index: var(--z-index-surface);
${({ active, size }) => active
? (0, styled_components_1.css) `
border: solid var(--md-tabs-active-tab-border-color);
border-width: var(--md-tabs-${size}-active-tab-border-width);
`
: (0, styled_components_1.css) `
border: solid var(--md-tabs-hover-tab-border-color);
border-width: var(--md-tabs-${size}-hover-tab-border-width);
&:hover {
border: solid var(--md-tabs-hover-tab-border-color);
border-width: var(--md-tabs-${size}-hover-tab-border-width);
}
`}
div > div > ul {
padding-left: var(--spacing-unit);
}
&:focus-visible {
outline: none;
position: relative;
&::after {
content: '';
position: absolute;
top: -2px;
right: -4px;
bottom: -2px;
left: -4px;
border: 1px solid var(--button-border-color-focused);
border-radius: 6px;
pointer-events: none;
}
}
`;
const DropdownWrapper = styled_components_1.default.div.attrs((props) => ({
style: Object.assign(Object.assign({}, (props.$top !== undefined && { '--dropdown-top': `${props.$top}px` })), (props.$left !== undefined && { '--dropdown-left': `${props.$left}px` })),
})) `
position: static;
z-index: var(--z-index-raised);
width: 100%;
min-width: 0;
`;
const FixedPositionDropdown = (0, styled_components_1.default)(Dropdown_1.Dropdown) `
position: static;
width: 100%;
min-width: 0;
> div:first-child {
width: 100%;
min-width: 0;
}
> div:last-child {
position: fixed;
top: var(--dropdown-top, 0);
left: var(--dropdown-left, 0);
right: auto;
bottom: auto;
transform: none;
padding-top: 0;
max-width: min(400px, calc(100vw - 32px));
max-height: calc(100vh - var(--dropdown-top, 0) - 32px);
overflow-y: auto;
z-index: var(--z-index-raised);
ul {
li {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
`;
const HighlightBar = styled_components_1.default.div `
position: absolute;
top: 0;
bottom: 0;
border: solid var(--md-tabs-active-tab-border-color);
border-width: var(--md-tabs-${({ size }) => size}-active-tab-border-width);
transition:
left 300ms ease-in-out,
width 300ms ease-in-out;
z-index: 0;
padding: var(--md-tabs-tab-wrapper-padding);
& > div {
width: 100%;
height: 100%;
background-color: var(--md-tabs-active-tab-bg-color);
border-radius: var(--md-tabs-${({ size }) => size}-active-tab-border-radius);
}
`;
const TabButtonText = styled_components_1.default.span `
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
`;
exports.TabButtonLink = (0, styled_components_1.default)(Button_1.Button) `
color: var(--md-tabs-tab-text-color);
font-family: var(--md-tabs-tab-font-family);
font-style: var(--md-tabs-tab-font-style);
background-color: var(--md-tabs-tab-bg-color);
width: 100%;
transition:
background-color 300ms ease-in-out,
color 300ms ease-in-out,
padding 300ms ease-in-out,
border-radius 300ms ease-in-out;
${({ size }) => size &&
(0, styled_components_1.css) `
padding: var(--md-tabs-${size}-tab-padding);
font-size: var(--md-tabs-${size}-tab-font-size);
font-weight: var(--md-tabs-${size}-tab-font-weight);
line-height: var(--md-tabs-${size}-tab-line-height);
border-radius: var(--md-tabs-${size}-tab-border-radius);
`}
&.active {
color: var(--md-tabs-active-tab-text-color);
font-family: var(--md-tabs-active-tab-font-family);
font-style: var(--md-tabs-active-tab-font-style);
font-size: var(--md-tabs-${({ size }) => size}-active-tab-font-size);
font-weight: var(--md-tabs-${({ size }) => size}-active-tab-font-weight);
line-height: var(--md-tabs-${({ size }) => size}-active-tab-line-height);
background-color: var(--md-tabs-active-tab-bg-color);
border-radius: var(--md-tabs-${({ size }) => size}-active-tab-border-radius);
padding: var(--md-tabs-${({ size }) => size}-active-tab-padding);
}
&:hover {
color: var(--md-tabs-hover-tab-text-color);
font-family: var(--md-tabs-hover-tab-font-family);
font-style: var(--md-tabs-hover-tab-font-style);
font-size: var(--md-tabs-${({ size }) => size}-hover-tab-font-size);
font-weight: var(--md-tabs-${({ size }) => size}-hover-tab-font-weight);
line-height: var(--md-tabs-${({ size }) => size}-hover-tab-line-height);
background-color: var(--md-tabs-hover-tab-bg-color);
border-radius: var(--md-tabs-${({ size }) => size}-hover-tab-border-radius);
padding: var(--md-tabs-${({ size }) => size}-hover-tab-padding);
}
${({ disabled }) => disabled &&
(0, styled_components_1.css) `
color: var(--md-tabs-tab-text-disabled-color);
cursor: not-allowed;
&:hover {
color: var(--md-tabs-tab-text-disabled-color);
background-color: transparent;
}
`}
svg {
flex-shrink: 0;
}
`;
//# sourceMappingURL=TabList.js.map