Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Introduce custom Stack spacing #DS-1079 #1175

Merged
merged 4 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/icons/scripts/prepareSvgReact.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const toPascalCase = (string) =>
.replace(/\w+/g, (word) => {
return word[0].toUpperCase() + word.slice(1).toLowerCase();
})
.replace('-', '');
.replaceAll('-', '');

const prepareSvgForReactComponent = (srcDir, distDir) => {
fs.readdir(srcDir, (err, files) => {
Expand Down
40 changes: 30 additions & 10 deletions packages/web-react/src/components/Stack/README.md
crishpeen marked this conversation as resolved.
Show resolved Hide resolved
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.
crishpeen marked this conversation as resolved.
Show resolved Hide resolved

```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
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)' });
crishpeen marked this conversation as resolved.
Show resolved Hide resolved
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) => (
crishpeen marked this conversation as resolved.
Show resolved Hide resolved
<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) => (
crishpeen marked this conversation as resolved.
Show resolved Hide resolved
<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 {
crishpeen marked this conversation as resolved.
Show resolved Hide resolved
[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