@ozen-ui/kit
Version:
React component library
255 lines (254 loc) • 13.2 kB
JavaScript
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';