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 Figure component #1795

Merged
merged 12 commits into from
Dec 20, 2024
Merged
5 changes: 5 additions & 0 deletions packages/css/src/components/figure/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- @license CC0-1.0 -->

# Figure

Groups media content with a caption that describes it.
33 changes: 33 additions & 0 deletions packages/css/src/components/figure/figure.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

@use "../../common/text-rendering" as *;

@mixin reset-figure {
margin-block: 0;
margin-inline: 0;
}

.ams-figure {
display: flex;
flex-direction: column;
gap: var(--ams-figure-gap);
alimpens marked this conversation as resolved.
Show resolved Hide resolved

@include reset-figure;
}

.ams-figure__caption {
color: var(--ams-figure-caption-color);
font-family: var(--ams-figure-caption-font-family);
font-size: var(--ams-figure-caption-font-size);
font-weight: var(--ams-figure-caption-font-weight);
line-height: var(--ams-figure-caption-line-height);

@include text-rendering;
}

.ams-figure__caption--inverse-color {
color: var(--ams-figure-caption-inverse-color);
}
1 change: 1 addition & 0 deletions packages/css/src/components/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

/* Append here */
@use "./figure/figure";
alimpens marked this conversation as resolved.
Show resolved Hide resolved
@use "file-list/file-list";
@use "action-group/action-group";
@use "breakout/breakout";
Expand Down
41 changes: 41 additions & 0 deletions packages/react/src/Figure/Figure.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { render, screen } from '@testing-library/react'
import { createRef } from 'react'
import { Figure } from './Figure'
import '@testing-library/jest-dom'

describe('Figure', () => {
it('renders', () => {
render(<Figure />)

const component = screen.getByRole('figure')

expect(component).toBeInTheDocument()
expect(component).toBeVisible()
})

it('renders a design system BEM class name', () => {
render(<Figure />)

const component = screen.getByRole('figure')

expect(component).toHaveClass('ams-figure')
})

it('renders an additional class name', () => {
render(<Figure className="extra" />)

const component = screen.getByRole('figure')

expect(component).toHaveClass('ams-figure extra')
})

it('supports ForwardRef in React', () => {
const ref = createRef<HTMLElement>()

render(<Figure ref={ref} />)

const component = screen.getByRole('figure')

expect(ref.current).toBe(component)
})
})
alimpens marked this conversation as resolved.
Show resolved Hide resolved
21 changes: 21 additions & 0 deletions packages/react/src/Figure/Figure.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import clsx from 'clsx'
import { forwardRef } from 'react'
import type { ForwardedRef, HTMLAttributes, PropsWithChildren } from 'react'
import { FigureCaption } from './FigureCaption'

export type FigureProps = PropsWithChildren<HTMLAttributes<HTMLElement>>

const FigureRoot = forwardRef(({ children, className, ...restProps }: FigureProps, ref: ForwardedRef<HTMLElement>) => (
<figure {...restProps} ref={ref} className={clsx('ams-figure', className)}>
{children}
</figure>
))

FigureRoot.displayName = 'Figure'

export const Figure = Object.assign(FigureRoot, { Caption: FigureCaption })
49 changes: 49 additions & 0 deletions packages/react/src/Figure/FigureCaption.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { render } from '@testing-library/react'
import { createRef } from 'react'
import { FigureCaption } from './FigureCaption'
import '@testing-library/jest-dom'

describe('Figure Caption', () => {
it('renders', () => {
const { container } = render(<FigureCaption />)

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

expect(component).toBeInTheDocument()
expect(component).toBeVisible()
})

it('renders a design system BEM class name', () => {
const { container } = render(<FigureCaption />)

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

expect(component).toHaveClass('ams-figure__caption')
})

it('renders the right inverse color class', () => {
const { container } = render(<FigureCaption inverseColor>Caption</FigureCaption>)

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

expect(component).toHaveClass('ams-figure__caption--inverse-color')
})

it('renders an additional class name', () => {
const { container } = render(<FigureCaption className="extra" />)

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

expect(component).toHaveClass('ams-figure__caption extra')
})

it('supports ForwardRef in React', () => {
const ref = createRef<HTMLAnchorElement>()
VincentSmedinga marked this conversation as resolved.
Show resolved Hide resolved

const { container } = render(<FigureCaption ref={ref} />)

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

expect(ref.current).toBe(component)
})
})
27 changes: 27 additions & 0 deletions packages/react/src/Figure/FigureCaption.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import clsx from 'clsx'
import { forwardRef } from 'react'
import type { ForwardedRef, HTMLAttributes, PropsWithChildren } from 'react'

