UNPKG

@redocly/theme

Version:

Shared UI components lib

431 lines (412 loc) 17.3 kB
"use strict"; 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