Skip to content

Anatomy of a Component

Edwin Guzman edited this page Jun 28, 2024 · 72 revisions

All Reservoir DS components are in the src/components/ directory. Alongside the DS component are Storybook documentation and other components that include AccessibilityGuide, Chakra, ComponentWrapper, DevelopmentGuide, and StyleGuide. Generally, a DS component folder consists of the following:

  1. A __snapshots__ folder: Snapshot tests created automatically by Jest through the --updateSnapshot flag. For example, if they need to be updated, run npm test -- --updateSnapshot.
  2. A .mdx file: This is the main file for a component's Storybook documentation page. The documentation includes the component version table, the table of contents, and other sections that describe the component and its features. The convention is to include little or no React code in this file besides the Storybook block components.
  3. A .stories.tsx file: This is the main file for the React code for a component, its variants, and working examples. The convention is to create a meta object to describe the component and its props, and various high-level Story components to render in the .mdx file described above.
  4. A .test.tsx file: This file contains Jest unit tests for the component.
  5. A .tsx file: This is the main React component.
  6. A [name]ChangelogData.ts file: This contains all the changes per release for the component.
  7. A .scss file: Only true for components that need SCSS overrides for third-party component styles.

For the purposes of this documentation, we'll be walking through the Checkbox component. Please note that what's in development may differ from the examples here.

.mdx file

Checkbox.mdx Description
import { ArgTypes, Canvas, Description, Meta } from "@storybook/blocks";
 
import ComponentChangelogTable from "../../utils/ComponentChangelogTable";
import * as CheckboxStories from "./Checkbox.stories";
import Link from "../Link/Link";
import { changelogData } from "./checkboxChangelogData";
 
<Meta of={CheckboxStories} />
 
# Checkbox
 
| Component Version | DS Version |
| ----------------- | ---------- |
| Added             | `0.1.0`    |
| Latest            | `3.1.4`    |
 
## Table of Contents
 
- {Overview}
- {Component Props}
- {Accessibility}
// ... other headings
 
## Overview
 
<Description of={CheckboxStories} />
 
The `Checkbox` component renders a checkbox input element.  ...
 
## Component Props
 
<Canvas of={CheckboxStories.WithControls} />
 
<ArgsTable of={CheckboxStories.WithControls} />
 
## Accessibility
 
The `Checkbox` component renders `` and ...
 
Resources:
 
