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