UNPKG

@ozen-ui/kit

Version:

React component library

255 lines (254 loc) 13.2 kB
import { __assign, __read, __rest } from "tslib"; import './Tabs.css'; import React, { useState, useCallback, useMemo, useEffect, forwardRef, useRef, } from 'react'; import { isFragment } from 'react-is'; import { isServer } from '../../constants/environment'; import { useDebounceCallback } from '../../hooks/useDebounceCallback'; import { useEventListener } from '../../hooks/useEventListener'; import { animateProperty } from '../../utils/animateProperty'; import { cn } from '../../utils/classname'; import { TabsIndicator, TabsScrollButton, Tab, } from './components'; import { TabsContext } from './TabsContext'; import { ControlKeys } from './types'; export var cnTabs = cn('Tabs'); export var Tabs = 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 = __rest(_a, ["children", "value", "onChange", "size", "className", "variant", "disableScrollButtons"]); var tabsListRef = useRef(null); var indicatorRef = useRef(null); var scrollBoxRef = useRef(null); /** Для вычисления размера кнопки */ var scrollButtonRef = useRef(null); var _f = __read(useState(false), 2), isMounted = _f[0], setIsMounted = _f[1]; var _g = __read(useState({ translateX: 0, width: 0 }), 2), indicatorStyles = _g[0], setIndicatorStyles = _g[1]; var _h = __read(useState({ start: false, end: false, }), 2), scrollButtonsActivity = _h[0], setScrollButtonActivity = _h[1]; /** * Привязка value дочернего компонента к его индексу * для более точного контроля */ var tabsValueToIndex = useMemo(function () { return new Map(); }, [children]); var resolvedChildren = isFragment(children) ? children.props.children : children; /** Children только ноды Табов */ var onlyTabsChildren = useMemo(function () { return React.Children.toArray(resolvedChildren) .filter(function (child) { return React.isValidElement(child) && (child === null || child === void 0 ? void 0 : child.type) === Tab; }) .map(function (child, index) { if (!React.isValidElement(child)) return null; var childValue = child.props.value || index; tabsValueToIndex.set(childValue, index); return React.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) { animateProperty('scrollLeft', scrollBoxRef.current, scrollValue); } else { scrollBoxRef.current.scrollLeft = scrollValue; } }; var handleKeyDown = useCallback(function (e) { var _a, _b; if (isServer) { return; } var event = e; var key = event.key; if (!Object.keys(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 === 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 === 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 = 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 = __read(useDebounceCallback(updateIndicatorStyles, 100), 1), debouncedUpdateIndicatorStyles = _j[0]; var _k = __read(useDebounceCallback(updateScrollButtonsActivity, 100), 1), debouncedUpdateScrollButtonsActivity = _k[0]; var _l = __read(useDebounceCallback(scrollCorrection, 100), 1), debouncedScrollCorrection = _l[0]; var handleChange = useCallback(function (e, value) { onChange === null || onChange === void 0 ? void 0 : onChange(e, value); }, [onChange]); /** Мемоизация контекста для исключения лишних перерендеров */ var context = useMemo(function () { return ({ onChange: handleChange, currentValue: value, size: size, }); }, [handleChange, value, size]); useEffect(function () { if (isMounted) { updateIndicatorStyles(); debouncedScrollCorrection(); } else { setIsMounted(true); // Обновить стили индикатора без анимации при первом рендере updateIndicatorStyles(); } }, [value, isMounted]); useEffect(function () { updateScrollButtonsActivity(); }, []); 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]); 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(); }; }, []); useEventListener({ eventName: 'resize', handler: function () { debouncedUpdateIndicatorStyles(); debouncedUpdateScrollButtonsActivity(); debouncedScrollCorrection(); }, }); useEventListener({ eventName: 'keydown', handler: handleKeyDown, element: scrollBoxRef, }); return (React.createElement("div", __assign({ className: cnTabs({}, [className]), ref: ref }, other), isScrollButtonsActive && (React.createElement(TabsScrollButton, { size: size, className: cnTabs('ScrollButton'), direction: "left", invisible: !scrollButtonsActivity.start, onClick: handleClickScrollButton('left') })), React.createElement("div", { ref: scrollBoxRef, onScroll: updateScrollButtonsActivity, className: cnTabs('ScrollBox') }, React.createElement("div", { className: cnTabs('IndicatorContainer') }, React.createElement("div", { className: cnTabs('List'), role: "tablist", ref: tabsListRef }, React.createElement(TabsContext.Provider, { value: context }, onlyTabsChildren)), isMounted && (React.createElement(TabsIndicator, { ref: indicatorRef, indicatorStyles: indicatorStyles, className: cnTabs('Indicator') })))), isScrollButtonsActive && (React.createElement(TabsScrollButton, { size: size, ref: scrollButtonRef, direction: "right", className: cnTabs('ScrollButton'), invisible: !scrollButtonsActivity.end, onClick: handleClickScrollButton('right') })))); }); Tabs.displayName = 'Tabs';