@zohodesk/components
Version:
Dot UI is a customizable React component library built to deliver a clean, accessible, and developer-friendly UI experience. It offers a growing set of reusable components designed to align with modern design systems and streamline application development
723 lines (668 loc) • 21.3 kB
JavaScript
/* eslint-disable react/forbid-component-props */
/* eslint css-modules/no-unused-class: [2, {
markAsUsed: [
'hidden','alpha','gamma','beta','delta','special','text','maxWidth','widgetList','menuContainer','border','tabText','block','tabAlpha','alphaActive','gammaActive','betaActive','deltaActive','tabGamma','tabBeta','tabDelta','specialActive','tabSpecial',
'alpha_padding','alpha_border','gamma_padding','gamma_border','beta_padding','beta_border','delta_padding','delta_border','textContainer','disabled','alphaActive_border','gammaActive_border','betaActive_border','deltaActive_border'
]
}]
*/
import React from 'react';
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, bind, 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 "./Tabs.module.css";
import Popup from "../Popup/Popup";
import TextBoxIcon from "../TextBoxIcon/TextBoxIcon";
import ResponsiveDropBox from "../ResponsiveDropBox/ResponsiveDropBox";
import { ResponsiveReceiver } from "../Responsive/CustomResponsive";
import { TAB_DIRECTION_MAPPING, TAB_POPUP_POSITION_MAPPING } from "./utils/tabConfigs";
import btnstyle from "../semantic/Button/semanticButton.module.css";
class Tabs extends React.Component {
constructor(props) {
super(props);
this.state = {
totalDimension: null,
tabDimensions: {},
highlightInitialDimension: null,
renderVirtualTabs: true,
tabKeys: [],
searchValue: ''
};
bind.apply(this, ['moreTabSelect', 'onResize', 'onTabResize', 'getHighlightRef', 'getTabsRef', 'getTabRef', 'getHighlightDim', 'onSizeChange', 'onScroll', 'togglePopup', 'setMaxDim', 'handleChange', 'handleSearchValueClear', 'getMoreList', 'renderEmptyState']);
this.tabsObserver = new ResizeObserver(this.onResize);
this.tabObserver = {};
}
onTabResize(size, target) {
let {
align
} = this.props;
let {
tabDimensions
} = this.state;
let newDim = Object.assign({}, tabDimensions);
let elemDim = getTotalDimension(target, align);
if (elemDim !== 0) {
newDim[target.dataset.key] = elemDim;
}
this.setState({
tabDimensions: newDim
});
}
componentDidMount() {
let {
children,
childType
} = this.props;
let totalDimension = this.getTotalDimension();
let tabDimensions = this.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);
}
}
});
this.setState({
totalDimension,
tabDimensions,
renderVirtualTabs: false,
tabKeys
});
this.highlight && this.setState({
highlightInitialDimension: this.getHighlightDim()
}, () => {
this.moveHighlight();
});
}
componentWillUnmount() {
if (this.tabsObserver) {
this.tabsObserver.disconnect();
this.tabsObserver = null;
}
if (this.tabObserver) {
Object.keys(this.tabObserver).forEach(observer => {
this.tabObserver[observer] && this.tabObserver[observer].disconnect();
});
this.tabObserver = null;
}
}
setMaxDim() {
let totalDimension = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
let {
selectedTab
} = this.props; // let actual = Object.keys(tabDimensions).reduce(
// (sum, tab) => sum + (tabDimensions[tab] || 0),
// 0
// );
if (totalDimension && this[selectedTab]) {
let {
align
} = this.props;
let newDim = `${remConvert(totalDimension)}rem`;
TAB_DIRECTION_MAPPING[align] === 'column' ? this[selectedTab].style.maxWidth = newDim : this[selectedTab].style.maxHeight = newDim;
}
}
getHighlightDim() {
let {
align
} = this.props;
return TAB_DIRECTION_MAPPING[align] === 'column' ? this.highlight.offsetLeft : this.highlight.offsetTop;
}
componentDidUpdate(prevProps) {
let {
children,
selectedTab,
childType,
isPopupOpen
} = this.props;
let {
tabDimensions,
totalDimension,
tabKeys,
searchValue
} = this.state;
if (prevProps.children && 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) {
this.setState({
renderVirtualTabs: true,
tabKeys: newTabKeys
}, () => {
totalDimension = this.getTotalDimension();
tabDimensions = this.getTabDimensions();
this.setState({
tabDimensions,
totalDimension,
renderVirtualTabs: false
});
});
}
if (prevProps.selectedTab !== selectedTab || newTabKeys.length === 1) {
this.moveHighlight();
}
if (prevProps.isPopupOpen != isPopupOpen && searchValue.length) {
this.handleSearchValueClear();
}
}
}
moveHighlight() {
let {
isAnimate,
needTabBorder,
selectedTab,
align
} = this.props;
let {
highlightInitialDimension
} = this.state;
let node = this.highlight;
if (isAnimate && needTabBorder && node) {
let tabActive = this[selectedTab];
if (tabActive) {
let position, dimension;
if (TAB_DIRECTION_MAPPING[align] === 'column') {
position = tabActive.offsetLeft;
dimension = tabActive.offsetWidth;
node.style.transform = `translateX(${remConvert(position - highlightInitialDimension)}rem)`;
node.style.width = `${remConvert(dimension)}rem`;
} else if (TAB_DIRECTION_MAPPING[align] === 'row') {
position = tabActive.offsetTop;
dimension = tabActive.offsetHeight;
node.style.transform = `translateY(${remConvert(position - highlightInitialDimension)}rem)`;
node.style.height = `${remConvert(dimension)}rem`;
}
}
}
}
getHighlightRef(ele) {
this.highlight = ele;
}
getTabsRef(ele) {
this.tabsEle = ele;
if (ele) {
this.tabsObserver.observe(ele);
} else {
this.tabsObserver && this.tabsObserver.disconnect();
}
}
onSizeChange() {
let totalDimension = this.getTotalDimension();
this.setState({
totalDimension
});
this.highlight && this.setState({
highlightInitialDimension: this.getHighlightDim()
}, () => {
this.moveHighlight();
});
}
onResize(timer) {
let {
isResponsive
} = this.props;
if (this.tabsEle && isResponsive) {
if (!timer) {
this.onSizeChange();
} else {
setTimeout(() => {
this.onSizeChange();
}, timer);
}
}
}
getTotalDimension() {
let {
align
} = this.props;
let totalDimension = this.tabsEle && getTotalDimension(this.tabsEle, align);
return totalDimension;
}
getTabDimensions() {
let {
children,
align,
childType
} = this.props;
let tabDimensions = {};
React.Children.forEach(children, child => /*#__PURE__*/React.isValidElement(child) && child.type === childType && this[child.props.id] && Object.assign(tabDimensions, {
[child.props.id]: this[child.props.id] && getTotalDimension(this[child.props.id], align)
}));
return tabDimensions;
}
responsiveTab(totalDimension, tabDimensions) {
let {
children,
maxTabsCount,
minTabsCount,
isResponsive
} = this.props;
let mainTabs = [],
moreTabs = [],
otherTabs = [];
if (totalDimension && isResponsive) {
let {
childType,
selectedTab
} = this.props;
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; */
this.setMaxDim(totalDimension);
}
} else if (!isResponsive) {
let {
childType
} = this.props;
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
};
}
getTabRef(refName, ele, isVirtual) {
this[refName] = ele;
let key = isVirtual ? `virtual_${refName}` : refName;
if (ele) {
this.tabObserver[key] = new ResizeObserver(this.onTabResize);
this.tabObserver[key].observe(ele);
} else {
if (this.tabObserver && this.tabObserver[key]) {
this.tabObserver[key].disconnect();
}
}
}
moreTabSelect(tab, value, index, e) {
let {
onSelect,
closePopupOnly
} = this.props;
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();
}
onScroll(e) {
e.persist();
let {
onScroll
} = this.props;
onScroll && throttle(onScroll, 100, e);
}
togglePopup(e) {
let {
boxPosition,
togglePopup,
align,
removeClose
} = this.props;
removeClose && removeClose(e);
boxPosition = boxPosition ? boxPosition : TAB_POPUP_POSITION_MAPPING[align];
togglePopup(e, boxPosition);
}
responsiveFunc(_ref) {
let {
mediaQueryOR
} = _ref;
return {
tabletMode: mediaQueryOR([{
maxWidth: 700
}])
};
}
handleChange(value, e) {
this.setState({
searchValue: value
});
}
handleSearchValueClear(e) {
this.setState({
searchValue: ''
});
}
getMoreList(_ref2) {
let {
moreTabs
} = _ref2;
const validElements = [];
React.Children.map(moreTabs, child => {
const isValidElement = /*#__PURE__*/React.isValidElement(child);
if (isValidElement) {
validElements.push(child.props);
}
});
return validElements;
}
handleFilterSuggestions(_ref3) {
let {
moreTabs
} = _ref3;
const {
showTitleInMoreOptions
} = this.props;
const {
searchValue
} = this.state;
const options = this.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;
}
renderEmptyState() {
const {
searchErrorText
} = this.props;
return /*#__PURE__*/React.createElement("div", {
className: tabsStyle.emptyStateContainer
}, /*#__PURE__*/React.createElement(EmptySearch, {
size: "small"
}), /*#__PURE__*/React.createElement("div", {
className: tabsStyle.emptyStateTitle
}, searchErrorText));
}
renderTabs(mainTabs, moreTabs, isVirtual) {
let {
selectedTab,
moreButtonClass,
moreButtonActiveClass,
moreBoxClass,
menuItemClass,
itemClass,
itemActiveClass,
iconName,
isPopupReady,
isPopupOpen,
getContainerRef,
showTitleInMoreOptions,
onSelect,
moreContainerClass,
align,
type,
isAnimate,
needTabBorder,
needAppearance,
iconSize,
getTargetRef,
position,
customProps,
getCustomDropBoxHeaderPlaceHolder,
dataSelectorId,
dataId,
placeHolderText,
searchBoxSize,
removeClose,
isAbsolutePositioningNeeded,
isRestrictScroll,
positionsOffset,
targetOffset
} = this.props;
const {
searchValue
} = this.state;
let {
DropBoxProps = {},
ListItemProps = {},
MoreButtonProps = {},
TextBoxIconProps = {}
} = customProps;
let popupClass = TAB_DIRECTION_MAPPING[align] === 'column' ? tabsStyle[position] ? tabsStyle[position] : '' : '';
const moreTabsListItems = this.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: this.getTabRef,
onSelect: onSelect,
recalculateDimension: this.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: this.togglePopup,
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: this.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,
dataId: `${dataId}_dropbox`,
onClick: removeClose,
onClose: this.togglePopup
}, getCustomDropBoxHeaderPlaceHolder && getCustomDropBoxHeaderPlaceHolder(this.props), hasSearch ? /*#__PURE__*/React.createElement(Box, {
className: tabsStyle.search
}, /*#__PURE__*/React.createElement(TextBoxIcon, {
placeHolder: placeHolderText,
onChange: this.handleChange,
value: searchValue,
onClear: this.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: this.onScroll,
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: this.moreTabSelect,
id: id,
title: title || text,
isLink: isLink,
href: href,
autoHover: true,
customClass: {
customListItem: menuItemClass
},
target: "self",
dataId: `${dataId}_Tab`,
...ListItemProps
}, !showTitleInMoreOptions ? children : null);
}) : this.renderEmptyState()));
})) : null);
}
render() {
let {
style,
className,
dataId,
highlightClass,
type,
isAnimate,
needTabBorder,
needBorder,
needPadding,
align,
needAppearance,
children,
containerClass,
dataSelectorId
} = this.props;
let {
totalDimension,
tabDimensions,
renderVirtualTabs
} = this.state;
let {
mainTabs,
moreTabs,
otherTabs
} = this.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: TAB_DIRECTION_MAPPING[align] === 'column' ? 'row' : 'column',
className: tabsClass,
style: style
}, /*#__PURE__*/React.createElement(Box, {
eleRef: this.getTabsRef,
flexible: true
}, renderVirtualTabs && /*#__PURE__*/React.createElement(Container, {
alignBox: TAB_DIRECTION_MAPPING[align] === 'column' ? 'row' : 'column',
className: tabsStyle.hidden
}, this.renderTabs(children, [], true)), /*#__PURE__*/React.createElement(Container, {
alignBox: TAB_DIRECTION_MAPPING[align] === 'column' ? 'row' : 'column'
}, this.renderTabs(mainTabs, moreTabs, false))), otherTabs.length ? /*#__PURE__*/React.createElement(Box, null, otherTabs) : null, isAnimate && needTabBorder && needAppearance && /*#__PURE__*/React.createElement("div", {
className: hlClass,
ref: this.getHighlightRef,
"data-id": dataId,
"data-test-id": dataId
})));
}
}
Tabs.propTypes = Tabs_propTypes;
Tabs.defaultProps = { ...Tabs_defaultProps,
childType: Tab
};
export default Popup(Tabs);