src/components/Tabset/index.js
/* eslint-disable react/no-unused-state */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Provider } from './context';
import RenderIf from '../RenderIf';
import { LEFT_KEY, RIGHT_KEY } from '../../libs/constants';
import {
getChildTabNodes,
insertChildOrderly,
getTabIndexFromName,
getChildrenTotalWidth,
getChildrenTotalWidthUpToClickedTab,
isNotSameChildren,
getUpdatedTabsetChildren,
getRightButtonDisabledState,
getLeftButtonDisabledState,
} from './utils';
import RightThinChevron from './rightThinChevron';
import LeftThinChevron from './leftThinChevron';
import ResizeSensor from '../../libs/ResizeSensor';
import debounce from '../../libs/debounce';
import StyledContainer from './styled/container';
import StyledObserver from './styled/observer';
import StyledTabset from './styled/tabset';
import StyledInnerContainer from './styled/innerContainer';
import StyledButtonGroup from './styled/buttonGroup';
import StyledButtonIcon from './styled/buttonIcon';
const RIGHT_SIDE = 1;
const LEFT_SIDE = -1;
/**
* Tabs make it easy to explore and switch between different views.
* @category Layout
*/
export default class Tabset extends Component {
constructor(props) {
super(props);
this.state = {
key: Date.now(),
areButtonsVisible: false,
};
this.isFirstTime = true;
this.tabsetRef = React.createRef();
this.resizeTarget = React.createRef();
this.registerTab = this.registerTab.bind(this);
this.unRegisterTab = this.unRegisterTab.bind(this);
this.updateTab = this.updateTab.bind(this);
this.handleKeyPressed = this.handleKeyPressed.bind(this);
this.handleLeftButtonClick = this.handleLeftButtonClick.bind(this);
this.handleRightButtonClick = this.handleRightButtonClick.bind(this);
this.updateButtonsVisibility = this.updateButtonsVisibility.bind(this);
this.handleSelect = this.handleSelect.bind(this);
this.keyHandlerMap = {
[RIGHT_KEY]: () => this.selectTab(RIGHT_SIDE),
[LEFT_KEY]: () => this.selectTab(LEFT_SIDE),
};
this.tabsetChildren = [];
}
componentDidMount() {
this.widthObserver = new ResizeSensor(
this.resizeTarget.current,
debounce(this.updateButtonsVisibility, 100),
);
}
componentDidUpdate(prevProp) {
const { children } = this.props;
const { isFirstTime } = this;
const areAllChildrenRegistered = children.length === this.tabsetChildren.length;
if (isNotSameChildren(children, prevProp.children)) {
this.updateButtonsVisibility();
}
if (areAllChildrenRegistered && isFirstTime) {
this.updateButtonsVisibility();
this.isFirstTime = false;
}
}
componentWillUnmount() {
this.widthObserver.detach(this.resizeTarget.current);
}
setAsSelectedTab(tabIndex) {
this.tabsetChildren[tabIndex].ref.click();
this.tabsetChildren[tabIndex].ref.focus();
}
updateButtonsVisibility() {
const { areButtonsVisible, variant } = this.state;
const tabset = this.tabsetRef.current;
const { offsetWidth: resizeWidth } = this.resizeTarget.current;
const { scrollWidth, scrollLeft, offsetWidth: tabsetWidth } = tabset;
const childrenTotalWidth = getChildrenTotalWidth(this.tabsetChildren);
const buttonWidth = areButtonsVisible ? 94 : 0;
const padding = resizeWidth - tabsetWidth - buttonWidth;
const delta = variant === 'line' ? 0 : 1;
const showButtons = childrenTotalWidth > resizeWidth - padding + delta;
this.screenWidth = window.innerWidth;
this.scrollLeft = scrollLeft;
this.maxScroll = scrollWidth - tabsetWidth;
this.tabsetWidth = tabsetWidth;
this.setState({ areButtonsVisible: showButtons });
}
handleKeyPressed(event) {
if (this.keyHandlerMap[event.keyCode]) {
return this.keyHandlerMap[event.keyCode]();
}
return null;
}
selectTab(side) {
const { activeTabName } = this.props;
const { tabsetChildren } = this;
const activeTabIndex = getTabIndexFromName(tabsetChildren, activeTabName);
if (activeTabIndex === tabsetChildren.length - 1 && side === RIGHT_SIDE) {
this.setAsSelectedTab(0);
} else if (activeTabIndex === 0 && side === LEFT_SIDE) {
this.setAsSelectedTab(tabsetChildren.length - 1);
} else {
this.setAsSelectedTab(activeTabIndex + side);
}
}
isLeftButtonDisabled() {
const { activeTabName } = this.props;
const { tabsetChildren } = this;
const { screenWidth, scrollLeft } = this;
return getLeftButtonDisabledState({
activeTabName,
tabsetChildren,
screenWidth,
scrollLeft,
});
}
isRightButtonDisabled() {
const { activeTabName } = this.props;
const { tabsetChildren } = this;
const { screenWidth, scrollLeft, maxScroll } = this;
return getRightButtonDisabledState({
activeTabName,
tabsetChildren,
screenWidth,
scrollLeft,
maxScroll,
});
}
handleRightButtonClick() {
const { screenWidth, tabsetWidth, scrollLeft } = this;
if (screenWidth > 600) {
return this.tabsetRef.current.scrollTo(scrollLeft + tabsetWidth, 0);
}
return this.selectTab(RIGHT_SIDE);
}
handleLeftButtonClick() {
const { screenWidth, tabsetWidth, scrollLeft } = this;
if (screenWidth > 600) {
return this.tabsetRef.current.scrollTo(scrollLeft - tabsetWidth, 0);
}
return this.selectTab(LEFT_SIDE);
}
updateTab(tab, nameToUpdate) {
const { tabsetChildren } = this;
const newTabsetChildren = getUpdatedTabsetChildren(tabsetChildren, tab, nameToUpdate);
this.tabsetChildren = newTabsetChildren;
this.setState({ key: Date.now() });
}
registerTab(tab) {
const { tabsetChildren } = this;
const [...nodes] = getChildTabNodes(this.tabsetRef.current);
const newChildrenRefs = insertChildOrderly(tabsetChildren, tab, nodes);
this.tabsetChildren = newChildrenRefs;
this.setState({ key: Date.now() });
}
unRegisterTab(tabName) {
const { tabsetChildren } = this;
const newTabsetChildren = tabsetChildren.filter(tab => tab.name !== tabName);
this.tabsetChildren = newTabsetChildren;
this.setState({ key: Date.now() });
}
scrollToSelectedTab(name) {
const { tabsetChildren } = this;
const tabset = this.tabsetRef.current;
const { scrollLeft, offsetWidth: tabsetWidth } = tabset;
const tabIndex = getTabIndexFromName(tabsetChildren, name);
const isFirstTab = tabIndex === 0;
if (isFirstTab) {
this.tabsetRef.current.scrollTo(0, 0);
} else {
const totalWidthUpToCurrentTab = getChildrenTotalWidthUpToClickedTab(
tabsetChildren,
tabIndex + 1,
);
const totalWidthUpToPrevTab = getChildrenTotalWidthUpToClickedTab(
tabsetChildren,
tabIndex,
);
const tabsetWidthUpToCurrentTab = tabsetWidth + scrollLeft;
const isCurrentTabOutOfViewOnRightSide =
totalWidthUpToCurrentTab > tabsetWidthUpToCurrentTab - 20;
const isCurrentTabOutOfViewOnLeftSide = scrollLeft > totalWidthUpToPrevTab;
if (isCurrentTabOutOfViewOnLeftSide) {
this.tabsetRef.current.scrollTo(totalWidthUpToPrevTab, 0);
}
if (isCurrentTabOutOfViewOnRightSide) {
const moveScroll = totalWidthUpToCurrentTab - tabsetWidthUpToCurrentTab + 20;
this.tabsetRef.current.scrollTo(scrollLeft + moveScroll, 0);
}
}
}
handleSelect(event, name) {
const { onSelect } = this.props;
this.scrollToSelectedTab(name);
onSelect(event, name);
}
render() {
const { activeTabName, fullWidth, variant, children, style, className, id } = this.props;
const { areButtonsVisible } = this.state;
const { screenWidth } = this;
const showButtons = areButtonsVisible || screenWidth < 600;
const context = {
activeTabName,
onSelect: this.handleSelect,
privateRegisterTab: this.registerTab,
privateUnRegisterTab: this.unRegisterTab,
privateUpdateTab: this.updateTab,
fullWidth,
variant,
};
return (
<StyledContainer variant={variant} className={className} style={style} id={id}>
<StyledObserver ref={this.resizeTarget} />
<StyledTabset variant={variant}>
<StyledInnerContainer
fullWidth={fullWidth}
role="tablist"
onKeyDown={this.handleKeyPressed}
onScroll={this.updateButtonsVisibility}
ref={this.tabsetRef}
>
<Provider value={context}>{children}</Provider>
</StyledInnerContainer>
<RenderIf isTrue={showButtons}>
<StyledButtonGroup>
<StyledButtonIcon
icon={<LeftThinChevron />}
disabled={this.isLeftButtonDisabled()}
onClick={this.handleLeftButtonClick}
assistiveText="previus tab button"
variant="border-filled"
/>
<StyledButtonIcon
icon={<RightThinChevron />}
disabled={this.isRightButtonDisabled()}
onClick={this.handleRightButtonClick}
assistiveText="next tab button"
variant="border-filled"
/>
</StyledButtonGroup>
</RenderIf>
</StyledTabset>
</StyledContainer>
);
}
}
Tabset.propTypes = {
/** The name of the tab that is selected. It must match the name of the tab. */
activeTabName: PropTypes.string,
/** Action fired when an item is selected.
* The event params include the `name` of the selected item. */
onSelect: PropTypes.func,
/** If true, the tabs will grow to use all the available space.
* This value defaults to false. */
fullWidth: PropTypes.bool,
/** The variant changes the appearance of the Tabset. Accepted variants include card and line. The default value is card. */
variant: PropTypes.oneOf(['card', 'line']),
/** The id of the outer element. */
id: PropTypes.string,
/** A CSS class for the outer element, in addition to the component's base classes. */
className: PropTypes.string,
/** An object with custom style applied for the outer element. */
style: PropTypes.object,
/**
* This prop that should not be visible in the documentation.
* @ignore
*/
children: PropTypes.node,
};
Tabset.defaultProps = {
activeTabName: undefined,
onSelect: () => {},
fullWidth: false,
variant: 'card',
className: undefined,
style: undefined,
children: null,
id: undefined,
};