# Component Development For information on how to design components, see the [component design docs][component-design]. Before working with OUI components or creating new ones, you may want to run a local server for the [documentation site][docs]. 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. ```shell yarn yarn start ``` Once the server boots up, you can visit it on your browser at: [http://localhost:8030/](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. - [Yeoman component creation guide][docs-yeoman] - [Manual component creation guide][docs-manual] ## 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](https://facebook.github.io/jest/docs/en/cli.html) 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](testing.md) for guidelines on writing and designing your tests. Refer to the [automated accessibility testing guide](automated-accessibility-testing.md) 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: ```bash 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: ```bash 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 (`` and ``), tightly-coupled components (`` and ``), or you just want to group some related components together (``, ``, and ``), 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][sass] of our documentation site for a guide to writing styles. [component-design]: component-design.md [docs]: https://oui.opensearch.org/ [docs-yeoman]: creating-components-yeoman.md [docs-manual]: creating-components-manually.md [sass]: https://oui.opensearch.org/#/guidelines/sass ## 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 ```ts // passes extra props to a button interface FooProps extends ButtonHTMLAttributes { 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` interface, and you should use `HTMLAttributes`. ```ts // passes extra props to a div interface FooProps extends HTMLAttributes { title: string } ``` If your component forwards a `ref` through to an underlying element, the interface needs to be further extended with `DetailedHTMLProps` ```ts // passes extra props and forwards the ref to a button interface FooProps extends DetailedHTMLProps, 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](https://www.npmjs.com/package/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 ```ts 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 (
...
); } ); 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: ```ts 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(null); const combinedRefs = useCombinedRefs([ref, localRef]); return (
...
); } ); 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 ```ts 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] ); ... } ); ```