import React from 'react';
import styled, { css } from 'styled-components';
import { filterGlobalProps, GlobalProps, spacing } from '@lendi-ui/commons/utils';
import type { Spacing } from '@lendi-ui/commons/utils';
import { useFlexGap, useFlexGapPolyfill } from './polyfill';
type Align = 'center' | 'start' | 'end' | 'stretch';
type Direction = 'row' | 'column';
interface BaseProps extends GlobalProps {
children: React.ReactNode;
className?: string;
align?: Align;
/**
* Extra margin to include at end of the container. On horizontal and tile spacers it applies to the right. On vertical spacers it applies to the bottom.
*/
end?: Spacing;
/**
* Extra margin to include at start of the container. On horizontal and tile spacers it applies to the left. On vertical spacers it applies to the top.
*/
start?: Spacing;
/**
* Should items wrap at the end of a row if they exceed the container width?
*/
shouldWrap?: boolean;
/**
* Add a width to the spacer container. Useful for nested containers.
*/
width?: string;
minWidth?: string;
maxWidth?: string;
}
interface Gap {
gap?: Spacing;
xgap?: never;
ygap?: never;
}
interface AxisGap {
xgap?: Spacing;
ygap?: Spacing;
gap?: never;
}
interface BaseAlign {
align?: Align;
xalign?: never;
yalign?: never;
}
interface AxisAlign {
xalign?: Align;
yalign?: Align;
align?: never;
}
type GapProps = Gap | AxisGap;
type AlignProps = BaseAlign | AxisAlign;
type SpacerProps = BaseProps & GapProps & AlignProps;
declare module 'react' {
interface CSSProperties {
'--row-gap'?: string; // TODO: change to template literal px type once we update TS
'--column-gap'?: string;
}
}
const getYSpacing = (props: GapProps) => {
if (props.ygap) {
return spacing[props.ygap];
}
if (props.gap) {
return spacing[props.gap];
}
return '0';
};
const getXSpacing = (props: GapProps) => {
if (props.xgap) {
return spacing[props.xgap];
}
if (props.gap) {
return spacing[props.gap];
}
return '0';
};
const getMarginTop = (direction: Direction, start?: Spacing, isFlexGapSupported = true) => {
if (!isFlexGapSupported) {
if (start) {
return css`
margin-top: calc(calc(var(--row-gap) / -2) + ${spacing[start]}px);
`;
}
return `margin-top: calc(var(--row-gap) / -2);`;
}
return start && direction === 'column'
? css`
margin-top: ${spacing[start]}px;
`
: null;
};
const getMarginBottom = (direction: Direction, end?: Spacing, isFlexGapSupported = true) => {
if (!isFlexGapSupported) {
if (end) {
return css`
margin-bottom: calc(calc(var(--row-gap) / -2) + ${spacing[end]}px);
`;
}
return `margin-bottom: calc(var(--row-gap) / -2);`;
}
return end && direction === 'column'
? css`
margin-bottom: ${spacing[end]}px;
`
: null;
};
const getMarginLeft = (direction: Direction, start?: Spacing) => {
return start && direction === 'row'
? css`
margin-left: ${spacing[start]}px;
`
: null;
};
const getMarginRight = (direction: Direction, end?: Spacing) => {
return end && direction === 'row'
? css`
margin-right: ${spacing[end]}px;
`
: null;
};
const getAlign = (align?: Align) => {
switch (align) {
case 'center':
return `center`;
case 'start':
return 'flex-start';
case 'end':
return 'flex-end';
case 'stretch':
return 'stretch';
default:
return 'flex-start';
}
};
const getAlignment = (props: AlignProps & { direction: Direction }) => {
if (props.align) {
return css`
justify-content: ${getAlign(props.align)};
align-items: ${getAlign(props.align)};
`;
}
// horizontal spacer
if (props.direction === 'row') {
if (props.xalign && props.yalign) {
return css`
justify-content: ${getAlign(props.yalign)};
align-items: ${getAlign(props.xalign)};
`;
}
if (props.xalign) {
return css`
align-items: ${getAlign(props.xalign)};
justify-content: flex-start;
`;
}
if (props.yalign) {
return css`
justify-content: ${getAlign(props.yalign)};
`;
} else {
return `
justify-content: flex-start;
align-items: flex-start;
`;
}
} else {
// vertical spacer
if (props.xalign && props.yalign) {
return css`
justify-content: ${getAlign(props.xalign)};
align-items: ${getAlign(props.yalign)};
`;
}
if (props.xalign) {
return css`
justify-content: ${getAlign(props.xalign)};
`;
}
if (props.yalign) {
return css`
align-items: ${getAlign(props.yalign)};
`;
} else {
return `
justify-content: flex-start;
align-items: flex-start;
`;
}
}
};
const Wrapper = styled.div<{
start?: Spacing;
$end?: Spacing;
shouldWrap?: boolean;
$direction: Direction;
xalign?: Align;
yalign?: Align;
align?: Align;
$width: string;
maxWidth?: string;
minWidth?: string;
}>`
display: flex;
flex-direction: ${(props) => props.$direction};
column-gap: var(--column-gap);
row-gap: var(--row-gap);
flex-wrap: ${(props) => (props.shouldWrap ? `wrap` : `nowrap`)};
width: ${(props) => props.$width};
${(props) => props.minWidth && `min-width: ${props.minWidth};`}
${(props) => props.maxWidth && `max-width: ${props.maxWidth};`}
${(props) => getMarginLeft(props.$direction, props.start)}
${(props) => getMarginRight(props.$direction, props.$end)}
${(props) => getMarginTop(props.$direction, props.start)}
${(props) => getMarginBottom(props.$direction, props.$end)}
${(props) =>
getAlignment(
props.align
? { align: props.align, direction: props.$direction }
: { xalign: props.xalign, yalign: props.yalign, direction: props.$direction }
)}
.no-flexbox-gap & {
gap: initial;
margin-left: calc(var(--column-gap) / -2);
margin-right: calc(var(--column-gap) / -2);
${(props) => getMarginTop(props.$direction, props.start, false)}
${(props) => getMarginBottom(props.$direction, props.$end, false)}
> * {
margin-top: calc(var(--row-gap) / 2);
margin-bottom: calc(var(--row-gap) / 2);
margin-left: calc(var(--column-gap) / 2);
margin-right: calc(var(--column-gap) / 2);
}
}
`;
const FlexGapWrapper = styled.div<{ $width?: string }>`
width: ${(props) => props.$width};
`;
export const Spacer = ({ width = '100%', xgap, ygap, gap, className, minWidth, maxWidth, ...props }: SpacerProps) => {
const globalProps = filterGlobalProps(props);
useFlexGapPolyfill();
const isFlexGapSupported = useFlexGap();
let direction: Direction = 'row';
let shouldWrap = false;
if (props?.shouldWrap) {
shouldWrap = props.shouldWrap;
} else if (gap) {
// if we have a gap or both xgap and ygap
shouldWrap = true;
} else if (xgap && ygap) {
shouldWrap = true;
}
// if we only have ygap
if (ygap && !xgap) {
direction = 'column';
}
// or if we have no gap
if (!xgap && !ygap && !gap) {
// default to 16
gap = '16';
}
const Markup = (
{props.children}
);
return !isFlexGapSupported ? (
{Markup}
) : (
Markup
);
};