diff --git a/src/components/grid/Grid.stories.tsx b/src/components/grid/Grid.stories.tsx
index 2a685e9d54..7fd9216bfc 100644
--- a/src/components/grid/Grid.stories.tsx
+++ b/src/components/grid/Grid.stories.tsx
@@ -27,6 +27,30 @@ const exampleStyles = {
const testContent =
Content
+type CustomGridProps = JSX.IntrinsicElements['li']
+
+const CustomGrid: React.FunctionComponent = ({
+ children,
+ className,
+ ...liProps
+}: CustomGridProps): React.ReactElement => (
+
+ {children}
+
+)
+
+type CustomGridContainerProps = JSX.IntrinsicElements['ul']
+
+const CustomGridContainer: React.FunctionComponent = ({
+ children,
+ className,
+ ...ulProps
+}: CustomGridContainerProps): React.ReactElement => (
+
+)
+
export const defaultContainer = (): React.ReactElement => (
@@ -37,6 +61,54 @@ export const defaultContainer = (): React.ReactElement => (
)
+export const customElements = (): React.ReactElement => (
+ asCustom={CustomGridContainer}>
+ asCustom={CustomGrid}>
+ {testContent}
+ {testContent}
+
+ asCustom={CustomGrid}>
+ {testContent}
+ {testContent}
+
+ asCustom={CustomGrid}>
+ {testContent}
+ {testContent}
+
+ asCustom={CustomGrid}>
+ {testContent}
+ {testContent}
+
+ asCustom={CustomGrid}>
+ {testContent}
+ {testContent}
+
+ asCustom={CustomGrid}>
+ {testContent}
+
+ asCustom={CustomGrid}>
+ {testContent}
+ {testContent}
+
+ asCustom={CustomGrid}>
+ {testContent}
+ {testContent}
+
+ asCustom={CustomGrid}>
+ {testContent}
+ {testContent}
+
+ asCustom={CustomGrid}>
+ {testContent}
+ {testContent}
+
+ asCustom={CustomGrid}>
+ {testContent}
+ {testContent}
+
+
+)
+
export const columnSpans = (): React.ReactElement => (
diff --git a/src/components/grid/Grid/Grid.test.tsx b/src/components/grid/Grid/Grid.test.tsx
index 593589ae7d..f4764ec566 100644
--- a/src/components/grid/Grid/Grid.test.tsx
+++ b/src/components/grid/Grid/Grid.test.tsx
@@ -71,53 +71,79 @@ describe('applyGridClasses function', () => {
})
describe('Grid component', () => {
- it('renders without errors', () => {
- const { queryByTestId } = render()
- expect(queryByTestId('grid')).toBeInTheDocument()
+ describe('with default component', () => {
+ it('renders without errors', () => {
+ const { queryByTestId } = render()
+ expect(queryByTestId('grid')).toBeInTheDocument()
+ })
+
+ it('renders its children', () => {
+ const { queryByText } = render(My Content)
+ expect(queryByText('My Content')).toBeInTheDocument()
+ })
+
+ it('implements the col prop', () => {
+ const { getByTestId } = render(My Content)
+ expect(getByTestId('grid')).toHaveClass('grid-col-5')
+ })
+
+ it('implements the col auto prop', () => {
+ const { getByTestId } = render(My Content)
+ expect(getByTestId('grid')).toHaveClass('grid-col-auto')
+ })
+
+ it('implements the col fill prop', () => {
+ const { getByTestId } = render(My Content)
+ expect(getByTestId('grid')).toHaveClass('grid-col-fill')
+ })
+
+ it('implements the offset prop', () => {
+ const { getByTestId } = render(My Content)
+ expect(getByTestId('grid')).toHaveClass('grid-offset-4')
+ })
+
+ it('implements the row prop', () => {
+ const { getByTestId } = render(My Content)
+ expect(getByTestId('grid')).toHaveClass('grid-row')
+ })
+
+ it('implements the gap prop', () => {
+ const { getByTestId } = render(My Content)
+ expect(getByTestId('grid')).toHaveClass('grid-gap')
+ })
+
+ it('implements the gap size prop', () => {
+ const { getByTestId } = render(My Content)
+ expect(getByTestId('grid')).toHaveClass('grid-gap-sm')
+ })
+
+ it('implements breakpoint props', () => {
+ const { getByTestId } = render(
+ My Content
+ )
+ expect(getByTestId('grid')).toHaveClass('tablet:grid-col-8')
+ })
})
- it('renders its children', () => {
- const { queryByText } = render(My Content)
- expect(queryByText('My Content')).toBeInTheDocument()
- })
-
- it('implements the col prop', () => {
- const { getByTestId } = render(My Content)
- expect(getByTestId('grid')).toHaveClass('grid-col-5')
- })
-
- it('implements the col auto prop', () => {
- const { getByTestId } = render(My Content)
- expect(getByTestId('grid')).toHaveClass('grid-col-auto')
- })
-
- it('implements the col fill prop', () => {
- const { getByTestId } = render(My Content)
- expect(getByTestId('grid')).toHaveClass('grid-col-fill')
- })
-
- it('implements the offset prop', () => {
- const { getByTestId } = render(My Content)
- expect(getByTestId('grid')).toHaveClass('grid-offset-4')
- })
-
- it('implements the row prop', () => {
- const { getByTestId } = render(My Content)
- expect(getByTestId('grid')).toHaveClass('grid-row')
- })
-
- it('implements the gap prop', () => {
- const { getByTestId } = render(My Content)
- expect(getByTestId('grid')).toHaveClass('grid-gap')
- })
+ describe('with custom component', () => {
+ type CustomGridProps = JSX.IntrinsicElements['section']
+
+ const CustomGrid: React.FunctionComponent = ({
+ children,
+ className,
+ ...sectionProps
+ }: CustomGridProps): React.ReactElement => (
+
+ )
- it('implements the gap size prop', () => {
- const { getByTestId } = render(My Content)
- expect(getByTestId('grid')).toHaveClass('grid-gap-sm')
- })
+ it('renders without errors', () => {
+ const { getByRole } = render(
+ asCustom={CustomGrid}>something
+ )
- it('implements breakpoint props', () => {
- const { getByTestId } = render(My Content)
- expect(getByTestId('grid')).toHaveClass('tablet:grid-col-8')
+ expect(getByRole('grid')).toBeInTheDocument()
+ })
})
})
diff --git a/src/components/grid/Grid/Grid.tsx b/src/components/grid/Grid/Grid.tsx
index c29a91899f..060fee53bc 100644
--- a/src/components/grid/Grid/Grid.tsx
+++ b/src/components/grid/Grid/Grid.tsx
@@ -8,10 +8,46 @@ export type GridProps = GridItemProps &
[P in BreakpointKeys]?: GridItemProps
}
+export type GridComponentProps = GridProps & { className?: string } & T
+
export type GridLayoutProp = {
gridLayout?: GridProps
}
+interface WithCustomGridProps {
+ asCustom: React.FunctionComponent
+}
+
+export type DefaultGridProps = GridComponentProps
+
+export type CustomGridProps = GridComponentProps<
+ React.PropsWithChildren
+> &
+ WithCustomGridProps>
+
+type omittedProps =
+ | 'mobile'
+ | 'tablet'
+ | 'desktop'
+ | 'widescreen'
+ | 'mobileLg'
+ | 'tabletLg'
+ | 'desktopLg'
+ | 'children'
+ | 'className'
+ | 'row'
+ | 'col'
+ | 'gap'
+ | 'offset'
+
+export function isCustomProps(
+ props:
+ | Omit
+ | Omit, omittedProps>
+): props is Omit, omittedProps> {
+ return 'asCustom' in props
+}
+
export const getGridClasses = (
itemProps: GridItemProps = {},
breakpoint?: BreakpointKeys
@@ -47,12 +83,14 @@ export const applyGridClasses = (gridLayout: GridProps): string => {
return classes
}
-export const Grid = ({
- children,
- className,
- ...props
-}: GridProps & React.HTMLAttributes): React.ReactElement => {
+export function Grid(props: DefaultGridProps): React.ReactElement
+export function Grid(props: CustomGridProps): React.ReactElement
+export function Grid(
+ props: DefaultGridProps | CustomGridProps
+): React.ReactElement {
const {
+ children,
+ className,
row,
col,
gap,
@@ -83,6 +121,7 @@ export const Grid = ({
desktopLg,
widescreen,
}
+
let classes = getGridClasses(itemProps)
Object.keys(breakpoints).forEach((b) => {
@@ -94,12 +133,25 @@ export const Grid = ({
}
})
- // Pass in any custom classes
classes = classnames(classes, className)
- return (
-
- {children}
-
- )
+ 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 (
+
+ {children}
+
+ )
+ }
}
diff --git a/src/components/grid/GridContainer/GridContainer.test.tsx b/src/components/grid/GridContainer/GridContainer.test.tsx
index c5186b6868..f7eeebdf50 100644
--- a/src/components/grid/GridContainer/GridContainer.test.tsx
+++ b/src/components/grid/GridContainer/GridContainer.test.tsx
@@ -2,22 +2,89 @@ import React from 'react'
import { render } from '@testing-library/react'
import { GridContainer } from './GridContainer'
+import { Grid } from '../Grid/Grid'
+
+const testContent = 'a grid container item'
+const testGridContent = (
+
+ {testContent}
+ {testContent}
+ {testContent}
+
+)
describe('GridContainer component', () => {
- it('renders without errors', () => {
- const { queryByTestId } = render()
- expect(queryByTestId('gridContainer')).toBeInTheDocument()
- })
+ describe('default component', () => {
+ it('renders without errors', () => {
+ const { queryByTestId } = render(
+ {testGridContent}
+ )
+ expect(queryByTestId('gridContainer')).toBeInTheDocument()
+ })
- it('renders its children', () => {
- const { queryByText } = render(My Content)
- expect(queryByText('My Content')).toBeInTheDocument()
+ it('renders its children', () => {
+ const { queryByText } = render(My Content)
+ expect(queryByText('My Content')).toBeInTheDocument()
+ })
+
+ it('implements the containerSize prop', () => {
+ const { getByTestId } = render(
+ My Content
+ )
+ expect(getByTestId('gridContainer')).toHaveClass(
+ 'grid-container-tablet-lg'
+ )
+ })
})
- it('implements the containerSize prop', () => {
- const { getByTestId } = render(
- My Content
+ describe('custom component', () => {
+ type CustomGridContainerProps = JSX.IntrinsicElements['ul']
+
+ const CustomGridContainer: React.FunctionComponent = ({
+ children,
+ className,
+ ...ulProps
+ }: CustomGridContainerProps): React.ReactElement => (
+
)
- expect(getByTestId('gridContainer')).toHaveClass('grid-container-tablet-lg')
+
+ it('renders without errors', () => {
+ const { getByRole } = render(
+ asCustom={CustomGridContainer}>
+ {testGridContent}
+ {testGridContent}
+ {testGridContent}
+ {testGridContent}
+
+ )
+
+ const list = getByRole('list')
+ expect(list).toBeInTheDocument()
+ expect(list).toHaveClass('grid-container')
+ })
+
+ it('handles own props', () => {
+ const { getByRole, getByTestId } = render(
+
+ asCustom={CustomGridContainer}
+ className="custom-class"
+ custom-attribute="customAtt"
+ data-testid="customTestId"
+ containerSize="mobile-lg">
+ {testGridContent}
+ {testGridContent}
+ {testGridContent}
+ {testGridContent}
+
+ )
+
+ const list = getByRole('list')
+ expect(list).toBeInTheDocument()
+ expect(list).toHaveAttribute('custom-attribute', 'customAtt')
+ expect(list).toHaveClass('grid-container-mobile-lg custom-class')
+ expect(getByTestId('customTestId')).toBeInTheDocument()
+ })
})
})
diff --git a/src/components/grid/GridContainer/GridContainer.tsx b/src/components/grid/GridContainer/GridContainer.tsx
index 6e08a6576b..fcaf86c626 100644
--- a/src/components/grid/GridContainer/GridContainer.tsx
+++ b/src/components/grid/GridContainer/GridContainer.tsx
@@ -3,17 +3,33 @@ import classnames from 'classnames'
import { ContainerSizes } from '../types'
-type GridContainerProps = {
+type GridContainerProps = {
containerSize?: ContainerSizes
+ className?: string
+ children: React.ReactNode
}
-export const GridContainer = ({
- children,
- containerSize,
- className,
- ...props
-}: GridContainerProps &
- React.HTMLAttributes): React.ReactElement => {
+interface WithCustomGridContainerProps {
+ asCustom: React.FunctionComponent
+}
+
+export type DefaultGridContainerProps = GridContainerProps<
+ JSX.IntrinsicElements['div']
+>
+
+export type CustomGridContainerProps = GridContainerProps &
+ WithCustomGridContainerProps
+
+export function isCustomProps(
+ props: DefaultGridContainerProps | CustomGridContainerProps
+): props is CustomGridContainerProps {
+ return 'asCustom' in props
+}
+
+function gridContainerClasses(
+ className: GridContainerProps['className'],
+ containerSize: GridContainerProps['containerSize']
+): string | undefined {
const classes = classnames(
{
'grid-container': !containerSize,
@@ -21,10 +37,48 @@ export const GridContainer = ({
},
className
)
+ return classes
+}
- return (
-
- {children}
-
- )
+export function GridContainer(
+ props: DefaultGridContainerProps
+): React.ReactElement
+export function GridContainer(
+ props: CustomGridContainerProps
+): React.ReactElement
+export function GridContainer(
+ props: DefaultGridContainerProps | CustomGridContainerProps
+): React.ReactElement {
+ if (isCustomProps(props)) {
+ const {
+ className,
+ containerSize,
+ asCustom,
+ children,
+ ...remainingProps
+ } = props
+ const gridContainerProps: FCProps = (remainingProps as unknown) as FCProps
+ const classes = gridContainerClasses(className, containerSize)
+ return React.createElement(
+ asCustom,
+ {
+ 'data-testid': 'gridContainer',
+ className: classes,
+ ...gridContainerProps,
+ },
+ children
+ )
+ } else {
+ const { className, containerSize, children, ...gridContainerProps } = props
+
+ const classes = gridContainerClasses(className, containerSize)
+ return (
+
+ {children}
+
+ )
+ }
}
diff --git a/yarn.lock b/yarn.lock
index b1ea3212d1..3419ab1ee8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1481,10 +1481,10 @@
is-plain-object "^5.0.0"
universal-user-agent "^6.0.0"
-"@octokit/openapi-types@^6.1.1":
- version "6.1.1"
- resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-6.1.1.tgz#27f9386fcbcb9846b27b1bc8a41ba6f313c922a6"
- integrity sha512-ICBhnEb+ahi/TTdNuYb/kTyKVBgAM0VD4k6JPzlhJyzt3Z+Tq/bynwCD+gpkJP7AEcNnzC8YO5R39trmzEo2UA==
+"@octokit/openapi-types@^6.2.0":
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-6.2.0.tgz#6ea796b20c7111b9e422a4d607f796c1179622cd"
+ integrity sha512-V2vFYuawjpP5KUb8CPYsq20bXT4qnE8sH1QKpYqUlcNOntBiRr/VzGVvY0s+YXGgrVbFUVO4EI0VnHYSVBWfBg==
"@octokit/plugin-paginate-rest@^1.1.1":
version "1.1.2"
@@ -1566,11 +1566,11 @@
"@types/node" ">= 8"
"@octokit/types@^6.0.3", "@octokit/types@^6.7.1":
- version "6.13.2"
- resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.13.2.tgz#e3423dc733567ac4836e116b34d154a8e9cbbf3c"
- integrity sha512-jN5LImYHvv7W6SZargq1UMJ3EiaqIz5qkpfsv4GAb4b16SGqctxtOU2TQAZxvsKHkOw2A4zxdsi5wR9en1/ezQ==
+ version "6.14.0"
+ resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.14.0.tgz#587529b4a461d8b7621b99845718dc5c79281f52"
+ integrity sha512-43qHvDsPsKgNt4W4al3dyU6s2XZ7ZMsiiIw8rQcM9CyEo7g9W8/6m1W4xHuRqmEjTfG1U4qsE/E4Jftw1/Ak1g==
dependencies:
- "@octokit/openapi-types" "^6.1.1"
+ "@octokit/openapi-types" "^6.2.0"
"@pmmmwh/react-refresh-webpack-plugin@^0.4.3":
version "0.4.3"