UNPKG

@hackplan/polaris

Version:

Shopify’s product component library

185 lines (184 loc) 8.44 kB
import React from 'react'; import { HorizontalDotsMinor } from '@shopify/polaris-icons'; import { classNames } from '../../utilities/css'; import Icon from '../Icon'; import Popover from '../Popover'; import { withAppProvider } from '../AppProvider'; import { getVisibleAndHiddenTabIndices } from './utilities'; import { List, Panel, Tab, TabMeasurer } from './components'; import styles from './Tabs.scss'; class Tabs extends React.PureComponent { constructor() { super(...arguments); this.state = { disclosureWidth: 0, containerWidth: Infinity, tabWidths: [], visibleTabs: [], hiddenTabs: [], showDisclosure: false, tabToFocus: -1, }; this.handleKeyPress = (event) => { const { tabToFocus, visibleTabs, hiddenTabs } = this.state; const tabsArrayInOrder = visibleTabs.concat(hiddenTabs); const key = event.key; let newFocus = tabsArrayInOrder.indexOf(tabToFocus); if (key === 'ArrowRight' || key === 'ArrowDown') { newFocus += 1; if (newFocus === tabsArrayInOrder.length) { newFocus = 0; } } if (key === 'ArrowLeft' || key === 'ArrowUp') { if (newFocus === -1 || newFocus === 0) { newFocus = tabsArrayInOrder.length - 1; } else { newFocus -= 1; } } this.setState({ showDisclosure: hiddenTabs.indexOf(tabsArrayInOrder[newFocus]) > -1, tabToFocus: tabsArrayInOrder[newFocus], }); }; this.renderTabMarkup = (tab, index) => { const { selected } = this.props; const { tabToFocus } = this.state; return (<Tab key={`${index}-${tab.id}`} id={tab.id} siblingTabHasFocus={tabToFocus > -1} focused={index === tabToFocus} selected={index === selected} onClick={this.handleTabClick} panelID={tab.panelID || `${tab.id}-panel`} accessibilityLabel={tab.accessibilityLabel} url={tab.url}> {tab.content} </Tab>); }; this.handleFocus = (event) => { const { selected, tabs } = this.props; // If we are explicitly focusing one of the non-selected tabs, use it // move the focus to it const target = event.target; if (target.classList.contains(styles.Tab) || target.classList.contains(styles.Item)) { let tabToFocus = -1; tabs.every((tab, index) => { if (tab.id === target.id) { tabToFocus = index; return false; } return true; }); this.setState({ tabToFocus }); return; } if (target.classList.contains(styles.DisclosureActivator)) { return; } // If we are coming in from somewhere other than another tab, focus the // selected tab, and the focus (click) is not on the disclosure activator, // focus the selected tab if (!event.relatedTarget) { this.setState({ tabToFocus: selected }); return; } const relatedTarget = event.relatedTarget; if (!relatedTarget.classList.contains(styles.Tab) && !relatedTarget.classList.contains(styles.Item) && !relatedTarget.classList.contains(styles.DisclosureActivator)) { this.setState({ tabToFocus: selected }); } }; this.handleBlur = (event) => { // If we blur and the target is not another tab, forget the focus position if (event.relatedTarget == null) { this.setState({ tabToFocus: -1 }); return; } const target = event.relatedTarget; // If we are going to anywhere other than another tab, lose the last focused tab if (!target.classList.contains(styles.Tab) && !target.classList.contains(styles.Item)) { this.setState({ tabToFocus: -1 }); } }; this.handleDisclosureActivatorClick = () => { this.setState(({ showDisclosure }) => ({ showDisclosure: !showDisclosure })); }; this.handleClose = () => { this.setState({ showDisclosure: false, }); }; this.handleMeasurement = (measurements) => { const { tabs, selected } = this.props; const { tabToFocus } = this.state; const { hiddenTabWidths: tabWidths, containerWidth, disclosureWidth, } = measurements; const { visibleTabs, hiddenTabs } = getVisibleAndHiddenTabIndices(tabs, selected, disclosureWidth, tabWidths, containerWidth); this.setState({ tabToFocus: tabToFocus === -1 ? -1 : selected, visibleTabs, hiddenTabs, disclosureWidth, containerWidth, tabWidths, }); }; this.handleTabClick = (id) => { const { tabs, onSelect = noop } = this.props; const tab = tabs.find((aTab) => aTab.id === id); if (tab == null) { return; } const selectedIndex = tabs.indexOf(tab); onSelect(selectedIndex); }; } static getDerivedStateFromProps(nextProps, prevState) { const { disclosureWidth, tabWidths, containerWidth } = prevState; const { visibleTabs, hiddenTabs } = getVisibleAndHiddenTabIndices(nextProps.tabs, nextProps.selected, disclosureWidth, tabWidths, containerWidth); return { visibleTabs, hiddenTabs, selected: nextProps.selected, }; } render() { const { tabs, selected, fitted, children, polaris: { intl }, } = this.props; const { tabToFocus, visibleTabs, hiddenTabs, showDisclosure } = this.state; const disclosureTabs = hiddenTabs.map((tabIndex) => tabs[tabIndex]); const panelMarkup = children ? (<Panel id={tabs[selected].panelID || `${tabs[selected].id}-panel`} tabID={tabs[selected].id}> {children} </Panel>) : null; const tabsMarkup = visibleTabs .sort((tabA, tabB) => tabA - tabB) .map((tabIndex) => this.renderTabMarkup(tabs[tabIndex], tabIndex)); const disclosureActivatorVisible = visibleTabs.length < tabs.length; const classname = classNames(styles.Tabs, fitted && styles.fitted, disclosureActivatorVisible && styles.fillSpace); const disclosureTabClassName = classNames(styles.DisclosureTab, disclosureActivatorVisible && styles['DisclosureTab-visible']); const activator = (<button tabIndex={-1} className={styles.DisclosureActivator} onClick={this.handleDisclosureActivatorClick} aria-label={intl.translate('Polaris.Tabs.toggleTabsLabel')}> <Icon source={HorizontalDotsMinor}/> </button>); return (<div> <ul role="tablist" className={classname} onFocus={this.handleFocus} onBlur={this.handleBlur} onKeyDown={handleKeyDown} onKeyUp={this.handleKeyPress}> {tabsMarkup} <li role="presentation" className={disclosureTabClassName}> <Popover preferredPosition="below" activator={activator} active={disclosureActivatorVisible && showDisclosure} onClose={this.handleClose}> <List focusIndex={hiddenTabs.indexOf(tabToFocus)} disclosureTabs={disclosureTabs} onClick={this.handleTabClick} onKeyPress={this.handleKeyPress}/> </Popover> </li> </ul> <TabMeasurer tabToFocus={tabToFocus} activator={activator} selected={selected} tabs={tabs} siblingTabHasFocus={tabToFocus > -1} handleMeasurement={this.handleMeasurement}/> {panelMarkup} </div>); } } Tabs.Panel = Panel; function noop() { } function handleKeyDown(event) { const { key } = event; if (key === 'ArrowUp' || key === 'ArrowDown' || key === 'ArrowLeft' || key === 'ArrowRight') { event.preventDefault(); event.stopPropagation(); } } export default withAppProvider()(Tabs);