import classNames from "classnames" ; import * as React from "react" ; import { polyfill } from "react-lifecycles-compat" ; import { AbstractPureComponent2 , Classes , Keys } from "../../common" ; import { DISPLAYNAME_PREFIX , Props } from "../../common/props" ; import * as Utils from "../../common/utils" ; import { TabProps , Tab , TabId } from "./tab" ; import { generateTabPanelId , generateTabTitleId , TabTitle } from "./tabTitle" ; export const Expander = ( ) => < div className = { Classes . FLEX_EXPANDER } />; type TabElement = React . ReactElement < TabProps & { children : React . ReactNode ; } > ; const TAB_SELECTOR = `.${ Classes . TAB }` ; export type TabsProps = ITabsProps ; deprecated export interface ITabsProps extends Props { default animate ? : boolean ; default defaultSelectedTabId ? : TabId ; /**
     * Unique identifier for this `Tabs` container. This will be combined with the `id` of each
     * `Tab` child to generate ARIA accessibility attributes. IDs are required and should be
     * unique on the page to support server-side rendering.
     */ id : TabId ; default large ? : boolean ; default renderActiveTabPanelOnly ? : boolean ; /**
     * Selected tab `id`, for controlled usage.
     * Providing this prop will put the component in controlled mode.
     * Unknown ids will result in empty selection (no errors).
     */ selectedTabId ? : TabId ; default vertical ? : boolean ; /**
     * A callback function that is invoked when a tab in the tab list is clicked.
     */ onChange ? ( newTabId , prevTabId , event ) : void ; } export interface ITabsState { indicatorWrapperStyle ? : React . CSSProperties ; selectedTabId ? : TabId ; } @ ( polyfill as Utils . LifecycleCompatPolyfill < ITabsProps , any > ) export class Tabs extends AbstractPureComponent2 < TabsProps , ITabsState > { /** Insert a `Tabs.Expander` between any two children to right-align all subsequent children. */ public static Expander = Expander ; public static Tab = Tab ; public static defaultProps : Partial < TabsProps > = { animate : true , large : false , renderActiveTabPanelOnly : false , vertical : false , } ; public static displayName = `${ DISPLAYNAME_PREFIX }.Tabs` ; public static getDerivedStateFromProps ( { selectedTabId } ) { if ( selectedTabId !== undefined ) { return { selectedTabId } ; } return null ; } private tablistElement : HTMLDivElement | null = null ; private refHandlers = { tablist : ( tabElement ) => ( this . tablistElement = tabElement ) , } ; constructor ( props ) { super ( props ) ; const selectedTabId = this . getInitialSelectedTabId ( ) ; this . state = { selectedTabId } ; } public render ( ) { const { indicatorWrapperStyle , selectedTabId } = this . state ; const tabTitles = React . Children . map ( this . props . children , this . renderTabTitle ) ; const tabPanels = this . getTabChildren ( ) . filter ( this . props . renderActiveTabPanelOnly ? tab => tab . props . id === selectedTabId : ( ) => true ) . map ( this . renderTabPanel ) ; const tabIndicator = this . props . animate ? ( < div className = { Classes . TAB_INDICATOR_WRAPPER } style = { indicatorWrapperStyle } > < div className = { Classes . TAB_INDICATOR } /> < /div>) : null; const classes = classNames ( Classes . TABS , { [ Classes . VERTICAL ] : this . props . vertical } , this . props . className ) ; const tabListClasses = classNames ( Classes . TAB_LIST , { [ Classes . LARGE ] : this . props . large , } ) ; return ( < div className = { classes } > < div className = { tabListClasses } onKeyDown = { this . handleKeyDown } onKeyPress = { this . handleKeyPress } ref = { this . refHandlers . tablist } role = "tablist" > { tabIndicator } { tabTitles } < /div> { tabPanels } < /div>); } public componentDidMount ( ) { this . moveSelectionIndicator ( false ) ; } public componentDidUpdate ( prevProps , prevState ) { if ( this . state . selectedTabId !== prevState . selectedTabId ) { this . moveSelectionIndicator ( ) ; } else if ( prevState . selectedTabId != null ) { const didChildrenChange = ! Utils . arraysEqual ( this . getTabChildrenProps ( prevProps ) , this . getTabChildrenProps ( ) , Utils . shallowCompareKeys ) ; if ( didChildrenChange ) { this . moveSelectionIndicator ( ) ; } } } private getInitialSelectedTabId ( ) { const { defaultSelectedTabId , selectedTabId } = this . props ; if ( selectedTabId !== undefined ) { return selectedTabId ; } else if ( defaultSelectedTabId !== undefined ) { return defaultSelectedTabId ; } else { const tabs = this . getTabChildren ( ) ; return tabs . length === 0 ? undefined : tabs [ 0 ] . props . id ; } } private getKeyCodeDirection ( e ) { if ( isEventKeyCode ( e , Keys . ARROW_LEFT , Keys . ARROW_UP ) ) { return - 1 ; } else if ( isEventKeyCode ( e , Keys . ARROW_RIGHT , Keys . ARROW_DOWN ) ) { return 1 ; } return undefined ; } private getTabChildrenProps ( props = this . props ) { return this . getTabChildren ( props ) . map ( child => child . props ) ; } /** Filters children to only `<Tab>`s */ private getTabChildren ( props = this . props ) { return React . Children . toArray ( props . children ) . filter ( isTabElement ) ; } /** Queries root HTML element for all tabs with optional filter selector */ private getTabElements ( subselector = "" ) { if ( this . tablistElement == null ) { return [ ] ; } return Array . from ( this . tablistElement . querySelectorAll ( TAB_SELECTOR + subselector ) ) ; } private handleKeyDown = ( e ) => { const focusedElement = document . activeElement ?. closest ( TAB_SELECTOR ) ; if ( focusedElement == null ) { return ; } const enabledTabElements = this . getTabElements ( ) . filter ( el => el . getAttribute ( "aria-disabled" ) === "false" ) ; const focusedIndex = enabledTabElements . indexOf ( focusedElement ) ; const direction = this . getKeyCodeDirection ( e ) ; if ( focusedIndex >= 0 && direction !== undefined ) { e . preventDefault ( ) ; const { length } = enabledTabElements ; const nextFocusedIndex = ( focusedIndex + direction + length ) % length ; ( enabledTabElements [ nextFocusedIndex ] as HTMLElement ) . focus ( ) ; } } ; private handleKeyPress = ( e ) => { const targetTabElement = ( e . target as HTMLElement ) . closest ( TAB_SELECTOR ) as HTMLElement ; if ( targetTabElement != null && Keys . isKeyboardClick ( e . which ) ) { e . preventDefault ( ) ; targetTabElement . click ( ) ; } } ; private handleTabClick = ( newTabId , event ) => { this . props . onChange ?. ( newTabId , this . state . selectedTabId , event ) ; if ( this . props . selectedTabId === undefined ) { this . setState ( { selectedTabId : newTabId } ) ; } } ; /**
     * Calculate the new height, width, and position of the tab indicator.
     * Store the CSS values so the transition animation can start.
     */ private moveSelectionIndicator ( animate = true ) { if ( this . tablistElement == null || ! this . props . animate ) { return ; } const tabIdSelector = `${ TAB_SELECTOR }[data-tab-id="${ this . state . selectedTabId }"]` ; const selectedTabElement = this . tablistElement . querySelector ( tabIdSelector ) as HTMLElement ; let indicatorWrapperStyle = { display : "none" } ; if ( selectedTabElement != null ) { const { clientHeight , clientWidth , offsetLeft , offsetTop } = selectedTabElement ; indicatorWrapperStyle = { height : clientHeight , transform : `translateX(${ Math . floor ( offsetLeft ) }px) translateY(${ Math . floor ( offsetTop ) }px)` , width : clientWidth , } ; if ( ! animate ) { indicatorWrapperStyle . transition = "none" ; } } this . setState ( { indicatorWrapperStyle } ) ; } private renderTabPanel = ( tab ) => { const { className , panel , id , panelClassName } = tab . props ; if ( panel === undefined ) { return undefined ; } return ( < div aria - labelledby = { generateTabTitleId ( this . props . id , id ) } aria - hidden = { id !== this . state . selectedTabId } className = { classNames ( Classes . TAB_PANEL , className , panelClassName ) } id = { generateTabPanelId ( this . props . id , id ) } key = { id } role = "tabpanel" > { panel } < /div>); } ; private renderTabTitle = ( child ) => { if ( isTabElement ( child ) ) { const { id } = child . props ; return ( < TabTitle { ... child . props } parentId = { this . props . id } onClick = { this . handleTabClick } selected = { id === this . state . selectedTabId } />); } return child ; } ; } function isEventKeyCode ( e , ... codes ) { return codes . indexOf ( e . which ) >= 0 ; } function isTabElement ( child ) { return Utils . isElementOfType ( child , Tab ) ; }