Skip to content

Commit

Permalink
feat: Add Description List component (#1139)
Browse files Browse the repository at this point in the history
Co-authored-by: Aram <37216945+alimpens@users.noreply.github.com>
Co-authored-by: Vincent Smedinga <v.smedinga@amsterdam.nl>
Co-authored-by: Aram Limpens <a.limpens@amsterdam.nl>
  • Loading branch information
4 people authored Apr 3, 2024
1 parent fedb1a1 commit ee3428a
Show file tree
Hide file tree
Showing 15 changed files with 391 additions and 0 deletions.
18 changes: 18 additions & 0 deletions packages/css/src/components/description-list/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!-- @license CC0-1.0 -->

# Description List

A collection of terms and their details.

## Design

On a narrow screen, details appear indented below their term.
From the medium breakpoint, terms and details appear next to each other.
The column for the details is twice as wide as the one for the term.

Details are set in bold text.

## References

- [MDN: `<dl>`: The Description List element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dl)
- [WCAG: Using description lists](https://www.w3.org/WAI/WCAG22/Techniques/html/H40)
55 changes: 55 additions & 0 deletions packages/css/src/components/description-list/description-list.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

@import "../../common/breakpoint";
@import "../../common/text-rendering";

@mixin reset {
box-sizing: border-box;
margin-block: 0;
}

.ams-description-list {
color: var(--ams-description-list-color);
display: grid;
font-family: var(--ams-description-list-font-family);
font-size: var(--ams-description-list-font-size);
font-weight: var(--ams-description-list-font-weight);
gap: var(--ams-description-list-gap);
line-height: var(--ams-description-list-line-height);

@media screen and (min-width: $ams-breakpoint-medium) {
grid-template-columns: 1fr 2fr;
}

@include reset;
@include text-rendering;
}

.ams-description-list--inverse-color {
color: var(--ams-description-list-inverse-color);
}

.ams-description-list__term {
@media screen and (min-width: $ams-breakpoint-medium) {
grid-column-start: 1;
}
}

@mixin reset-details {
margin-inline: 0;
}

.ams-description-list__details {
font-weight: var(--ams-description-list-details-font-weight);
padding-inline-start: var(--ams-description-list-details-padding-inline-start);

@media screen and (min-width: $ams-breakpoint-medium) {
grid-column-start: 2;
padding-inline-start: 0;
}

@include reset-details;
}
1 change: 1 addition & 0 deletions packages/css/src/components/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
@import "./document/document";
@import "./avatar/avatar";
@import "./form-field-character-counter/form-field-character-counter";
@import "./description-list/description-list";
@import "./row/row";
@import "./radio/radio";
@import "./tabs/tabs";
Expand Down
49 changes: 49 additions & 0 deletions packages/react/src/DescriptionList/DescriptionList.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 { DescriptionList } from './DescriptionList'
import '@testing-library/jest-dom'

describe('Description list', () => {
it('renders', () => {
const { container } = render(<DescriptionList />)

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

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

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

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

expect(component).toHaveClass('ams-description-list')
})

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

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

expect(component).toHaveClass('ams-description-list extra')
})

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

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

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

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

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

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

expect(component).toHaveClass('ams-description-list--inverse-color')
})
})
33 changes: 33 additions & 0 deletions packages/react/src/DescriptionList/DescriptionList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

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

export type DescriptionListProps = {
inverseColor?: boolean
} & PropsWithChildren<HTMLAttributes<HTMLDListElement>>

const DescriptionListRoot = forwardRef(
({ children, className, inverseColor, ...restProps }: DescriptionListProps, ref: ForwardedRef<HTMLDListElement>) => (
<dl
{...restProps}
ref={ref}
className={clsx('ams-description-list', inverseColor && 'ams-description-list--inverse-color', className)}
>
{children}
</dl>
),
)

DescriptionListRoot.displayName = 'DescriptionList'

export const DescriptionList = Object.assign(DescriptionListRoot, {
Term: DescriptionListTerm,
Details: DescriptionListDetails,
})
41 changes: 41 additions & 0 deletions packages/react/src/DescriptionList/DescriptionListDetails.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 { DescriptionList } from './DescriptionList'
import '@testing-library/jest-dom'