- [W3C WAI ARIA Checkbox Example](https://www.w3.org...)
// ... more accessibility links
 
## Checked
 
Note that the `isChecked` property in this example is...
 
<Canvas& of={CheckboxStories.Checked} />
 
// More examples

MDX Story files outline the DS component documentation page in the Storybook viewer. Without this file, they don't appear in a way where other engineers can understand what they look like and how they function to a user.

This file uses the .mdx file extension for the ability to use JSX and React components in markdown files.

Storybook has great documentation if you've never used it before. Check out their documentation here.

We use Controls to show off component functionality.

When you're designing the Storybook experience for a component, ask yourself the following questions:

  • Do the available stories show off all the functionality of the component?
  • Will a developer understand how to use this component in their existing applications?
  • Does it look good as I resize my browser?
  • Do the available Controls accurately represent what is customizable about the component?

Full Checkbox.mdx example

.stories.tsx file

Checkbox.stories.tsx Description
import { Box, HStack, VStack } from "@chakra-ui/react";
import Heading from "../Heading/Heading";
import type { Meta, StoryObj } from "@storybook/react";
 
import Checkbox from "./Checkbox";
 
const argsBooleanType = (defaultValue = false) => ({
  control: { type: "boolean" },
  table: { defaultValue: { summary: defaultValue } },
});
 
const meta: Meta<typeof Checkbox> = {
  title: "Components/Form Elements/Checkbox",
  component: Checkbox,
  argTypes: {
    className: { control: false },
    helperText: { control: { type: "text" } },
    id: { control: false },
    invalidText: { control: { type: "text" } },
    isChecked: argsBooleanType(),
    isDisabled: argsBooleanType(),
    isIndeterminate: argsBooleanType(),
    isInvalid: argsBooleanType(),
    isRequired: argsBooleanType(),
    labelText: { control: { type: "text" } },
    name: { control: { type: "text" } },
    onChange: { control: false },
    showHelperInvalidText: argsBooleanType(true),
    showLabel: argsBooleanType(true),
    value: { control: { type: "text" } },
  },
};
 
export default meta;
type Story = StoryObj<typeof Checkbox>;
 
/**
 * Main Story for the Checkbox component. This must contains the `args`
 * and `parameters` properties in this object.
 */
export const WithControls: Story = {
  args: {
    className: undefined,
    helperText: "This is the helper text!",
    id: "checkbox_id",
    invalidText: "This is the error text :(",
    isChecked: undefined,
    isDisabled: false,
    isIndeterminate: false,
    isInvalid: false,
    isRequired: false,
    labelText: "Test Label",
    name: "test_name",
    onChange: undefined,
    showHelperInvalidText: true,
    showLabel: true,
    value: "1",
  },
  parameters: {
    design: {
      type: "figma",
      url: "https://www.figma.com/file/qShodlfNCJHb8n03IFyApM/Main?node-id=11895%3A658",
    },
    jest: ["Checkbox.test.tsx"],
  },
};
 
// The following are additional Checkbox example Stories.
export const Checked: Story = {
  render: () => (
    <Checkbox id="isChecked" labelText="I am checked" isChecked value="1" />
  ),
};
 
// More examples

This file must contain the base `meta` object to define the component, its type, and the props. Once created, individual `Story`-typed components objects are created and exported so they can be declared in the `.mdx` file either through the `Canvas` or `ArgTypes` components.

Full Checkbox.stories.tsx example

.test.tsx file

Checkbox.test.tsx Description
import { Flex, Spacer } from "@chakra-ui/react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { axe } from "jest-axe";
import * as React from "react";
import renderer from "react-test-renderer";
 
import Checkbox from "./Checkbox";
 
describe("Checkbox Accessibility", () => {
  it("passes axe accessibility test with string label", async () => {
    const { container } = render(
      <Checkbox id="inputID" onChange={jest.fn()} labelText="Test Label" />
    );
    expect(await axe(container)).toHaveNoViolations();
  });
  // ...
});
 
describe("Checkbox", () => {
  it("Renders with a checkbox input and label", () => {
    render(<Checkbox id="inputID" labelText="Test Label" />);
    expect(screen.getByLabelText("Test Label")).toBeInTheDocument();
    expect(screen.getByRole("checkbox")).toBeInTheDocument();
  });
 
  // more tests
 
  it("Renders the UI snapshot correctly", () => {
    const primary = renderer
      .create(<Checkbox id="inputID" labelText="Test Label" />)
      .toJSON();
    const isInvalid = renderer
      .create(
        <Checkbox id="checkbox-invalid" labelText="Test Label" isInvalid />
      )
      .toJSON();
    // More snapshot variations
 
    expect(primary).toMatchSnapshot();
    expect(isInvalid).toMatchSnapshot();
  });
});

Unit tests describe how a component should act in different situations. We use Jest> and React Testing Library to do this.

Here are some good things to try and test for:

  • Does the component error in certain situations? Make sure there's a test for that situation. Same goes for warnings.
  • Should it have certain properties in the browser? It's good to check that they're rendering as expected.
  • Does the UI change based on a user action? It's good to check that this is updating as expected.

Full Checkbox.test.tsx example

.tsx file

Checkbox.tsx Description
import {
  chakra,
  ChakraComponent,
  Checkbox as ChakraCheckbox,
  Icon,
  useMultiStyleConfig,
} from "@chakra-ui/react";
import React, { forwardRef } from "react";
 
import ComponentWrapper from "../ComponentWrapper/ComponentWrapper";
import { HelperErrorTextType } from "../HelperErrorText/HelperErrorText";
import { getAriaAttrs } from "../../utils/utils";
 
interface CheckboxIconProps {
  // ...
}
 
export interface CheckboxProps extends CheckboxIconProps {
  // ...
  /** The checkbox's label. This will serve as the
    * text content for a `` element if
    * `showlabel` is true, or an "aria-label" if
    * `showLabel` is false. */
  labelText: string | JSX.Element;
  // ...
}
 
export const Checkbox: ChakraComponent<
  React.ForwardRefExoticComponent<
    CheckboxProps & React.RefAttributes<HTMLInputElement>
  >,
  CheckboxProps
> = chakra(
  forwardRef<HTMLInputElement, CheckboxProps>((props, ref?) => {
    const {
      className,
      helperText,
      id,
      invalidText,
      isChecked,
      isDisabled = false,
      isIndeterminate = false,
      isInvalid = false,
      isRequired = false,
      labelText,
      name,
      onChange,
      showHelperInvalidText = true,
      showLabel = true,
      value,
      ...rest
    } = props;
 
    const styles = useMultiStyleConfig("Checkbox", {});
    const footnote = isInvalid ? invalidText : helperText;
    // ...
 
    return (
      <ComponentWrapper
        helperText={helperText}
        // ...
        {...rest}
      >
        <ChakraCheckbox
          className={className}
          icon={icon}
          id={id}
          // ...
        >
          {showLabel && labelText}
        </ChakraCheckbox>
      <ComponentWrapper/>
    );
  }
);
 
export default Checkbox;

Typescript is a superset of JavaScript—it's like writing JavaScript as you're used to, but with stricter rules. We use it in the Design System to enforce type safety.

Components should define the props they can receive. Here, we both define the props and their types, such as string, boolean, etc. If you want to know more about different propTypes, there's a useful guide here. The comments attached to each propType build out the table in the Docs tab of Storybook, and are used to build the controls panel underneath each story.

Like with testing, this is a good place to think about a few things such as:

  • Should the component error if it receives one prop but not the other? This is where you should throw it. (Same for warnings.)
  • Does the component naturally have a subcomponent (such as how a SearchBar has a Button in it)? Import it and use it—don't reinvent the wheel when other components that are already built are at your disposal.
  • Do the product requirements specify a component needing aria-* attributes in certain scenarios and in a particular mapping? This is a good place to build those into the components if it makes sense.

Full Checkbox.tsx example

[name]ChangelogData.ts file

checkboxChangelogData.ts Description
/** This data is used to populate the ComponentChangelogTable component.
 *
 * date: string (when adding new entry during development, set value as "Prerelease")
 * version: string (when adding new entry during development, set value as "Prerelease")
 * type: "Bug Fix" | "New Feature" | "Update";
 * affects: array["Accessibility" | "Documentation" | "Functionality" | "Styles"];
 * notes: array (will render as a bulleted list, add one array element for each list element)
 */
import { ChangelogData } from "../../utils/ComponentChangelogTable";
 
import ComponentWrapper from "../ComponentWrapper/ComponentWrapper";
import { HelperErrorTextType } from "../HelperErrorText/HelperErrorText";
import { getAriaAttrs } from "../../utils/utils";
 
export const changelogData: ChangelogData[] = [
  {
    date: "2024-05-23",
    version: "3.1.4",
    type: "Update",
    affects: ["Styles"],
    notes: [
      "Sets position relative so that is it visible when focused in a scrollable container.",
    ],
  },
  // ...
];

This file contains an array of changelog data objects. Each object describes the type of changes per version for the component.

Full checkboxChangelogData.ts example