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