diff --git a/packages/component-library-react/CONTRIBUTING.md b/packages/component-library-react/CONTRIBUTING.md
new file mode 100644
index 00000000000..ebf07263036
--- /dev/null
+++ b/packages/component-library-react/CONTRIBUTING.md
@@ -0,0 +1,169 @@
+
+
+# Developing components
+
+Warning: the code examples are deliberately simplified to show one concept at a time. Do not use the code snippets as-is.
+
+For more complete examples, look at the source code of existing components.
+
+## Develop for extensibility
+
+### Class names
+
+Enable front-end developers to add their own class names to the outermost HTML element of your component. Since all components have BEM class names, you must combine your BEM class names with any class names from the parameters. We typically use `clsx` to format the `class` attribute.
+
+```jsx
+import clsx from "clsx";
+
+export const MyComponent = ({ children, className }) => (
+
{children}
+);
+```
+
+### Allow rich text content
+
+Allow `ReactNode` contents for text parameters, so front-end developers can use accessible and meaningful markup. Using `PropsWithChildren` is recommended for non-empty components, because it allows `ReactNode` for children.
+
+```tsx
+import clsx from "clsx";
+import type { PropsWithChildren } from "react";
+
+export interface MyComponentProps {
+ // ...
+}
+
+export const MyComponent = ({ children }: PropsWithChildren) =>
{children}
;
+```
+
+For other parameters using `ReactNode` might not be as obvious, since you might feel like starting with `string`. For example:
+
+```tsx
+import clsx from "clsx";
+import type { PropsWithChildren } from "react";
+
+export interface MyLandmarkComponentProps {
+ label: ReactNode;
+}
+
+export const MyLandmarkComponent = ({ children, label }: PropsWithChildren) => {
+ const headingId = useId();
+ return (
+
+ {label &&
{label}
}
+ {children}
+
+ );
+};
+```
+
+This allows front-end developers to use any markup:
+
+```jsx
+
+ Landmark label
+ >
+ }
+>
+
Landmark content
+
+```
+
+Allowing rich text is one more reason to use `aria-labelledby` instead of `aria-label`.
+
+## Export interfaces and types
+
+Export the type definitions for parameters, so other developers can easily use those to develop wrapper components.
+
+```tsx
+export type TextboxTypes = "password" | "text";
+
+export interface TextboxProps extends InputHTMLAttributes {
+ type?: TextboxTypes;
+}
+
+export const Textbox = ({ type }: TextboxProps) => ;
+```
+
+This way another developer could extend your component:
+
+```tsx
+import type { TextboxProps, TextboxTypes } from "@my/textbox";
+
+export interface AdvancedTextboxProps extends TextboxProps {
+ type?: TextboxTypes | "date";
+}
+
+export const AdvancedTextbox = ({ type }: AdvancedTextboxProps) => ;
+```
+
+## Use `forwardRef`
+
+Use [`forwardRef`](https://react.dev/reference/react/forwardRef) to expose the DOM node with a [ref](https://react.dev/learn/manipulating-the-dom-with-refs).
+
+```tsx
+import clsx from "clsx";
+import type { ForwardedRef, HTMLAttributes, PropsWithChildren } from "react";
+
+export interface MyComponentProps extends HTMLAttributes {}
+
+export const MyComponent = forwardRef(
+ ({ children, ...restProps }: PropsWithChildren, ref: ForwardedRef) => (
+
+ {children}
+
+ )
+);
+```
+
+This allows front-end developers to perform actions that need access to the DOM, such as focusing an element:
+
+```tsx
+const ref = useRef(null);
+
+render();
+
+ref.current?.focus();
+```
+
+## Don't break native HTML
+
+### Global attributes
+
+Use `restProps` to allow front-end developers to global attributes as well as specific attributes to an HTML element:
+
+```jsx
+import clsx from "clsx";
+
+export const MyComponent = ({ children, ...restProps }) =>
{children}
;
+```
+
+With TypeScript you will need to extend the interface with the `HTMLAttributes` of the outermost element, and it will look like this:
+
+```tsx
+import clsx from "clsx";
+import type { HTMLAttributes, PropsWithChildren } from "react";
+
+export interface MyComponentProps extends HTMLAttributes {}
+
+export const MyComponent = ({ children, ...restProps }) =>
{children}
;
+```
+
+For different elements you need to import different types `HTMLAttributes`, but I don't think React offers documentation on this subject. The most effective approach might be using an IDE like Visual Code with a TypeScript plugin, to find out which interface you need — or simply check existing components that use the same HTML element.
+
+### Prevent duplicate IDs
+
+Generate `id` attributes with [`useId()` from React](https://react.dev/reference/react/useId). Do not use hardcoded `id` values, because that could break accessibility.
+
+```jsx
+export const MyLandmarkComponent = ({ children, label }) => {
+ const headingId = useId();
+ return (
+
+ {label &&
{label}
}
+ {children}
+
+ );
+};
+```
diff --git a/packages/component-library-react/TESTING.md b/packages/component-library-react/TESTING.md
new file mode 100644
index 00000000000..7bccb8152e2
--- /dev/null
+++ b/packages/component-library-react/TESTING.md
@@ -0,0 +1,203 @@
+
+
+# Testing components
+
+## Test for extensibility
+
+### Class names
+
+Front-end developers rely on the BEM class names to add their own CSS. When the component renames or removes a class name, there is a breaking change. Unit tests must check each class name, so they are reliable APIs.
+
+You will find many tests like this:
+
+```jsx
+it("renders a design system BEM class name: my-component", () => {
+ const { container } = render();
+
+ const field = container.querySelector("div");
+
+ expect(field).toHaveClass(".my-component");
+});
+```
+
+### So I put some HTML in your HTML
+
+Text in components can sometimes be improved with markup: language metadata, code, emphasis or images. Each property that ends up in the HTML should be tested to be extensible with rich text content.
+
+```jsx
+it("renders rich text content", () => {
+ const { container } = render(
+
+ The French national motto: Liberté, égalité, fraternité
+
+ );
+
+ const richText = container.querySelector("span");
+
+ expect(richText).toBeInTheDocument();
+});
+```
+
+Testing properties is perhaps even more important, because `children` usually already allows HTML content:
+
+```jsx
+it('renders rich text content', () => {
+ const { container } = render(
+ E-mail address
+ }>,
+ );
+
+ const richText = container.querySelector('svg');
+
+ expect(richText).toBeInTheDocument();
+});
+```
+
+## Don't break native HTML
+
+### Global attributes
+
+[Global attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes) can be used on all HTML elements, so components that render HTML must support them too. In React this is easy to support using `...restProps`. The following code examples use global attributes:
+
+- ``
+- ``
+- ``
+- ``
+- ``
+- ``
+- ``
+- ``
+
+### The `hidden` property
+
+The CSS for a component frequently break the `hidden` attribute, because code like `display: flex` overrides the default styles. Test that the `hidden` attribute still makes the invisible.
+
+```jsx
+it("can be hidden", () => {
+ const { container } = render();
+
+ const component = container.querySelector("div");
+
+ expect(component).not.toBeVisible();
+});
+```
+
+### The `className` property
+
+Components render BEM class names, but front-end developers need to by able to use their own class names as well. Additional class names must extend the class list, not overwrite the component class names.
+
+```jsx
+it("can have a additional class name", () => {
+ const { container } = render();
+
+ const component = container.querySelector(":only-child");
+
+ expect(component).toHaveClass("large");
+ expect(component).toHaveClass("my-component");
+});
+```
+
+## Test the accessibility tree
+
+### Landmarks
+
+```jsx
+it("renders an complementary role element", () => {
+ render();
+
+ const aside = screen.getByRole("complementary");
+
+ expect(aside).toBeInTheDocument();
+});
+```
+
+### Label for landmarks
+
+Some components have an API to configure the label:
+
+```jsx
+it("renders an complementary role element with a name", () => {
+ render();
+
+ const nav = screen.getByRole("navigation", { name: "Breadcrumbs" });
+
+ expect(nav).toBeInTheDocument();
+});
+```
+
+Other components need to rely on `aria-labelledby` or `aria-label`.
+
+```jsx
+it('renders an complementary role element with a name', () => {
+ render(
+
+ );
+
+ const aside = screen.getByRole('complementary', { name: 'See also' });
+
+ expect(aside).toBeInTheDocument();
+});
+```
+
+### States
+
+Voor [WCAG 4.1.2](https://nldesignsystem.nl/wcag/4.1.2) is het belangrijk dat de state van componenten beschikbaar is in de accessibility tree. [Testing Library heeft APIs](https://testing-library.com/docs/queries/byrole) om de informatie uit de accessibility tree op te vragen, in plaats van via de DOM.
+
+Voorbeelden van state zijn:
+
+- Een `checkbox` die `checked` is.
+- Een `textbox` die `disabled` is.
+- Een `textarea` die `required` is.
+- Een `button` die `expanded` is.
+
+```jsx
+describe("checked variant", () => {
+ it("is not checked by default", () => {
+ const { container } = render();
+
+ const checkbox = screen.getByRole("checkbox");
+
+ expect(checkbox).not.toBeChecked();
+ });
+
+ it("can have a checked state", () => {
+ const handleChange = () => {};
+ render();
+
+ const checkbox = screen.getByRole("checkbox");
+
+ expect(checkbox).toBeChecked();
+ });
+
+ it("can have a defaultChecked state (in React)", () => {
+ render();
+
+ const checkbox = screen.getByRole("checkbox");
+
+ expect(checkbox).toBeChecked();
+ });
+});
+```
+
+Helaas ondersteunt Testing Library nog niet elke state in de accessibility tree. Maak alvast wel de test, maar sla de test over `todo`. Gebruik de DOM om de test op een alternative manier te doen.
+
+```jsx
+// `aria-disabled` is somehow not recognized as disabled state on a listbox by Testing Library
+// https://github.com/testing-library/jest-dom/issues/144
+it.todo("has a disabled listbox in the accessibility tree", () => {});
+
+// Temporary alternative to the accessibility tree test
+it("has a disabled listbox", () => {
+ render();
+
+ const listbox = screen.getByRole("listbox");
+
+ // Look at the DOM instead of the accessibility tree
+ expect(listbox).toHaveAttribute("aria-disabled", "true");
+});
+```
+
+Controleer periodiek of een nieuwe versie van Testing Library de state wel ondersteunt.
diff --git a/packages/component-library-react/src/Article.test.tsx b/packages/component-library-react/src/Article.test.tsx
index edb97961b3c..30c35b2ad31 100644
--- a/packages/component-library-react/src/Article.test.tsx
+++ b/packages/component-library-react/src/Article.test.tsx
@@ -13,6 +13,18 @@ describe('Article', () => {
expect(article).toBeVisible();
});
+ it('can render an article role with a name', () => {
+ render(
+
+
Heading
+ ,
+ );
+
+ const article = screen.getByRole('article', { name: 'Heading' });
+
+ expect(article).toBeInTheDocument();
+ });
+
it('renders an article HTML element', () => {
const { container } = render();
diff --git a/packages/component-library-react/src/BadgeCounter.test.tsx b/packages/component-library-react/src/BadgeCounter.test.tsx
index d071c83e1f0..56f17fcc6ad 100644
--- a/packages/component-library-react/src/BadgeCounter.test.tsx
+++ b/packages/component-library-react/src/BadgeCounter.test.tsx
@@ -3,7 +3,7 @@ import { createRef } from 'react';
import { BadgeCounter } from './BadgeCounter';
import '@testing-library/jest-dom';
-describe('Data badge', () => {
+describe('Badge counter', () => {
it('renders an HTML span element', () => {
const { container } = render({'42'});
@@ -29,6 +29,7 @@ describe('Data badge', () => {
expect(badge).toHaveClass('utrecht-badge-counter');
});
+
it('can have a additional class name', () => {
const { container } = render();
diff --git a/packages/component-library-react/src/Checkbox.test.tsx b/packages/component-library-react/src/Checkbox.test.tsx
index 5b0dde3baa1..3ff0dc3ad60 100644
--- a/packages/component-library-react/src/Checkbox.test.tsx
+++ b/packages/component-library-react/src/Checkbox.test.tsx
@@ -36,6 +36,7 @@ describe('Checkbox', () => {
expect(link).toHaveClass('utrecht-checkbox');
});
+
it('can have a additional class name', () => {
const { container } = render();
@@ -47,9 +48,9 @@ describe('Checkbox', () => {
});
describe('checked variant', () => {
it('is not checked by default', () => {
- const { container } = render();
+ render();
- const checkbox = container.querySelector(':only-child');
+ const checkbox = screen.getByRole('checkbox');
expect(checkbox).not.toBeChecked();
});
@@ -67,9 +68,17 @@ describe('Checkbox', () => {
it('can have a checked state', () => {
const handleChange = () => {};
- const { container } = render();
+ render();
- const checkbox = container.querySelector(':only-child');
+ const checkbox = screen.getByRole('checkbox');
+
+ expect(checkbox).toBeChecked();
+ });
+
+ it('can have a defaultChecked state (in React)', () => {
+ render();
+
+ const checkbox = screen.getByRole('checkbox');
expect(checkbox).toBeChecked();
});
diff --git a/packages/component-library-react/src/IBANData.tsx b/packages/component-library-react/src/IBANData.tsx
index 63ed55a0bab..762ac3190dc 100644
--- a/packages/component-library-react/src/IBANData.tsx
+++ b/packages/component-library-react/src/IBANData.tsx
@@ -27,7 +27,7 @@ export const IBANData = forwardRef(
({ children, value, className, ...restProps }: IBANDataProps, ref: ForwardedRef) => {
const normalized = normalizeIBAN(value);
const formatted = formatIBAN(normalized);
- console.log({ value, normalized, formatted });
+
return (
{children || formatted}
diff --git a/packages/component-library-react/src/Listbox.test.tsx b/packages/component-library-react/src/Listbox.test.tsx
index 12cf7afbaae..3d9c1e69b5b 100644
--- a/packages/component-library-react/src/Listbox.test.tsx
+++ b/packages/component-library-react/src/Listbox.test.tsx
@@ -39,13 +39,17 @@ describe('Listbox', () => {
});
describe('disabled state', () => {
- // `aria-disabled` is somehow not recognized as disabled state
- it.skip('has a disabled listbox', () => {
+ // `aria-disabled` is somehow not recognized as disabled state on a listbox by Testing Library
+ it.todo('has a disabled listbox in the accessibility tree');
+
+ // Temporary alternative to the accessibility tree test
+ it('has a disabled listbox in the DOM', () => {
render();
const listbox = screen.getByRole('listbox');
- expect(listbox).toBeDisabled();
+ // Look at the DOM instead of the accessibility tree
+ expect(listbox).toHaveAttribute('aria-disabled', 'true');
});
});
diff --git a/packages/storybook-react/config/preview.tsx b/packages/storybook-react/config/preview.tsx
index 96a32982898..ff7f28d0512 100644
--- a/packages/storybook-react/config/preview.tsx
+++ b/packages/storybook-react/config/preview.tsx
@@ -11,7 +11,7 @@ import '@utrecht/storybook-helpers/src/storybook-docs.scss';
import '@nl-design-system-unstable/amsterdam-design-tokens/dist/index.css';
import '@nl-design-system-unstable/rotterdam-design-tokens/dist/index.css';
import '@gemeente-denhaag/design-tokens-components/dist/theme/index.css';
-console.log(results);
+
const preview: Preview = {
decorators: [
(Story: any) =>
{Story()}
,
@@ -44,6 +44,11 @@ const preview: Preview = {
);
},
},
+ options: {
+ storySort: {
+ order: ['React Component', ['Component Introduction', 'Developing components', 'Testing components']],
+ },
+ },
...addonStatus,
...addonThemes,
...addonViewport,
diff --git a/packages/storybook-react/src/stories/Introduction.stories.mdx b/packages/storybook-react/src/stories/Introduction.stories.mdx
index 28297032fdf..66cd03b375b 100644
--- a/packages/storybook-react/src/stories/Introduction.stories.mdx
+++ b/packages/storybook-react/src/stories/Introduction.stories.mdx
@@ -1,8 +1,8 @@
-import { Description, Meta } from "@storybook/addon-docs";
-import README from "@utrecht/component-library-react/README.md?raw";
+import { Markdown, Meta } from "@storybook/blocks";
+import document from "@utrecht/component-library-react/README.md?raw";
{/* @license CC0-1.0 */}
-
+{document}
diff --git a/packages/storybook-react/src/stories/contributing.stories.mdx b/packages/storybook-react/src/stories/contributing.stories.mdx
new file mode 100644
index 00000000000..69f973a19b1
--- /dev/null
+++ b/packages/storybook-react/src/stories/contributing.stories.mdx
@@ -0,0 +1,8 @@
+import { Markdown, Meta } from "@storybook/blocks";
+import document from "@utrecht/component-library-react/CONTRIBUTING.md?raw";
+
+
+
+{/* @license CC0-1.0 */}
+
+{document}
diff --git a/packages/storybook-react/src/stories/testing.stories.mdx b/packages/storybook-react/src/stories/testing.stories.mdx
new file mode 100644
index 00000000000..ee246495072
--- /dev/null
+++ b/packages/storybook-react/src/stories/testing.stories.mdx
@@ -0,0 +1,8 @@
+import { Markdown, Meta } from "@storybook/blocks";
+import document from "@utrecht/component-library-react/TESTING.md?raw";
+
+
+
+{/* @license CC0-1.0 */}
+
+{document}