diff --git a/packages/web-react/src/components/Grid/Grid.tsx b/packages/web-react/src/components/Grid/Grid.tsx index 6bfb2bfc4d..7d615108c1 100644 --- a/packages/web-react/src/components/Grid/Grid.tsx +++ b/packages/web-react/src/components/Grid/Grid.tsx @@ -6,11 +6,18 @@ import { useGridStyleProps } from './useGridStyleProps'; export const Grid = (props: SpiritGridProps): JSX.Element => { const { elementType: ElementTag = 'div', children, ...restProps } = props; - const { classProps, props: modifiedProps } = useGridStyleProps(restProps); + const { classProps, props: modifiedProps, styleProps: gridStyle } = useGridStyleProps(restProps); const { styleProps, props: otherProps } = useStyleProps(modifiedProps); + const gridStyleProps = { + style: { + ...styleProps.style, + ...gridStyle, + }, + }; + return ( - + {children} ); diff --git a/packages/web-react/src/components/Grid/README.md b/packages/web-react/src/components/Grid/README.md index 5b439c01c0..a9de797f4a 100644 --- a/packages/web-react/src/components/Grid/README.md +++ b/packages/web-react/src/components/Grid/README.md @@ -15,12 +15,46 @@ Use Grid to build multiple column layouts. This Grid works on twelve column syst ``` +## Custom Spacing + +You can use the `spacing` prop to apply custom spacing between items. The prop +accepts either a spacing token (e.g. `space-100`) or an object with breakpoint keys and spacing token values. + +You can set custom spacing in the horizontal (x-axis) and vertical (y-axis) direction using the `spacing-x` and `spacing-y` props. + +Custom spacing: + +```twig + + + +``` + +Custom responsive spacing: + +```twig + + + +``` + +Custom vertical spacing: + +```twig + + + +``` + ## API -| Name | Type | Default | Required | Description | -| ------------- | ------------------------------------------------------------ | ------- | -------- | ---------------------------------------------------------------------------------------------------------- | -| `cols` | [`1` \| `2` \| `3` \| `4` \| `5` \| `6` \| `12` \| `object`] | — | ✕ | Number of columns to use, use object to set responsive values, e.g. `{ mobile: 1, tablet: 2, desktop: 3 }` | -| `elementType` | HTML element | `div` | ✕ | Element type to use for the Grid | +| Name | Type | Default | Required | Description | +| ------------- | ---------------------------------------------------------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------- | +| `cols` | [`1` \| `2` \| `3` \| `4` \| `5` \| `6` \| `12` \| `object`] | — | ✕ | Number of columns to use, use object to set responsive values, e.g. `{ mobile: 1, tablet: 2, desktop: 3 }` | +| `elementType` | HTML element | `div` | ✕ | Element type to use for the Grid | +| `spacing` | [`SpaceToken` \| `Partial>`] | — | ✕ | Apply [custom spacing](#custom-spacing) between items | +| `spacingX` | [`SpaceToken` \| `Partial>`] | — | ✕ | Apply horizontal [custom spacing](#custom-spacing) between items | +| `spacingY` | [`SpaceToken` \| `Partial>`] | — | ✕ | Apply vertical [custom spacing](#custom-spacing) between items | On top of the API options, the components accept [additional attributes][readme-additional-attributes]. If you need more control over the styling of a component, you can use [style props][readme-style-props] diff --git a/packages/web-react/src/components/Grid/__tests__/Grid.test.tsx b/packages/web-react/src/components/Grid/__tests__/Grid.test.tsx index 97c9105cc4..5abc51db8e 100644 --- a/packages/web-react/src/components/Grid/__tests__/Grid.test.tsx +++ b/packages/web-react/src/components/Grid/__tests__/Grid.test.tsx @@ -49,4 +49,48 @@ describe('Grid', () => { const element = dom.container.querySelector('div') as HTMLElement; expect(element).toHaveClass('Grid--cols-2'); }); + + it('should render with custom spacing', () => { + const dom = render(); + + const element = dom.container.querySelector('div') as HTMLElement; + expect(element).toHaveStyle({ '--grid-spacing-x': 'var(--spirit-space-600)' }); + expect(element).toHaveStyle({ '--grid-spacing-y': 'var(--spirit-space-600)' }); + }); + + it('should render with custom spacing for each breakpoint', () => { + const dom = render(); + + const element = dom.container.querySelector('div') as HTMLElement; + expect(element).toHaveStyle({ '--grid-spacing-x': 'var(--spirit-space-100)' }); + expect(element).toHaveStyle({ '--grid-spacing-y': 'var(--spirit-space-100)' }); + expect(element).toHaveStyle({ '--grid-spacing-x-tablet': 'var(--spirit-space-1000)' }); + expect(element).toHaveStyle({ '--grid-spacing-y-tablet': 'var(--spirit-space-1000)' }); + expect(element).toHaveStyle({ '--grid-spacing-x-desktop': 'var(--spirit-space-1200)' }); + expect(element).toHaveStyle({ '--grid-spacing-y-desktop': 'var(--spirit-space-1200)' }); + }); + + it('should render with custom vertical spacing for each breakpoint', () => { + const dom = render(); + + const element = dom.container.querySelector('div') as HTMLElement; + expect(element).toHaveStyle({ '--grid-spacing-y': 'var(--spirit-space-100)' }); + expect(element).toHaveStyle({ '--grid-spacing-y-tablet': 'var(--spirit-space-1000)' }); + expect(element).toHaveStyle({ '--grid-spacing-y-desktop': 'var(--spirit-space-1200)' }); + expect(element).not.toHaveStyle({ '--grid-spacing-x': 'var(--spirit-space-100)' }); + expect(element).not.toHaveStyle({ '--grid-spacing-x-tablet': 'var(--spirit-space-1000)' }); + expect(element).not.toHaveStyle({ '--grid-spacing-x-desktop': 'var(--spirit-space-1200)' }); + }); + + it('should render with custom spacing for only one breakpoint', () => { + const dom = render(); + + const element = dom.container.querySelector('div') as HTMLElement; + expect(element).toHaveStyle({ '--grid-spacing-x-tablet': 'var(--spirit-space-1000)' }); + expect(element).toHaveStyle({ '--grid-spacing-y-tablet': 'var(--spirit-space-1000)' }); + expect(element).not.toHaveStyle({ '--grid-spacing-x': 'var(--spirit-space-100)' }); + expect(element).not.toHaveStyle({ '--grid-spacing-y': 'var(--spirit-space-100)' }); + expect(element).not.toHaveStyle({ '--grid-spacing-x-desktop': 'var(--spirit-space-1200)' }); + expect(element).not.toHaveStyle({ '--grid-spacing-y-desktop': 'var(--spirit-space-1200)' }); + }); }); diff --git a/packages/web-react/src/components/Grid/demo/GridCustomSpacing.tsx b/packages/web-react/src/components/Grid/demo/GridCustomSpacing.tsx new file mode 100644 index 0000000000..8d73ff1924 --- /dev/null +++ b/packages/web-react/src/components/Grid/demo/GridCustomSpacing.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import Grid from '../Grid'; +import GridItemFactory from './GridItemFactory'; + +const GridCustomSpacing = () => ( + + + +); + +export default GridCustomSpacing; diff --git a/packages/web-react/src/components/Grid/demo/GridResponsiveCustomSpacing.tsx b/packages/web-react/src/components/Grid/demo/GridResponsiveCustomSpacing.tsx new file mode 100644 index 0000000000..a3e9f60786 --- /dev/null +++ b/packages/web-react/src/components/Grid/demo/GridResponsiveCustomSpacing.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import Grid from '../Grid'; +import GridItemFactory from './GridItemFactory'; + +const GridResponsiveCustomSpacing = () => ( + + + +); + +export default GridResponsiveCustomSpacing; diff --git a/packages/web-react/src/components/Grid/demo/index.tsx b/packages/web-react/src/components/Grid/demo/index.tsx index b50bd4ba48..0d10a07cc3 100644 --- a/packages/web-react/src/components/Grid/demo/index.tsx +++ b/packages/web-react/src/components/Grid/demo/index.tsx @@ -8,6 +8,8 @@ import GridNestedGridItem from './GridNestedGridItem'; import GridResponsive from './GridResponsive'; import GridResponsiveCenteredGridItem from './GridResponsiveCenteredGridItem'; import GridResponsiveGridItem from './GridResponsiveGridItem'; +import GridCustomSpacing from './GridCustomSpacing'; +import GridResponsiveCustomSpacing from './GridResponsiveCustomSpacing'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( @@ -17,6 +19,12 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + + diff --git a/packages/web-react/src/components/Grid/stories/Grid.stories.tsx b/packages/web-react/src/components/Grid/stories/Grid.stories.tsx index 81b5fae2aa..0252ee53ac 100644 --- a/packages/web-react/src/components/Grid/stories/Grid.stories.tsx +++ b/packages/web-react/src/components/Grid/stories/Grid.stories.tsx @@ -26,6 +26,15 @@ const meta: Meta = { defaultValue: { summary: 'div' }, }, }, + spacing: { + control: 'object', + }, + spacingX: { + control: 'object', + }, + spacingY: { + control: 'object', + }, }, args: { children: ( @@ -43,6 +52,21 @@ const meta: Meta = { desktop: 4, }, elementType: 'div', + spacing: { + mobile: 'space-600', + tablet: 'space-600', + desktop: 'space-600', + }, + spacingX: { + mobile: 'space-600', + tablet: 'space-600', + desktop: 'space-600', + }, + spacingY: { + mobile: 'space-600', + tablet: 'space-600', + desktop: 'space-600', + }, }, }; diff --git a/packages/web-react/src/components/Grid/useGridStyleProps.ts b/packages/web-react/src/components/Grid/useGridStyleProps.ts index d4114de41b..f049a59b22 100644 --- a/packages/web-react/src/components/Grid/useGridStyleProps.ts +++ b/packages/web-react/src/components/Grid/useGridStyleProps.ts @@ -1,37 +1,97 @@ import classNames from 'classnames'; -import { ElementType } from 'react'; +import { CSSProperties, ElementType } from 'react'; import { useClassNamePrefix } from '../../hooks'; import { SpiritGridProps } from '../../types'; +interface GridCSSProperties extends CSSProperties { + [key: string]: string | undefined | number; +} + +const setStyleProperty = (styleObject: GridCSSProperties, propertyName: string, value: string | undefined) => { + (styleObject as Record)[propertyName] = `var(--spirit-${value})`; +}; + +const setGridStyle = (styleObject: GridCSSProperties, baseVarName: string, propValue: GridCSSProperties) => { + if (typeof propValue === 'object' && propValue !== null) { + Object.keys(propValue).forEach((key) => { + const suffix = key === 'mobile' ? '' : `-${key}`; + const propName = `--${baseVarName}${suffix}`; + setStyleProperty(styleObject, propName, propValue[key as keyof GridCSSProperties]?.toString()); + }); + } else { + const propName = `--${baseVarName}`; + setStyleProperty(styleObject, propName, propValue); + } +}; + export interface GridStyles { /** className props */ classProps: string; /** Props for the grid element. */ props: T; + /** Style props for the element */ + styleProps: GridCSSProperties; } export function useGridStyleProps(props: SpiritGridProps): GridStyles> { - const { cols, ...restProps } = props; + const { cols, spacing, spacingX, spacingY, ...restProps } = props; const gridClass = useClassNamePrefix('Grid'); + const gridStyle: GridCSSProperties = {}; + + const typePropNames = Object.keys(props).filter((propName) => propName.startsWith('spacing')); + + typePropNames.forEach((propName) => { + let type: string; + + if (props[propName]) { + if (propName.startsWith('spacingX')) { + type = 'spacing-x'; + } else if (propName.startsWith('spacingY')) { + type = 'spacing-y'; + } else { + type = propName; + } + + if (type === 'spacing') { + setGridStyle( + gridStyle, + `grid-spacing-x${propName.replace(type, '').toLowerCase()}`, + props[propName] as GridCSSProperties, + ); + setGridStyle( + gridStyle, + `grid-spacing-y${propName.replace(type, '').toLowerCase()}`, + props[propName] as GridCSSProperties, + ); + } else { + setGridStyle(gridStyle, `grid-${type}`, props[propName] as GridCSSProperties); + } + } + + delete props[propName]; + }); + + let classes: string; + let gridColsClass: string; + if (typeof cols === 'object' && cols !== null) { - const classes: string[] = []; + const classList: string[] = []; Object.keys(cols).forEach((key) => { const infix = key === 'mobile' ? '' : `--${key}`; - classes.push(`${gridClass}${infix}--cols-${cols[key as keyof typeof cols]}`); + classList.push(`${gridClass}${infix}--cols-${cols[key as keyof typeof cols]}`); }); - return { - classProps: classNames(gridClass, classes), - props: restProps, - }; + classes = classNames(gridClass, classList); + } else { + gridColsClass = `${gridClass}--cols-${cols}`; + classes = classNames(gridClass, { [gridColsClass]: cols }); } - const gridColsClass = `${gridClass}--cols-${cols}`; - const classes = classNames(gridClass, { [gridColsClass]: cols }); return { classProps: classes, props: restProps, + styleProps: gridStyle, }; } diff --git a/packages/web-react/src/types/grid.ts b/packages/web-react/src/types/grid.ts index 4a6ee19e88..75bd6d8c1a 100644 --- a/packages/web-react/src/types/grid.ts +++ b/packages/web-react/src/types/grid.ts @@ -1,5 +1,5 @@ import { ElementType, JSXElementConstructor } from 'react'; -import { ChildrenProps, StyleProps, TransferProps } from './shared'; +import { BreakpointToken, ChildrenProps, SpaceToken, StyleProps, TransferProps } from './shared'; export type GridColumns = 1 | 2 | 3 | 4 | 5 | 6 | 12; export type GridColsBreakpoints = { @@ -35,6 +35,12 @@ export interface GridItemElementTypeProps { export interface GridCustomLayoutProps { cols?: GridColumns | GridColsBreakpoints; + /** Custom gutter between items */ + spacing?: SpaceToken | Partial>; + /** Custom horizontal gutter between items */ + spacingX?: SpaceToken | Partial>; + /** Custom vertical gutter between items */ + spacingY?: SpaceToken | Partial>; } export interface GridItemCustomLayoutProps { diff --git a/tests/e2e/demo-components-compare.spec.ts-snapshots/grid-chromium-linux.png b/tests/e2e/demo-components-compare.spec.ts-snapshots/grid-chromium-linux.png index b6fdf5bcc1..64299905f9 100644 Binary files a/tests/e2e/demo-components-compare.spec.ts-snapshots/grid-chromium-linux.png and b/tests/e2e/demo-components-compare.spec.ts-snapshots/grid-chromium-linux.png differ