Skip to content

Commit

Permalink
Feat(web-react): Add spacing property to Grid #DS-1388
Browse files Browse the repository at this point in the history
  • Loading branch information
dlouhak committed Jul 24, 2024
1 parent a7176b4 commit 750436e
Show file tree
Hide file tree
Showing 12 changed files with 285 additions and 16 deletions.
11 changes: 9 additions & 2 deletions packages/web-react/src/components/Grid/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@ import { useGridStyleProps } from './useGridStyleProps';

export const Grid = <T extends ElementType = 'div'>(props: SpiritGridProps<T>): 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 (
<ElementTag {...otherProps} {...styleProps} className={classNames(classProps, styleProps.className)}>
<ElementTag {...otherProps} {...gridStyleProps} className={classNames(classProps, styleProps.className)}>
{children}
</ElementTag>
);
Expand Down
50 changes: 46 additions & 4 deletions packages/web-react/src/components/Grid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,54 @@ Use Grid to build multiple column layouts. This Grid works on twelve column syst
</Grid>
```

## 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:

```jsx
<Grid spacing="space-1200">
<!-- Grid content -->
</Grid>
```

Custom responsive spacing:

```jsx
<Grid spacing="{{ { mobile: 'space-400', tablet: 'space-800' } }}">
<!-- Grid content -->
</Grid>
```

Custom vertical spacing:

```jsx
<Grid spacingY="{{ { mobile: 'space-400', tablet: 'space-800' } }}">
<!-- Grid content -->
</Grid>
```

Custom horizontal spacing:

```jsx
<Grid spacingX="{{ { mobile: 'space-400', tablet: 'space-800' } }}">
<!-- Grid content -->
</Grid>
```

## 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<Record<BreakpointToken, SpaceToken>>`] ||| Apply [custom spacing](#custom-spacing) between items |
| `spacingX` | [`SpaceToken` \| `Partial<Record<BreakpointToken, SpaceToken>>`] ||| Apply horizontal [custom spacing](#custom-spacing) between items |
| `spacingY` | [`SpaceToken` \| `Partial<Record<BreakpointToken, SpaceToken>>`] ||| 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]
Expand Down
44 changes: 44 additions & 0 deletions packages/web-react/src/components/Grid/__tests__/Grid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Grid spacing="space-600" />);

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(<Grid spacing={{ mobile: 'space-100', tablet: 'space-1000', desktop: 'space-1200' }} />);

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(<Grid spacingY={{ mobile: 'space-100', tablet: 'space-1000', desktop: 'space-1200' }} />);

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(<Grid spacing={{ tablet: 'space-1000' }} />);

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)' });
});
});
11 changes: 11 additions & 0 deletions packages/web-react/src/components/Grid/demo/GridCustomSpacing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import Grid from '../Grid';
import GridItemFactory from './GridItemFactory';

const GridCustomSpacing = () => (
<Grid cols={{ mobile: 2, tablet: 3, desktop: 4 }} spacing="space-1000">
<GridItemFactory items={4} label="1/2, 1/3, 1/4" />
</Grid>
);

export default GridCustomSpacing;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import Grid from '../Grid';
import GridItemFactory from './GridItemFactory';

const GridResponsiveCustomHorizontalSpacing = () => (
<Grid
cols={{ mobile: 2, tablet: 3, desktop: 4 }}
spacingX={{
mobile: 'space-100',
tablet: 'space-400',
desktop: 'space-800',
}}
>
<GridItemFactory items={4} label="1/2, 1/3, 1/4" />
</Grid>
);

export default GridResponsiveCustomHorizontalSpacing;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import Grid from '../Grid';
import GridItemFactory from './GridItemFactory';

const GridResponsiveCustomSpacing = () => (
<Grid
cols={{ mobile: 2, tablet: 3, desktop: 4 }}
spacingY={{
mobile: 'space-800',
tablet: 'space-1000',
desktop: 'space-0',
}}
spacingX={{
mobile: 'space-1000',
tablet: 'space-900',
desktop: 'space-1200',
}}
>
<GridItemFactory items={4} label="1/2, 1/3, 1/4" />
</Grid>
);

export default GridResponsiveCustomSpacing;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import Grid from '../Grid';
import GridItemFactory from './GridItemFactory';

const GridResponsiveCustomVerticalSpacing = () => (
<Grid
cols={{ mobile: 2, tablet: 3, desktop: 4 }}
spacingY={{
mobile: 'space-100',
tablet: 'space-400',
desktop: 'space-800',
}}
>
<GridItemFactory items={8} label="1/2, 1/3, 1/4" />
</Grid>
);

export default GridResponsiveCustomVerticalSpacing;
16 changes: 16 additions & 0 deletions packages/web-react/src/components/Grid/demo/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import DocsSection from '../../../../docs/DocsSections';
import GridCustomSpacing from './GridCustomSpacing';
import GridEqual from './GridEqual';
import GridItem from './GridItem';
import GridItemRow from './GridItemRow';
import GridNestedGridItem from './GridNestedGridItem';
import GridResponsive from './GridResponsive';
import GridResponsiveCenteredGridItem from './GridResponsiveCenteredGridItem';
import GridResponsiveCustomHorizontalSpacing from './GridResponsiveCustomHorizontalSpacing';
import GridResponsiveCustomSpacing from './GridResponsiveCustomSpacing';
import GridResponsiveCustomVerticalSpacing from './GridResponsiveCustomVerticalSpacing';
import GridResponsiveGridItem from './GridResponsiveGridItem';

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
Expand All @@ -17,6 +21,18 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<DocsSection title="Responsive Columns">
<GridResponsive />
</DocsSection>
<DocsSection title="Custom Spacing">
<GridCustomSpacing />
</DocsSection>
<DocsSection title="Responsive Custom Spacing">
<GridResponsiveCustomSpacing />
</DocsSection>
<DocsSection title="Responsive Custom Vertical Spacing">
<GridResponsiveCustomVerticalSpacing />
</DocsSection>
<DocsSection title="Responsive Custom Horizontal Spacing">
<GridResponsiveCustomHorizontalSpacing />
</DocsSection>
<DocsSection title="Grid Item">
<GridItem />
</DocsSection>
Expand Down
24 changes: 24 additions & 0 deletions packages/web-react/src/components/Grid/stories/Grid.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ const meta: Meta<typeof Grid> = {
defaultValue: { summary: 'div' },
},
},
spacing: {
control: 'object',
},
spacingX: {
control: 'object',
},
spacingY: {
control: 'object',
},
},
args: {
children: (
Expand All @@ -43,6 +52,21 @@ const meta: Meta<typeof Grid> = {
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',
},
},
};

Expand Down
78 changes: 69 additions & 9 deletions packages/web-react/src/components/Grid/useGridStyleProps.ts
Original file line number Diff line number Diff line change
@@ -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<string, string | undefined>)[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<T> {
/** className props */
classProps: string;
/** Props for the grid element. */
props: T;
/** Style props for the element */
styleProps: GridCSSProperties;
}

export function useGridStyleProps(props: SpiritGridProps<ElementType>): GridStyles<SpiritGridProps<ElementType>> {
const { cols, ...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,
};
}
Loading

0 comments on commit 750436e

Please sign in to comment.