Skip to content

Commit

Permalink
feat: Add Figure component (#1795)
Browse files Browse the repository at this point in the history
Co-authored-by: Aram <[email protected]>
  • Loading branch information
VincentSmedinga and alimpens authored Dec 20, 2024
1 parent 8d1c266 commit 3505dcc
Show file tree
Hide file tree
Showing 15 changed files with 280 additions and 2 deletions.
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);

@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 @@ -25,6 +25,7 @@
@use "error-message/error-message";
@use "field-set/field-set";
@use "field/field";
@use "figure/figure";
@use "file-input/file-input";
@use "file-list/file-list";
@use "footer/footer";
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)
})
})
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<HTMLElement>()

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 @@ -23,6 +23,7 @@ export * from './Dialog'
export * from './ErrorMessage'
export * from './Field'
export * from './FieldSet'
export * from './Figure'
export * from './FileInput'
export * from './FileList'
export * from './Footer'
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}" }
}
}
}
}
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} />
45 changes: 45 additions & 0 deletions storybook/src/components/Figure/Figure.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* @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,
args: {
children: caption,
inverseColor: false,
},
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>
),
} satisfies Meta<typeof Figure.Caption>
// We use the Caption type here to allow inverseColor. This works as long as Figure has no props of its own.

export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {}

export const InverseColour: Story = {
args: {
inverseColor: true,
},
}
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
4 changes: 2 additions & 2 deletions storybook/src/styles/overrides.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
.sbdocs-content.sbdocs-content > div:not(.sb-unstyled) > :is(ol, ul) li,
.sbdocs-content.sbdocs-content > table:not(.sb-unstyled) :is(td, th),
.sbdocs-content.sbdocs-content > div:not(.sb-unstyled) > table:not(.sb-unstyled) :is(td, th),
.sbdocs-content.sbdocs-content > div:not(.sb-unstyled) figcaption {
.sbdocs-content.sbdocs-content > div:not(.sb-unstyled) > figure > figcaption {
color: #000;
font-family: "Amsterdam Sans", "Arial", sans-serif;
}
Expand Down Expand Up @@ -110,7 +110,7 @@
font-size: 1rem;
}

.sbdocs-content.sbdocs-content > div:not(.sb-unstyled) figcaption {
.sbdocs-content.sbdocs-content > div:not(.sb-unstyled) > figure > figcaption {
font-size: 0.875rem;
}

Expand Down

0 comments on commit 3505dcc

Please sign in to comment.