Skip to content

Latest commit

 

History

History
216 lines (152 loc) · 8.38 KB

component-development.md

File metadata and controls

216 lines (152 loc) · 8.38 KB

Component Development

For information on how to design components, see the component design docs.

Before working with OUI components or creating new ones, you may want to run a local server for the documentation site. This is where we demonstrate how the components in our design system work.

Launching the Documentation Server

To view interactive documentation, start the development server using the command below.

yarn
yarn start

Once the server boots up, you can visit it on your browser at: http://localhost:8030/. The development server watches for changes to the source code files and will automatically recompile the components for you when you make changes.

Creating Components

There are four steps to creating a new component:

  1. Create the SCSS for the component in src/components
  2. Create the React portion of the component
  3. Write tests
  4. Document it with examples in src-docs

You can do this using Yeoman, or you can do it manually if you prefer.

Testing the component

yarn run test-unit runs the Jest unit tests once.

yarn run test-unit button will run tests with "button" in the spec name. You can pass other Jest CLI arguments by just adding them to the end of the command like this:

yarn run test-unit -- -u will update your snapshots. To pass flags or other options you'll need to follow the format of yarn run test-unit -- [arguments]. Note: if you are experiencing failed builds in Jenkins related to snapshots, then try clearing the cache first yarn run test-unit -- --clearCache.

yarn run test-unit -- --watch watches for changes and runs the tests as you code.

yarn run test-unit -- --coverage generates a code coverage report showing you how fully-tested the code is, located at reports/jest-coverage.

Refer to the testing guide for guidelines on writing and designing your tests.

Refer to the automated accessibility testing guide for info more info on those.

Testing the component with OpenSearch Dashboards

Note that yarn link currently does not work with OpenSearch Dashboards. You'll need to manually pack and insert it into OpenSearch Dashboards to test locally.

In OUI run:

yarn build && npm pack

This will create a .tgz file with the changes in your OUI directory. At this point you can move it anywhere.

In OpenSearch Dashboards:

Point the package.json file in OpenSearch Dashboards to that file: "@opensearch-project/oui": "/path/to/opensearch-project-oui-xx.x.x.tgz". Then run the following commands at OpenSearch Dashboards root folder:

yarn osd bootstrap --no-validate && cd packages/osd-ui-shared-deps/ && yarn osd:bootstrap && cd ../../ && FORCE_DLL_CREATION=true node scripts/osd --dev
  • The --no-validate flag is required when bootstrapping with a .tgz.
    • Change the name of the .tgz after subsequent yarn build and npm pack steps (e.g., opensearch-project-oui-xx.x.x-1.tgz, opensearch-project-oui-xx.x.x-2.tgz). This is required for yarn to recognize new changes to the package.
  • Running yarn osd:bootstrap inside of OpenSearch-Dashboards/packages/osd-ui-shared-deps/ rebuilds OpenSearch Dashboards shared-ui-deps.
  • Running OpenSearch Dashboards with FORCE_DLL_CREATION=true node scripts/osd --dev ensures it doesn't use a previously cached version of OUI.

Principles

Logically-grouped components

If a component has subcomponents (<OuiToolBar> and <OuiToolBarSearch>), tightly-coupled components (<OuiButton> and <OuiButtonGroup>), or you just want to group some related components together (<OuiTextInput>, <OuiTextArea>, and <OuiCheckBox>), then they belong in the same logical grouping. In this case, you can create additional SCSS files for these components in the same component directory.

Writing CSS

Refer to the SASS page of our documentation site for a guide to writing styles.

TypeScript definitions

Pass-through props

Many of our components use rest parameters and the spread operator to pass props through to an underlying DOM element. In those instances the component's TypeScript definition needs to properly include the target DOM element's props.

A Foo component that passes ...rest through to a button element would have the props interface

// passes extra props to a button
interface FooProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  title: string
}

Some DOM elements (e.g. div, span) do not have attributes beyond the basic ones provided by all HTML elements. In these cases there isn't a specific *HTMLAttributes<T> interface, and you should use HTMLAttributes<HTMLDivElement>.

// passes extra props to a div
interface FooProps extends HTMLAttributes<HTMLDivElement> {
  title: string
}

If your component forwards a ref through to an underlying element, the interface needs to be further extended with DetailedHTMLProps

// passes extra props and forwards the ref to a button
interface FooProps extends DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
  title: string
}

forwardRef

React's forwardRef should be used to provide access to the component's outermost element. We impose two additional requirements when using forwardRef:

  1. use forwardRef instead of React.forwardRef, otherwise react-docgen-typescript does not understand it and the component's props will not be rendered in our documentation
  2. the resulting component must have a displayName, this is useful when the component is included in a snapshot or when inspected in devtools. There is an eslint rule which checks for this.

Simple forward/pass-through

import React, { forwardRef } from 'react';

interface MyComponentProps {...}

export const MyComponent = forwardRef<
  HTMLDivElement, // type of element or component the ref will be passed to
  MyComponentProps // what properties apart from `ref` the component accepts
>(
  (
    { destructure, props, here, ...rest },
    ref
  ) => {
    return (
      <div ref={ref} {...rest}>
        ...
      </div>
    );
  }
);

MyComponent.displayName = 'MyComponent';

Combining with additional refs

Sometimes an element needs to have 2+ refs passed to it, for example a component interacts with the same element the forwarded ref needs to be given to. For this OUI provides a useCombinedRefs hook:

import React, { forwardRef, createRef } from 'react';
import { useCombinedRefs } from '../../services';

interface MyComponentProps {...}

export const MyComponent = forwardRef<
  HTMLDivElement, // type of element or component the ref will be passed to
  MyComponentProps // what properties apart from `ref` the component accepts
>(
  (
    { destructure, props, here, ...rest },
    ref
  ) => {
    const localRef = useRef<HTMLDivElement>(null);
    const combinedRefs = useCombinedRefs([ref, localRef]);
    return (
      <div ref={combinedRefs} {...rest}>
        ...
      </div>
    );
  }
);

MyComponent.displayName = 'MyComponent';

Providing custom or additional data

Rarely, a component's ref needs to be something other than a DOM element, or provide additional information. In these cases, React's useImperativeHandle can be used to provide a custom object as the ref's value. For example, OuiMarkdownEditor's ref includes both its textarea element and the replaceNode method to interact with the abstract syntax tree. https://github.com/opensearch-project/oui/blob/main/src/components/markdown_editor/markdown_editor.tsx#L342

import React, { useImperativeHandle } from 'react';

export const OuiMarkdownEditor = forwardRef<
  OuiMarkdownEditorRef,
  OuiMarkdownEditorProps
  >(
  (props, ref) => {
    ...

    // combines the textarea element & `replaceNode` into a single object, which is then passed back to the forwarded `ref`
    useImperativeHandle(
      ref,
      () => ({ textarea: textareaRef.current, replaceNode }),
      [replaceNode]
    );

    ...
  }
);