-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat(web-react): Introduce Flex component
- Loading branch information
1 parent
b17afbd
commit 70313d6
Showing
32 changed files
with
896 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
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: gridStyle } = useFlexStyleProps(restProps); | ||
const { styleProps, props: otherProps } = useStyleProps(modifiedProps); | ||
|
||
const flexStyleProps = { | ||
style: { | ||
...styleProps.style, | ||
...gridStyle, | ||
}, | ||
}; | ||
|
||
return ( | ||
<ElementTag {...otherProps} {...flexStyleProps} className={classNames(classProps, styleProps.className)}> | ||
{children} | ||
</ElementTag> | ||
); | ||
}; | ||
|
||
export default Flex; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
# 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 a 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 layout, pass a object as the value for the `isWrapping` property, using breakpoint keys to specify different layouts 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), or justified 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]. Using a corresponding prop will align the Flex items accordingly: | ||
|
||
### Vertical Alignment | ||
|
||
Similarly to the horizontal alignment, Flex can be vertically aligned as stretched (default), or justified to the top, | ||
center, or bottom. There is also an option to align the items to the baseline. These values come from the extended | ||
[alignmentY dictionary][dictionary-alignment]. Using a corresponding prop will align the Flex items accordingly: | ||
|
||
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 layout, pass a object as the value for the property, using breakpoint keys to specify different layouts 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][alignment-dictionary] \| `object`] | `stretch` | ✕ | Apply horizontal alignment of items, use object to set responsive values, e.g. `{ mobile: 'left', tablet: 'center', desktop: 'right' }` | | ||
| `alignmentY` | [[AlignmentYExtended dictionary][alignment-dictionary] \| `object`] | `stretch` | ✕ | Apply vertical alignment of items, use object to set responsive values, e.g. `{ mobile: 'top', tablet: 'center', desktop: 'bottom' }` | | ||
| `direction` | [`row` \| `column`] | `row` | ✕ | Direction of the items, use 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` | `false` | ✕ | Whether items will wrap, use 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 | ||
[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 |
81 changes: 81 additions & 0 deletions
81
packages/web-react/src/components/Flex/__tests__/Flex.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
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 className', () => { | ||
render(<Flex direction="column" data-testid={testId} />); | ||
|
||
expect(screen.getByTestId(testId)).toHaveClass('Flex--column'); | ||
}); | ||
|
||
it('should have alignmentX className', () => { | ||
render(<Flex alignmentX="left" data-testid={testId} />); | ||
|
||
expect(screen.getByTestId(testId)).toHaveClass('Flex--alignmentXLeft'); | ||
}); | ||
|
||
it('should have alignmentY className', () => { | ||
render(<Flex alignmentY="top" data-testid={testId} />); | ||
|
||
expect(screen.getByTestId(testId)).toHaveClass('Flex--alignmentYTop'); | ||
}); | ||
|
||
it('should have both alignmentX and alignmentY classNames', () => { | ||
render(<Flex alignmentX="left" alignmentY="top" data-testid={testId} />); | ||
|
||
expect(screen.getByTestId(testId)).toHaveClass('Flex--alignmentYTop Flex--alignmentYTop'); | ||
}); | ||
|
||
it('should have wrapping className', () => { | ||
render(<Flex isWrapping data-testid={testId} />); | ||
|
||
expect(screen.getByTestId(testId)).toHaveClass('Flex--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)' }); | ||
}); | ||
}); |
80 changes: 80 additions & 0 deletions
80
packages/web-react/src/components/Flex/__tests__/useFlexStyleProps.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { renderHook } from '@testing-library/react'; | ||
import { SpiritFlexProps } from '../../../types'; | ||
import { useFlexStyleProps } from '../useFlexStyleProps'; | ||
|
||
describe('useFlexStyleProps', () => { | ||
it('should return defaults', () => { | ||
const props = {}; | ||
const { result } = renderHook(() => useFlexStyleProps(props)); | ||
|
||
expect(result.current.classProps).toBe('Flex Flex--noWrap'); | ||
}); | ||
|
||
it('should return spacing CSS properties', () => { | ||
const props = { spacing: 'space-100' } as SpiritFlexProps; | ||
const { result } = renderHook(() => useFlexStyleProps(props)); | ||
|
||
expect(result.current.styleProps).toEqual({ | ||
'--flex-spacing': 'var(--spirit-space-100)', | ||
}); | ||
}); | ||
|
||
it('should return responsive spacing CSS properties', () => { | ||
const props = { spacing: { mobile: 'space-100', tablet: 'space-200', desktop: 'space-400' } } as SpiritFlexProps; | ||
const { result } = renderHook(() => useFlexStyleProps(props)); | ||
|
||
expect(result.current.styleProps).toEqual({ | ||
'--flex-spacing': 'var(--spirit-space-100)', | ||
'--flex-spacing-tablet': 'var(--spirit-space-200)', | ||
'--flex-spacing-desktop': 'var(--spirit-space-400)', | ||
}); | ||
}); | ||
|
||
it('should return wrapping CSS classes', () => { | ||
const props = { isWrapping: true } as SpiritFlexProps; | ||
const { result } = renderHook(() => useFlexStyleProps(props)); | ||
|
||
expect(result.current.classProps).toBe('Flex Flex--wrap'); | ||
}); | ||
|
||
it('should return row direction CSS classes', () => { | ||
const props = { direction: 'row' } as SpiritFlexProps; | ||
const { result } = renderHook(() => useFlexStyleProps(props)); | ||
|
||
expect(result.current.classProps).toBe('Flex Flex--noWrap Flex--row'); | ||
}); | ||
|
||
it('should return column direction CSS classes', () => { | ||
const props = { direction: 'column' } as SpiritFlexProps; | ||
const { result } = renderHook(() => useFlexStyleProps(props)); | ||
|
||
expect(result.current.classProps).toBe('Flex Flex--noWrap Flex--column'); | ||
}); | ||
|
||
it.each([ | ||
// alignmentX, alignmentY, expectedClasses | ||
[undefined, undefined, 'Flex Flex--noWrap'], | ||
['left', undefined, 'Flex Flex--noWrap Flex--alignmentXLeft'], | ||
['left', 'top', 'Flex Flex--noWrap Flex--alignmentXLeft Flex--alignmentYTop'], | ||
[ | ||
{ mobile: 'left', tablet: 'center', desktop: 'right' }, | ||
undefined, | ||
'Flex Flex--noWrap Flex--alignmentXLeft Flex--tablet--alignmentXCenter Flex--desktop--alignmentXRight', | ||
], | ||
[ | ||
{ mobile: 'left', tablet: 'center', desktop: 'right' }, | ||
{ mobile: 'top', tablet: 'center', desktop: 'bottom' }, | ||
'Flex Flex--noWrap Flex--alignmentXLeft Flex--tablet--alignmentXCenter Flex--desktop--alignmentXRight Flex--alignmentYTop Flex--tablet--alignmentYCenter Flex--desktop--alignmentYBottom', | ||
], | ||
[ | ||
'left', | ||
{ mobile: 'top', tablet: 'center', desktop: 'bottom' }, | ||
'Flex Flex--noWrap Flex--alignmentXLeft Flex--alignmentYTop Flex--tablet--alignmentYCenter Flex--desktop--alignmentYBottom', | ||
], | ||
])('should return alignment CSS classes', (alignmentX, alignmentY, expectedClasses) => { | ||
const props: SpiritFlexProps = { alignmentX, alignmentY } as SpiritFlexProps; | ||
const { result } = renderHook(() => useFlexStyleProps(props)); | ||
|
||
expect(result.current.classProps).toBe(expectedClasses); | ||
}); | ||
}); |
Oops, something went wrong.