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(web-react): Introduce Flex component #1594

Merged
merged 2 commits into from
Sep 3, 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
12 changes: 6 additions & 6 deletions docs/DICTIONARIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ This project uses `dictionaries` to unify props between different components.

### Alignment

| Dictionary | Values | Code name |
| ------------------ | ------------------------- | ------------------ |
| AlignmentX | `left`, `center`, `right` | AlignmentX |
| AlignmentXExtended | AlignmentX + `stretch` | AlignmentXExtended |
| AlignmentY | `top`, `center`, `bottom` | AlignmentY |
| AlignmentYExtended | AlignmentY + `stretch` | AlignmentYExtended |
| Dictionary | Values | Code name |
| ------------------ | --------------------------------------- | ------------------ |
| AlignmentX | `left`, `center`, `right` | AlignmentX |
| AlignmentXExtended | AlignmentX + `stretch`, `space-between` | AlignmentXExtended |
| AlignmentY | `top`, `center`, `bottom` | AlignmentY |
| AlignmentYExtended | AlignmentY + `stretch`, `baseline` | AlignmentYExtended |

### Color

Expand Down
2 changes: 1 addition & 1 deletion exporters/variables-scss/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@
"prettier-config-spirit": "workspace:^",
"typescript": "5.5.2"
}
}
}
1 change: 1 addition & 0 deletions packages/web-react/scripts/entryPoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const entryPoints = [
{ dirs: ['components', 'Field'] },
{ dirs: ['components', 'FieldGroup'] },
{ dirs: ['components', 'FileUploader'] },
{ dirs: ['components', 'Flex'] },
{ dirs: ['components', 'Grid'] },
{ dirs: ['components', 'Header'] },
{ dirs: ['components', 'Heading'] },
Expand Down
38 changes: 38 additions & 0 deletions packages/web-react/src/components/Flex/Flex.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

import classNames from 'classnames';
import React, { ElementType } from 'react';
import { AlignmentXExtended, AlignmentYExtended } from '../../constants';
import { useStyleProps } from '../../hooks';
import { SpiritFlexProps } from '../../types';
import { useFlexStyleProps } from './useFlexStyleProps';

const defaultProps: Partial<SpiritFlexProps> = {
alignmentX: AlignmentXExtended.STRETCH,
alignmentY: AlignmentYExtended.STRETCH,
direction: 'row',
pavelklibani marked this conversation as resolved.
Show resolved Hide resolved
elementType: 'div',
isWrapping: false,
};

export const Flex = <T extends ElementType = 'div'>(props: SpiritFlexProps<T>): JSX.Element => {
const propsWithDefaults = { ...defaultProps, ...props };
const { elementType: ElementTag = 'div', children, ...restProps } = propsWithDefaults;
const { classProps, props: modifiedProps, styleProps: flexStyle } = useFlexStyleProps(restProps);
const { styleProps, props: otherProps } = useStyleProps(modifiedProps);

const flexStyleProps = {
style: {
...styleProps.style,
...flexStyle,
},
};

return (
<ElementTag {...otherProps} {...flexStyleProps} className={classNames(classProps, styleProps.className)}>
{children}
</ElementTag>
);
};

export default Flex;
162 changes: 162 additions & 0 deletions packages/web-react/src/components/Flex/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Flex

Flex is a component that allows you to create a flexible one-dimensional layout.

## Basic Usage

Row layout:

```jsx
<Flex>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</Flex>
```

Column layout:

```jsx
<Flex direction="column">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</div>
```

Usage with a list:

```jsx
<Flex elementType="ul" direction="column">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</Flex>
```

ℹ️ For the row layout, the Flex component uses the [`display: flex`][mdn-display-flex] CSS property. For the column
layout, [`display: grid`][mdn-display-grid] is used because of technical advantages: better overflow control or
alignment API consistency.

## Responsive Direction

To create a responsive layout, pass an object as the value for the `direction` property, using breakpoint keys to specify different layouts for each screen size.

```jsx
<Flex direction={{ mobile: 'column', tablet: 'row' }}>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</Flex>
```

## Wrapping

By default, Flex items will not wrap. To enable wrapping on all breakpoints, use the
`isWrapping` prop.

```jsx
<Flex isWrapping>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</Flex>
```

### Responsive Wrapping

To create a responsive wrapping layout, pass an object as the value for the `isWrapping` property, using breakpoint keys to specify different wrapping for each screen size.

```jsx
<Flex isWrapping={{ mobile: true, tablet: false }}>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</Flex>
```

## Alignment

### Horizontal Alignment

Flex can be horizontally aligned as stretched (default), to the left, center, or right. Additionally, you
can evenly distribute the items using the space-between value. These values come from the extended
[alignmentX dictionary][dictionary-alignment].

### Vertical Alignment

Similarly to the horizontal alignment, Flex can be vertically aligned as stretched (default), to the top,
center, bottom. There is also an option to align the items to the baseline. These values come from the extended
[alignmentY dictionary][dictionary-alignment].

Example:

```jsx
<Flex alignmentX="right" alignmentY="baseline">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</div>
```

### Responsive Alignment

To create a responsive alignment, pass an object as the value for the property, using breakpoint keys to specify different alignments for each screen size.

Example:

```jsx
<Flex alignmentX={{ mobile: 'left', tablet: 'space-between' }} alignmentY={{ mobile: 'stretch', tablet: 'baseline' }}>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</Flex>
```

## Custom Spacing

You can use the `spacing` prop to apply custom spacing between items in both horizontal and vertical directions. The prop
accepts either a spacing token (e.g. `space-100`) or an object with breakpoint keys and spacing token values.

Custom spacing:

```jsx
<Flex spacing="space-1200">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</div>
```

Custom responsive spacing:

```jsx
<Flex spacing={{ mobile: 'space-400', tablet: 'space-800' }}>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</Flex>
```

## API

| Name | Type | Default | Required | Description |
| ------------- | -------------------------------------------------------------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `alignmentX` | \[[AlignmentXExtended dictionary][dictionary-alignment] \| `object`] | `stretch` | ✕ | Apply horizontal alignment of items, use an object to set responsive values, e.g. `{ mobile: 'left', tablet: 'center', desktop: 'right' }` |
| `alignmentY` | \[[AlignmentYExtended dictionary][dictionary-alignment] \| `object`] | `stretch` | ✕ | Apply vertical alignment of items, use an object to set responsive values, e.g. `{ mobile: 'top', tablet: 'center', desktop: 'bottom' }` |
| `direction` | \[[Direction dictionary][direction-dictionary] \| `object` ] | `row` | ✕ | Direction of the items, use an object to set responsive values, e.g. `{ mobile: 'row', tablet: 'row', desktop: 'column' }` |
| `elementType` | HTML element | `div` | ✕ | Element type to use for the Grid |
| `isWrapping` | \[ `bool` \| `object` ] | `false` | ✕ | Whether items will wrap, use an object to set responsive values, e.g. `{ mobile: true, tablet: true, desktop: false }` |
| `spacing` | \[`SpaceToken` \| `Partial<Record<BreakpointToken, SpaceToken>>`] | — | ✕ | Apply [custom spacing](#custom-spacing) in both horizontal and vertical directions 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]
and [escape hatches][readme-escape-hatches].

[dictionary-alignment]: https://github.com/lmc-eu/spirit-design-system/blob/main/docs/DICTIONARIES.md#alignment
[dictionary-direction]: https://github.com/lmc-eu/spirit-design-system/blob/main/docs/DICTIONARIES.md#direction
[mdn-display-flex]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout
[mdn-display-grid]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout
[readme-additional-attributes]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#additional-attributes
[readme-escape-hatches]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#escape-hatches
[readme-style-props]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#style-props
123 changes: 123 additions & 0 deletions packages/web-react/src/components/Flex/__tests__/Flex.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest';
import { restPropsTest } from '../../../../tests/providerTests/restPropsTest';
import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest';
import Flex from '../Flex';

describe('Flex', () => {
const text = 'Hello world';
const testId = 'flex-test-id';

classNamePrefixProviderTest(Flex, 'Flex');

stylePropsTest(Flex);

restPropsTest(Flex, 'div');

it('should render text children', () => {
render(<Flex data-testid={testId}>{text}</Flex>);

expect(screen.getByText(text)).toBeInTheDocument();
expect(screen.getByTestId(testId)).toHaveClass(
'Flex Flex--noWrap Flex--row Flex--alignmentXStretch Flex--alignmentYStretch',
);
});

it('should have direction class name', () => {
render(<Flex direction="column" data-testid={testId} />);

expect(screen.getByTestId(testId)).toHaveClass('Flex--column');
pavelklibani marked this conversation as resolved.
Show resolved Hide resolved
});

it('should have responsive direction class name', () => {
render(<Flex direction={{ mobile: 'row', tablet: 'column', desktop: 'column' }} data-testid={testId} />);

expect(screen.getByTestId(testId)).toHaveClass('Flex--row Flex--tablet--column Flex--desktop--column');
});

it('should have alignmentX class name', () => {
render(<Flex alignmentX="left" data-testid={testId} />);

expect(screen.getByTestId(testId)).toHaveClass('Flex--alignmentXLeft');
});

it('should have responsive alignmentX class name', () => {
render(<Flex alignmentX={{ mobile: 'left', tablet: 'center', desktop: 'right' }} data-testid={testId} />);

expect(screen.getByTestId(testId)).toHaveClass(
'Flex--alignmentXLeft Flex--tablet--alignmentXCenter Flex--desktop--alignmentXRight',
);
});

it('should have alignmentY class name', () => {
render(<Flex alignmentY="top" data-testid={testId} />);

expect(screen.getByTestId(testId)).toHaveClass('Flex--alignmentYTop');
});

it('should have responsive alignmentY class name', () => {
render(<Flex alignmentY={{ mobile: 'top', tablet: 'center', desktop: 'bottom' }} data-testid={testId} />);

expect(screen.getByTestId(testId)).toHaveClass(
'Flex--alignmentYTop Flex--tablet--alignmentYCenter Flex--desktop--alignmentYBottom',
);
});

it('should have both alignmentX and alignmentY class name', () => {
render(<Flex alignmentX="left" alignmentY="top" data-testid={testId} />);

expect(screen.getByTestId(testId)).toHaveClass('Flex--alignmentYTop Flex--alignmentYTop');
});

it('should have responsive both alignmentX and alignmentY class name', () => {
render(
<Flex
alignmentX={{ mobile: 'left', tablet: 'center', desktop: 'right' }}
alignmentY={{ mobile: 'top', tablet: 'center', desktop: 'bottom' }}
data-testid={testId}
/>,
);

expect(screen.getByTestId(testId)).toHaveClass(
'Flex--alignmentXLeft Flex--tablet--alignmentXCenter Flex--desktop--alignmentXRight Flex--alignmentYTop Flex--tablet--alignmentYCenter Flex--desktop--alignmentYBottom',
);
});

it('should have wrapping class name', () => {
render(<Flex isWrapping data-testid={testId} />);

expect(screen.getByTestId(testId)).toHaveClass('Flex--wrap');
});

it('should have responsive wrapping class name', () => {
render(<Flex isWrapping={{ mobile: true, tablet: false, desktop: true }} data-testid={testId} />);

expect(screen.getByTestId(testId)).toHaveClass('Flex--wrap Flex--tablet--noWrap Flex--desktop--wrap');
});

it('should have custom elementType', () => {
render(<Flex elementType="ul" />);

expect(screen.getByRole('list')).toBeInTheDocument();
});

it('should render with custom spacing', () => {
render(<Flex spacing="space-600" data-testid={testId} />);

expect(screen.getByTestId(testId)).toHaveStyle({ '--flex-spacing': 'var(--spirit-space-600)' });
});

it('should render with custom spacing for each breakpoint', () => {
render(
<Flex spacing={{ mobile: 'space-100', tablet: 'space-1000', desktop: 'space-1200' }} data-testid={testId} />,
);

const element = screen.getByTestId(testId) as HTMLElement;

expect(element).toHaveStyle({ '--flex-spacing': 'var(--spirit-space-100)' });
expect(element).toHaveStyle({ '--flex-spacing-tablet': 'var(--spirit-space-1000)' });
expect(element).toHaveStyle({ '--flex-spacing-desktop': 'var(--spirit-space-1200)' });
});
});
Loading
Loading