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: + +- `` +- `` +- `