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.

257 lines (235 loc) 6.63 kB
import React, { useRef, useContext, useEffect } from 'react'; import { useFela } from 'react-fela'; import PropTypes from 'prop-types'; import { Click } from '../click'; import { TabNavContext } from '../tab-nav'; import { getThemeStyle } from '../../get-theme-style'; const style = ({ reverseOut, isActiveTab, duration, theme }) => ({ minWidth: 50, alignSelf: 'initial', justifyContent: 'center', flexGrow: 0, flexShrink: 0, flexBasis: 'auto', whiteSpace: 'nowrap', paddingTop: theme.baselineGrid * 2, paddingBottom: theme.baselineGrid * 2, display: 'flex', marginRight: theme.baselineGrid * 2.5, transitionProperty: 'color', transitionTimingFunction: 'ease-out', transitionDuration: duration + 'ms', color: reverseOut ? theme.color.primitive.white : theme.color.foreground.primary, textAlign: 'left', letterSpacing: 0.3, fontWeight: 200, fontSize: 16, boxSizing: 'border-box', borderBottomWidth: duration === 0 ? 3 : 0, borderBottomStyle: 'solid', borderBottomColor: 'transparent', ':focus': { outline: 0, color: reverseOut ? theme.color.primitive.white : theme.color.ornament.highlight, }, ':hover': { color: reverseOut ? theme.color.primitive.white : theme.color.ornament.highlight, }, extend: [ { condition: isActiveTab, style: { borderBottomColor: reverseOut ? theme.color.primitive.white : theme.color.ornament.highlight, color: reverseOut ? theme.color.primitive.white : theme.color.ornament.highlight, }, }, { condition: duration === 0, style: { ':hover': { borderBottomColor: reverseOut ? theme.color.primitive.white : theme.color.ornament.highlight, }, }, }, ], }); function calculatePosition(currentItem, newItem, scrollItem, isRTL) { const currentOffset = currentItem ? currentItem.offsetLeft : 0; const currentWidth = currentItem ? currentItem.offsetWidth : 0; const newWidth = newItem.offsetWidth; const newOffset = newItem.offsetLeft; const currentDifference = !isRTL ? currentOffset - scrollItem.offsetLeft : scrollItem.offsetWidth - (currentOffset - scrollItem.offsetLeft) - newWidth; const newDifference = !isRTL ? newOffset - scrollItem.offsetLeft : scrollItem.offsetWidth - (newOffset - scrollItem.offsetLeft) - newWidth; return { currentOffset, currentWidth, currentDifference, newWidth, newOffset, newDifference, }; } function activateTab( setActive, currentItem, newItem, scrollItem, borderItem, duration, scrollTolerance, isRTL ) { const { currentOffset, currentWidth, currentDifference, newOffset, newWidth, newDifference, } = calculatePosition(currentItem, newItem, scrollItem, isRTL); const property = isRTL ? 'marginRight' : 'marginLeft'; const toLeft = currentOffset > newOffset; // just a small helper to conditionally apply styles to the border element function setStyle(style, value, condition = true) { if (condition) { borderItem.style[style] = value + 'px'; } } // scroll into view if scrollItem is actually collapsed if ( scrollItem.offsetWidth < scrollItem.scrollWidth && scrollItem.offsetWidth > newWidth ) { const newScrollLeft = newItem.offsetLeft - scrollItem.offsetLeft - scrollItem.scrollLeft; const spaceRight = scrollItem.offsetWidth - newWidth - newScrollLeft; if ( spaceRight - scrollTolerance < 0 || scrollItem.offsetWidth - (spaceRight + scrollTolerance) < newWidth ) { scrollItem.scrollLeft = newScrollLeft; } } // set the new active item first to make sure calculation is always correct setActive(newItem); // initial load if (!currentItem) { setStyle('width', newWidth); setStyle(property, newDifference); return; } /** * Depending on the direction, it transforms the width and the x-offset * It uses a helper div (borderRef, see tab-nav/TabNavItemBorder) that is usually hidden * It removes the current TabNavItem border and adds it back once the animation is done */ if (toLeft) { setStyle('width', currentOffset - newOffset + currentWidth); setStyle(property, newDifference, !isRTL); setTimeout(() => { setStyle(property, newDifference, isRTL); setStyle('width', newWidth); }, (duration * 3) / 2); } else { setStyle(property, newDifference, isRTL); setStyle('width', newWidth + currentDifference - newDifference, isRTL); setStyle('width', newWidth + newDifference - currentDifference, !isRTL); setTimeout(() => { setStyle('width', newWidth); setStyle(property, newDifference, !isRTL); }, (duration * 3) / 2); } } export const TabNavItem = React.forwardRef( ({ children, isActive, onClick, ...props }, ref) => { const { active, setActive, scrollRef, borderRef, reverseOut, duration = 0, } = useContext(TabNavContext); const { theme } = useFela(); const itemRef = useRef(); const isRTL = theme.direction !== 'ltr'; const activate = () => activateTab( setActive, active, itemRef.current, scrollRef.current, borderRef.current, duration, theme.baselineGrid * 2.5, isRTL ); useEffect( () => { if (isActive && setActive) { activate(); } }, // also update the active state if isActive is manually updated [isActive, isRTL] ); const styleProps = { isActiveTab: active ? active === itemRef.current : isActive, reverseOut, duration, theme, }; const onClickHandler = e => { if (setActive) { activate(); } if (onClick) { onClick(e); } }; return ( <Click {...props} ref={el => { if (itemRef) itemRef.current = el; if (ref) ref.current = el; }} onClick={onClickHandler} extend={[ style(styleProps), getThemeStyle('tabNavItem', theme, styleProps), ]} > {children} </Click> ); } ); TabNavItem.displayName = 'TabNavItem'; TabNavItem.propTypes = { children: PropTypes.node, /** Indicate if the TabNavItem will open a dropdown menu */ isDropdown: PropTypes.bool, /** Indicate if the TabNavItem is in an active state */ isActive: PropTypes.bool, /** Onclick function */ onClick: PropTypes.func, };