Skip to content

Commit

Permalink
feat: Introduce separate Link List component (#1051)
Browse files Browse the repository at this point in the history
  • Loading branch information
VincentSmedinga authored Jan 26, 2024
1 parent d97d023 commit 7ccf23d
Show file tree
Hide file tree
Showing 20 changed files with 687 additions and 118 deletions.
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 */
@import "./link-list/link-list";
@import "./badge/badge";
@import "./table/table";
@import "./mega-menu/mega-menu";
Expand Down
16 changes: 16 additions & 0 deletions packages/css/src/components/link-list/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Link List

A collection of related links.

## Design

Every list item starts with a chevron.
It emphasizes the list structure and thematic coherence.
The chevron is part of the link.
Therefore, it is blue and clickable.

## Guidelines

Use a Link List to present multiple links within a theme.

For additional guidelines, refer to the [Link](?path=/docs/navigation-link--docs) component.
80 changes: 80 additions & 0 deletions packages/css/src/components/link-list/link-list.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* @license EUPL-1.2+
* Copyright (c) 2024 Gemeente Amsterdam
*/

@mixin reset-list {
box-sizing: border-box;
list-style: none;
margin-block: 0;
padding-inline-start: 0;
-webkit-text-size-adjust: 100%;
}

.amsterdam-link-list {
@include reset-list;

display: grid;
gap: var(--amsterdam-link-list-gap);
}

.amsterdam-link-list__link {
align-items: flex-start;
color: var(--amsterdam-link-list-link-color);
display: inline-flex;
font-family: var(--amsterdam-link-list-link-font-family);
font-size: var(--amsterdam-link-list-link-spacious-medium-font-size);
font-weight: var(--amsterdam-link-list-link-font-weight);
gap: var(--amsterdam-link-list-link-gap);
line-height: var(--amsterdam-link-list-link-spacious-medium-line-height);
outline-offset: var(--amsterdam-link-list-link-outline-offset);
text-decoration-line: var(--amsterdam-link-list-link-text-decoration-line);
text-decoration-thickness: var(--amsterdam-link-list-link-text-decoration-thickness);
text-underline-offset: var(--amsterdam-link-list-link-text-underline-offset);

.amsterdam-theme--compact & {
font-size: var(--amsterdam-link-list-link-compact-medium-font-size);
line-height: var(--amsterdam-link-list-link-compact-medium-line-height);
}

&:hover {
color: var(--amsterdam-link-list-link-hover-color);
text-decoration-line: var(--amsterdam-link-list-link-hover-text-decoration-line);
}
}

.amsterdam-link-list__link--small {
font-size: var(--amsterdam-link-list-link-spacious-small-font-size);
line-height: var(--amsterdam-link-list-link-spacious-small-line-height);

.amsterdam-theme--compact & {
font-size: var(--amsterdam-link-list-link-compact-small-font-size);
line-height: var(--amsterdam-link-list-link-compact-small-line-height);
}
}

.amsterdam-link-list__link--large {
font-size: var(--amsterdam-link-list-link-spacious-large-font-size);
line-height: var(--amsterdam-link-list-link-spacious-large-line-height);

.amsterdam-theme--compact & {
font-size: var(--amsterdam-link-list-link-compact-large-font-size);
line-height: var(--amsterdam-link-list-link-compact-large-line-height);
}
}

.amsterdam-link-list__link--on-background-dark {
color: var(--amsterdam-link-list-link-on-background-dark-color);

&:hover {
color: var(--amsterdam-link-list-link-on-background-dark-hover-color);
}
}

.amsterdam-link-list__link--on-background-light {
color: var(--amsterdam-link-list-link-on-background-light-color);

&:hover {
color: var(--amsterdam-link-list-link-on-background-light-hover-color);
}
}
4 changes: 3 additions & 1 deletion packages/react/src/Link/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import type { AnchorHTMLAttributes, ForwardedRef, PropsWithChildren } from 'reac
import { Icon } from '../Icon/Icon'

type LinkOnBackground = 'default' | 'light' | 'dark'
type LinkVariant = 'standalone' | 'inline' | 'inList'
/** @deprecated Use `LinkList` instead. */
type DeprecatedLinkVariantInList = 'inList'
type LinkVariant = 'standalone' | 'inline' | DeprecatedLinkVariantInList

interface CommonProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'placeholder'> {
variant?: LinkVariant
Expand Down
41 changes: 41 additions & 0 deletions packages/react/src/LinkList/LinkList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import '@testing-library/jest-dom'
import { render, screen } from '@testing-library/react'
import { createRef } from 'react'
import { LinkList } from './LinkList'

describe('Link list', () => {
it('renders', () => {
render(<LinkList />)

const component = screen.getByRole('list')

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

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

const component = screen.getByRole('list')

expect(component).toHaveClass('amsterdam-link-list')
})

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

const component = screen.getByRole('list')

expect(component).toHaveClass('amsterdam-link-list extra')
})

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

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

const component = screen.getByRole('list')

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

import clsx from 'clsx'
import { forwardRef } from 'react'
import type { ForwardedRef, ForwardRefExoticComponent, HTMLAttributes, PropsWithChildren, RefAttributes } from 'react'
import { LinkListLink } from './LinkListLink'

export interface LinkListProps extends PropsWithChildren<HTMLAttributes<HTMLUListElement>> {}

interface LinkListComponent extends ForwardRefExoticComponent<LinkListProps & RefAttributes<HTMLUListElement>> {
Link: typeof LinkListLink
}

/** A collection of related links. */
export const LinkList = forwardRef(
({ children, className, ...restProps }: LinkListProps, ref: ForwardedRef<HTMLUListElement>) => {
return (
<ul ref={ref} className={clsx('amsterdam-link-list', className)} {...restProps}>
{children}
</ul>
)
},
) as LinkListComponent

LinkList.Link = LinkListLink
LinkList.displayName = 'LinkList'
63 changes: 63 additions & 0 deletions packages/react/src/LinkList/LinkListLink.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import '@testing-library/jest-dom'
import { render, screen } from '@testing-library/react'
import { createRef } from 'react'
import { LinkList } from './LinkList'

describe('Link list link', () => {
it('renders', () => {
const { container } = render(<LinkList.Link href="#" />)

const listItem = screen.getByRole('listitem')
const link = screen.getByRole('link')
const icon = container.querySelector('svg')

expect(listItem).toBeInTheDocument()
expect(listItem).toBeVisible()
expect(link).toBeInTheDocument()
expect(link).toBeVisible()
expect(icon).toBeInTheDocument()
expect(icon).toBeVisible()
})

it('renders a design system BEM class name', () => {
render(<LinkList.Link href="#" />)

const component = screen.getByRole('link')

expect(component).toHaveClass('amsterdam-link-list__link')
})

it('renders an additional class name', () => {
render(<LinkList.Link href="#" className="extra" />)

const component = screen.getByRole('link')

expect(component).toHaveClass('amsterdam-link-list__link extra')
})

it('renders a class name for the small size', () => {
render(<LinkList.Link href="#" size="small" />)

const component = screen.getByRole('link')

expect(component).toHaveClass('amsterdam-link-list__link--small')
})

it('renders a class name for the background color', () => {
render(<LinkList.Link href="#" onBackground="dark" />)

const component = screen.getByRole('link')

expect(component).toHaveClass('amsterdam-link-list__link--on-background-dark')
})

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

render(<LinkList.Link href="#" ref={ref} />)

const component = screen.getByRole('link')

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

import { ChevronRightIcon } from '@amsterdam/design-system-react-icons'
import clsx from 'clsx'
import { forwardRef } from 'react'
import type {
AnchorHTMLAttributes,
ForwardedRef,
ForwardRefExoticComponent,
PropsWithChildren,
RefAttributes,
} from 'react'
import { Icon } from '../Icon'

type BackgroundName = 'default' | 'light' | 'dark'

export interface LinkListLinkProps extends PropsWithChildren<AnchorHTMLAttributes<HTMLAnchorElement>> {
/** The target url for the link. */
href: string
/**
* An icon to display instead of the default chevron.
* Don’t mix custom icons with chevrons in one list.
*/
icon?: Function
/** Whether the link sits on a dark background. */
onBackground?: BackgroundName
/**
* The text size for the link.
* Use the same size for all items in the list.
*/
size?: 'small' | 'large'
}

interface LinkListLinkComponent
extends ForwardRefExoticComponent<LinkListLinkProps & RefAttributes<HTMLAnchorElement>> {}

const iconSizeMap = {
small: 'level-6',
medium: 'level-5',
large: 'level-4',
} as const

/** One link with a Link List. */
export const LinkListLink = forwardRef(
(
{ children, className, href, icon, onBackground, size, ...restProps }: LinkListLinkProps,
ref: ForwardedRef<HTMLAnchorElement>,
) => {
return (
<li>
<a
className={clsx(
'amsterdam-link-list__link',
onBackground && `amsterdam-link-list__link--on-background-${onBackground}`,
size && `amsterdam-link-list__link--${size}`,
className,
)}
href={href}
ref={ref}
{...restProps}
>
<Icon svg={icon ?? ChevronRightIcon} size={iconSizeMap[size ?? 'medium']} />
{children}
</a>
</li>
)
},
) as LinkListLinkComponent

LinkListLink.displayName = 'LinkList.Link'
3 changes: 3 additions & 0 deletions packages/react/src/LinkList/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# React Link List component

[Link List documentation](../../../css/src/link-list/README.md)
3 changes: 3 additions & 0 deletions packages/react/src/LinkList/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { LinkList } from './LinkList'
export type { LinkListProps } from './LinkList'
export type { LinkListLinkProps } from './LinkListLink'
3 changes: 2 additions & 1 deletion 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 './LinkList'
export * from './Badge'
export * from './Table'
export * from './MegaMenu'
Expand Down Expand Up @@ -35,7 +36,7 @@ export * from './OrderedList'
export * from './Heading'
export * from './Breadcrumb'
export * from './Link'
export * from './Button/'
export * from './Button'
export * from './Paragraph'
export * from './FormLabel'
export * from './UnorderedList'
Expand Down
Loading

0 comments on commit 7ccf23d

Please sign in to comment.