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 startOnce 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 Kibana. You'll need to manually pack and insert it into Kibana to test locally.
yarn build && npm packThis 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 && cd packages/kbn-ui-shared-deps/ && yarn kbn:bootstrap && cd ../../ && FORCE_DLL_CREATION=true node scripts/kibana --dev- The
--no-validateflag is required when bootstrapping with a.tgz.- Change the name of the
.tgzafter subsequentyarn buildandnpm packsteps (e.g.,elastic-eui-xx.x.x-1.tgz,elastic-eui-xx.x.x-2.tgz). This is required foryarnto recognize new changes to the package.
- Change the name of the
- Running
yarn kbn:bootstrapinside ofkibana/packages/kbn-ui-shared-deps/rebuilds Kibana's shared-ui-deps. - Running Kibana with
FORCE_DLL_CREATION=true node scripts/kibana --devensures it 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.
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
forwardRefinstead 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 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]
);
...
}
);