diff --git a/components/button-link/README.md b/components/button-link/README.md new file mode 100644 index 0000000000..4a7a7b7e79 --- /dev/null +++ b/components/button-link/README.md @@ -0,0 +1,39 @@ + + +# Button link: link die er uit ziet als button + +Een link die er uitziet als een button, om gebruikers aan te sporen op de link te klikken en actie te gaan ondernemen. + +## Toepassing + +Een link button mag alleen gebruikt worden voor navigatie naar een pagina waar je een actie uitvoert, de link klikken mag niet gelijk een _side-effect_ hebben. + +Wel: + +- Log in met eIDAS (navigeert naar formulier voor inloggen) +- Maak een afspraak (navigeert naar formulier) + +Niet: + +- Uitloggen (logt direct uit) +- Stop de parkeermeter (je mag hierna niet meer parkeren) + +## Activeren + +Een link button moet op dezelfde manier geactiveerd kunnen worden als een button: + +- Klikken +- `Enter` op toetsenbord +- `Space` op toetsenbord + +Een HTML `a` element wordt standaard niet geactiveerd met `Space`, daarvoor is ondersteunende JavaScript nodig. + +## States + +- hover +- focus +- focus-visible + +De link button heeft geen disabled state, net als de normale link: die heeft ook geen disabled state. + +De link button heeft geen `visited` state zoals normale links, maar ziet er altijd hetzelfde uit zoals een normale button. diff --git a/components/button-link/css/index.scss b/components/button-link/css/index.scss new file mode 100644 index 0000000000..c49bc26861 --- /dev/null +++ b/components/button-link/css/index.scss @@ -0,0 +1,26 @@ +/** + * @license EUPL-1.2 + * Copyright (c) 2021 The Knights Who Say NIH! B.V. + * Copyright (c) 2021 Gemeente Utrecht + */ + +@import "../../button/bem"; + +.utrecht-button-link { + @extend .utrecht-button; + + cursor: pointer; + text-decoration: none; +} + +.utrecht-button-link--hover { + @extend .utrecht-button--hover; +} + +.utrecht-button-link--focus { + @extend .utrecht-button--focus; +} + +.utrecht-button-link--focus-visible { + @extend .utrecht-button--focus-visible; +} diff --git a/components/button-link/css/stories/01-default.stories.mdx b/components/button-link/css/stories/01-default.stories.mdx new file mode 100644 index 0000000000..c9741f09c2 --- /dev/null +++ b/components/button-link/css/stories/01-default.stories.mdx @@ -0,0 +1,66 @@ + + +import { ArgsTable, Canvas, Meta, Story } from "@storybook/addon-docs"; +import { ButtonLink, defaultArgs } from "../template"; +import "../index.scss"; + + + +# Button link: link die er uit ziet als button + +Styling met de `.utrecht-button-link` class naam: + + + + {ButtonLink.bind({})} + + + + diff --git a/components/button-link/css/stories/readme.stories.mdx b/components/button-link/css/stories/readme.stories.mdx new file mode 100644 index 0000000000..243f297e21 --- /dev/null +++ b/components/button-link/css/stories/readme.stories.mdx @@ -0,0 +1,8 @@ + + +import { Description, Meta } from "@storybook/addon-docs"; +import document from "../../README.md"; + + + +{document} diff --git a/components/button-link/css/template.js b/components/button-link/css/template.js new file mode 100644 index 0000000000..ac49503806 --- /dev/null +++ b/components/button-link/css/template.js @@ -0,0 +1,25 @@ +import clsx from 'clsx'; + +export const defaultArgs = { + external: false, + hover: false, + href: '', + focus: false, + focusVisible: false, + textContent: '', +}; + +export const ButtonLink = ({ + external = false, + hover = false, + href = '', + focus = false, + focusVisible = false, + textContent = '', +}) => + `${textContent}`; diff --git a/components/button-link/react/index.stories.mdx b/components/button-link/react/index.stories.mdx new file mode 100644 index 0000000000..9d4b71c4fc --- /dev/null +++ b/components/button-link/react/index.stories.mdx @@ -0,0 +1,22 @@ + + +import { Canvas, Meta } from "@storybook/addon-docs"; +import { ButtonLink } from "@utrecht/component-library-react"; +import "../css/index.scss"; + + + +# Link that looks like a button + + + + Start je aanvraag + + diff --git a/packages/component-library-css/src/bem.scss b/packages/component-library-css/src/bem.scss index 862a62e89f..43e24822b7 100644 --- a/packages/component-library-css/src/bem.scss +++ b/packages/component-library-css/src/bem.scss @@ -25,6 +25,7 @@ @import "../../../components/breadcrumb/css"; @import "../../../components/button/bem"; @import "../../../components/button-group/css"; +@import "../../../components/button-link/css"; @import "../../../components/checkbox/css"; @import "../../../components/custom-checkbox/css"; @import "../../../components/digid-button/css"; diff --git a/packages/component-library-react/package.json b/packages/component-library-react/package.json index aef27cf813..3b46257133 100644 --- a/packages/component-library-react/package.json +++ b/packages/component-library-react/package.json @@ -41,6 +41,10 @@ "types": "./dist/Button.d.ts", "default": "./dist/cjs/Button.js" }, + "./ButtonLink": { + "types": "./dist/ButtonLink.d.ts", + "default": "./dist/cjs/ButtonLink.js" + }, "./Checkbox": { "types": "./dist/Checkbox.d.ts", "default": "./dist/cjs/Checkbox.js" diff --git a/packages/component-library-react/src/ButtonLink.test.tsx b/packages/component-library-react/src/ButtonLink.test.tsx new file mode 100644 index 0000000000..a93f184328 --- /dev/null +++ b/packages/component-library-react/src/ButtonLink.test.tsx @@ -0,0 +1,252 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { createRef } from 'react'; +import { ButtonLink } from './ButtonLink'; +import '@testing-library/jest-dom'; + +describe('Link that looks like a button', () => { + it('renders an link role element', () => { + render(Home); + + const link = screen.getByRole('link', { name: 'Home' }); + + expect(link).toBeInTheDocument(); + expect(link).toBeVisible(); + }); + + it('renders an HTML a element', () => { + const { container } = render({'https://example.com/'}); + + const link = container.querySelector('a:only-child'); + + expect(link).toBeInTheDocument(); + }); + + it('is focusable', async () => { + const handleFocus = jest.fn(); + + render( + + Home + , + ); + + const link = screen.getByRole('link'); + + link?.focus(); + + expect(handleFocus).toHaveBeenCalled(); + }); + + it('can be activated with Enter', async () => { + const handleClick = jest.fn(); + + render( + + Home + , + ); + + const link = screen.getByRole('link'); + + if (link) { + link.focus(); + await userEvent.keyboard('{Enter}'); + } + + expect(handleClick).toHaveBeenCalled(); + }); + + it('can NOT be activated with Space', async () => { + const handleClick = jest.fn(); + + render( + + Home + , + ); + + const link = screen.getByRole('link'); + + if (link) { + link.focus(); + await userEvent.keyboard(' '); + } + + expect(handleClick).not.toHaveBeenCalled(); + }); + + describe('with button role', () => { + it('can render a link with button role element', () => { + render( + + Back + , + ); + + const button = screen.getByRole('button', { name: 'Back' }); + + expect(button).toBeInTheDocument(); + expect(button).toBeVisible(); + }); + + it('can be activated with Enter', async () => { + const handleClick = jest.fn(); + + render( + + Home + , + ); + + const link = screen.getByRole('button'); + + if (link) { + link.focus(); + await userEvent.keyboard('{Enter}'); + } + + expect(handleClick).toHaveBeenCalled(); + }); + + it('can be activated with Space', async () => { + const handleClick = jest.fn(); + + render( + + Home + , + ); + + const link = screen.getByRole('button'); + + if (link) { + link.focus(); + await userEvent.keyboard(' '); + } + + expect(handleClick).toHaveBeenCalled(); + }); + }); + + it('renders a design system BEM class name', () => { + const { container } = render(); + + const link = container.querySelector(':only-child'); + + expect(link).toHaveClass('utrecht-button-link'); + }); + + it('renders rich text content', () => { + const { container } = render( + + https: + {'//example.com/'} + , + ); + + const link = container.querySelector(':only-child'); + + const richText = link?.querySelector('strong'); + + expect(richText).toBeInTheDocument(); + }); + + it('can be hidden', () => { + const { container } = render(