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