describe('Description list details', () => {
it('renders', () => {
render(<DescriptionList.Details>Test</DescriptionList.Details>)

const component = screen.getByRole('definition')

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

it('renders a design system BEM class name', () => {
render(<DescriptionList.Details>Test</DescriptionList.Details>)

const component = screen.getByRole('definition')

expect(component).toHaveClass('ams-description-list__details')
})

it('renders an additional class name', () => {
render(<DescriptionList.Details className="extra">Test</DescriptionList.Details>)

const component = screen.getByRole('definition')

expect(component).toHaveClass('ams-description-list__details extra')
})

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

render(<DescriptionList.Details ref={ref}>Test</DescriptionList.Details>)

const component = screen.getByRole('definition')

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

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

export type DescriptionListDetailsProps = PropsWithChildren<HTMLAttributes<HTMLElement>>

export const DescriptionListDetails = forwardRef(
({ children, className, ...restProps }: DescriptionListDetailsProps, ref: ForwardedRef<HTMLElement>) => (
<dd {...restProps} ref={ref} className={clsx('ams-description-list__details', className)}>
{children}
</dd>
),
)

DescriptionListDetails.displayName = 'DescriptionList.Details'
41 changes: 41 additions & 0 deletions packages/react/src/DescriptionList/DescriptionListTerm.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 { DescriptionList } from './DescriptionList'
import '@testing-library/jest-dom'

describe('Description list term', () => {
it('renders', () => {
render(<DescriptionList.Term>Test</DescriptionList.Term>)

const component = screen.getByRole('term')

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

it('renders a design system BEM class name', () => {
render(<DescriptionList.Term>Test</DescriptionList.Term>)

const component = screen.getByRole('term')

expect(component).toHaveClass('ams-description-list__term')
})

it('renders an additional class name', () => {
render(<DescriptionList.Term className="extra">Test</DescriptionList.Term>)

const component = screen.getByRole('term')

expect(component).toHaveClass('ams-description-list__term extra')
})

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

render(<DescriptionList.Term ref={ref}>Test</DescriptionList.Term>)

const component = screen.getByRole('term')

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

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

export type DescriptionListTermProps = PropsWithChildren<HTMLAttributes<HTMLElement>>

export const DescriptionListTerm = forwardRef(
({ children, className, ...restProps }: DescriptionListTermProps, ref: ForwardedRef<HTMLElement>) => (
<dt {...restProps} ref={ref} className={clsx('ams-description-list__term', className)}>
{children}
</dt>
),
)

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

# React Description List component

[Description List documentation](../../../css/src/components/description-list/README.md)
4 changes: 4 additions & 0 deletions packages/react/src/DescriptionList/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { DescriptionList } from './DescriptionList'
export type { DescriptionListProps } from './DescriptionList'
export type { DescriptionListTermProps } from './DescriptionListTerm'
export type { DescriptionListDetailsProps } from './DescriptionListDetails'
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
/* Append here */
export * from './Avatar'
export * from './FormFieldCharacterCounter'
export * from './DescriptionList'
export * from './Row'
export * from './Radio'
export * from './Tabs'
Expand Down
20 changes: 20 additions & 0 deletions proprietary/tokens/src/components/ams/description-list.tokens.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"ams": {
"description-list": {
"color": { "value": "{ams.color.primary-black}" },
"font-family": { "value": "{ams.text.font-family}" },
"font-size": { "value": "{ams.text.level.5.font-size}" },
"font-weight": { "value": "{ams.text.font-weight.normal}" },
"gap": { "value": "{ams.space.stack.md}" },
"inverse-color": { "value": "{ams.color.primary-white}" },
"line-height": { "value": "{ams.text.level.5.line-height}" },
"row": {
"gap": { "value": "{ams.space.stack.md}" }
},
"details": {
"font-weight": { "value": "{ams.text.font-weight.bold}" },
"padding-inline-start": { "value": "{ams.space.inside.xl}" }
}
}
}
}
24 changes: 24 additions & 0 deletions storybook/src/components/DescriptionList/DescriptionList.docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Canvas, Controls, Markdown, Meta, Primary } from "@storybook/blocks";
import * as DescriptionListStories from "./DescriptionList.stories.tsx";
import README from "../../../../packages/css/src/components/description-list/README.md?raw";

<Meta of={DescriptionListStories} />

<Markdown>{README}</Markdown>

<Primary />

<Controls />

## Multiple details

A term may have multiple details.

<Canvas of={DescriptionListStories.MultipleDetails} />

### Inverse colour

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

<Canvas of={DescriptionListStories.InvertedColor} />
Loading

0 comments on commit ee3428a

Please sign in to comment.