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
JavaScript
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 };