Skip to content

Commit

Permalink
feat: Add Skip link component (#988)
Browse files Browse the repository at this point in the history
Co-authored-by: Vincent Smedinga <[email protected]>
  • Loading branch information
alimpens and VincentSmedinga authored Dec 22, 2023
1 parent 1b2e87d commit 82323b5
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 4 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 "./skip-link/skip-link";
@import "./overlap/overlap";
@import "./header/header";
@import "./mark/mark";
Expand Down
33 changes: 33 additions & 0 deletions packages/css/src/components/skip-link/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Skip Link

Gebruik een Skip Link om makkelijk met het toetsenbord naar de belangrijkste inhoud te navigeren.
Met een Skip Link kun je terugkerende navigatieblokken (zoals het hoofdmenu of het kruimelpad) overslaan.

De Skip Link staat boven de header.
De link is verborgen totdat deze met de tab-toets geactiveerd wordt.
Als de link getoond wordt, duwt deze de hele pagina omlaag.

## Richtlijnen

### Zo gebruiken

- Plaats de Skip Link als eerste element in `<body>`, tenzij je een cookie-banner hebt.
Plaats de Skip Link dan direct na de cookie-banner.
- Gebruik de Skip Link om naar de belangrijkste inhoud te navigeren.
Op een artikelpagina is dat bijvoorbeeld de titel van het artikel, op een zoekpagina is dat het zoekveld.
- Voor complexe pagina's met meerdere secties kun je meer dan 1 Skip Link gebruiken.
In de meeste gevallen is dit niet nodig.

### Dit vermijden

- Skip Links zijn niet nodig op een simpele pagina waar alleen tekst staat en weinig navigatie.
Het doel van een Skip Link is om terugkerende navigatieblokken over te slaan.
Als die blokken er niet zijn, is een Skip Link niet nodig.
- Plaats de Skip Link niet in een `nav` regio, of in de Header.

## Relevante WCAG eisen

- Voor dit component gelden dezelfde WCAG eisen als voor [het link component](https://amsterdam.github.io/design-system/?path=/docs/react_navigation-link--docs).
- [WCAG 2.4.1](https://www.w3.org/TR/WCAG22/#bypass-blocks): gebruik een Skip Link op elke pagina die begint met een terugkerend navigatieblok.
- [WCAG 3.2.3](https://www.w3.org/TR/WCAG22/#consistent-navigation): een Skip Link staat op elke pagina op dezelfde plek.
- [WCAG 3.2.4](https://www.w3.org/TR/WCAG22/#consistent-identification): een Skip Link heeft dezelfde labels op alle pagina's. Bijvoorbeeld niet: "Navigatie overslaan" op een gedeelte van de site, en "Naar de inhoud" op andere pagina's.
28 changes: 28 additions & 0 deletions packages/css/src/components/skip-link/skip-link.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* @license EUPL-1.2+
* Copyright (c) 2023 Gemeente Amsterdam
*/

.amsterdam-skip-link {
background-color: var(--amsterdam-skip-link-background-color);
color: var(--amsterdam-skip-link-color);
display: block;
font-family: var(--amsterdam-skip-link-font-family);
font-size: var(--amsterdam-skip-link-font-size);
font-weight: var(--amsterdam-skip-link-font-weight);
line-height: var(--amsterdam-skip-link-line-height);
outline-offset: var(--amsterdam-skip-link-outline-offset);
padding-block: 0.5rem;
padding-inline: 1rem;
text-align: center;
text-decoration: none;

&:hover {
background-color: var(--amsterdam-skip-link-hover-background-color);
}

.amsterdam-theme--compact & {
font-size: var(--amsterdam-skip-link-compact-font-size);
line-height: var(--amsterdam-skip-link-compact-line-height);
}
}
3 changes: 3 additions & 0 deletions packages/react/src/SkipLink/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# React Skip Link component

[Skip Link documentation](../../../css/src/skip-link/README.md)
41 changes: 41 additions & 0 deletions packages/react/src/SkipLink/SkipLink.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 { SkipLink } from './SkipLink'
import '@testing-library/jest-dom'

describe('Skip Link', () => {
it('renders', () => {
render(<SkipLink href="/" />)

const component = screen.getByRole('link')

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

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

const component = screen.getByRole('link')

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

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

const component = screen.getByRole('link')

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

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

render(<SkipLink href="/" ref={ref} />)

const component = screen.getByRole('link')

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

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

export interface SkipLinkProps extends PropsWithChildren<AnchorHTMLAttributes<HTMLAnchorElement>> {}

export const SkipLink = forwardRef(
({ children, className, ...restProps }: SkipLinkProps, ref: ForwardedRef<HTMLAnchorElement>) => (
<a {...restProps} ref={ref} className={clsx('amsterdam-skip-link', 'amsterdam-visually-hidden', className)}>
{children}
</a>
),
)

SkipLink.displayName = 'SkipLink'
2 changes: 2 additions & 0 deletions packages/react/src/SkipLink/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { SkipLink } from './SkipLink'
export type { SkipLinkProps } from './SkipLink'
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 './SkipLink'
export * from './Overlap'
export * from './Header'
export * from './Mark'
Expand Down
4 changes: 1 addition & 3 deletions plop-templates/react.test.tsx.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ describe('{{sentenceCase name}}', () => {

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

expect(component).toHaveClass('extra')

expect(component).toHaveClass('amsterdam-{{kebabCase name}}')
expect(component).toHaveClass('amsterdam-{{kebabCase name}} extra')
})

it('supports ForwardRef in React', () => {
Expand Down
3 changes: 2 additions & 1 deletion plop-templates/react.tsx.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
*/

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

export interface {{pascalCase name}}Props extends PropsWithChildren<HTMLAttributes<HTMLElement>> {}

Expand Down
20 changes: 20 additions & 0 deletions proprietary/tokens/src/components/amsterdam/skip-link.tokens.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"amsterdam": {
"skip-link": {
"background-color": { "value": "{amsterdam.color.primary-blue}" },
"color": { "value": "{amsterdam.color.primary-white}" },
"font-family": { "value": "{amsterdam.typography.font-family}" },
"font-weight": { "value": "{amsterdam.typography.font-weight.normal}" },
"font-size": { "value": "{amsterdam.typography.spacious.text-level.6.font-size}" },
"line-height": { "value": "{amsterdam.typography.spacious.text-level.6.line-height}" },
"outline-offset": { "value": "{amsterdam.focus.outline-offset}" },
"compact": {
"font-size": { "value": "{amsterdam.typography.compact.text-level.6.font-size}" },
"line-height": { "value": "{amsterdam.typography.compact.text-level.6.line-height}" }
},
"hover": {
"background-color": { "value": "{amsterdam.color.dark-blue}" }
}
}
}
}
24 changes: 24 additions & 0 deletions storybook/storybook-react/src/SkipLink/SkipLink.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 SkipLinkStories from "./SkipLink.stories.tsx";
import README from "../../../../packages/css/src/components/skip-link/README.md?raw";

<Meta of={SkipLinkStories} />

<Markdown>{README}</Markdown>

<Primary />

<Controls />

## Toon bij focus

Een Skip Link wordt pas getoond als deze focus krijgt.

<Canvas of={SkipLinkStories.OnFocus} />

## Meerdere links

Als je een complexe pagina met meerdere secties hebt, kun je meer dan 1 Skip Link gebruiken.
In de meeste gevallen is dit niet nodig.

<Canvas of={SkipLinkStories.MultipleLinks} />
79 changes: 79 additions & 0 deletions storybook/storybook-react/src/SkipLink/SkipLink.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* @license EUPL-1.2+
* Copyright (c) 2023 Gemeente Amsterdam
*/

import { Grid, Paragraph, Screen, SkipLink } from '@amsterdam/design-system-react'
import { Meta, StoryObj } from '@storybook/react'

const meta = {
title: 'Navigation/Skip Link',
component: SkipLink,
args: {
children: 'Direct naar inhoud',
href: '#',
},
argTypes: {
style: {
table: {
disable: true,
},
},
},
decorators: [
(Story) => (
<Screen>
<Grid>
<Grid.Cell span="all">
<Story />
</Grid.Cell>
</Grid>
</Screen>
),
],
} satisfies Meta<typeof SkipLink>

export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {
args: {
// This resets the default behaviour of only showing the link
// on focus, in order to always show the link in Storybook
style: {
clip: 'initial',
clipPath: 'initial',
height: 'initial',
overflow: 'initial',
position: 'initial',
whiteSpace: 'initial',
width: 'initial',
},
},
}

export const OnFocus: Story = {
decorators: [
(Story) => (
<>
<Paragraph size="small" style={{ marginBottom: '2rem' }}>
Klik op deze tekst en druk vervolgens op tab om de Skip Link te tonen.
</Paragraph>
<Story />
</>
),
],
}

export const MultipleLinks: Story = {
render: () => (
<>
<Paragraph size="small" style={{ marginBottom: '2rem' }}>
Klik op deze tekst en druk vervolgens twee keer op tab om de Skip Links te tonen.
</Paragraph>
<SkipLink href="#">Direct naar inhoud</SkipLink>
<SkipLink href="#">Direct naar contactgegevens</SkipLink>
</>
),
}

0 comments on commit 82323b5

Please sign in to comment.