Skip to content

Commit

Permalink
Reimplement Layout component using CSS Grid (#1451)
Browse files Browse the repository at this point in the history
  • Loading branch information
craigpalermo authored Feb 9, 2024
1 parent a732e52 commit 761b8a7
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 156 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "pcln-design-system",
"comment": "Change Layout implementation from Flex to Grid",
"type": "minor"
}
],
"packageName": "pcln-design-system"
}
61 changes: 0 additions & 61 deletions packages/core/src/Layout/Layout.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react'
import { Box } from '../Box/Box'
import { render } from '../__test__/testing-library'
import { theme } from '../theme/theme'
import { Layout } from './Layout'

describe('Layout', () => {
Expand Down Expand Up @@ -38,64 +37,4 @@ describe('Layout', () => {
)
expect(getByText(1)).toHaveStyle('width: 0.25')
})

it('renders with gap', () => {
const { getByTestId } = render(
<Layout variation='50-50' gap='sm'>
<Box>1</Box>
<Box>2</Box>
<Box>3</Box>
</Layout>
)

expect(getByTestId('layout-flex')).toHaveStyle(`margin-left: -${theme.space[1]};`)
expect(getByTestId('layout-flex')).toHaveStyle(`margin-right: -${theme.space[1]};`)
expect(getByTestId('box-1')).toHaveStyle(`padding-left: ${theme.space[1]};`)
expect(getByTestId('box-1')).toHaveStyle(`padding-right: ${theme.space[1]};`)
expect(getByTestId('box-2')).toHaveStyle(`padding-left: ${theme.space[1]};`)
})

it('renders with gap as responsive array', () => {
const { getByTestId } = render(
<Layout variation='50-50' gap={['sm']}>
<Box>1</Box>
<Box>2</Box>
<Box>3</Box>
</Layout>
)

expect(getByTestId('box-1')).toHaveStyle(`padding-left: ${theme.space[1]};`)
expect(getByTestId('box-1')).toHaveStyle(`padding-right: ${theme.space[1]};`)
expect(getByTestId('box-2')).toHaveStyle(`padding-left: ${theme.space[1]};`)
})

it('renders with row gap', () => {
const { getByTestId } = render(
<Layout variation='50-50' rowGap='sm'>
<Box>1</Box>
<Box>2</Box>
<Box>3</Box>
</Layout>
)

expect(getByTestId('layout-flex')).toHaveStyle(`margin-top: -${theme.space[1]};`)
expect(getByTestId('layout-flex')).toHaveStyle(`margin-bottom: -${theme.space[1]};`)
expect(getByTestId('box-1')).toHaveStyle(`padding-top: ${theme.space[1]};`)
expect(getByTestId('box-1')).toHaveStyle(`padding-bottom: ${theme.space[1]};`)
expect(getByTestId('box-2')).toHaveStyle(`padding-top: ${theme.space[1]};`)
})

it('renders with row gap as responsive array', () => {
const { getByTestId } = render(
<Layout variation='50-50' rowGap={['sm']}>
<Box>1</Box>
<Box>2</Box>
<Box>3</Box>
</Layout>
)

expect(getByTestId('box-1')).toHaveStyle(`padding-top: ${theme.space[1]};`)
expect(getByTestId('box-1')).toHaveStyle(`padding-bottom: ${theme.space[1]};`)
expect(getByTestId('box-2')).toHaveStyle(`padding-top: ${theme.space[1]};`)
})
})
141 changes: 47 additions & 94 deletions packages/core/src/Layout/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,18 @@
import React, { useEffect, useState } from 'react'
import styled from 'styled-components'
import { FlexWrapProps, zIndex } from 'styled-system'
import { zIndex } from 'styled-system'
import { Box } from '../Box/Box'
import { Flex } from '../Flex/Flex'
import { Grid } from '../Grid/Grid'
import { spaceValues } from '../theme'

/**
* Returns an array of values with the same length as numChildren with each item
* set to the corresponding value in the variation string. If numChildren is greater than
* the number of digits in variation, the same variation values will be repeated.
* @param variationWidth - variation string
* @param numChildren - number of children
* @returns
*/
const getNonhomogeneousWidths = (variationWidth, numChildren) => {
const variationWidths = variationWidth.split('-').map((width) => parseInt(width, 10))
const widths = []

for (let i = 0; i < numChildren; i++) {
widths.push(variationWidths[i % variationWidths.length] / 100)
}

return widths
}

