src/js/components/Box/StyledBox.js
import styled, { css } from 'styled-components';
import { defaultProps } from '../../default-props';
import {
alignContentStyle,
alignStyle,
backgroundStyle,
borderStyle,
breakpointStyle,
edgeStyle,
fillStyle,
focusStyle,
genericStyles,
getBreakpointStyle,
getHoverIndicatorStyle,
heightStyle,
overflowStyle,
parseMetricToNum,
responsiveBorderStyle,
widthStyle,
} from '../../utils';
import { roundStyle } from '../../utils/styles';
import { animationBounds, animationObjectStyle } from '../../utils/animation';
const BASIS_MAP = {
auto: 'auto',
full: '100%',
'1/2': '50%',
'1/4': '25%',
'2/4': '50%',
'3/4': '75%',
'1/3': '33.33%',
'2/3': '66.66%',
};
const basisStyle = css`
flex-basis: ${(props) =>
BASIS_MAP[props.basis] ||
props.theme.global.size[props.basis] ||
props.basis};
`;
// min-width and min-height needed because of this
// https://stackoverflow.com/questions/36247140/why-doesnt-flex-item-shrink-past-content-size
// we assume we are in the context of a Box going the other direction
// TODO: revisit this
const directionStyle = (direction, theme) => {
const styles = [
css`
min-width: 0;
min-height: 0;
flex-direction: ${direction === 'row-responsive' ? 'row' : direction};
`,
];
if (direction === 'row-responsive' && theme.box.responsiveBreakpoint) {
const breakpoint = getBreakpointStyle(
theme,
theme.box.responsiveBreakpoint,
);
if (breakpoint) {
styles.push(
breakpointStyle(
breakpoint,
`
flex-direction: column;
flex-basis: auto;
justify-content: flex-start;
align-items: stretch;
`,
),
);
}
}
return styles;
};
const elevationStyle = (elevation) => css`
box-shadow: ${(props) =>
props.theme.global.elevation[props.theme.dark ? 'dark' : 'light'][
elevation
]};
`;
const FLEX_MAP = {
[true]: '1 1',
[false]: '0 0',
grow: '1 0',
shrink: '0 1',
};
const flexGrowShrinkProp = (flex) => {
if (typeof flex === 'boolean' || typeof flex === 'string') {
return FLEX_MAP[flex];
}
return `${flex.grow ? flex.grow : 0} ${flex.shrink ? flex.shrink : 0}`;
};
const flexStyle = css`
flex: ${(props) =>
`${flexGrowShrinkProp(props.flex)}${
props.flex !== true && !props.basis ? ' auto' : ''
}`};
`;
const JUSTIFY_MAP = {
around: 'space-around',
between: 'space-between',
center: 'center',
end: 'flex-end',
evenly: 'space-evenly',
start: 'flex-start',
};
const justifyStyle = css`
justify-content: ${(props) => JUSTIFY_MAP[props.justify]};
`;
const WRAP_MAP = {
true: 'wrap',
reverse: 'wrap-reverse',
};
const wrapStyle = css`
flex-wrap: ${(props) => WRAP_MAP[props.wrapProp]};
`;
const animationItemStyle = (item, theme) => {
if (typeof item === 'string') {
return animationObjectStyle({ type: item }, theme);
}
if (Array.isArray(item)) {
return item.reduce(
(style, a, index) =>
css`
${style}${index > 0 ? ',' : ''} ${animationItemStyle(a, theme)}
`,
'',
);
}
if (typeof item === 'object') {
return animationObjectStyle(item, theme);
}
return '';
};
const animationAncilaries = (animation) => {
if (animation.type === 'flipIn' || animation.type === 'flipOut') {
return 'perspective: 1000px; transform-style: preserve-3d;';
}
return '';
};
const animationObjectInitialStyle = (animation) => {
const bounds = animationBounds(animation.type, animation.size);
if (bounds) {
return `${bounds[0]} ${animationAncilaries(animation)}`;
}
return '';
};
const animationInitialStyle = (item) => {
if (typeof item === 'string') {
return animationObjectInitialStyle({ type: item });
}
if (Array.isArray(item)) {
return item
.map((a) =>
typeof a === 'string'
? animationObjectInitialStyle({ type: a })
: animationObjectInitialStyle(a),
)
.join('');
}
if (typeof item === 'object') {
return animationObjectInitialStyle(item);
}
return '';
};
const animationStyle = css`
${(props) => css`
${animationInitialStyle(props.animation)}
animation: ${animationItemStyle(props.animation, props.theme)};
`};
`;
const interactiveStyle = css`
cursor: pointer;
&:hover {
${(props) =>
props.kindProp?.hover &&
getHoverIndicatorStyle(props.kindProp.hover, props.theme)}
${(props) =>
props.hoverIndicator &&
getHoverIndicatorStyle(props.hoverIndicator, props.theme)}
}
`;
const gapStyle = (directionProp, gap, responsive, wrap, theme) => {
const metric = theme.global.edgeSize[gap] || gap;
const breakpoint = getBreakpointStyle(theme, theme.box.responsiveBreakpoint);
const responsiveMetric = responsive && breakpoint && breakpoint.edgeSize[gap];
const styles = [];
if (typeof gap === 'object') {
if (gap.row !== undefined && gap.column !== undefined) {
styles.push(
`gap: ${theme.global.edgeSize[gap.row] || gap.row} ${
theme.global.edgeSize[gap.column] || gap.column
};`,
);
if (responsiveMetric) {
styles.push(breakpointStyle(breakpoint, `gap: ${responsiveMetric};`));
}
} else if (gap.row !== undefined) {
styles.push(`row-gap: ${theme.global.edgeSize[gap.row] || gap.row};`);
if (responsiveMetric) {
styles.push(
breakpointStyle(breakpoint, `row-gap: ${responsiveMetric};`),
);
}
} else if (gap.column !== undefined) {
styles.push(
`column-gap: ${theme.global.edgeSize[gap.column] || gap.column};`,
);
if (responsiveMetric) {
styles.push(
breakpointStyle(breakpoint, `column-gap: ${responsiveMetric};`),
);
}
}
} else if (directionProp === 'column' || directionProp === 'column-reverse') {
styles.push(`row-gap: ${metric};`);
if (responsiveMetric) {
styles.push(breakpointStyle(breakpoint, `row-gap: ${responsiveMetric};`));
}
} else {
styles.push(`column-gap: ${metric};`);
if (wrap) styles.push(`row-gap: ${metric};`);
if (responsiveMetric) {
if (directionProp === 'row' || directionProp === 'row-reverse') {
styles.push(
breakpointStyle(breakpoint, `column-gap: ${responsiveMetric};`),
);
} else if (directionProp === 'row-responsive') {
styles.push(
breakpointStyle(
breakpoint,
`
row-gap: ${responsiveMetric};
`,
),
);
}
}
}
return styles;
};
// NOTE: basis must be after flex! Otherwise, flex overrides basis
const StyledBox = styled.div`
display: flex;
box-sizing: border-box;
${(props) => !props.basis && 'max-width: 100%;'};
${genericStyles}
${(props) => props.align && alignStyle}
${(props) => props.alignContent && alignContentStyle}
${(props) =>
props.background && backgroundStyle(props.background, props.theme)}
${(props) =>
props.border && borderStyle(props.border, props.responsive, props.theme)}
${(props) =>
props.directionProp && directionStyle(props.directionProp, props.theme)}
${(props) => props.heightProp && heightStyle(props.heightProp, props.theme)}
${(props) => props.widthProp && widthStyle(props.widthProp, props.theme)}
${(props) => props.flex !== undefined && flexStyle}
${(props) => props.basis && basisStyle}
${(props) => props.fillProp && fillStyle(props.fillProp)}
${(props) => props.justify && justifyStyle}
${(props) =>
props.pad &&
edgeStyle(
'padding',
props.pad,
props.responsive,
props.theme.box.responsiveBreakpoint,
props.theme,
)}
${(props) =>
props.round && roundStyle(props.round, props.responsive, props.theme)}
${(props) => props.wrapProp && wrapStyle}
${(props) => props.overflowProp && overflowStyle(props.overflowProp)}
${(props) => props.elevationProp && elevationStyle(props.elevationProp)}
${(props) =>
props.gap &&
gapStyle(
props.directionProp,
props.gap,
props.responsive,
props.wrapProp,
props.theme,
)}
${(props) => props.animation && animationStyle}
${(props) => props.onClick && interactiveStyle}
${(props) =>
props.onClick &&
props.focus &&
props.focusIndicator !== false &&
focusStyle()}
${(props) => props.theme.box && props.theme.box.extend}
${(props) => props.kindProp && props.kindProp.extend}
`;
StyledBox.defaultProps = {};
Object.setPrototypeOf(StyledBox.defaultProps, defaultProps);
const gapGapStyle = (directionProp, gap, responsive, border, theme) => {
const metric = theme.global.edgeSize[gap] || gap;
const breakpoint = getBreakpointStyle(theme, theme.box.responsiveBreakpoint);
const responsiveMetric = responsive && breakpoint && breakpoint.edgeSize[gap];
const styles = [];
if (directionProp === 'column' || directionProp === 'column-reverse') {
styles.push(`height: ${metric};`);
if (responsiveMetric) {
styles.push(breakpointStyle(breakpoint, `height: ${responsiveMetric};`));
}
} else {
styles.push(`width: ${metric};`);
if (responsiveMetric) {
if (directionProp === 'row' || directionProp === 'row-reverse') {
styles.push(breakpointStyle(breakpoint, `width: ${responsiveMetric};`));
} else if (directionProp === 'row-responsive') {
styles.push(
breakpointStyle(
breakpoint,
`
width: auto;
height: ${responsiveMetric};
`,
),
);
}
}
}
if (border === 'between' || (border && border.side === 'between')) {
const borderSize = border.size || 'xsmall';
const borderMetric = theme.global.borderSize[borderSize] || borderSize;
const borderOffset = `${
parseMetricToNum(metric) / 2 - parseMetricToNum(borderMetric) / 2
}px`;
const responsiveBorderMetric =
responsive &&
breakpoint &&
(breakpoint.borderSize[borderSize] || borderSize);
const responsiveBorderOffset =
responsiveBorderMetric &&
`${
parseMetricToNum(responsiveMetric || metric) / 2 -
parseMetricToNum(responsiveBorderMetric) / 2
}px`;
if (directionProp === 'column' || directionProp === 'column-reverse') {
const adjustedBorder =
typeof border === 'string' ? 'top' : { ...border, side: 'top' };
styles.push(css`
position: relative;
&:after {
content: '';
position: absolute;
width: 100%;
top: ${borderOffset};
${borderStyle(adjustedBorder, responsive, theme)}
}
`);
if (responsiveBorderOffset) {
styles.push(
breakpointStyle(
breakpoint,
`
&:after {
content: '';
top: ${responsiveBorderOffset};
}`,
),
);
}
} else {
const adjustedBorder =
typeof border === 'string' ? 'left' : { ...border, side: 'left' };
styles.push(css`
position: relative;
&:after {
content: '';
position: absolute;
height: 100%;
left: ${borderOffset};
${borderStyle(
adjustedBorder,
directionProp !== 'row-responsive' && responsive,
theme,
)}
}
`);
if (responsiveBorderOffset) {
if (directionProp === 'row' || directionProp === 'row-reverse') {
styles.push(
breakpointStyle(
breakpoint,
`
&:after {
content: '';
left: ${responsiveBorderOffset};
}`,
),
);
} else if (directionProp === 'row-responsive') {
const adjustedBorder2 =
typeof border === 'string' ? 'top' : { ...border, side: 'top' };
styles.push(
breakpointStyle(
breakpoint,
`
&:after {
content: '';
height: auto;
left: unset;
width: 100%;
top: ${responsiveBorderOffset};
border-left: none;
${responsiveBorderStyle(adjustedBorder2, theme)}
}`,
),
);
}
}
}
}
return styles;
};
const StyledBoxGap = styled.div`
flex: 0 0 auto;
align-self: stretch;
${(props) =>
props.gap &&
gapGapStyle(
props.directionProp,
props.gap,
props.responsive,
props.border,
props.theme,
)};
`;
StyledBoxGap.defaultProps = {};
Object.setPrototypeOf(StyledBoxGap.defaultProps, defaultProps);
export { StyledBox, StyledBoxGap };