export type FigureCaptionProps = {
/** Changes the text colour for readability on a dark background. */
inverseColor?: boolean
} & PropsWithChildren<HTMLAttributes<HTMLElement>>

export const FigureCaption = forwardRef(
({ children, className, inverseColor, ...restProps }: FigureCaptionProps, ref: ForwardedRef<HTMLElement>) => (
<figcaption
{...restProps}
ref={ref}
className={clsx('ams-figure__caption', inverseColor && 'ams-figure__caption--inverse-color', className)}
>
{children}
</figcaption>
),
)

FigureCaption.displayName = 'Figure.Caption'
5 changes: 5 additions & 0 deletions packages/react/src/Figure/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- @license CC0-1.0 -->

# React Figure component

[Figure documentation](../../../css/src/components/figure/README.md)
2 changes: 2 additions & 0 deletions packages/react/src/Figure/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Figure } from './Figure'
export type { FigureProps } from './Figure'
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 './Figure'
export * from './FileList'
export * from './ActionGroup'
export * from './Breakout'
Expand Down
15 changes: 15 additions & 0 deletions proprietary/tokens/src/components/ams/figure.tokens.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"ams": {
"figure": {
"gap": { "value": "{ams.space.sm}" },
"caption": {
"color": { "value": "{ams.brand.color.neutral.100}" },
"font-family": { "value": "{ams.text.font-family}" },
"font-size": { "value": "{ams.text.level.6.font-size}" },
"font-weight": { "value": "{ams.text.font-weight.normal}" },
"line-height": { "value": "{ams.text.level.6.line-height}" },
"inverse-color": { "value": "{ams.brand.color.neutral.0}" }
}
}
}
}
6 changes: 6 additions & 0 deletions storybook/src/components/Breakout/Breakout.docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ On narrower screens, let the text move below the image.

## Examples

### With caption

To add a caption to the image, wrap both in a Figure.

<Canvas of={BreakoutStories.WithCaption} />
alimpens marked this conversation as resolved.
Show resolved Hide resolved

### Vertical layout

A large figure can be placed at the top of the Spotlight, with related text positioned underneath.
Expand Down
33 changes: 32 additions & 1 deletion storybook/src/components/Breakout/Breakout.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Copyright Gemeente Amsterdam
*/

import { Image, Paragraph, Screen, Spotlight } from '@amsterdam/design-system-react'
import { Figure, Image, Paragraph, Screen, Spotlight } from '@amsterdam/design-system-react'
import { Breakout } from '@amsterdam/design-system-react/src'
import { Meta, StoryObj } from '@storybook/react'

Expand Down Expand Up @@ -53,6 +53,37 @@ export const Default: Story = {
},
}

export const WithCaption: Story = {
args: {
children: [
<Breakout.Cell colSpan="all" has="spotlight" rowSpan={{ medium: 2, narrow: 2, wide: 1 }} rowStart={2}>
alimpens marked this conversation as resolved.
Show resolved Hide resolved
<Spotlight />
</Breakout.Cell>,
<Breakout.Cell
colSpan={{ medium: 8, narrow: 4, wide: 6 }}
colStart={1}
rowStart={{ medium: 3, narrow: 3, wide: 2 }}
>
<Paragraph inverseColor>
Het doel van deze club is om ervoor te zorgen dat de Zuidas steeds duurzamer wordt.
</Paragraph>
</Breakout.Cell>,
<Breakout.Cell
colSpan={{ medium: 8, narrow: 4, wide: 6 }}
colStart={{ medium: 1, narrow: 1, wide: 7 }}
has="figure"
rowSpan={2}
rowStart={1}
>
<Figure>
<Image alt="" src="https://picsum.photos/960/540" />
<Figure.Caption inverseColor>Metrostation Zuid in 2022. Foto: Gemeente Amsterdam.</Figure.Caption>
</Figure>
</Breakout.Cell>,
],
},
}

