UNPKG

@targetprocess/tabs

Version:

400 lines (337 loc) 11.5 kB
import React from 'react' import PropTypes from 'prop-types' import classNames from 'classnames' import ResizeDetector from 'react-resize-detector' import ShowMore from './ShowMore' import {TabPanel} from './TabPanel' import TabInner from './TabInner' import styles from './style/index.css' import {throttle} from '../../base/utils/throttle' import {debounce} from '../../base/utils/debounce' const tabPrefix = 'tab-' const panelPrefix = 'panel-' // By itself, Tab is just a dummy component that is used for a cleaner Tabs API // and as marker for actual tabs to prevent storing them as huge array export const Tab = () => null Tab.propTypes = { header: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, id: PropTypes.string.isRequired, children: PropTypes.node, disabled: PropTypes.bool } /** * Tabs component is an aggregate root for several content panes * (via Tabs.Tab component), each with its own children, and a header * to switch between them. * * Each `<Tab />` must have a `header` (that can be arbitary react element but * is most commonly just a string), `id` and regular children. * * Top-level `<Tabs />` can work in either controlled or uncontrolled mode. * In controlled mode, it uses `initialActiveTabId` to determine which * tab is open by default, and then manages manually manages changing tabs * on appropriate header clicks. * In uncontrolled mode, it uses `activeTabId` prop to determine which tab * is open and exposes `onActiveTabChanged` callback that is invoked each time * a user clicks on a tab. **/ export class Tabs extends React.Component { static propTypes = { /** Name of some important part of the component */ name: PropTypes.string.isRequired, activeTabId: PropTypes.string, initialActiveTabId: PropTypes.string, onActiveTabChanged: PropTypes.func, resizeThrottle: PropTypes.number, tabSizeTimeout: PropTypes.number, className: PropTypes.string } static defaultProps = { resizeThrottle: 100, tabSizeTimeout: 200 } tabRefs = {} activeTabElement = null state = { activeTabId: this.props.initialActiveTabId, sliderLeft: 0, sliderWidth: 0, tabDimensions: {}, blockWidth: 0, tabsTotalWidth: 0, showMoreWidth: 40, focusedTabKey: null } isControlledMode = () => typeof this.props.activeTabId !== 'undefined' changeTab = tabId => { this.setState({activeTabId: tabId}) if (typeof this.props.onActiveTabChanged === 'function') { this.props.onActiveTabChanged(tabId) } } componentDidMount() { setTimeout(() => { this.setTabsDimensions() this.adjustSlider() }, this.props.tabSizeTimeout) } componentDidUpdate(previousProps, previousState) { const activeTabChanged = this.isControlledMode() ? this.props.activeTabId !== previousProps.activeTabId : this.state.activeTabId !== previousState.activeTabId const {blockWidth} = this.state if (activeTabChanged) { this.adjustSlider() } if (!blockWidth) { this.setTabsDimensions() } } shouldComponentUpdate(nextProps, nextState) { const { activeTabId, blockWidth, showMoreWidth, sliderLeft, sliderWidth, tabsTotalWidth, focusedTabKey } = this.state const { initialActiveTabId, onActiveTabChanged, resizeThrottle, tabSizeTimeout, children } = this.props return ( nextState.blockWidth !== blockWidth || nextState.showMoreWidth !== showMoreWidth || nextState.sliderLeft !== sliderLeft || nextState.sliderWidth !== sliderWidth || nextState.tabsTotalWidth !== tabsTotalWidth || nextState.focusedTabKey !== focusedTabKey || nextState.activeTabId !== activeTabId || nextProps.initialActiveTabId !== initialActiveTabId || nextProps.onActiveTabChanged !== onActiveTabChanged || nextProps.resizeThrottle !== resizeThrottle || nextProps.tabSizeTimeout !== tabSizeTimeout || nextProps.children !== children ) } setTabsDimensions = () => { window.requestAnimationFrame(() => { if (!this.tabsWrapper) { // it shouldn't happens evern. Just paranoic check return } // initial wrapper width calculation const blockWidth = this.tabsWrapper.offsetWidth // calculate width and offset for each tab let tabsTotalWidth = 0 const tabDimensions = {} Object.keys(this.tabRefs).forEach(key => { if (this.tabRefs[key]) { const width = this.tabRefs[key].tab.offsetWidth tabDimensions[key.replace(tabPrefix, '')] = {width, offset: tabsTotalWidth} tabsTotalWidth += width } }) this.setState({tabDimensions, tabsTotalWidth, blockWidth}) }) } adjustSlider = () => { window.requestAnimationFrame(() => { if (this.activeTabElement && this.activeTabElement.tab) { this.setState({ sliderLeft: this.activeTabElement.tab.firstChild.offsetLeft, sliderWidth: this.activeTabElement.tab.firstChild.clientWidth }) } else { this.setState({sliderLeft: 0, sliderWidth: 0}) } }) } onResizeThrottled = throttle(() => { if (this.tabsWrapper) { this.setState({blockWidth: this.tabsWrapper.offsetWidth}) } this.adjustSlider() }, this.props.resizeThrottle) handleHeaderResize = debounce(() => { this.adjustSlider() }, 50) renderTabHeader = headerProp => { const isRenderProp = typeof headerProp === 'function' if (!isRenderProp) { return headerProp } return headerProp({ onResize: this.handleHeaderResize }) } getTabs = allTabs => { const {blockWidth, tabsTotalWidth, tabDimensions, showMoreWidth} = this.state const activeTabId = this.getActiveTabId(allTabs) const availableWidth = blockWidth - (tabsTotalWidth > blockWidth ? showMoreWidth : 0) return allTabs.reduce( (result, item) => { const {id, header, children, disabled} = item.props const selected = activeTabId === id const payload = {tabIndex: result.tabIndex, selected, disabled, id} const tabPayload = { ...payload, header } const panelPayload = { ...payload, children } const tabWidth = tabDimensions[id] ? tabDimensions[id].width : 0 /* eslint-disable no-param-reassign */ if ( // initial call !blockWidth || // all tabs are fit into the block blockWidth > tabsTotalWidth || // current tab fit into the block result.availableWidth - tabWidth - showMoreWidth > 0 ) { result.tabsVisible.push(tabPayload) } else { result.tabsHidden.push(tabPayload) if (selected) { result.isSelectedTabHidden = true } } /* eslint-enable no-param-reassign */ result.panels[id] = panelPayload // eslint-disable-line no-param-reassign result.availableWidth -= tabWidth result.tabIndex += 1 return result }, { tabsVisible: [], tabsHidden: [], panels: {}, isSelectedTabHidden: false, tabIndex: 0, availableWidth } ) } getActiveTabId = tabs => { const [firstTab] = tabs if (this.isControlledMode()) { const tabId = this.props.activeTabId if (tabId && tabs.some(tab => tab.props.id === tabId)) { return tabId } return firstTab.props.id } const {activeTabId} = this.state if (typeof activeTabId === 'undefined') { if (!(firstTab && firstTab.props)) { return null } return firstTab.props.id || null } return activeTabId } onFocusTab = focusedTabKey => () => this.setState({focusedTabKey}) onBlurTab = () => this.setState({focusedTabKey: null}) onKeyDown = event => { if (event.key === 'Enter' && this.state.focusedTabKey !== null) { this.changeTab(this.state.focusedTabKey) } } onClick = tab => !tab.disabled && this.changeTab(tab.id) getTabProps = ({header, id, selected, tabIndex, disabled}) => ({ selected, tabIndex, children: this.renderTabHeader(header), key: tabPrefix + id, id: tabPrefix + id, ref: tab => { if (selected) { this.activeTabElement = tab } return (this.tabRefs[tabPrefix + id] = tab) }, originalKey: id, onClick: () => this.onClick({disabled, id}), getOnFocusCallback: this.onFocusTab, onBlur: this.onBlurTab, panelId: panelPrefix + id, classNames: classNames( styles['tabs__header__tab-item'], selected && styles['tabs__header__tab-item--active'], disabled && styles['tabs__header__tab-item--disabled'] ) }) getPanelProps = ({id, children}) => ({ children, key: panelPrefix + id, id: panelPrefix + id, tabId: tabPrefix + id }) showMoreChanged = element => { if (!element) { return } const showMoreWidth = element.offsetWidth if (this.state.showMoreWidth === showMoreWidth) { return } this.setState({ showMoreWidth }) } getShowMoreProps = isSelectedTabHidden => ({ onShowMoreChanged: this.showMoreChanged, hasChildSelected: isSelectedTabHidden }) getSliderStyle = isSelectedTabHidden => ({ left: isSelectedTabHidden ? '100%' : this.state.sliderLeft, width: isSelectedTabHidden ? '0' : this.state.sliderWidth }) render() { const allTabs = React.Children.toArray(this.props.children).filter(child => child.type === Tab) const activeTabId = this.getActiveTabId(allTabs) const {tabsVisible, tabsHidden, panels, isSelectedTabHidden} = this.getTabs(allTabs) const activeTab = panels[activeTabId] || allTabs[0] return ( <div className={classNames(styles['tabs'], this.props.className)}> <div className={styles.tabs__header}> <div ref={tabs => (this.tabsWrapper = tabs)} onKeyDown={this.onKeyDown} className={styles.tabs__header__container} > {tabsVisible.map(tab => ( <TabInner {...this.getTabProps(tab)} /> ))} { <ShowMore {...this.getShowMoreProps(isSelectedTabHidden)}> {tabsHidden.map(tab => ( <TabInner {...this.getTabProps(tab)} /> ))} </ShowMore> } </div> <div className={styles.tabs__header__line}> <div className={styles.tabs__header__line__slider} style={this.getSliderStyle(isSelectedTabHidden)} /> </div> </div> <div className={styles.tabs__body}> {activeTab && <TabPanel {...this.getPanelProps(activeTab)} />} {<ResizeDetector handleWidth onResize={this.onResizeThrottled} />} </div> </div> ) } } Tabs.Tab = Tab