// @flow import * as React from 'react'; import classNames from 'classnames'; import omit from 'lodash/omit'; import uniqueId from 'lodash/uniqueId'; import IconPageBack from '../../icons/general/IconPageBack'; import IconPageForward from '../../icons/general/IconPageForward'; import LinkButton from '../link/LinkButton'; import './Tabs.scss'; export const TAB_KEY = 'Tab'; export const TAB_PANEL_ROLE = 'tabpanel'; type Props = { children: React.Node, className?: string, focusedIndex: number, isDynamic?: boolean, onKeyUp?: Function, onTabFocus: Function, onTabSelect?: Function, resetActiveTab: Function, resetFocusedTab: Function, selectedIndex: number, }; type State = { tabsContainerOffsetLeft: number, }; class TabViewPrimitive extends React.Component { constructor(props: Props) { super(props); this.tabviewID = uniqueId('tabview'); this.state = { tabsContainerOffsetLeft: 0, }; } componentDidMount() { const { isDynamic, focusedIndex } = this.props; if (isDynamic) { // set initial tabsContainerOffsetLeft state after first mounting this.scrollToTab(focusedIndex); } } componentDidUpdate(prevProps: Props) { const { focusedIndex: prevFocusedIndex, selectedIndex: prevSelectedIndex } = prevProps; const { focusedIndex, selectedIndex } = this.props; if (this.props.isDynamic) { if (prevFocusedIndex !== focusedIndex) { this.scrollToTab(focusedIndex); } // update tabsContainerOffsetLeft state when receiving a new prop if (prevSelectedIndex !== selectedIndex) { this.scrollToTab(selectedIndex); } } if (prevFocusedIndex !== focusedIndex) { // have to focus after render otherwise, the focus will be lost this.focusOnTabElement(focusedIndex); } } onClickTab = (tabIndex: number) => { const { onTabFocus, onTabSelect } = this.props; if (onTabSelect) { onTabSelect(tabIndex); } onTabFocus(tabIndex); }; getLastElementsAnchorPoint = () => { if (this.tabsElements.length === 0) { return 0; } const lastTabElement = this.tabsElements[this.tabsElements.length - 1]; return lastTabElement.offsetLeft + lastTabElement.offsetWidth; }; getTabsContainerOffsetLeft = () => { if (!this.tabsContainer) { return 0; } const { tabsContainerOffsetLeft } = this.state; let viewportOffset = parseInt(tabsContainerOffsetLeft, 10) * -1; viewportOffset = viewportOffset || 0; return viewportOffset; }; getTabsContainerWidth = () => (this.tabsContainer ? parseInt(this.tabsContainer.offsetWidth, 10) : 0); tabviewID: string; scrollToTab = (tabIndex: number) => { if ( !this.props.isDynamic || this.tabsContainer === null || this.tabsElements.length === 0 || tabIndex < 0 || tabIndex > this.tabsElements.length - 1 ) { return; } const tabElementOfInterest = this.tabsElements[tabIndex]; const lastElementsAnchorPoint = this.getLastElementsAnchorPoint(); // if tabs don't overflow at all, no need to scroll const tabsContainerWidth = this.getTabsContainerWidth(); if (lastElementsAnchorPoint <= tabsContainerWidth) { this.setState({ tabsContainerOffsetLeft: 0 }); return; } // do not scroll any more if we will go past the rightmost anchor const newOffset = Math.min(lastElementsAnchorPoint - tabsContainerWidth, tabElementOfInterest.offsetLeft); // move the viewport const newViewportOffset = -1 * newOffset; this.setState({ tabsContainerOffsetLeft: newViewportOffset }); }; isRightArrowVisible = () => { if (!this.tabsContainer) { return false; } const tabsContainerOffsetLeft = this.getTabsContainerOffsetLeft(); const lastElementsAnchorPoint = this.getLastElementsAnchorPoint(); const tabsContainerWidth = this.getTabsContainerWidth(); return tabsContainerOffsetLeft + tabsContainerWidth < lastElementsAnchorPoint; }; isLeftArrowVisible = () => { const { focusedIndex, selectedIndex } = this.props; const tabsContainerOffsetLeft = this.getTabsContainerOffsetLeft(); return tabsContainerOffsetLeft !== 0 && (selectedIndex !== 0 || focusedIndex !== 0); }; focusOnTabElement = (focusedIndex: number) => { if (focusedIndex + 1 > this.tabsElements.length || focusedIndex < 0) { return; } this.tabsElements[focusedIndex].focus(); }; tabsElements = []; tabsContainer = null; handleKeyDown = (event: SyntheticKeyboardEvent<>) => { const { children, focusedIndex, onTabFocus, resetFocusedTab, resetActiveTab } = this.props; const childrenCount = React.Children.count(children); switch (event.key) { case 'ArrowRight': onTabFocus(this.calculateNextIndex(focusedIndex, childrenCount)); event.preventDefault(); event.stopPropagation(); break; case 'ArrowLeft': onTabFocus(this.calculatePrevIndex(focusedIndex, childrenCount)); event.preventDefault(); event.stopPropagation(); break; case 'Escape': resetActiveTab(); break; case TAB_KEY: resetFocusedTab(); break; default: break; } }; calculateNextIndex = (currentIndex: number, childrenCount: number) => (currentIndex + 1) % childrenCount; calculatePrevIndex = (currentIndex: number, childrenCount: number) => (currentIndex - 1 + childrenCount) % childrenCount; /* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role */ renderTabs() { const { children, selectedIndex, isDynamic } = this.props; const { tabsContainerOffsetLeft } = this.state; const style = isDynamic ? { left: `${tabsContainerOffsetLeft}px` } : {}; return (
{ this.tabsContainer = ref; }} style={style} onKeyDown={!isDynamic ? this.handleKeyDown : null} > {React.Children.map(children, (tab, i) => { const buttonProps = omit(tab.props, ['className', 'children', 'title']); const classes = classNames('btn-plain', 'tab', i === selectedIndex ? 'is-selected' : ''); const ariaControls = `${this.tabviewID}-panel-${i + 1}`; const ariaSelected = i === selectedIndex; const id = `${this.tabviewID}-tab-${i + 1}`; const { href, component, refProp } = tab.props; const tabIndex = i === selectedIndex ? '0' : '-1'; if (href) { return ( { this.tabsElements[i] = ref; }} refProp={refProp} tabIndex={tabIndex} to={href} component={component} >
{tab.props.title}
); } return ( ); })}
); } /* eslint-enable jsx-a11y/no-noninteractive-element-to-interactive-role */ renderDynamicTabs() { const { onTabFocus, focusedIndex } = this.props; return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
{this.renderTabs()}
); } render() { const { children, className = '', isDynamic = false, onKeyUp, selectedIndex } = this.props; return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
{!isDynamic ? this.renderTabs() : this.renderDynamicTabs()}
{React.Children.toArray(children).map((child, i) => (
{child.props.children}
))}
); } } export default TabViewPrimitive;