const getWidthsForVariation = (variation: string, numChildren: number) => {
if (!variation) {
return null
}

if (variation === '50-50') {
return Array(numChildren).fill(1 / 2)
} else if (variation === '33-33-33') {
return Array(numChildren).fill(1 / 3)
} else if (variation === '25-25-25-25') {
return Array(numChildren).fill(1 / 4)
} else if (variation !== '100') {
return getNonhomogeneousWidths(variation, numChildren)
}

// For when variation is 100
return Array(numChildren).fill(1)
const getWidthsForVariation = (variation: string) => {
return (
variation &&
variation
.split('-')
.map((width) => `${parseInt(width, 10) / 10}fr`)
.join(' ')
)
}

/**
Expand All @@ -49,31 +22,26 @@ const getWidthsForVariation = (variation: string, numChildren: number) => {
* @param numChildren - number of children
* @returns
*/
const getChildrenWidths = (variation: LayoutVariation, numChildren: number) => {
const getGridTemplateColumns = (variation: LayoutVariation) => {
if (Array.isArray(variation)) {
const variationWidths = variation.map((v) => {
return getWidthsForVariation(v, numChildren)
return variation.map((v) => {
return getWidthsForVariation(v)
})

const widthsPerChild = []

for (let i = 0; i < numChildren; i++) {
const widths = variationWidths.map((v) => v && v[i])
widthsPerChild.push(widths)
}

return widthsPerChild
} else {
return getWidthsForVariation(variation, numChildren)
return getWidthsForVariation(variation)
}
}

// After converting Layout from using Flex to Grid, we need to multiply
// the space values by 2 since we're using them for gap instead of padding on each child.
const spaceToGapValue = (idx) => `${spaceValues[idx] * 2}px`

// Map named sizes to responsive size values from theme
const gapValues = {
sm: 1,
md: 2,
lg: 3,
xl: 4,
sm: spaceToGapValue(1),
md: spaceToGapValue(2),
lg: spaceToGapValue(3),
xl: spaceToGapValue(4),
}

/**
Expand All @@ -83,35 +51,24 @@ const gapValues = {
* @returns
*/
const getGapValues = (gapProp, rowGapProp) => {
let boxPaddingX
let boxPaddingY
let flexMarginX
let flexMarginY
let columnGap
let rowGap

if (Array.isArray(gapProp)) {
boxPaddingX = gapProp.map((gap) => gapValues[gap])

// These values are negative because we're trying to offset the padding
// applied to the left of the first item and right of the last item.
flexMarginX = gapProp.map((gap) => (gapValues[gap] ? -1 * gapValues[gap] : null))
if (Array.isArray(rowGapProp)) {
rowGap = rowGapProp.map((gap) => gapValues[gap] || null)
} else {
boxPaddingX = gapValues[gapProp]
flexMarginX = -1 * gapValues[gapProp]
rowGap = gapValues[rowGapProp]
}

if (Array.isArray(rowGapProp)) {
boxPaddingY = rowGapProp.map((gap) => gapValues[gap] || null)
flexMarginY = rowGapProp.map((gap) => (gapValues[gap] ? -1 * gapValues[gap] : null))
if (Array.isArray(gapProp)) {
columnGap = gapProp.map((gap) => gapValues[gap] || null)
} else {
boxPaddingY = gapValues[rowGapProp]
flexMarginY = -1 * gapValues[rowGapProp]
columnGap = gapValues[gapProp]
}

return { boxPaddingX, boxPaddingY, flexMarginX, flexMarginY }
return { columnGap, rowGap }
}

const memoGetGapValues = getGapValues

/**
* @public
*/
Expand Down Expand Up @@ -150,7 +107,7 @@ export type LayoutVariation =
/**
* @public
*/
export type LayoutProps = FlexWrapProps & {
export type LayoutProps = {
children: React.ReactElement | React.ReactElement[]
gap?: LayoutGap
rowGap?: LayoutGap
Expand All @@ -161,33 +118,33 @@ export type LayoutProps = FlexWrapProps & {
/**
* @public
*/
export function Layout({ children, gap, rowGap, variation, stretchHeight, flexWrap, ...props }: LayoutProps) {
const numChildren = React.Children.count(children)

export function Layout({ children, gap, rowGap, variation, stretchHeight, ...props }: LayoutProps) {
const [gapValues, setGapValues] = useState(getGapValues(gap, rowGap))
const [widths, setChildrenWidths] = useState(getChildrenWidths(variation, numChildren))
const [gridTemplateColumns, setGridTemplateColumns] = useState(getGridTemplateColumns(variation))

useEffect(() => {
setGapValues(memoGetGapValues(gap, rowGap))
setGapValues(getGapValues(gap, rowGap))
}, [gap, rowGap])

useEffect(() => {
setChildrenWidths(getChildrenWidths(variation, numChildren))
}, [variation, numChildren])

const { boxPaddingX, boxPaddingY, flexMarginX, flexMarginY } = gapValues
setGridTemplateColumns(getGridTemplateColumns(variation))
}, [variation])

return (
<Flex flexWrap={flexWrap} mx={flexMarginX} my={flexMarginY} data-testid='layout-flex' {...props}>
<Grid
{...props}
data-testid='layout-grid'
width={1}
gridTemplateColumns={gridTemplateColumns}
rowGap={gapValues.rowGap}
columnGap={gapValues.columnGap}
>
{React.Children.map(
children,
(child, idx) =>
child &&
React.isValidElement(child) && (
<ZIndexBox
width={widths[idx]}
px={boxPaddingX}
py={boxPaddingY}
data-testid={`box-${idx}`}
// TS ignored because the type of child.props is unknown
// @ts-ignore
Expand All @@ -199,12 +156,8 @@ export function Layout({ children, gap, rowGap, variation, stretchHeight, flexWr
</ZIndexBox>
)
)}
</Flex>
</Grid>
)
}

Layout.defaultProps = {
flexWrap: 'wrap',
}

Layout.displayName = 'Layout'
3 changes: 2 additions & 1 deletion packages/core/src/theme/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ addAliases(mediaQueries, aliases)
/**
* @public
*/
export const space = [0, 4, 8, 16, 32, 64, 128].map((n) => n + 'px')
export const spaceValues = [0, 4, 8, 16, 32, 64, 128]
export const space = spaceValues.map((n) => n + 'px')

/**
* @public
Expand Down

0 comments on commit 761b8a7

Please sign in to comment.