-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3855 from Royal-Navy/feat/stack-group-utility-com…
…ponents feat(ReactComponentLibrary): Stack and Group utility components
- Loading branch information
Showing
12 changed files
with
3,710 additions
and
9,041 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
105 changes: 105 additions & 0 deletions
105
packages/react-component-library/src/components/Group/Group.stories.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,105 @@ | ||
import React from 'react' | ||
import styled from 'styled-components' | ||
import { Meta, StoryFn } from '@storybook/react' | ||
|
||
import { Group } from '.' | ||
import { Button } from '../Button' | ||
|
||
export default { | ||
component: Group, | ||
title: 'Utility/Group', | ||
parameters: { | ||
docs: { | ||
description: { | ||
component: | ||
'Group is a horizontal flex container. If you need a vertical flex container, use Stack component instead.', | ||
}, | ||
}, | ||
}, | ||
argTypes: { | ||
gap: { | ||
options: [ | ||
...Array.from({ length: 21 }, (_, i) => i.toString()), | ||
'px', | ||
'half', | ||
'full', | ||
], | ||
}, | ||
justify: { | ||
options: [ | ||
'flex-start', | ||
'flex-end', | ||
'center', | ||
'space-between', | ||
'space-around', | ||
'space-evenly', | ||
], | ||
}, | ||
align: { | ||
options: ['stretch', 'flex-start', 'flex-end', 'center', 'baseline'], | ||
}, | ||
wrap: { | ||
options: ['wrap', 'nowrap', 'wrap-reverse'], | ||
}, | ||
}, | ||
} as Meta<typeof Group> | ||
|
||
export const Default: StoryFn<typeof Group> = ({ children: _, ...rest }) => ( | ||
<Group {...rest}> | ||
<Button variant="primary">1</Button> | ||
<Button variant="primary">2</Button> | ||
<Button variant="primary">3</Button> | ||
</Group> | ||
) | ||
|
||
Default.args = { | ||
gap: '10', | ||
justify: 'flex-start', | ||
grow: false, | ||
} | ||
|
||
const StyledButton = styled(Button)` | ||
overflow: hidden; | ||
` | ||
|
||
const StyledP = styled.p` | ||
margin: 1rem 0; | ||
` | ||
|
||
export const PreventGrowOverflow: StoryFn<typeof Group> = (_props) => ( | ||
<div> | ||
<StyledP> | ||
preventGrowOverflow: <b>true</b> – each child width is always limited to | ||
33% of parent width (since there are 3 children) | ||
</StyledP> | ||
|
||
<Group grow wrap="nowrap" gap="10"> | ||
<Button variant="primary">First button</Button> | ||
<StyledButton variant="primary"> | ||
Second button with large content | ||
</StyledButton> | ||
<Button variant="primary">Third button</Button> | ||
</Group> | ||
|
||
<StyledP> | ||
preventGrowOverflow: <b>false</b> – children will grow based on their | ||
content, they can take more than 33% of parent width | ||
</StyledP> | ||
|
||
<Group grow preventGrowOverflow={false} wrap="nowrap" gap="10"> | ||
<Button variant="primary">First button</Button> | ||
<Button variant="primary">Second button with large content</Button> | ||
<Button variant="primary">Third button</Button> | ||
</Group> | ||
</div> | ||
) | ||
|
||
PreventGrowOverflow.storyName = 'preventGrowOverflow' | ||
PreventGrowOverflow.parameters = { | ||
docs: { | ||
description: { | ||
story: | ||
'preventGrowOverflow prop allows you to control how Group children should behave when there is not enough space to fit them all on one line. By default, children are not allowed to take more space than (1 / children.length) * 100% of parent width (preventGrowOverflow is set to true). To change this behavior, set preventGrowOverflow to false and children will be allowed to grow and take as much space as they need.', | ||
}, | ||
}, | ||
} |
91 changes: 91 additions & 0 deletions
91
packages/react-component-library/src/components/Group/Group.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,91 @@ | ||
import React from 'react' | ||
import { render } from '@testing-library/react' | ||
import { spacing } from '@royalnavy/design-tokens' | ||
|
||
import { Group } from './Group' | ||
|
||
describe('Group', () => { | ||
it('render with default styles', () => { | ||
const { container } = render(<Group>Content</Group>) | ||
expect(container.firstChild).toHaveStyleRule('display', 'flex') | ||
expect(container.firstChild).toHaveStyleRule('flex-direction', 'row') | ||
expect(container.firstChild).toHaveStyleRule('gap', spacing('0')) | ||
expect(container.firstChild).toHaveStyleRule('align-items', 'stretch') | ||
expect(container.firstChild).toHaveStyleRule( | ||
'justify-content', | ||
'flex-start' | ||
) | ||
expect(container.firstChild).toHaveStyleRule('flex-wrap', 'wrap') | ||
}) | ||
|
||
it('apply custom gap', () => { | ||
const customGap = '2' | ||
const { container } = render(<Group gap={customGap}>Content</Group>) | ||
expect(container.firstChild).toHaveStyleRule('gap', spacing(customGap)) | ||
}) | ||
|
||
it('apply custom justify-content', () => { | ||
const { container } = render(<Group justify="center">Content</Group>) | ||
expect(container.firstChild).toHaveStyleRule('justify-content', 'center') | ||
}) | ||
|
||
it('apply custom align-items', () => { | ||
const { container } = render(<Group align="flex-end">Content</Group>) | ||
expect(container.firstChild).toHaveStyleRule('align-items', 'flex-end') | ||
}) | ||
|
||
it('apply custom flex-wrap', () => { | ||
const { container } = render(<Group wrap="nowrap">Content</Group>) | ||
expect(container.firstChild).toHaveStyleRule('flex-wrap', 'nowrap') | ||
}) | ||
|
||
it('apply flex-grow to children when grow is true', () => { | ||
const { container } = render(<Group grow>Content</Group>) | ||
expect(container.firstChild).toHaveStyleRule('flex-grow', '1', { | ||
modifier: '> *', | ||
}) | ||
}) | ||
|
||
it('apply max-width to children when preventGrowOverflow is true', () => { | ||
const customGap = '2' | ||
const { container } = render( | ||
<Group grow preventGrowOverflow gap={customGap}> | ||
<div>Child 1</div> | ||
<div>Child 2</div> | ||
<div>Child 3</div> | ||
</Group> | ||
) | ||
expect(container.firstChild).toHaveStyleRule( | ||
'max-width', | ||
`calc(33.333333333333336% - (${spacing(customGap)} / 3))`, | ||
{ modifier: '> *' } | ||
) | ||
}) | ||
|
||
it('render with the specified HTML element', () => { | ||
const { container } = render(<Group el="section">Content</Group>) | ||
expect(container?.firstChild?.nodeName).toBe('SECTION') | ||
}) | ||
|
||
it('apply className prop', () => { | ||
const className = 'custom-class' | ||
const { container } = render(<Group className={className}>Content</Group>) | ||
expect(container.firstChild).toHaveClass(className) | ||
}) | ||
|
||
it('render children correctly', () => { | ||
const { getByText } = render( | ||
<Group> | ||
<div>Child 1</div> | ||
<div>Child 2</div> | ||
</Group> | ||
) | ||
expect(getByText('Child 1')).toBeInTheDocument() | ||
expect(getByText('Child 2')).toBeInTheDocument() | ||
}) | ||
|
||
it('not render when there are no children', () => { | ||
const { container } = render(<Group />) | ||
expect(container.firstChild).toBeNull() | ||
}) | ||
}) |
100 changes: 100 additions & 0 deletions
100
packages/react-component-library/src/components/Group/Group.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,100 @@ | ||
import React, { type JSX } from 'react' | ||
import type * as CSS from 'csstype' | ||
import styled, { css } from 'styled-components' | ||
import { type Spacing, spacing } from '@royalnavy/design-tokens' | ||
|
||
import { ComponentWithClass } from '../../common/ComponentWithClass' | ||
import { PrefixKeys } from '../../helpers' | ||
|
||
type HTMLElementName = keyof JSX.IntrinsicElements | ||
|
||
interface GroupProps extends ComponentWithClass { | ||
/** | ||
* Controls the gap between children, `'0'` by default. | ||
*/ | ||
gap?: Spacing | ||
/** | ||
* Controls `justify-content` CSS property, `'flex-start'` by default. | ||
*/ | ||
justify?: CSS.Properties['justifyContent'] | ||
/** | ||
* Controls `align-items` CSS property, `'center'` by default. | ||
*/ | ||
align?: CSS.Properties['alignItems'] | ||
/** | ||
* Controls `flex-wrap` CSS property, `'wrap'` by default. | ||
*/ | ||
wrap?: CSS.Properties['flexWrap'] | ||
/** | ||
* Determines whether each child element should have `flex-grow: 1` | ||
* style, `false` by default. | ||
*/ | ||
grow?: boolean | ||
/** | ||
* Determines whether children should take only dedicated amount of | ||
* space (`max-width` style is set based on the number of children) | ||
* , `true` by default. | ||
*/ | ||
preventGrowOverflow?: boolean | ||
/** | ||
* The type of element to use for the root node, `'div'` by default. | ||
*/ | ||
el?: HTMLElementName | ||
} | ||
|
||
type StyledGroupProps = PrefixKeys< | ||
Omit<GroupProps, 'el' | 'children' | 'className'>, | ||
'$' | ||
> | ||
|
||
const StyledGroup = styled.div<StyledGroupProps>` | ||
display: flex; | ||
flex-direction: row; | ||
gap: ${({ $gap }) => spacing($gap!)}; | ||
align-items: ${({ $align }) => $align}; | ||
justify-content: ${({ $justify }) => $justify}; | ||
flex-wrap: ${({ $wrap }) => $wrap}; | ||
${({ $grow, $gap, $preventGrowOverflow }) => | ||
$grow && | ||
css` | ||
> * { | ||
flex-grow: 1; | ||
${$preventGrowOverflow && | ||
css` | ||
max-width: ${$gap === '0' | ||
? '33.333333333333336%' | ||
: `calc(33.333333333333336% - (${spacing($gap!)} / 3))`}; | ||
`} | ||
} | ||
`} | ||
` | ||
|
||
export const Group = ({ | ||
children, | ||
className, | ||
el = 'div', | ||
gap = '0', | ||
align = 'stretch', | ||
justify = 'flex-start', | ||
wrap = 'wrap', | ||
grow = false, | ||
preventGrowOverflow = true, | ||
}: GroupProps) => { | ||
return ( | ||
children && ( | ||
<StyledGroup | ||
as={el} | ||
className={className} | ||
$gap={gap} | ||
$align={align} | ||
$justify={justify} | ||
$wrap={wrap} | ||
$grow={grow} | ||
$preventGrowOverflow={preventGrowOverflow} | ||
> | ||
{children} | ||
</StyledGroup> | ||
) | ||
) | ||
} |
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 @@ | ||
export * from './Group' |
3 changes: 1 addition & 2 deletions
3
packages/react-component-library/src/components/Modal/partials/StyledMain.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
55 changes: 55 additions & 0 deletions
55
packages/react-component-library/src/components/Stack/Stack.stories.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,55 @@ | ||
import React from 'react' | ||
import { Meta, StoryFn } from '@storybook/react' | ||
|
||
import { Stack } from '.' | ||
import { Button } from '../Button' | ||
|
||
export default { | ||
component: Stack, | ||
title: 'Utility/Stack', | ||
parameters: { | ||
docs: { | ||
description: { | ||
component: | ||
'Stack is a vertical flex container. If you need a horizontal flex container, use Group component instead.', | ||
}, | ||
}, | ||
}, | ||
argTypes: { | ||
gap: { | ||
options: [ | ||
...Array.from({ length: 21 }, (_, i) => i.toString()), | ||
'px', | ||
'half', | ||
'full', | ||
], | ||
}, | ||
justify: { | ||
options: [ | ||
'flex-start', | ||
'flex-end', | ||
'center', | ||
'space-between', | ||
'space-around', | ||
'space-evenly', | ||
], | ||
}, | ||
align: { | ||
options: ['stretch', 'flex-start', 'flex-end', 'center', 'baseline'], | ||
}, | ||
}, | ||
} as Meta<typeof Stack> | ||
|
||
export const Default: StoryFn<typeof Stack> = ({ children: _, ...rest }) => ( | ||
<Stack {...rest}> | ||
<Button variant="primary">1</Button> | ||
<Button variant="primary">2</Button> | ||
<Button variant="primary">3</Button> | ||
</Stack> | ||
) | ||
|
||
Default.args = { | ||
gap: '10', | ||
align: 'stretch', | ||
justify: 'center', | ||
} |
Oops, something went wrong.