Skip to content

Commit

Permalink
Merge pull request #3855 from Royal-Navy/feat/stack-group-utility-com…
Browse files Browse the repository at this point in the history
…ponents

feat(ReactComponentLibrary): Stack and Group utility components
  • Loading branch information
m7kvqbe1 authored Aug 8, 2024
2 parents c6239fc + 24553d9 commit 35f470f
Show file tree
Hide file tree
Showing 12 changed files with 3,710 additions and 9,041 deletions.
1 change: 1 addition & 0 deletions packages/react-component-library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
"chromatic": "^11.3.0",
"clean-webpack-plugin": "^4.0.0",
"concurrently": "^7.3.0",
"csstype": "^3.1.3",
"eslint": "^8.2.0",
"eslint-plugin-playwright": "^0.12.0",
"eslint-plugin-storybook": "^0.6.15",
Expand Down
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.',
},
},
}
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 packages/react-component-library/src/components/Group/Group.tsx
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>
)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Group'
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { color, mq, shadow } from '@royalnavy/design-tokens'
import { color, mq } from '@royalnavy/design-tokens'
import styled from 'styled-components'

export const StyledMain = styled.article`
text-shadow: ${shadow('2')};
border: 1px solid ${color('neutral', '200')};
background: ${color('neutral', 'white')};
width: 100%;
Expand Down
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',
}
Loading

0 comments on commit 35f470f

Please sign in to comment.