@zohodesk/components
Version:
In this Package, we Provide Some Basic Components to Build Web App
619 lines (566 loc) • 19.3 kB
JavaScript
import React, { useState, useRef, useEffect, useCallback } from 'react';
import useEffectCallOnlyAfterState from '@zohodesk/hooks/es/utils/useEffectCallOnlyAfterState';
import TextBoxIcon from "../TextBoxIcon/TextBoxIcon";
import EmptySearch from '@zohodesk/svg/lib/emptystate/version3/EmptySearch';
import Tab from "./Tab";
import { Tabs_defaultProps } from "./props/defaultProps";
import { Tabs_propTypes } from "./props/propTypes";
import { remConvert, getTotalDimension, cs, throttle, cancelBubblingEffect } from "../../utils/Common";
import { Box, Container } from "../Layout";
import ResizeObserver from "../../Responsive/ResizeObserver";
import ListItem from "../ListItem/ListItem";
import { Icon } from '@zohodesk/icons';
import tabsStyle from "./v1Tabs.module.css";
import Popup from "../Popup/Popup";
import ResponsiveDropBox from "../ResponsiveDropBox/ResponsiveDropBox";
import { ResponsiveReceiver } from "../../Responsive/CustomResponsive";
import btnstyle from "../../semantic/Button/semanticButton.module.css";
const Tabs = props => {
const [totalDimension, setTotalDimension] = useState(null);
const [tabDimensions, setTabDimensions] = useState({});
const highlightInitialDimension = useRef(null);
const [renderVirtualTabs, setRenderVirtualTabs] = useState(true);
const [tabKeys, setTabKeys] = useState([]);
const [searchValue, setSearchValue] = useState('');
const tabsObserver = useRef(new ResizeObserver(onResize));
const tabObserver = useRef({});
const highlight = useRef(null);
const tabsEle = useRef(null);
const tabElement = useRef({});
let {
style,
className,
dataId,
highlightClass,
type,
isAnimate,
needTabBorder,
needBorder,
needPadding,
align,
boxPosition,
removeClose,
togglePopup,
needAppearance,
children,
onSelect,
onScroll,
isResponsive,
maxTabsCount,
minTabsCount,
childType,
containerClass,
dataSelectorId,
selectedTab,
showTitleInMoreOptions,
searchErrorText,
placeHolderText,
searchBoxSize,
isPopupOpen,
closePopupOnly
} = props;
function handleChange(value, e) {
setSearchValue(value);
}
function handleSearchValueClear(e) {
setSearchValue('');
}
function getMoreList(_ref) {
let {
moreTabs
} = _ref;
const validElements = [];
React.Children.map(moreTabs, child => {
const isValidElement = /*#__PURE__*/React.isValidElement(child);
if (isValidElement) {
validElements.push(child.props);
}
});
return validElements;
}
function handleFilterSuggestions(_ref2) {
let {
moreTabs
} = _ref2;
const options = getMoreList({
moreTabs
});
const filteredOptions = options.filter(list => {
const {
text,
title
} = list;
const value = text ? text : showTitleInMoreOptions ? title : '';
return value.toLowerCase().includes(searchValue.toLowerCase());
});
return filteredOptions;
}
function renderEmptyState() {
return /*#__PURE__*/React.createElement("div", {
className: tabsStyle.emptyStateContainer
}, /*#__PURE__*/React.createElement(EmptySearch, {
size: "small"
}), /*#__PURE__*/React.createElement("div", {
className: tabsStyle.emptyStateTitle
}, searchErrorText));
}
function onTabResize(size, target) {
let newDim = Object.assign({}, tabDimensions);
let elemDim = getTotalDimension(target, align);
if (elemDim !== 0) {
newDim[target.dataset.key] = elemDim;
}
setTabDimensions(newDim);
}
useEffect(() => {
let totalDimension = getTotalDimensionLocal();
let tabDimensions = getTabDimensions();
let tabKeys = [];
let restrictedKeys = ['state', 'props', 'refs', 'context'];
React.Children.toArray(children).forEach(child => {
if (child && child.props.id && child.type === childType) {
if (restrictedKeys.includes(child.props.id)) {
throw new Error('Restricted id name found in Tabs, Please avoid these names => ' + restrictedKeys.toString());
} else {
tabKeys.push(child.props.id);
}
}
});
setTotalDimension(totalDimension);
setTabDimensions(tabDimensions);
setRenderVirtualTabs(false);
setTabKeys(tabKeys);
if (highlight.current) {
highlightInitialDimension.current = getHighlightDim();
moveHighlight();
}
return () => {
if (tabsObserver.current) {
tabsObserver.current.disconnect();
tabsObserver.current = null;
}
if (tabObserver.current) {
Object.keys(tabObserver.current).forEach(observer => {
tabObserver.current[observer] && tabObserver.current[observer].disconnect();
});
tabObserver.current = null;
}
};
}, []);
useEffect(() => {
if (searchValue.length) {
handleSearchValueClear();
}
}, [isPopupOpen]);
function setMaxDim() {
let totalDimension = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
// let actual = Object.keys(tabDimensions).reduce(
// (sum, tab) => sum + (tabDimensions[tab] || 0),
// 0
// );
if (totalDimension && tabElement.current[selectedTab]) {
let newDim = `${remConvert(totalDimension)}rem`;
align === 'vertical' ? tabElement.current[selectedTab].style.maxWidth = newDim : tabElement.current[selectedTab].style.maxHeight = newDim;
}
}
function getHighlightDim() {
return align === 'vertical' ? highlight.current.offsetLeft : highlight.current.offsetTop;
}
useEffectCallOnlyAfterState(() => {
if (children) {
/**
* To recalculate the dimensions of tabs we are checking the children length.
* and also checking the tab id and their order.
*/
let newTabKeys = [];
React.Children.toArray(children).forEach(child => {
if (child && child.props.id && child.type === childType) {
newTabKeys.push(child.props.id);
}
});
let isSameTabKeys = newTabKeys.every((current, index) => tabKeys[index] === current);
if (newTabKeys.length !== tabKeys.length || !isSameTabKeys) {
setRenderVirtualTabs(true);
setTabKeys(newTabKeys);
setTimeout(() => {
setTabDimensions(getTabDimensions());
setTotalDimension(getTotalDimensionLocal());
setRenderVirtualTabs(false);
});
}
if (newTabKeys.length === 1) {
moveHighlight();
}
}
});
useEffectCallOnlyAfterState(() => {
tabsObserver.current && tabsObserver.current.replaceResizeHandler(onResize);
if (children) {
moveHighlight();
}
}, [selectedTab]);
function moveHighlight() {
let node = highlight.current;
if (isAnimate && needTabBorder && node) {
let tabActive = tabElement.current[selectedTab];
if (tabActive) {
let position, dimension;
if (align === 'vertical') {
position = tabActive.offsetLeft;
dimension = tabActive.offsetWidth;
node.style.transform = `translateX(${remConvert(position - highlightInitialDimension.current)}rem)`;
node.style.width = `${remConvert(dimension)}rem`;
} else if (align === 'horizontal') {
position = tabActive.offsetTop;
dimension = tabActive.offsetHeight;
node.style.transform = `translateY(${remConvert(position - highlightInitialDimension.current)}rem)`;
node.style.height = `${remConvert(dimension)}rem`;
}
}
}
}
const getHighlightRef = useCallback(ele => {
highlight.current = ele;
}, []);
const getTabsRef = useCallback(ele => {
tabsEle.current = ele;
if (ele) {
tabsObserver.current.observe(ele);
} else {
tabsObserver.current && tabsObserver.current.disconnect();
}
}, []);
function onSizeChange() {
let totalDimension = getTotalDimensionLocal();
setTotalDimension(totalDimension);
if (highlight.current) {
highlightInitialDimension.current = getHighlightDim();
moveHighlight();
}
}
function onResize(timer) {
if (tabsEle.current && isResponsive) {
if (!timer) {
onSizeChange();
} else {
setTimeout(() => {
onSizeChange();
}, timer);
}
}
}
function getTotalDimensionLocal() {
let totalDimension = tabsEle.current && getTotalDimension(tabsEle.current, align);
return totalDimension;
}
function getTabDimensions() {
let tabDimensions = {};
React.Children.forEach(children, child => /*#__PURE__*/React.isValidElement(child) && child.type === childType && tabElement.current[child.props.id] && Object.assign(tabDimensions, {
[child.props.id]: tabElement.current[child.props.id] && getTotalDimension(tabElement.current[child.props.id], align)
}));
return tabDimensions;
}
function responsiveTab(totalDimension, tabDimensions) {
let mainTabs = [],
moreTabs = [],
otherTabs = [];
if (totalDimension && isResponsive) {
let selectedTabDimension = tabDimensions[selectedTab] || 0;
let remainingDimension = totalDimension - selectedTabDimension;
let maxTabsTobeVisible = maxTabsCount - 1;
let minTabsToBeVisible = 1;
React.Children.forEach(children, child => {
if (child && child.type === childType && child.props.id) {
const elemDimension = tabDimensions[child.props.id];
let isMaxCountExceeded = maxTabsTobeVisible <= 0 ? true : false;
let isMinCountNotExceeded = minTabsToBeVisible < minTabsCount ? true : false;
if (child.props.id === selectedTab) {
mainTabs.push(child);
} else if (isMinCountNotExceeded) {
mainTabs.push(child);
remainingDimension -= tabDimensions[child.props.id];
minTabsToBeVisible++;
maxTabsTobeVisible--;
} else if (remainingDimension - elemDimension >= 20 && !moreTabs.length && !isMaxCountExceeded) {
mainTabs.push(child);
remainingDimension -= tabDimensions[child.props.id];
maxTabsTobeVisible--;
} else {
moreTabs.push(child);
}
} else if ( /*#__PURE__*/React.isValidElement(child)) {
otherTabs.push(child);
}
});
if (selectedTabDimension > totalDimension) {
/* let { align } = this.props;
let newDim = `${remConvert(totalDimension)}rem`;
let newTabs;
if (align === 'vertical') {
newTabs = mainTabs.map(tab => React.cloneElement(tab, { style: { maxWidth: newDim } }));
} else {
newTabs = mainTabs.map(tab => React.cloneElement(tab, { style: { maxHeight: newDim } }));
}
mainTabs = newTabs; */
setMaxDim(totalDimension);
}
} else if (!isResponsive) {
React.Children.forEach(children, child => {
if (child && child.type === childType && child.props.id) {
mainTabs.push(child);
} else {
otherTabs.push(child);
}
});
} else {
moreTabs = children;
}
return {
mainTabs,
moreTabs,
otherTabs
};
}
function getTabRef(refName, ele, isVirtual) {
if (ele) {
tabElement.current[refName] = ele;
} // tabElement.current[refName] = ele
let key = isVirtual ? `virtual_${refName}` : refName;
if (ele) {
tabObserver.current[key] = new ResizeObserver(onTabResize);
tabObserver.current[key].observe(ele);
} else {
if (tabObserver.current && tabObserver.current[key]) {
tabObserver.current[key].disconnect();
}
}
}
function moreTabSelect(tab, value, index, e) {
if (e && e.target && e.target.parentElement.tagName === 'A' && (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)) {
cancelBubblingEffect(e);
return;
}
e.preventDefault();
onSelect(tab);
closePopupOnly();
}
function onScrollLocal(e) {
e.persist();
onScroll && throttle(onScroll, 100, e);
}
function togglePopupLocal(e) {
removeClose && removeClose(e);
let popupPosition = align === 'vertical' ? 'bottomLeft' : 'rightTop';
boxPosition = boxPosition ? boxPosition : popupPosition;
togglePopup(e, boxPosition);
}
function responsiveFunc(_ref3) {
let {
mediaQueryOR
} = _ref3;
return {
tabletMode: mediaQueryOR([{
maxWidth: 700
}])
};
}
function renderTabs(mainTabs, moreTabs, isVirtual) {
let {
moreButtonClass,
moreButtonActiveClass,
moreBoxClass,
menuItemClass,
itemClass,
itemActiveClass,
iconName,
isPopupReady,
isPopupOpen,
getContainerRef,
showTitleInMoreOptions,
moreContainerClass,
align,
type,
isAnimate,
needTabBorder,
needAppearance,
iconSize,
getTargetRef,
position,
customProps,
getCustomDropBoxHeaderPlaceHolder,
dataId,
isAbsolutePositioningNeeded,
isRestrictScroll,
positionsOffset,
targetOffset
} = props;
let {
DropBoxProps = {},
ListItemProps = {},
MoreButtonProps = {},
TextBoxIconProps = {}
} = customProps;
let popupClass = align === 'vertical' ? tabsStyle[position] ? tabsStyle[position] : '' : '';
const moreTabsListItems = handleFilterSuggestions({
moreTabs
});
const hasSearch = moreTabs.length > 4;
return /*#__PURE__*/React.createElement(React.Fragment, null, React.Children.map(mainTabs, child => {
if (!child) {
return null;
}
let MainTab = child.type;
let classProps = {};
if (itemActiveClass) {
classProps.activeClass = itemActiveClass;
}
if (itemClass) {
classProps.className = itemClass;
}
return /*#__PURE__*/React.isValidElement(child) && /*#__PURE__*/React.createElement(MainTab, { ...child.props,
key: child.props.id,
getTabRef: getTabRef,
onSelect: onSelect // recalculateDimension={tabElement.current[recalculateDimension]}
,
isActive: child.props.id === selectedTab,
type: type,
isAnimate: isAnimate,
needBorder: needTabBorder,
needAppearance: needAppearance,
align: align,
isVirtual: isVirtual,
...classProps
});
}), React.Children.count(moreTabs) ? /*#__PURE__*/React.createElement(Box, {
className: `${tabsStyle.menu} `,
dataSelectorId: `${dataSelectorId}_moreIcon`
}, /*#__PURE__*/React.createElement(Container, {
className: `${btnstyle.buttonReset} ${moreButtonClass} ${isPopupOpen ? moreButtonActiveClass : ''}`,
align: "both",
onClick: togglePopupLocal,
dataId: "moreTabs",
eleRef: getTargetRef,
...MoreButtonProps,
"aria-label": "MoreTabs",
role: "link",
tagName: "button"
}, /*#__PURE__*/React.createElement(Icon, {
name: iconName,
size: iconSize
})), isPopupOpen && /*#__PURE__*/React.createElement(ResponsiveReceiver, {
query: responsiveFunc,
responsiveId: "Helmet"
}, _ref4 => {
let {
tabletMode
} = _ref4;
return /*#__PURE__*/React.createElement(ResponsiveDropBox, {
isActive: isPopupReady,
isAnimate: true,
size: "medium",
customClass: {
customDropBoxWrap: `${moreContainerClass} ${popupClass}`
},
boxPosition: position,
getRef: getContainerRef,
isBoxPaddingNeed: false,
isArrow: false,
isAbsolutePositioningNeeded: isAbsolutePositioningNeeded,
isRestrictScroll: isRestrictScroll,
positionsOffset: positionsOffset,
targetOffset: targetOffset,
...DropBoxProps,
isResponsivePadding: true,
needFocusScope: true,
onClose: togglePopupLocal,
onClick: removeClose,
dataId: `${dataId}_dropbox`
}, getCustomDropBoxHeaderPlaceHolder && getCustomDropBoxHeaderPlaceHolder(props), hasSearch ? /*#__PURE__*/React.createElement(Box, {
className: tabsStyle.search
}, /*#__PURE__*/React.createElement(TextBoxIcon, {
placeHolder: placeHolderText,
onChange: handleChange,
value: searchValue,
onClear: handleSearchValueClear,
size: searchBoxSize,
customProps: {
TextBoxProps: {
'data-a11y-autofocus': true
}
},
dataId: `${dataId}_search`,
autoComplete: false,
name: "search",
...TextBoxIconProps
})) : null, /*#__PURE__*/React.createElement(Box, {
flexible: true,
shrink: true,
scroll: "vertical",
className: `${tabsStyle.listWrapper} ${tabletMode ? '' : tabsStyle.menuBox} ${moreBoxClass}`,
onScroll: onScrollLocal,
dataId: `${dataId}_Tabs`
}, moreTabsListItems.length ? moreTabsListItems.map(data => {
let {
text,
id,
title,
isLink,
href,
children,
dataId
} = data;
const value = text ? text : showTitleInMoreOptions ? title : null;
return /*#__PURE__*/React.createElement(ListItem, {
key: id,
value: value,
onClick: moreTabSelect,
id: id,
title: title || text,
isLink: isLink,
href: href,
autoHover: true,
customClass: {
customListItem: menuItemClass
},
target: "self",
dataId: `${dataId}_Tab`,
...ListItemProps
}, !showTitleInMoreOptions ? children : null);
}) : renderEmptyState()));
})) : null);
}
let {
mainTabs,
moreTabs,
otherTabs
} = responsiveTab(totalDimension, tabDimensions);
let appearanceClass = cs([tabsStyle[type], needBorder && tabsStyle[`${type}_border`], needPadding && tabsStyle[`${type}_padding`]]);
let tabsClass = cs([tabsStyle.tab, className, needAppearance && appearanceClass]);
let hlClass = cs([tabsStyle.highlight, highlightClass, isAnimate && tabsStyle.lineAnimate]);
return /*#__PURE__*/React.createElement(Box, {
className: containerClass,
dataSelectorId: dataSelectorId
}, /*#__PURE__*/React.createElement(Container, {
alignBox: align === 'vertical' ? 'row' : 'column',
className: tabsClass,
style: style
}, /*#__PURE__*/React.createElement(Box, {
eleRef: getTabsRef,
flexible: true
}, renderVirtualTabs && /*#__PURE__*/React.createElement(Container, {
alignBox: align === 'vertical' ? 'row' : 'column',
className: tabsStyle.hidden
}, renderTabs(children, [], true)), /*#__PURE__*/React.createElement(Container, {
alignBox: align === 'vertical' ? 'row' : 'column'
}, renderTabs(mainTabs, moreTabs, false))), otherTabs.length ? /*#__PURE__*/React.createElement(Box, null, otherTabs) : null, isAnimate && needTabBorder && needAppearance && /*#__PURE__*/React.createElement("div", {
className: hlClass,
ref: getHighlightRef,
"data-id": dataId,
"data-test-id": dataId
})));
};
Tabs.propTypes = Tabs_propTypes;
Tabs.defaultProps = { ...Tabs_defaultProps,
childType: Tab
};
export default Popup(Tabs);