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.
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.
There are four steps to creating a new component:
- Create the SCSS for the component in
src/components
- Create the React portion of the component
- Write tests
- Document it with examples in
src-docs
You can do this using Yeoman, or you can do it manually if you prefer.
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.
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.
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.
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 subsequentyarn build
andnpm pack
steps (e.g.,opensearch-project-oui-xx.x.x-1.tgz
,opensearch-project-oui-xx.x.x-2.tgz
). This is required foryarn
to recognize new changes to the package.
- Change the name of the
- Running
yarn osd:bootstrap
inside ofOpenSearch-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.
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.
Refer to the SASS page of our documentation site for a guide to writing styles.
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
}
React's forwardRef
should be used to provide access to the component's outermost element. We impose two additional requirements when using forwardRef
:
- use
forwardRef
instead ofReact.forwardRef
, otherwise react-docgen-typescript does not understand it and the component's props will not be rendered in our documentation - 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.
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';
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';
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]
);
...
}
);