UNPKG

@ozen-ui/kit

Version:

React component library

258 lines (257 loc) 14.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Tabs = exports.cnTabs = void 0; var tslib_1 = require("tslib"); require("./Tabs.css"); var react_1 = tslib_1.__importStar(require("react")); var react_is_1 = require("react-is"); var environment_1 = require("../../constants/environment"); var useDebounceCallback_1 = require("../../hooks/useDebounceCallback"); var useEventListener_1 = require("../../hooks/useEventListener"); var animateProperty_1 = require("../../utils/animateProperty"); var classname_1 = require("../../utils/classname"); var components_1 = require("./components"); var TabsContext_1 = require("./TabsContext"); var types_1 = require("./types"); exports.cnTabs = (0, classname_1.cn)('Tabs'); exports.Tabs = (0, react_1.forwardRef)(function (_a, ref) { var _b, _c; var children = _a.children, value = _a.value, onChange = _a.onChange, _d = _a.size, size = _d === void 0 ? 'm' : _d, className = _a.className, _e = _a.variant, variant = _e === void 0 ? 'default' : _e, disableScrollButtons = _a.disableScrollButtons, other = tslib_1.__rest(_a, ["children", "value", "onChange", "size", "className", "variant", "disableScrollButtons"]); var tabsListRef = (0, react_1.useRef)(null); var indicatorRef = (0, react_1.useRef)(null); var scrollBoxRef = (0, react_1.useRef)(null); /** Для вычисления размера кнопки */ var scrollButtonRef = (0, react_1.useRef)(null); var _f = tslib_1.__read((0, react_1.useState)(false), 2), isMounted = _f[0], setIsMounted = _f[1]; var _g = tslib_1.__read((0, react_1.useState)({ translateX: 0, width: 0 }), 2), indicatorStyles = _g[0], setIndicatorStyles = _g[1]; var _h = tslib_1.__read((0, react_1.useState)({ start: false, end: false, }), 2), scrollButtonsActivity = _h[0], setScrollButtonActivity = _h[1]; /** * Привязка value дочернего компонента к его индексу * для более точного контроля */ var tabsValueToIndex = (0, react_1.useMemo)(function () { return new Map(); }, [children]); var resolvedChildren = (0, react_is_1.isFragment)(children) ? children.props.children : children; /** Children только ноды Табов */ var onlyTabsChildren = (0, react_1.useMemo)(function () { return react_1.default.Children.toArray(resolvedChildren) .filter(function (child) { return react_1.default.isValidElement(child) && (child === null || child === void 0 ? void 0 : child.type) === components_1.Tab; }) .map(function (child, index) { if (!react_1.default.isValidElement(child)) return null; var childValue = child.props.value || index; tabsValueToIndex.set(childValue, index); return react_1.default.cloneElement(child, { value: childValue }); }); }, [resolvedChildren]); var isScrollButtonsActive = variant === 'scrollable' && !disableScrollButtons && tabsListRef.current && scrollBoxRef.current && ((_b = tabsListRef.current) === null || _b === void 0 ? void 0 : _b.clientWidth) > ((_c = scrollBoxRef.current) === null || _c === void 0 ? void 0 : _c.clientWidth); var getTabsMeta = function () { var scrollBoxNode = scrollBoxRef.current; var tabsListNode = tabsListRef.current; var currentChildIndex = tabsValueToIndex.get(value); var activeTabNode = tabsListNode === null || tabsListNode === void 0 ? void 0 : tabsListNode.children[currentChildIndex]; var tabsListMeta = null; var scrollBoxMeta = null; var activeTabMeta = null; if (tabsListNode) { tabsListMeta = tabsListNode.getBoundingClientRect(); } if (activeTabNode) { activeTabMeta = activeTabNode.getBoundingClientRect(); } if (scrollBoxNode) { var tabsRect = scrollBoxNode.getBoundingClientRect(); scrollBoxMeta = { scrollLeft: scrollBoxNode.scrollLeft, left: tabsRect.left, right: tabsRect.right, }; } return { tabsListMeta: tabsListMeta, activeTabMeta: activeTabMeta, scrollBoxMeta: scrollBoxMeta }; }; /** * Обновляет активность кнопок скоролла в * зависимости от размеров оберток и значения скролла * */ var updateScrollButtonsActivity = function () { var _a = getTabsMeta(), scrollBoxMeta = _a.scrollBoxMeta, tabsListMeta = _a.tabsListMeta; if (!scrollBoxMeta || !tabsListMeta) return; var scrollLeft = scrollBoxMeta.scrollLeft; var isStartButtonActive = scrollLeft > 0; var isEndButtonActive = tabsListMeta.right - scrollBoxMeta.right > 1; setScrollButtonActivity(function (prevState) { var start = prevState.start, end = prevState.end; if (isStartButtonActive !== start || isEndButtonActive !== end) { return { start: isStartButtonActive, end: isEndButtonActive }; } return prevState; }); }; var updateIndicatorStyles = function () { var _a = getTabsMeta(), tabsListMeta = _a.tabsListMeta, activeTabMeta = _a.activeTabMeta; if (!tabsListMeta || !activeTabMeta) return; var translateX = activeTabMeta.left - tabsListMeta.left; var width = activeTabMeta.width; setIndicatorStyles({ translateX: translateX, width: width, }); }; /** Скроллит в нужную позицию */ var scroll = function (scrollValue, animationOptions) { if (animationOptions === void 0) { animationOptions = { animate: true }; } var animate = animationOptions.animate; if (!scrollBoxRef.current) return; if (animate) { (0, animateProperty_1.animateProperty)('scrollLeft', scrollBoxRef.current, scrollValue); } else { scrollBoxRef.current.scrollLeft = scrollValue; } }; var handleKeyDown = (0, react_1.useCallback)(function (e) { var _a, _b; if (environment_1.isServer) { return; } var event = e; var key = event.key; if (!Object.keys(types_1.ControlKeys).includes(key)) return; // Убирает скролл на стрелки e.preventDefault(); var focusedChild = document.activeElement; var tabsListNode = tabsListRef.current; if ((focusedChild === null || focusedChild === void 0 ? void 0 : focusedChild.parentElement) !== tabsListNode || !tabsListNode) { return; } var tabsListChildNodes = Array.from(tabsListNode === null || tabsListNode === void 0 ? void 0 : tabsListNode.children).filter(function (el) { return !el.className.includes('disabled'); }); var focusedChildIndex = tabsListChildNodes.indexOf(focusedChild); if (key === types_1.ControlKeys.ArrowLeft) { var prevElementIndex = focusedChildIndex === 0 ? tabsListChildNodes.length - 1 : focusedChildIndex - 1; (_a = tabsListChildNodes === null || tabsListChildNodes === void 0 ? void 0 : tabsListChildNodes[prevElementIndex]) === null || _a === void 0 ? void 0 : _a.focus(); } if (key === types_1.ControlKeys.ArrowRight) { var nextElementIndex = focusedChildIndex === tabsListChildNodes.length - 1 ? 0 : focusedChildIndex + 1; (_b = tabsListChildNodes === null || tabsListChildNodes === void 0 ? void 0 : tabsListChildNodes[nextElementIndex]) === null || _b === void 0 ? void 0 : _b.focus(); } }, []); var handleClickScrollButton = (0, react_1.useCallback)(function (direction) { return function () { var scrollBoxNode = scrollBoxRef.current; if (!scrollBoxNode) return; if (direction === 'right') { scroll(scrollBoxNode.scrollLeft + scrollBoxNode.clientWidth); } if (direction === 'left') { scroll(scrollBoxNode.scrollLeft - scrollBoxNode.clientWidth); } }; }, []); /** Корректирует скролл в зависимости от позиции выбранного таба */ var scrollCorrection = function () { var _a; var _b = getTabsMeta(), scrollBoxMeta = _b.scrollBoxMeta, activeTabMeta = _b.activeTabMeta; var buttonWidth = ((_a = scrollButtonRef.current) === null || _a === void 0 ? void 0 : _a.clientWidth) || 0; if (!scrollBoxMeta || !activeTabMeta) return; if (activeTabMeta.left - buttonWidth < scrollBoxMeta.left) { // Если выбранный таб слева от видимой области var nextScrollLeft = scrollBoxMeta.scrollLeft + (activeTabMeta.left - scrollBoxMeta.left); scroll(nextScrollLeft - buttonWidth); } else if (activeTabMeta.right + buttonWidth > scrollBoxMeta.right) { // Если выбранный таб справа от видимой области var nextScrollLeft = scrollBoxMeta.scrollLeft + (activeTabMeta.right - scrollBoxMeta.right); scroll(nextScrollLeft + buttonWidth); } }; var _j = tslib_1.__read((0, useDebounceCallback_1.useDebounceCallback)(updateIndicatorStyles, 100), 1), debouncedUpdateIndicatorStyles = _j[0]; var _k = tslib_1.__read((0, useDebounceCallback_1.useDebounceCallback)(updateScrollButtonsActivity, 100), 1), debouncedUpdateScrollButtonsActivity = _k[0]; var _l = tslib_1.__read((0, useDebounceCallback_1.useDebounceCallback)(scrollCorrection, 100), 1), debouncedScrollCorrection = _l[0]; var handleChange = (0, react_1.useCallback)(function (e, value) { onChange === null || onChange === void 0 ? void 0 : onChange(e, value); }, [onChange]); /** Мемоизация контекста для исключения лишних перерендеров */ var context = (0, react_1.useMemo)(function () { return ({ onChange: handleChange, currentValue: value, size: size, }); }, [handleChange, value, size]); (0, react_1.useEffect)(function () { if (isMounted) { updateIndicatorStyles(); debouncedScrollCorrection(); } else { setIsMounted(true); // Обновить стили индикатора без анимации при первом рендере updateIndicatorStyles(); } }, [value, isMounted]); (0, react_1.useEffect)(function () { updateScrollButtonsActivity(); }, []); (0, react_1.useEffect)(function () { var _a; var resizeObserver; if (typeof ResizeObserver !== 'undefined') { /** При изменении размера самого таба */ resizeObserver = new ResizeObserver(debouncedUpdateIndicatorStyles); Array.from(((_a = tabsListRef === null || tabsListRef === void 0 ? void 0 : tabsListRef.current) === null || _a === void 0 ? void 0 : _a.children) || []).forEach(function (child) { resizeObserver.observe(child); }); } return function () { resizeObserver === null || resizeObserver === void 0 ? void 0 : resizeObserver.disconnect(); }; }, [onlyTabsChildren]); (0, react_1.useEffect)(function () { var resizeObserver; if (typeof ResizeObserver !== 'undefined' && scrollBoxRef.current) { /** При изменении размера обертки для скролла */ resizeObserver = new ResizeObserver(debouncedUpdateScrollButtonsActivity); resizeObserver.observe(scrollBoxRef.current); } return function () { resizeObserver === null || resizeObserver === void 0 ? void 0 : resizeObserver.disconnect(); }; }, []); (0, useEventListener_1.useEventListener)({ eventName: 'resize', handler: function () { debouncedUpdateIndicatorStyles(); debouncedUpdateScrollButtonsActivity(); debouncedScrollCorrection(); }, }); (0, useEventListener_1.useEventListener)({ eventName: 'keydown', handler: handleKeyDown, element: scrollBoxRef, }); return (react_1.default.createElement("div", tslib_1.__assign({ className: (0, exports.cnTabs)({}, [className]), ref: ref }, other), isScrollButtonsActive && (react_1.default.createElement(components_1.TabsScrollButton, { size: size, className: (0, exports.cnTabs)('ScrollButton'), direction: "left", invisible: !scrollButtonsActivity.start, onClick: handleClickScrollButton('left') })), react_1.default.createElement("div", { ref: scrollBoxRef, onScroll: updateScrollButtonsActivity, className: (0, exports.cnTabs)('ScrollBox') }, react_1.default.createElement("div", { className: (0, exports.cnTabs)('IndicatorContainer') }, react_1.default.createElement("div", { className: (0, exports.cnTabs)('List'), role: "tablist", ref: tabsListRef }, react_1.default.createElement(TabsContext_1.TabsContext.Provider, { value: context }, onlyTabsChildren)), isMounted && (react_1.default.createElement(components_1.TabsIndicator, { ref: indicatorRef, indicatorStyles: indicatorStyles, className: (0, exports.cnTabs)('Indicator') })))), isScrollButtonsActive && (react_1.default.createElement(components_1.TabsScrollButton, { size: size, ref: scrollButtonRef, direction: "right", className: (0, exports.cnTabs)('ScrollButton'), invisible: !scrollButtonsActivity.end, onClick: handleClickScrollButton('right') })))); }); exports.Tabs.displayName = 'Tabs';