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 => ( +
    + {children} +
    + ) - 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 => ( +
      + {children} +
    ) - 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"