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: Add horizontal and vertical alignment options to Row #1330

Merged
merged 12 commits into from
Jul 19, 2024
Merged
36 changes: 36 additions & 0 deletions packages/css/src/components/row/row.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,39 @@
.ams-row--wrap {
flex-wrap: wrap;
}

.ams-row--align-around {
justify-content: space-around;
}

.ams-row--align-between {
justify-content: space-between;
}

.ams-row--align-center {
justify-content: center;
}

.ams-row--align-end {
justify-content: flex-end;
}

.ams-row--align-evenly {
justify-content: space-evenly;
}

.ams-row--align-vertical-baseline {
align-items: baseline;
}

.ams-row--align-vertical-center {
align-items: center;
}

.ams-row--align-vertical-end {
align-items: flex-end;
}

.ams-row--align-vertical-start {
align-items: flex-start;
}
23 changes: 23 additions & 0 deletions packages/react/src/Row/Row.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { render, screen } from '@testing-library/react'
import { createRef } from 'react'
import { Row, rowGapSizes } from './Row'
import { crossAlignOptions, mainAlignOptions } from '../common/layout'
import '@testing-library/jest-dom'

describe('Row', () => {
Expand Down Expand Up @@ -64,4 +65,26 @@ describe('Row', () => {

expect(ref.current).toBe(component)
})

describe('Alignment', () => {
mainAlignOptions.map((align) =>
it(`sets the ‘${align}’ alignment`, () => {
const { container } = render(<Row align={align} />)

const component = container.querySelector(':only-child')

expect(component).toHaveClass(`ams-row--align-${align}`)
}),
)

crossAlignOptions.map((align) =>
it(`sets the ‘${align}’ vertical alignment`, () => {
const { container } = render(<Row alignVertical={align} />)

const component = container.querySelector(':only-child')

expect(component).toHaveClass(`ams-row--align-vertical-${align}`)
}),
)
})
})
27 changes: 23 additions & 4 deletions packages/react/src/Row/Row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,43 @@
import clsx from 'clsx'
import { forwardRef } from 'react'
import type { HTMLAttributes, PropsWithChildren } from 'react'
import type { CrossAlign, MainAlign } from '../common/layout'

export const rowGapSizes: Array<string> = ['extra-small', 'small', 'medium', 'large', 'extra-large']

type RowTag = 'article' | 'div' | 'section'
type RowGap = (typeof rowGapSizes)[number]
type RowTag = 'article' | 'div' | 'section'

export type RowProps = {
/** The horizontal alignment of the items in the row. */
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved
align?: MainAlign
/** The vertical alignment of the items in the row. */
alignVertical?: CrossAlign
/** The HTML element to use. */
as?: RowTag
/** The amount of vertical space between items. */
gap?: RowGap
/** Whether items of the row can wrap onto multiple lines. */
wrap?: Boolean
wrap?: boolean
} & PropsWithChildren<HTMLAttributes<HTMLElement>>

