Skip to content

Commit

Permalink
feat: Update Grid components to render any type of element (#1166)
Browse files Browse the repository at this point in the history
* Update GridContainer to handle any component but default to a div, update tests, update storybook with customContainer story

* Combine GridContainer examples in storybook

* Update Grid component to render any type of element

* Regenerate yarn.lock

* Combine custom components into one Storybook example

* Remove superfluous li element from custom storybook example

* Remove unnecessary spaces from storybook file

* Move call to classes function outside of if statement separating default from custom component creation

* Update isCustomProps to accept "otherProps" (#1194)

* Update isCustomProps to accept "otherProps"

* Refactor out ommited props

Co-authored-by: Brandon Lenz <brandonalenz@gmail.com>
  • Loading branch information
SirenaBorracha and brandonlenz committed May 12, 2021
1 parent 286cca0 commit cd0a68f
Show file tree
Hide file tree
Showing 6 changed files with 358 additions and 87 deletions.
72 changes: 72 additions & 0 deletions src/components/grid/Grid.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,30 @@ const exampleStyles = {

const testContent = <div style={exampleStyles}>Content</div>

type CustomGridProps = JSX.IntrinsicElements['li']

const CustomGrid: React.FunctionComponent<CustomGridProps> = ({
children,
className,
...liProps
}: CustomGridProps): React.ReactElement => (
<li className={className} {...liProps}>
{children}
</li>
)

type CustomGridContainerProps = JSX.IntrinsicElements['ul']

const CustomGridContainer: React.FunctionComponent<CustomGridContainerProps> = ({
children,
className,
...ulProps
}: CustomGridContainerProps): React.ReactElement => (
<ul className={className} {...ulProps}>
{children}
</ul>
)

export const defaultContainer = (): React.ReactElement => (
<GridContainer>
<Grid row>
Expand All @@ -37,6 +61,54 @@ export const defaultContainer = (): React.ReactElement => (
</GridContainer>
)

export const customElements = (): React.ReactElement => (
<GridContainer<CustomGridContainerProps> asCustom={CustomGridContainer}>
<Grid<CustomGridProps> asCustom={CustomGrid}>
<Grid col={11}>{testContent}</Grid>
<Grid col={2}>{testContent}</Grid>
</Grid>
<Grid<CustomGridProps> asCustom={CustomGrid}>
<Grid col={10}>{testContent}</Grid>
<Grid col={3}>{testContent}</Grid>
</Grid>
<Grid<CustomGridProps> asCustom={CustomGrid}>
<Grid col={9}>{testContent}</Grid>
<Grid col={4}>{testContent}</Grid>
</Grid>
<Grid<CustomGridProps> asCustom={CustomGrid}>
<Grid col={8}>{testContent}</Grid>
<Grid col={5}>{testContent}</Grid>
</Grid>
<Grid<CustomGridProps> asCustom={CustomGrid}>
<Grid col={7}>{testContent}</Grid>
<Grid col={6}>{testContent}</Grid>
</Grid>
<Grid<CustomGridProps> asCustom={CustomGrid}>
<Grid col={5}>{testContent}</Grid>
</Grid>
<Grid<CustomGridProps> asCustom={CustomGrid}>
<Grid col={6}>{testContent}</Grid>
<Grid col={7}>{testContent}</Grid>
</Grid>
<Grid<CustomGridProps> asCustom={CustomGrid}>
<Grid col={5}>{testContent}</Grid>
<Grid col={8}>{testContent}</Grid>
</Grid>
<Grid<CustomGridProps> asCustom={CustomGrid}>
<Grid col={4}>{testContent}</Grid>
<Grid col={9}>{testContent}</Grid>
</Grid>
<Grid<CustomGridProps> asCustom={CustomGrid}>
<Grid col={3}>{testContent}</Grid>
<Grid col={10}>{testContent}</Grid>
</Grid>
<Grid<CustomGridProps> asCustom={CustomGrid}>
<Grid col={2}>{testContent}</Grid>
<Grid col={11}>{testContent}</Grid>
</Grid>
</GridContainer>
)

export const columnSpans = (): React.ReactElement => (
<GridContainer>
<Grid row>
Expand Down
114 changes: 70 additions & 44 deletions src/components/grid/Grid/Grid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,53 +71,79 @@ describe('applyGridClasses function', () => {
})

describe('Grid component', () => {
it('renders without errors', () => {
const { queryByTestId } = render(<Grid />)
expect(queryByTestId('grid')).toBeInTheDocument()
describe('with default component', () => {
it('renders without errors', () => {
const { queryByTestId } = render(<Grid />)
expect(queryByTestId('grid')).toBeInTheDocument()
})

it('renders its children', () => {
const { queryByText } = render(<Grid>My Content</Grid>)
expect(queryByText('My Content')).toBeInTheDocument()
})

it('implements the col prop', () => {
const { getByTestId } = render(<Grid col={5}>My Content</Grid>)
expect(getByTestId('grid')).toHaveClass('grid-col-5')
})

it('implements the col auto prop', () => {
const { getByTestId } = render(<Grid col="auto">My Content</Grid>)
expect(getByTestId('grid')).toHaveClass('grid-col-auto')
})

it('implements the col fill prop', () => {
const { getByTestId } = render(<Grid col="fill">My Content</Grid>)
expect(getByTestId('grid')).toHaveClass('grid-col-fill')
})

it('implements the offset prop', () => {
const { getByTestId } = render(<Grid offset={4}>My Content</Grid>)
expect(getByTestId('grid')).toHaveClass('grid-offset-4')
})

it('implements the row prop', () => {
const { getByTestId } = render(<Grid row>My Content</Grid>)
expect(getByTestId('grid')).toHaveClass('grid-row')
})

it('implements the gap prop', () => {
const { getByTestId } = render(<Grid gap>My Content</Grid>)
expect(getByTestId('grid')).toHaveClass('grid-gap')
})

it('implements the gap size prop', () => {
const { getByTestId } = render(<Grid gap="sm">My Content</Grid>)
expect(getByTestId('grid')).toHaveClass('grid-gap-sm')
})

it('implements breakpoint props', () => {
const { getByTestId } = render(
<Grid tablet={{ col: 8 }}>My Content</Grid>
)
expect(getByTestId('grid')).toHaveClass('tablet:grid-col-8')
})
})

it('renders its children', () => {
const { queryByText } = render(<Grid>My Content</Grid>)
expect(queryByText('My Content')).toBeInTheDocument()
})

it('implements the col prop', () => {
const { getByTestId } = render(<Grid col={5}>My Content</Grid>)
expect(getByTestId('grid')).toHaveClass('grid-col-5')
})

it('implements the col auto prop', () => {
const { getByTestId } = render(<Grid col="auto">My Content</Grid>)
expect(getByTestId('grid')).toHaveClass('grid-col-auto')
})

it('implements the col fill prop', () => {
const { getByTestId } = render(<Grid col="fill">My Content</Grid>)
expect(getByTestId('grid')).toHaveClass('grid-col-fill')
})

it('implements the offset prop', () => {
const { getByTestId } = render(<Grid offset={4}>My Content</Grid>)
expect(getByTestId('grid')).toHaveClass('grid-offset-4')
})

it('implements the row prop', () => {
const { getByTestId } = render(<Grid row>My Content</Grid>)
expect(getByTestId('grid')).toHaveClass('grid-row')
})

it('implements the gap prop', () => {
const { getByTestId } = render(<Grid gap>My Content</Grid>)
expect(getByTestId('grid')).toHaveClass('grid-gap')
})
describe('with custom component', () => {
type CustomGridProps = JSX.IntrinsicElements['section']

const CustomGrid: React.FunctionComponent<CustomGridProps> = ({
children,
className,
...sectionProps
}: CustomGridProps): React.ReactElement => (
<section role="grid" className={className} {...sectionProps}>
{children}
</section>
)

it('implements the gap size prop', () => {
const { getByTestId } = render(<Grid gap="sm">My Content</Grid>)
expect(getByTestId('grid')).toHaveClass('grid-gap-sm')
})
it('renders without errors', () => {
const { getByRole } = render(
<Grid<CustomGridProps> asCustom={CustomGrid}>something</Grid>
)

it('implements breakpoint props', () => {
const { getByTestId } = render(<Grid tablet={{ col: 8 }}>My Content</Grid>)
expect(getByTestId('grid')).toHaveClass('tablet:grid-col-8')
expect(getByRole('grid')).toBeInTheDocument()
})
})
})
74 changes: 63 additions & 11 deletions src/components/grid/Grid/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,46 @@ export type GridProps = GridItemProps &
[P in BreakpointKeys]?: GridItemProps
}

export type GridComponentProps<T> = GridProps & { className?: string } & T

export type GridLayoutProp = {
gridLayout?: GridProps
}

interface WithCustomGridProps<T> {
asCustom: React.FunctionComponent<T>
}

export type DefaultGridProps = GridComponentProps<JSX.IntrinsicElements['div']>

export type CustomGridProps<T> = GridComponentProps<
React.PropsWithChildren<T>
> &
WithCustomGridProps<React.PropsWithChildren<T>>

type omittedProps =
| 'mobile'
| 'tablet'
| 'desktop'
| 'widescreen'
| 'mobileLg'
| 'tabletLg'
| 'desktopLg'
| 'children'
| 'className'
| 'row'
| 'col'
| 'gap'
| 'offset'

export function isCustomProps<T>(
props:
| Omit<DefaultGridProps, omittedProps>
| Omit<CustomGridProps<T>, omittedProps>
): props is Omit<CustomGridProps<T>, omittedProps> {
return 'asCustom' in props
}

export const getGridClasses = (
itemProps: GridItemProps = {},
breakpoint?: BreakpointKeys
Expand Down Expand Up @@ -47,12 +83,14 @@ export const applyGridClasses = (gridLayout: GridProps): string => {
return classes
}

export const Grid = ({
children,
className,
...props
}: GridProps & React.HTMLAttributes<HTMLDivElement>): React.ReactElement => {
export function Grid(props: DefaultGridProps): React.ReactElement
export function Grid<T>(props: CustomGridProps<T>): React.ReactElement
export function Grid<FCProps = DefaultGridProps>(
props: DefaultGridProps | CustomGridProps<FCProps>
): React.ReactElement {
const {
children,
className,
row,
col,
gap,
Expand Down Expand Up @@ -83,6 +121,7 @@ export const Grid = ({
desktopLg,
widescreen,
}

let classes = getGridClasses(itemProps)

Object.keys(breakpoints).forEach((b) => {
Expand All @@ -94,12 +133,25 @@ export const Grid = ({
}
})

// Pass in any custom classes
classes = classnames(classes, className)

return (
<div className={classes} data-testid="grid" {...otherProps}>
{children}
</div>
)
if (isCustomProps(otherProps)) {
const { asCustom, ...remainingProps } = otherProps

const gridProps: FCProps = (remainingProps as unknown) as FCProps
return React.createElement(
asCustom,
{
className: classes,
...gridProps,
},
children
)
} else {
return (
<div className={classes} data-testid="grid" {...otherProps}>
{children}
</div>
)
}
}
Loading

0 comments on commit cd0a68f

Please sign in to comment.