For information on how to design components, see the component design docs.
Before working with EUI 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 test-unit
runs the Jest unit tests once.
yarn test-unit button
will run tests with "button" in the spec name.
You can pass other Jest CLI arguments. For example:
yarn test-unit -u
will update your snapshots.
Note: if you are experiencing failed builds in Jenkins related to snapshots, then try clearing the cache first yarn test-unit --clearCache
.
yarn test-unit --watch
watches for changes and runs the tests as you code.
yarn 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 Cypress testing guide for guidelines on when and how to write Cypress tests.
Refer to the automated accessibility testing guide for more info on those.
Note that yarn link
currently does not work with Kibana. You'll need to manually pack and insert it into Kibana to test locally.
yarn build-pack
This will create a .tgz
file with the changes in your EUI directory. At this point you can move it anywhere.
Point the package.json
file in Kibana to that file: "@elastic/eui": "/path/to/elastic-eui-xx.x.x.tgz"
. Then run the following commands at Kibana's root folder:
yarn kbn bootstrap --no-validate && yarn start
- The
--no-validate
flag is required when bootstrapping with a.tgz
.- Change the name of the
.tgz
after subsequentyarn build && yarn pack
steps (e.g.,elastic-eui-xx.x.x-1.tgz
,elastic-eui-xx.x.x-2.tgz
). This is required foryarn
to recognize new changes to the package.
- Change the name of the
- Running Kibana with
yarn start
ensures it starts in dev mode and doesn't use a previously cached version of EUI.
If a component has subcomponents (<EuiToolBar>
and <EuiToolBarSearch>
), tightly-coupled components (<EuiButton>
and <EuiButtonGroup>
), or you just want to group some related components together (<EuiTextInput>
, <EuiTextArea>
, and <EuiCheckBox>
), 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.
We use react-docgen-typescript combined with some custom props filters to automatically generate our Props tab/table from our Typescript component types.
⚠️ react-docgen-typescript currently has a bug that does not correctly generate props for all components if a file has multiple components that set adisplayName
. To avoid this bug and broken props tables, keep your component files atomic / limited to 1 major component per file.
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 table will error in our documentation - The resulting component must have a
displayName
, which 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 EUI 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, EuiMarkdownEditor's ref includes both its textarea element and the replaceNode
method to interact with the abstract syntax tree. https://github.com/elastic/eui/blob/v31.10.0/src/components/markdown_editor/markdown_editor.tsx#L331
import React, { useImperativeHandle } from 'react';
export const EuiMarkdownEditor = forwardRef<
EuiMarkdownEditorRef,
EuiMarkdownEditorProps
>(
(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]
);
...
}
);