export const Row = forwardRef(
({ as: Tag = 'div', children, className, gap = 'medium', wrap = false, ...restProps }: RowProps, ref: any) => (
<Tag {...restProps} ref={ref} className={clsx('ams-row', `ams-row--${gap}`, wrap && 'ams-row--wrap', className)}>
(
{ align, alignVertical, as: Tag = 'div', children, className, gap = 'medium', wrap, ...restProps }: RowProps,
ref: any,
) => (
<Tag
{...restProps}
ref={ref}
className={clsx(
'ams-row',
`ams-row--${gap}`,
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved
align && `ams-row--align-${align}`,
alignVertical && `ams-row--align-vertical-${alignVertical}`,
wrap && 'ams-row--wrap',
className,
)}
>
{children}
</Tag>
),
Expand Down
5 changes: 5 additions & 0 deletions packages/react/src/common/layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const crossAlignOptions: Array<string> = ['baseline', 'center', 'end', 'start']
export type CrossAlign = (typeof crossAlignOptions)[number]

export const mainAlignOptions: Array<string> = ['around', 'between', 'center', 'end', 'evenly']
export type MainAlign = (typeof mainAlignOptions)[number]
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

/* Append here */
export * from './common/layout'
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved
export * from './FormErrorList'
export * from './TableOfContents'
export * from './ErrorMessage'
Expand Down
49 changes: 49 additions & 0 deletions storybook/src/components/Row/Row.docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,55 @@ Wrap a Row around a set of components that need the same amount of white space b

<Controls />

### Horizontal Alignment

Items in the row can be aligned horizontally in several ways:

- **Center**: center items within the row.
- **End**: align items to the end of the row. This is the right side in left-to-right languages.
- **Space Between**: distribute whitespace between items.
- **Space Around**: distribute whitespace around items.
- **Space Evenly**: distribute whitespace evenly around items.

By default, items align to the **start** of the row – the left side in right-to-left languages.
This options has no class name or prop.

We left out the word ‘space’ from the values for the alignment prop, e.g. `align="around"`.

This example ensures that the spaces between and around all items are equally wide.

<Canvas of={RowStories.HorizontalAlignment} />
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved

#### End-align a Single Child

To align a single component to the right (in left-to-right languages), wrap it in a `<Row>` and set the `align` prop to `end`.

<Canvas of={RowStories.EndAlignASingleChild} />

#### Align Opposing Texts

This example shows a right-aligned link next to a heading.
Use `align="between"` to position them at opposing ends of the row.
Add `alignVertical="baseline"` to align them vertically to their common baseline.
Include `wrap` to allow the link to wrap to a new line if both components don’t fit on the same line.

<Canvas of={RowStories.AlignOpposingTexts} />

### Vertical Alignment

Items in the row can be aligned vertically in several ways:

- **Start**: align items to the top of the row.
- **Center**: align items to the middle of the row.
- **End**: align items to the bottom of the row.
- **Baseline**: align items to their common text baseline.

By default, items **stretch** to the height of the row. This options has no class name or prop.

This example centers three items vertically.

<Canvas of={RowStories.VerticalAlignment} />

### Wrapping

Items that may not fit together on a single line should be allowed to wrap onto multiple lines.
Expand Down
72 changes: 64 additions & 8 deletions storybook/src/components/Row/Row.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,32 @@
* Copyright Gemeente Amsterdam
*/

import { Button, Paragraph, Row } from '@amsterdam/design-system-react/src'
import {
Avatar,
Button,
crossAlignOptions,
Heading,
Link,
mainAlignOptions,
Paragraph,
Row,
} from '@amsterdam/design-system-react/src'
import { Meta, StoryObj } from '@storybook/react'

const ThreeButtons = Array.from(Array(3).keys()).map((i) => <Button key={i}>Button {i + 1}</Button>)

const FourBoxes = Array.from(Array(4).keys()).map((i) => (
<Paragraph className="ams-docs-pink-box" key={i}>
Wrapping row item {i + 1}
</Paragraph>
))
const ThreeParagraphs = [
<Paragraph className="ams-docs-pink-box" key={1} style={{ inlineSize: '8rem' }}>
One line
</Paragraph>,
<Paragraph className="ams-docs-pink-box" key={2} style={{ inlineSize: '8rem' }}>
Two
<br />
lines
</Paragraph>,
<Paragraph className="ams-docs-pink-box" key={3} style={{ inlineSize: '8rem' }}>
One line
</Paragraph>,
]

const meta = {
title: 'Components/Layout/Row',
Expand All @@ -21,6 +37,14 @@ const meta = {
children: ThreeButtons,
},
argTypes: {
align: {
control: 'radio',
options: mainAlignOptions,
},
alignVertical: {
control: 'radio',
options: crossAlignOptions,
},
gap: {
control: 'radio',
options: ['extra-small', 'small', 'medium', 'large', 'extra-large'],
Expand All @@ -34,9 +58,41 @@ type Story = StoryObj<typeof meta>

export const Default: Story = {}

export const HorizontalAlignment: Story = {
args: {
align: 'evenly',
children: ThreeParagraphs,
},
}

export const EndAlignASingleChild: Story = {
args: {
align: 'end',
children: <Avatar label="AB" />,
},
}

export const AlignOpposingTexts: Story = {
args: {
align: 'between',
alignVertical: 'baseline',
children: [<Heading level={3}>An example heading</Heading>, <Link href="#">An example link</Link>],
wrap: true,
},
}

export const VerticalAlignment: Story = {
args: {
alignVertical: 'center',
children: ThreeParagraphs,
},
}

export const Wrapping: Story = {
args: {
children: FourBoxes,
children: Array.from(Array(4).keys()).map((i) => (
<span className="ams-docs-pink-box" key={i} style={{ inlineSize: '16rem' }} />
)),
wrap: true,
},
}