UNPKG

@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
/* 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);