UNPKG

vcc-ui

Version:

VCC UI is a collection of React UI Components that can be used for developing front-end applications at Volvo Car Corporation.

319 lines (286 loc) 7.63 kB
import React, { Children, cloneElement, createContext } from "react"; import PropTypes from "prop-types"; import { withTheme } from "react-fela"; import { Block } from "../block"; import { Inline } from "../inline"; import { Click } from "../click"; import { Arrow } from "../arrow"; import { TabNavItem } from "../tab-nav-item"; import styles from "./styles"; import ActiveLineWithTransition from "./active-line-with-transition"; import { getThemeStyle } from "../../get-theme-style"; export const LineTransitionContext = createContext({}); class TabNavComponent extends React.Component { constructor(props) { super(props); this.root = React.createRef(); this.navItemGroup = React.createRef(); this.backButton = React.createRef(); } state = {}; componentDidMount() { let resizeTimer; this.resizeEventHandler = () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { this.forceUpdate(); }, 1); }; this.forceUpdate(); window.addEventListener("resize", this.resizeEventHandler); } componentWillUnmount() { window.removeEventListener("resize", this.resizeEventHandler); } getElementProperty(element, property) { return element && element.getBoundingClientRect ? element.getBoundingClientRect()[property] : 0; } getElementWidth(element) { return this.getElementProperty(element, "width"); } componentDidUpdate(_, prevState) { const { textAlign } = this.props; const navItemsWidth = this.getElementWidth(this.navItemGroup.current); const width = this.getElementWidth(this.root.current); const { overflow, useSimpleLayout } = prevState; if (this.isCenterAlignedWithBackButton()) { const backButtonWidth = this.getElementWidth(this.backButton.current); if (width < backButtonWidth + navItemsWidth && !useSimpleLayout) { this.setState(() => ({ useSimpleLayout: true, overflow: "scroll" })); } else if (width >= backButtonWidth + navItemsWidth && useSimpleLayout) { this.setState(() => ({ useSimpleLayout: false, overflow: null })); } } if ( navItemsWidth >= width && !this.shouldShowBackButton() && textAlign === "center" && overflow !== "scroll" ) { this.setState(() => ({ overflow: "scroll" })); } else if ( navItemsWidth < width && overflow === "scroll" && !useSimpleLayout ) { this.setState(() => ({ overflow: null })); } } shouldShowBackButton({ backButton = {} } = this.props) { return !!(backButton.clickHandler || backButton.href); } isCenterAlignedWithBackButton({ textAlign } = this.props) { return textAlign === "center" && this.shouldShowBackButton(); } renderCenteredLayoutWithBackButton() { return ( <React.Fragment> <Block extend={styles.centeredLayoutLeft}> <Inline extend={styles.centeredLayoutLeftInner}> {this.renderBackButton()} </Inline> </Block> <Block extend={styles.centeredLayoutCenter}> <Block extend={styles.centeredLayoutCenterInner}> <Inline extend={styles.navItemGroupInner({ showBackButton: true })} innerRef={this.navItemGroup} > {this.renderChildren()} </Inline> </Block> </Block> <Block extend={styles.centeredLayoutRight}> <Inline extend={styles.centeredLayoutRightInner} /> </Block> </React.Fragment> ); } renderBackButton() { const { backButton = {}, showBackButtonOn, theme, variant } = this.props; const { href, clickHandler = () => {}, text } = backButton; const reverseOut = variant === "dark"; const styleProps = { theme, reverseOut, showBackButtonOn }; return ( <Click extend={[ styles.backButton(styleProps), getThemeStyle("tabNavBackButton", theme, styleProps) ]} innerRef={this.backButton} href={href} onClick={clickHandler} > <Arrow direction="left" size={10} color={reverseOut ? theme.colors.white : theme.colors.primary} /> <Inline extend={styles.backButtonText({ theme })} > {text || ""} </Inline> </Click> ); } renderSimpleLayout({ textAlign } = this.props) { const isScrolling = this.state.overflow === "scroll"; const showBackButton = this.shouldShowBackButton(); return ( <React.Fragment> {showBackButton && this.renderBackButton()} <Block extend={styles.navItemGroup({ textAlign, isScrolling })} > <Inline extend={styles.navItemGroupInner({ showBackButton })} innerRef={this.navItemGroup} > {this.renderChildren()} </Inline> </Block> </React.Fragment> ); } getActiveIndex = () => { const { children } = this.props; return children.length ? children.filter(Boolean).findIndex(child => { let props = child.props; let nestLimit = 5; while ( !("isActive" in props) && nestLimit > 0 && props.children && props.children.props ) { props = props.children.props; nestLimit -= 1; } return props.isActive; }) : 0; }; renderChildren() { const { children, enableLineTransition } = this.props; if (enableLineTransition) { let { itemsDimensions } = this.state; const updateDimensions = (index, newDimensions) => { if (!itemsDimensions) { itemsDimensions = children.map(() => ({ x: 0, width: 0 })); } if (itemsDimensions[index] === newDimensions) { return; } const newItemsDimensions = itemsDimensions; newItemsDimensions[index] = newDimensions; this.setState({ itemsDimensions: newItemsDimensions }); }; const activeIndex = this.getActiveIndex(); return ( <LineTransitionContext.Provider value={{ activeIndex, itemsDimensions, updateDimensions }} > {Children.map( children, (child, index) => child.type === TabNavItem ? cloneElement(child, { index }) : cloneElement(child) )} <ActiveLineWithTransition /> </LineTransitionContext.Provider> ); } return children; } render() { const { theme, textAlign, variant } = this.props; const { useSimpleLayout } = this.state; const reverseOut = variant === "dark"; const showBackButton = this.shouldShowBackButton(); const styleProps = { showBackButton, textAlign, reverseOut, theme }; return ( <Block as="nav" extend={[ styles.nav(styleProps), getThemeStyle("tabNav", theme, styleProps) ]} innerRef={this.root} > {!this.isCenterAlignedWithBackButton() || useSimpleLayout ? this.renderSimpleLayout() : this.renderCenteredLayoutWithBackButton()} </Block> ); } } const propTypes = { /** Dark text on light background, or vice versa */ variant: PropTypes.oneOf(["light", "dark"]), /** Text-align: left or center */ textAlign: PropTypes.oneOf(["center", "left"]), /** Back button text, href, click handler */ backButton: PropTypes.shape({ text: PropTypes.string, href: PropTypes.string, clickHandler: PropTypes.func }), /** Which viewports to show the back button on */ showBackButtonOn: PropTypes.arrayOf(PropTypes.oneOf(["s", "m", "l"])), /** Enable fancy transition when changing active tabs */ enableLineTransition: PropTypes.bool }; const defaultProps = { variant: "light", textAlign: "center", showBackButtonOn: ["s", "m", "l"] }; TabNavComponent.propTypes = propTypes; TabNavComponent.defaultProps = defaultProps; TabNavComponent.displayName = "TabNav"; const TabNav = withTheme(TabNavComponent); export { TabNav };