export const VerticalLayout: Story = {
args: {
children: [
Expand Down
20 changes: 20 additions & 0 deletions storybook/src/components/Figure/Figure.docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{/* @license CC0-1.0 */}

import { Canvas, Markdown, Meta, Primary } from "@storybook/blocks";
import * as FigureStories from "./Figure.stories.tsx";
import README from "../../../../packages/css/src/components/figure/README.md?raw";

<Meta of={FigureStories} />

<Markdown>{README}</Markdown>

<Primary />

## Examples

### Inverse colour

Set the `inverseColor` prop if the Figure Caption sits on a dark background.
This ensures the colour of the text provides enough contrast.

<Canvas of={FigureStories.InverseColour} />
60 changes: 60 additions & 0 deletions storybook/src/components/Figure/Figure.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import { Image } from '@amsterdam/design-system-react'
import { Figure } from '@amsterdam/design-system-react/src'
import { Meta, StoryObj } from '@storybook/react'
import { exampleCaption } from '../shared/exampleContent'

const caption = exampleCaption()

const meta = {
title: 'Components/Media/Figure',
component: Figure,
} satisfies Meta<typeof Figure>

export default meta

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const captionMeta = {
alimpens marked this conversation as resolved.
Show resolved Hide resolved
component: Figure.Caption,
} satisfies Meta<typeof Figure.Caption>

type Story = StoryObj<typeof meta>
type CaptionStory = StoryObj<typeof captionMeta>

export const Default: Story = {
args: {
children: [
<Image
alt=""
aspectRatio="2x-wide"
sizes="(max-width: 36rem) 640px, (max-width: 68rem) 1280px, 1600px"
src="https://picsum.photos/1600/500"
srcSet="https://picsum.photos/640/200 640w, https://picsum.photos/1280/400 1280w, https://picsum.photos/1600/500 1600w"
/>,
<Figure.Caption>{caption}</Figure.Caption>,
],
},
}

export const InverseColour: CaptionStory = {
args: {
children: caption,
inverseColor: true,
},
render: ({ children, ...args }) => (
<Figure>
<Image
alt=""
aspectRatio="2x-wide"
sizes="(max-width: 36rem) 640px, (max-width: 68rem) 1280px, 1600px"
src="https://picsum.photos/1600/500"
srcSet="https://picsum.photos/640/200 640w, https://picsum.photos/1280/400 1280w, https://picsum.photos/1600/500 1600w"
/>
<Figure.Caption {...args}>{children}</Figure.Caption>
</Figure>
),
}
13 changes: 13 additions & 0 deletions storybook/src/components/shared/exampleContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ export const exampleAccordionHeading = () =>
'Voorgaande versies van ramingen',
])

export const exampleCaption = () =>
pickRandomContent<string>([
'Een rustige Amsterdamse gracht met eeuwenoude gevels die weerspiegelen in het water, terwijl fietsen nonchalant tegen de brugleuning rusten – een alledaags tafereel vol historie en charme. Foto: Liam Dekker.',
'Een rij geparkeerde fietsen langs een smalle gracht met klassieke Amsterdamse gevels op de achtergrond.',
'Een klein houten bootje dobbert rustig op het water, omringd door bomen en bakstenen panden met grote ramen. Foto: Sophie van der Brugge.',
'Een typische Amsterdamse brug met smeedijzeren leuningen, vol met fietsen en uitzicht op een grachtenpand met een klokgevel.',
'Een stille gracht met weerspiegelende gevels, terwijl een tram in de verte over een brug rijdt. Foto: Isabel Groeneveld.',
'Een zonovergoten terras aan de gracht, met stoelen op de kade en uitzicht op een sierlijke ophaalbrug.',
'Een grachtenpand met vrolijke bloemenbakken op de vensterbanken en een smalle trap naar de voordeur. Foto: Joris Zandvoort.',
'Een schuin geplaatste fiets tegen een lantaarnpaal, met op de achtergrond een karakteristiek houten bruggetje.',
'Een groep Ajax-supporters in rood-witte sjaals verzamelt zich op een plein, klaar voor een wedstrijd in de Johan Cruijff ArenA. Foto: Louis Flitskamp.',
])

export const exampleHeading = () =>
pickRandomContent<string>([
'Meer plekken voor kunst en cultuur, verspreid over de stad',
Expand Down
Loading
Loading