Skip to content

Commit

Permalink
Feat(web-react): Introduce custom Stack spacing #DS-1079
Browse files Browse the repository at this point in the history
  • Loading branch information
crishpeen committed Jan 4, 2024
1 parent cb55f5b commit c0a678a
Show file tree
Hide file tree
Showing 19 changed files with 242 additions and 22 deletions.
40 changes: 30 additions & 10 deletions packages/web-react/src/components/Stack/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Stack

[Stack] is a component that allows you to compose elements vertically.
[Stack][stack] is a component that allows you to compose elements vertically.

Basic example usage:

Expand Down Expand Up @@ -28,17 +28,37 @@ Advanced example usage:
</Stack>
```

## Custom Spacing

You can use the `spacing` prop to apply custom spacing between items. The prop
accepts either a spacing token (eg. `space-100`) or an object with breakpoint keys and spacing token values.

```jsx
<Stack hasSpacing spacing="space-1200">
<div>Block 1</div>
<div>Block 2</div>
<div>Block 3</div>
</Stack>

<Stack hasSpacing spacing={{ mobile: 'space-400', tablet: 'space-800' }}>
<div>Block 1</div>
<div>Block 2</div>
<div>Block 3</div>
</Stack>
```

## API

| Name | Type | Default | Required | Description |
| ------------------------- | --------------- | ------- | -------- | -------------------------------------- |
| `elementType` | `string` | `div` || Element type of the wrapper element |
| `hasEndDivider` | `bool` | `false` || Render a divider after the last item |
| `hasIntermediateDividers` | `bool` | `false` || Render dividers between items |
| `hasSpacing` | `bool` | `false` || Apply a spacing between items |
| `hasStartDivider` | `bool` | `false` || Render a divider before the first item |
| `UNSAFE_className` | `string` ||| Wrapper custom class name |
| `UNSAFE_style` | `CSSProperties` ||| Wrapper custom style |
| Name | Type | Default | Required | Description |
| ------------------------- | ---------------------------------------------------------------- | ------- | -------- | ------------------------------------------------------------------- |
| `elementType` | `string` | `div` || Element type of the wrapper element |
| `hasEndDivider` | `bool` | `false` || Render a divider after the last item |
| `hasIntermediateDividers` | `bool` | `false` || Render dividers between items |
| `hasSpacing` | `bool` | `false` || Apply a spacing between items |
| `hasStartDivider` | `bool` | `false` || Render a divider before the first item |
| `spacing` | [`SpaceToken` \| `Partial<Record<BreakpointToken, SpaceToken>>`] ||| Custom spacing between items, see [Custom Spacing](#custom-spacing) |
| `UNSAFE_className` | `string` ||| Wrapper custom class name |
| `UNSAFE_style` | `CSSProperties` ||| Wrapper custom style |

You can add `id`, `data-*` or `aria-*` attributes to further extend component's
descriptiveness and accessibility.
Expand Down
11 changes: 9 additions & 2 deletions packages/web-react/src/components/Stack/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@ const defaultProps: SpiritStackProps = {

export const Stack = <T extends ElementType = 'div'>(props: SpiritStackProps<T>): JSX.Element => {
const { elementType: ElementTag = 'div', children, ...restProps } = props;
const { classProps, props: modifiedProps } = useStackStyleProps(restProps);
const { classProps, props: modifiedProps, styleProps: stackStyle } = useStackStyleProps(restProps);
const { styleProps, props: otherProps } = useStyleProps(modifiedProps);

const stackStyleProps = {
style: {
...styleProps.style,
...stackStyle,
},
};

return (
<ElementTag {...otherProps} {...styleProps} className={classNames(classProps, styleProps.className)}>
<ElementTag {...otherProps} {...stackStyleProps} className={classNames(classProps, styleProps.className)}>
{children}
</ElementTag>
);
Expand Down
58 changes: 58 additions & 0 deletions packages/web-react/src/components/Stack/__tests__/Stack.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,62 @@ describe('Stack', () => {
const element = dom.container.querySelector('div') as HTMLElement;
expect(element.textContent).toBe('Hello World');
});

it('should render element children', () => {
const dom = render(
<Stack>
<span>Child 1</span>
<span>Child 2</span>
</Stack>,
);

const element = dom.container.querySelector('div') as HTMLElement;
expect(element.children).toHaveLength(2);
expect(element.children[0].textContent).toBe('Child 1');
expect(element.children[1].textContent).toBe('Child 2');
});

it('should render a ul element', () => {
const dom = render(<Stack elementType="ul" />);

const element = dom.container.querySelector('ul') as HTMLElement;
expect(element).toBeInTheDocument();
});

it('should render with spacing', () => {
const dom = render(<Stack hasSpacing />);

const element = dom.container.querySelector('div') as HTMLElement;
expect(element).toHaveClass('Stack--hasSpacing');
});

it('should render with custom spacing', () => {
const dom = render(<Stack hasSpacing spacing="space-1000" />);

const element = dom.container.querySelector('div') as HTMLElement;
expect(element).toHaveClass('Stack--hasSpacing');
expect(element).toHaveStyle({ '--stack-spacing': 'var(--spirit-space-1000)' });
});

it('should render with custom spacing for each breakpoint', () => {
const dom = render(
<Stack hasSpacing spacing={{ mobile: 'space-100', tablet: 'space-1000', desktop: 'space-1200' }} />,
);

const element = dom.container.querySelector('div') as HTMLElement;
expect(element).toHaveClass('Stack--hasSpacing');
expect(element).toHaveStyle({ '--stack-spacing': 'var(--spirit-space-100)' });
expect(element).toHaveStyle({ '--stack-spacing-tablet': 'var(--spirit-space-1000)' });
expect(element).toHaveStyle({ '--stack-spacing-desktop': 'var(--spirit-space-1200)' });
});

it('should render with custom spacing for only one breakpoint', () => {
const dom = render(<Stack hasSpacing spacing={{ tablet: 'space-1000' }} />);

const element = dom.container.querySelector('div') as HTMLElement;
expect(element).toHaveClass('Stack--hasSpacing');
expect(element).toHaveStyle({ '--stack-spacing-tablet': 'var(--spirit-space-1000)' });
expect(element).not.toHaveStyle({ '--stack-spacing': 'var(--spirit-space-100)' });
expect(element).not.toHaveStyle({ '--stack-spacing-desktop': 'var(--spirit-space-1200)' });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,24 @@ describe('useStackStyleProps', () => {
expect(result.current.classProps).toBe(expectedClasses);
},
);

it.each([
// spacing, expectedStyle
[undefined, {}],
['space-100', { '--stack-spacing': 'var(--spirit-space-100)' }],
[{ tablet: 'space-100' }, { '--stack-spacing-tablet': 'var(--spirit-space-100)' }],
[
{ mobile: 'space-100', tablet: 'space-200', desktop: 'space-300' },
{
'--stack-spacing': 'var(--spirit-space-100)',
'--stack-spacing-tablet': 'var(--spirit-space-200)',
'--stack-spacing-desktop': 'var(--spirit-space-300)',
},
],
])('should return style', (spacing, expectedStyle) => {
const props = { spacing } as SpiritStackProps;
const { result } = renderHook(() => useStackStyleProps(props));

expect(result.current.styleProps).toEqual(expectedStyle);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Stack from '../Stack';
const StackBlocks = () => (
<Stack elementType="ul">
{[1, 2, 3].map((i) => (
<li key={i}>
<li key={`stack-default-${i}`}>
<DocsBox>Block {i}</DocsBox>
</li>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import DocsBox from '../../../../docs/DocsBox';
import Stack from '../Stack';

const StackBlocksWithCustomSpacing = () => (
<Stack elementType="ul" hasSpacing spacing="space-1200">
{[1, 2, 3].map((i) => (
<li key={`stack-custom-spacing-${i}`}>
<DocsBox>Block {i}</DocsBox>
</li>
))}
</Stack>
);

export default StackBlocksWithCustomSpacing;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import DocsBox from '../../../../docs/DocsBox';
import Stack from '../Stack';

const StackBlocksWithCustomSpacingAndDividers = () => (
<Stack elementType="ul" hasSpacing hasStartDivider hasEndDivider hasIntermediateDividers spacing="space-800">
{[1, 2, 3].map((i) => (
<li key={`stack-custom-spacing-dividers-${i}`}>
<DocsBox>Block {i}</DocsBox>
</li>
))}
</Stack>
);

export default StackBlocksWithCustomSpacingAndDividers;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import DocsBox from '../../../../docs/DocsBox';
import Stack from '../Stack';

const StackBlocksWithCustomSpacingForEachBreakpoint = () => (
<Stack elementType="ul" hasSpacing spacing={{ mobile: 'space-100', tablet: 'space-1000', desktop: 'space-1200' }}>
{[1, 2, 3].map((i) => (
<li key={`stack-custom-spacing-breakpoints-${i}`}>
<DocsBox>Block {i}</DocsBox>
</li>
))}
</Stack>
);

export default StackBlocksWithCustomSpacingForEachBreakpoint;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import DocsBox from '../../../../docs/DocsBox';
import Stack from '../Stack';

const StackBlocksWithCustomSpacingFromTabletBreakpoint = () => (
<Stack elementType="ul" hasSpacing spacing={{ tablet: 'space-1200' }}>
{[1, 2, 3].map((i) => (
<li key={`stack-custom-spacing-tablet-${i}`}>
<DocsBox>Block {i}</DocsBox>
</li>
))}
</Stack>
);

export default StackBlocksWithCustomSpacingFromTabletBreakpoint;
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Stack from '../Stack';
const StackBlocksWithInnerAndOuterDividersAndVerticalSpacing = () => (
<Stack elementType="ul" hasSpacing hasIntermediateDividers hasStartDivider hasEndDivider>
{[1, 2, 3].map((i) => (
<li key={i}>
<li key={`stack-dividers-${i}`}>
<DocsBox>Block {i}</DocsBox>
</li>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Stack from '../Stack';
const StackBlocksWithInnerDividersAndVerticalSpacing = () => (
<Stack elementType="ul" hasSpacing hasIntermediateDividers>
{[1, 2, 3].map((i) => (
<li key={i}>
<li key={`stack-dividers-spacing-${i}`}>
<DocsBox>Block {i}</DocsBox>
</li>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Stack from '../Stack';
const StackBlocksWithInnerDividersWithoutVerticalSpacing = () => (
<Stack elementType="ul" hasIntermediateDividers>
{[1, 2, 3].map((i) => (
<li key={i}>
<li key={`stack-dividers-without-spacing-${i}`}>
<DocsBox>Block {i}</DocsBox>
</li>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Stack from '../Stack';
const StackBlocksWithVerticalSpacing = () => (
<Stack elementType="ul" hasSpacing>
{[1, 2, 3].map((i) => (
<li key={i}>
<li key={`stack-spacing-${i}`}>
<DocsBox>Block {i}</DocsBox>
</li>
))}
Expand Down
16 changes: 16 additions & 0 deletions packages/web-react/src/components/Stack/demo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import StackBlocksWithVerticalSpacing from './StackBlocksWithVerticalSpacing';
import StackBlocksWithInnerDividersAndVerticalSpacing from './StackBlocksWithInnerDividersAndVerticalSpacing';
import StackBlocksWithInnerAndOuterDividersAndVerticalSpacing from './StackBlocksWithInnerAndOuterDividersAndVerticalSpacing';
import StackBlocksWithInnerDividersWithoutVerticalSpacing from './StackBlocksWithInnerDividersWithoutVerticalSpacing';
import StackBlocksWithCustomSpacing from './StackBlocksWithCustomSpacing';
import StackBlocksWithCustomSpacingFromTabletBreakpoint from './StackBlocksWithCustomSpacingFromTabletBreakpoint';
import StackBlocksWithCustomSpacingForEachBreakpoint from './StackBlocksWithCustomSpacingForEachBreakpoint';
import StackBlocksWithCustomSpacingAndDividers from './StackBlocksWithCustomSpacingAndDividers';

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
Expand All @@ -30,5 +34,17 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<DocsSection title="Stacked Blocks with Inner Dividers without Vertical Spacing" stackAlignment="stretch">
<StackBlocksWithInnerDividersWithoutVerticalSpacing />
</DocsSection>
<DocsSection title="Stacked Blocks with Custom Spacing" stackAlignment="stretch">
<StackBlocksWithCustomSpacing />
</DocsSection>
<DocsSection title="Stacked Blocks with Custom Spacing from Tablet Breakpoint" stackAlignment="stretch">
<StackBlocksWithCustomSpacingFromTabletBreakpoint />
</DocsSection>
<DocsSection title="Stacked Blocks with Custom Spacing for Each Breakpoint" stackAlignment="stretch">
<StackBlocksWithCustomSpacingForEachBreakpoint />
</DocsSection>
<DocsSection title="Stacked Blocks with Custom Spacing and Inner and Outer Dividers" stackAlignment="stretch">
<StackBlocksWithCustomSpacingAndDividers />
</DocsSection>
</React.StrictMode>,
);
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ const meta: Meta<typeof Stack> = {
defaultValue: { summary: false },
},
},
spacing: {
control: 'object',
},
},
args: {
children: (
Expand All @@ -65,6 +68,10 @@ const meta: Meta<typeof Stack> = {
hasIntermediateDividers: false,
hasSpacing: false,
hasStartDivider: false,
spacing: {
mobile: 'space-500',
tablet: 'space-600',
},
},
};

Expand Down
24 changes: 22 additions & 2 deletions packages/web-react/src/components/Stack/useStackStyleProps.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import classNames from 'classnames';
import { ElementType } from 'react';
import { CSSProperties, ElementType } from 'react';
import { useClassNamePrefix } from '../../hooks';
import { SpiritStackProps } from '../../types';

interface StackCSSProperties extends CSSProperties {
[index: `--${string}`]: string | undefined | number;
}

export interface StackStyles {
/** className props */
classProps: string;
/** props to be passed to the element */
props: SpiritStackProps;
/** Style props for the element */
styleProps: StackCSSProperties;
}

export function useStackStyleProps<T extends ElementType = 'div'>(props: SpiritStackProps<T>): StackStyles {
const { hasEndDivider, hasIntermediateDividers, hasSpacing, hasStartDivider, ...restProps } = props;
const { hasEndDivider, hasIntermediateDividers, hasSpacing, hasStartDivider, spacing, ...restProps } = props;

const StackClass = useClassNamePrefix('Stack');
const StackBottomDividerClass = `${StackClass}--hasEndDivider`;
Expand All @@ -25,8 +31,22 @@ export function useStackStyleProps<T extends ElementType = 'div'>(props: SpiritS
[StackTopDividerClass]: hasStartDivider,
});

const stackStyle: StackCSSProperties = {};

if (typeof spacing === 'object' && spacing !== null) {
Object.keys(spacing).forEach((key) => {
const suffix = key === 'mobile' ? '' : `-${key}`;
(stackStyle as Record<string, string | undefined>)[`--stack-spacing${suffix}`] = `var(--spirit-${spacing[
key as keyof typeof spacing
]?.toString()})`;
});
} else if (spacing) {
(stackStyle as Record<string, string | undefined>)['--stack-spacing'] = `var(--spirit-${spacing})`;
}

return {
classProps,
props: restProps,
styleProps: stackStyle,
};
}
3 changes: 2 additions & 1 deletion packages/web-react/src/types/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ export * from './element';
export * from './events';
export * from './inputs';
export * from './item';
export * from './style';
export * from './refs';
export * from './rest';
export * from './style';
export * from './tokens';

export interface ChildrenProps {
/** The content to display in the component. */
Expand Down
4 changes: 4 additions & 0 deletions packages/web-react/src/types/shared/tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { breakpoints, space } from '@lmc-eu/spirit-design-tokens';

export type BreakpointToken = keyof typeof breakpoints | string;
export type SpaceToken = `${'space-'}${Extract<keyof typeof space, string>}` | `${'space-'}${number}`;
Loading

0 comments on commit c0a678a

Please sign in to comment.