- You can make interactive elements keyboard-accessible with this
- component. This is necessary for non-button elements and{' '}
- a tags without
+ You can make interactive elements keyboard-accessible with the{' '}
+ EuiKeyboardAccessible component. This is necessary
+ for non-button elements and a tags without{' '}
href attributes.
- This class can be useful to add accessibility to older designs that
- are still in use, but it shouldn’t be a permanent solution.
- See{' '}
- {
-
- http://webaim.org/techniques/css/invisiblecontent/
-
- }{' '}
- for more information.
-
-
- Use a screenreader to verify that there is a second paragraph in
- this example:
+ Use the EuiScreenReaderOnly component to visually
+ hide elements while still allowing them to be read by screen
+ readers. In certain cases, you may want to use the{' '}
+ showOnFocus prop to display screen reader-only
+ content when in focus.
+
+
+ "In most cases, if content (particularly content that
+ provides functionality or interactivity) is important enough to
+ provide to screen reader users, it should probably be made
+ available to all users."{' '}
+
+ Learn more about invisible content
+
+
);
diff --git a/src-docs/src/views/accessibility/props.tsx b/src-docs/src/views/accessibility/props.tsx
new file mode 100644
index 000000000000..29e44b11cc84
--- /dev/null
+++ b/src-docs/src/views/accessibility/props.tsx
@@ -0,0 +1,6 @@
+import React, { FunctionComponent } from 'react';
+import { EuiScreenReaderOnlyProps } from '../../../../src/components/accessibility/screen_reader';
+
+export const ScreenReaderOnlyDocsComponent: FunctionComponent<
+ EuiScreenReaderOnlyProps
+> = () => ;
diff --git a/src-docs/src/views/accessibility/screen_reader.tsx b/src-docs/src/views/accessibility/screen_reader.tsx
index c5d74bc5b316..dc43ab1e0c11 100644
--- a/src-docs/src/views/accessibility/screen_reader.tsx
+++ b/src-docs/src/views/accessibility/screen_reader.tsx
@@ -1,16 +1,51 @@
import React from 'react';
import { EuiScreenReaderOnly } from '../../../../src/components/accessibility/screen_reader';
+import { EuiCallOut } from '../../../../src/components/call_out';
+import { EuiText } from '../../../../src/components/text';
+import { EuiTitle } from '../../../../src/components/title';
+import { EuiLink } from '../../../../src/components/link';
export default () => (
-
This is the first paragraph. It is visible to all.
-
+
+
+
Visually hide content
+
- This is the second paragraph. It is hidden for sighted users but visible
- to screen readers.
+
+ Use a screenreader to verify that there is a second paragraph in this
+ example:
+
-
-
This is the third paragraph. It is visible to all.
+
This is the first paragraph. It is visible to all.
+
+
+ This is the second paragraph. It is hidden for sighted users but
+ visible to screen readers.
+
+
+
This is the third paragraph. It is visible to all.
+
+
Show on focus
+
+
+
+ Tab through this section with your keyboard to display a ‘Skip
+ navigation’ link:
+
+
+
+ This link is visible to all on focus:{' '}
+
+ Skip navigation
+
+
This button has the heaviest visual weight to draw users'
attention.
@@ -67,7 +67,7 @@ export default () => (
-
Standard buttons are for secondary actions
+
Standard buttons are for secondary actions
Such actions include Add and Apply. This button type works well for
multiple actions of equal weight.
@@ -85,7 +85,7 @@ export default () => (
-
Empty buttons are for complementary, UI-specific actions
+
Empty buttons are for complementary, UI-specific actions
Close, cancel, filter, refresh, and other actions that reconfigure
the UI are appropriate for empty buttons.
@@ -111,7 +111,7 @@ export default () => (
-
Icon buttons are for saving space
+
Icon buttons are for saving space
The icon must be immediately understood, for example, a trash can
for delete. Use these buttons sparingly, and never for the primary
diff --git a/src-docs/src/views/guidelines/modals.js b/src-docs/src/views/guidelines/modals.js
index d15fe10ab64c..cfefd39f44f2 100644
--- a/src-docs/src/views/guidelines/modals.js
+++ b/src-docs/src/views/guidelines/modals.js
@@ -70,17 +70,17 @@ export default () => (
-
The header sets the context
+
The header sets the context
Short and sentence-case, the header should indicate what the modal
is about.
-
The body is for a single task
+
The body is for a single task
This task should not require a lot of explanation or user
interaction.
-
Buttons are right-aligned
+
Buttons are right-aligned
The primary action is a filled button, and the secondary action is a
link button. Labels should use strong action verbs.
diff --git a/src-docs/src/views/guidelines/sass.js b/src-docs/src/views/guidelines/sass.js
index 304f7af2f383..b4c9f4c560e8 100644
--- a/src-docs/src/views/guidelines/sass.js
+++ b/src-docs/src/views/guidelines/sass.js
@@ -388,7 +388,7 @@ export const SassGuidelines = ({ selectedTheme }) => {
diff --git a/src-docs/src/views/guidelines/toasts.js b/src-docs/src/views/guidelines/toasts.js
index becacc3b7518..9beaed046531 100644
--- a/src-docs/src/views/guidelines/toasts.js
+++ b/src-docs/src/views/guidelines/toasts.js
@@ -109,7 +109,7 @@ and space to read it properly. Alternatively just link to a full page.
-
Success toasts indicate that everything worked out
+
Success toasts indicate that everything worked out
They are the most-commonly used toasts.
@@ -128,9 +128,9 @@ and space to read it properly. Alternatively just link to a full page.
-
+
Warning toasts direct user attention to a potential problem
-
+
These toasts work well in monitoring apps when something
significant requires action.
@@ -152,7 +152,7 @@ and space to read it properly. Alternatively just link to a full page.
-
Error toasts report a problem
+
Error toasts report a problem
An error toast might let users know an action didn't
complete or that a form has errors.
@@ -176,7 +176,7 @@ and space to read it properly. Alternatively just link to a full page.
-
Info toasts relay neutral information
+
Info toasts relay neutral information
The default toast, an info toast might notify users about an
ongoing action.
diff --git a/src-docs/src/views/guidelines/writing.js b/src-docs/src/views/guidelines/writing.js
index c04ab55df7c3..b6035036ce39 100644
--- a/src-docs/src/views/guidelines/writing.js
+++ b/src-docs/src/views/guidelines/writing.js
@@ -319,13 +319,16 @@ export default () => (
-
+
-
+
diff --git a/src-docs/src/views/header/header_example.js b/src-docs/src/views/header/header_example.js
index 32e8ad7eca89..72b503e9988f 100644
--- a/src-docs/src/views/header/header_example.js
+++ b/src-docs/src/views/header/header_example.js
@@ -1,4 +1,5 @@
import React from 'react';
+import { Link } from 'react-router';
import { renderToHtml } from '../../services';
@@ -17,10 +18,20 @@ import {
EuiHeaderLink,
} from '../../../../src/components';
+import { EuiHeaderSectionsProp } from './props';
+
import Header from './header';
const headerSource = require('!!raw-loader!./header');
const headerHtml = renderToHtml(Header);
+import HeaderSections from './header_sections';
+const headerSectionsSource = require('!!raw-loader!./header_sections');
+const headerSectionsHtml = renderToHtml(HeaderSections);
+
+import HeaderPosition from './header_position';
+const headerPositionSource = require('!!raw-loader!./header_position');
+const headerPositionHtml = renderToHtml(HeaderPosition);
+
import HeaderAlert from './header_alert';
const headerAlertSource = require('!!raw-loader!./header_alert');
const headerAlertHtml = renderToHtml(HeaderAlert);
@@ -45,10 +56,27 @@ const headerSnippet = ``;
+const headerSectionsSnippet = ``;
+
const headerLinksSnippet = `
@@ -78,7 +106,11 @@ export const HeaderExample = {
code: headerHtml,
},
],
- text:
The header is made up of several individual components.
,
+ text: (
+
+ The header is made up of many individual components.
+
+ Alternatively, you can pass an array objects to the{' '}
+ sections props that takes a key of{' '}
+ items (array of children to wrap in an{' '}
+ EuiHeaderSectionItem) and/or{' '}
+ breadcrumbs (array of{' '}
+ breadcrumb objects). Each
+ item in the array will be wrapped in an{' '}
+ EuiHeaderSection.
+
+
+ Note: Passing sections and{' '}
+ children will disregard the{' '}
+ children as it is not easily interpreted at what
+ location the children should be placed.
+
+ Most consumer need a header that does not scroll way with the page
+ contents. You can apply this display by changing{' '}
+ position to fixed. It will
+ also add the appropriate padding to the window body by applying a
+ class.
+
- The ListGroup component is used to present
- ListGroupItems in a neatly formatted list. Use the
- flush and bordered{' '}
- properties for full-width and bordered presentations, respectively.
-
+ <>
+
+ The EuiListGroup component is used to present{' '}
+ EuiListGroupItems in a neatly formatted list. Use
+ the flush and bordered{' '}
+ properties for full-width and bordered presentations, respectively.
+
+
+ Adjust the gutterSize prop to increase or
+ decrease the spacing between items.
+
- Present ListGroupItems as links by providing an
- href value and change their appearance with
- the size, isActive, and
- isDisabled properties. As done in this example, the
- ListGroup component can also accept an array
- of items via the listItems property.
-
+ <>
+
+ Display EuiListGroupItems as links by providing an{' '}
+ href value and change their state with the{' '}
+ isActive and isDisabled{' '}
+ properties.
+
+
+ As is done in this example, the EuiListGroup{' '}
+ component can also accept an array of items via the{' '}
+ listItems property.
+
The extraAction property adds a secondary icon
button to any list item. It accepts several properties of its own,
- including color, onClick,
+ including color, onClick,{' '}
iconType, and alwaysShow, and
can be used for actions such as pinning, favoriting, or deleting an
item.
+ EuiListGroupItems will inherit the color from their
+ element type whether it is a button,{' '}
+ anchor, or span. You can
+ enforce a different color of primary,{' '}
+ text, or subdued with the{' '}
+ color prop.
+
+
+ They also accept options for text size;{' '}
+ xs | s | m | l.
+
- A EuiSearchBar is a toolbar that enables the user
- to create/define a search query. This can be done either by entering
- the query syntax in a search box or by clicking any of the
+ An EuiSearchBar is a toolbar that enables the
+ user to create/define a search query. This can be done either by
+ entering the query syntax in a search box or by clicking any of the
configured filters. The query language is not meant to be full blown
search language for arbitrary data (e.g. as required in the Discover
App in Kibana), yet it does provide some useful features:
@@ -231,8 +231,8 @@ export const SearchBarExample = {
text: (
- A EuiSearchBar can have its query controlled by a
- parent component by passing the query prop.
+ An EuiSearchBar can have its query controlled by
+ a parent component by passing the query prop.
Changes to the query will be passed back up through the{' '}
onChange callback where the new query must be
stored in state and passed back into the search bar.
@@ -256,7 +256,7 @@ export const SearchBarExample = {
text: (
- A EuiSearchBar can have custom filter dropdowns
+ An EuiSearchBar can have custom filter dropdowns
that control how a user can search.
diff --git a/src-docs/src/views/search_bar/search_bar_filters.js b/src-docs/src/views/search_bar/search_bar_filters.js
index 5a5efe6eac18..1b74a9676721 100644
--- a/src-docs/src/views/search_bar/search_bar_filters.js
+++ b/src-docs/src/views/search_bar/search_bar_filters.js
@@ -122,6 +122,9 @@ export class SearchBarFilters extends Component {
const schema = {
strict: true,
fields: {
+ type: {
+ type: 'string',
+ },
active: {
type: 'boolean',
},
diff --git a/src-docs/src/views/suggest/_global_filter_group.scss b/src-docs/src/views/suggest/_global_filter_group.scss
index 82908a215aa0..3187c9551fbf 100644
--- a/src-docs/src/views/suggest/_global_filter_group.scss
+++ b/src-docs/src/views/suggest/_global_filter_group.scss
@@ -2,7 +2,7 @@
@import 'saved_queries';
.globalFilterGroup__filterBar {
- margin-top: $euiSizeM;
+ margin-top: $euiSizeXS;
}
// sass-lint:disable quotes
@@ -20,9 +20,9 @@
.globalFilterGroup__filterFlexItem {
overflow: hidden;
- padding-bottom: 2px; // Allow the shadows of the pills to show
+ padding: $euiSizeS;
}
.globalFilterBar__flexItem {
max-width: calc(100% - #{$euiSizeXS}); // Width minus margin around each flex itm
-}
\ No newline at end of file
+}
diff --git a/src-docs/src/views/suggest/global_filter_bar.js b/src-docs/src/views/suggest/global_filter_bar.js
index 5bd19d2e0771..aefa66cb3dd5 100644
--- a/src-docs/src/views/suggest/global_filter_bar.js
+++ b/src-docs/src/views/suggest/global_filter_bar.js
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import { EuiFlexGroup, EuiFlexItem } from '../../../../src/components';
+import { EuiBadgeGroup } from '../../../../src/components';
import GlobalFilterAdd from './global_filter_add';
import { GlobalFilterItem } from './global_filter_item';
@@ -12,45 +12,22 @@ export const GlobalFilterBar = ({ filters, className, ...rest }) => {
const pinnedFilters = filters
.filter(filter => filter.isPinned)
.map(filter => {
- return (
-
-
-
- );
+ return ;
});
const unpinnedFilters = filters
.filter(filter => !filter.isPinned)
.map(filter => {
- return (
-
-
-
- );
+ return ;
});
return (
-
+
{/* Show pinned filters first and in a specific group */}
{pinnedFilters}
{unpinnedFilters}
-
-
-
-
-
+
+
);
};
diff --git a/src-docs/src/views/suggest/global_filter_item.js b/src-docs/src/views/suggest/global_filter_item.js
index a575f10bccd9..87eb2ac55ed0 100644
--- a/src-docs/src/views/suggest/global_filter_item.js
+++ b/src-docs/src/views/suggest/global_filter_item.js
@@ -100,6 +100,7 @@ export class GlobalFilterItem extends Component {
title={title}
iconOnClick={this.deleteFilter}
iconOnClickAriaLabel={'Delete filter'}
+ color="hollow"
iconType="cross"
iconSide="right"
onClick={this.togglePopover}
diff --git a/src-docs/src/views/suggest/saved_queries.js b/src-docs/src/views/suggest/saved_queries.js
index 353ec1f925ce..8f7e04b70221 100644
--- a/src-docs/src/views/suggest/saved_queries.js
+++ b/src-docs/src/views/suggest/saved_queries.js
@@ -62,7 +62,8 @@ export default class extends Component {
},
{
id: 'filter1',
- field: '@tags.keyword',
+ field:
+ 'Filter with a very long title to test if the badge will properly get truncated in the separate set of filter badges that are not quite as long but man does it really need to be long',
operator: 'IS',
value: 'value',
isDisabled: true,
diff --git a/src-docs/src/views/tables/in_memory/in_memory_selection.js b/src-docs/src/views/tables/in_memory/in_memory_selection.js
index 44c6721872e4..630f07f03f49 100644
--- a/src-docs/src/views/tables/in_memory/in_memory_selection.js
+++ b/src-docs/src/views/tables/in_memory/in_memory_selection.js
@@ -97,7 +97,7 @@ export class Table extends Component {
renderToolsLeft() {
const selection = this.state.control_columns;
- if (selection.length === 0) {
+ if (!selection || selection.length === 0) {
return;
}
diff --git a/src-docs/webpack.config.js b/src-docs/webpack.config.js
index 644e79d63d25..4c907dc8ef79 100644
--- a/src-docs/webpack.config.js
+++ b/src-docs/webpack.config.js
@@ -7,9 +7,10 @@ const { NODE_ENV, CI } = process.env;
const isDevelopment = NODE_ENV !== 'production' && CI == null;
const isProduction = NODE_ENV === 'production';
+const bypassCache = NODE_ENV === 'puppeteer';
function useCache(loaders) {
- if (isDevelopment) {
+ if (isDevelopment && !bypassCache) {
return ['cache-loader'].concat(loaders);
}
diff --git a/src/components/accessibility/__snapshots__/screen_reader.test.tsx.snap b/src/components/accessibility/__snapshots__/screen_reader.test.tsx.snap
index e218d61cb91c..39b7d8e177a8 100644
--- a/src/components/accessibility/__snapshots__/screen_reader.test.tsx.snap
+++ b/src/components/accessibility/__snapshots__/screen_reader.test.tsx.snap
@@ -15,3 +15,12 @@ exports[`EuiScreenReaderOnly adds an accessibility class to a child element when
This paragraph is not visibile to sighted users but will be read by screenreaders.
`;
+
+exports[`EuiScreenReaderOnly will show on focus 1`] = `
+
+ Link
+
+`;
diff --git a/src/components/accessibility/__snapshots__/skip_link.test.tsx.snap b/src/components/accessibility/__snapshots__/skip_link.test.tsx.snap
new file mode 100644
index 000000000000..685870e4087a
--- /dev/null
+++ b/src/components/accessibility/__snapshots__/skip_link.test.tsx.snap
@@ -0,0 +1,68 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiSkipLink is rendered 1`] = `
+
+
+
+
+
+`;
+
+exports[`EuiSkipLink position absolute is rendered 1`] = `
+
+
+
+
+
+`;
+
+exports[`EuiSkipLink position fixed is rendered 1`] = `
+
+
+
+
+
+`;
+
+exports[`EuiSkipLink position static is rendered 1`] = `
+
+
+
+
+
+`;
diff --git a/src/components/accessibility/_index.scss b/src/components/accessibility/_index.scss
index 98294afdf558..b8b7ffe9a8f1 100644
--- a/src/components/accessibility/_index.scss
+++ b/src/components/accessibility/_index.scss
@@ -1 +1,2 @@
@import 'screen_reader';
+@import 'skip_link';
diff --git a/src/components/accessibility/_screen_reader.scss b/src/components/accessibility/_screen_reader.scss
index 398beafda858..6cc8a8e64071 100644
--- a/src/components/accessibility/_screen_reader.scss
+++ b/src/components/accessibility/_screen_reader.scss
@@ -1,3 +1,4 @@
-.euiScreenReaderOnly {
+.euiScreenReaderOnly,
+.euiScreenReaderOnly--showOnFocus:not(:focus) {
@include euiScreenReaderOnly;
}
diff --git a/src/components/accessibility/_skip_link.scss b/src/components/accessibility/_skip_link.scss
new file mode 100644
index 000000000000..38aa7fd814d6
--- /dev/null
+++ b/src/components/accessibility/_skip_link.scss
@@ -0,0 +1,20 @@
+.euiSkipLink {
+ transition: none !important; // sass-lint:disable-line no-important
+
+ &:focus {
+ animation: none !important; // sass-lint:disable-line no-important
+ }
+
+ // Set positions on focus only as to no override screenReaderOnly position
+ // When positioned absolutely, consumers still need to tell it WHERE (top,left,etc...)
+ &.euiSkipLink--absolute:focus {
+ position: absolute;
+ }
+
+ &.euiSkipLink--fixed:focus {
+ position: fixed;
+ top: $euiSizeXS;
+ left: $euiSizeXS;
+ z-index: $euiZHeader + 1;
+ }
+}
diff --git a/src/components/accessibility/index.ts b/src/components/accessibility/index.ts
index d63ef5aa8008..06e7f202092d 100644
--- a/src/components/accessibility/index.ts
+++ b/src/components/accessibility/index.ts
@@ -1,2 +1,3 @@
export { EuiKeyboardAccessible } from './keyboard_accessible';
export { EuiScreenReaderOnly } from './screen_reader';
+export { EuiSkipLink } from './skip_link';
diff --git a/src/components/accessibility/screen_reader.test.tsx b/src/components/accessibility/screen_reader.test.tsx
index 5b6c06e59afb..db021b454ac2 100644
--- a/src/components/accessibility/screen_reader.test.tsx
+++ b/src/components/accessibility/screen_reader.test.tsx
@@ -30,4 +30,14 @@ describe('EuiScreenReaderOnly', () => {
expect($paragraph).toMatchSnapshot();
});
});
+
+ test('will show on focus', () => {
+ const component = render(
+
+ Link
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
});
diff --git a/src/components/accessibility/screen_reader.tsx b/src/components/accessibility/screen_reader.tsx
index 437d8d5a5a0e..55485b541e19 100644
--- a/src/components/accessibility/screen_reader.tsx
+++ b/src/components/accessibility/screen_reader.tsx
@@ -3,12 +3,23 @@ import classNames from 'classnames';
export interface EuiScreenReaderOnlyProps {
children: ReactElement;
+
+ /**
+ * For keyboard navigation, force content to display visually upon focus.
+ */
+ showOnFocus?: boolean;
}
export const EuiScreenReaderOnly: FunctionComponent<
EuiScreenReaderOnlyProps
-> = ({ children }) => {
- const classes = classNames('euiScreenReaderOnly', children.props.className);
+> = ({ children, showOnFocus }) => {
+ const classes = classNames(
+ {
+ euiScreenReaderOnly: !showOnFocus,
+ 'euiScreenReaderOnly--showOnFocus': showOnFocus,
+ },
+ children.props.className
+ );
const props = {
...children.props,
diff --git a/src/components/accessibility/skip_link.test.tsx b/src/components/accessibility/skip_link.test.tsx
new file mode 100644
index 000000000000..e7a0589d50c3
--- /dev/null
+++ b/src/components/accessibility/skip_link.test.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { render } from 'enzyme';
+import { requiredProps } from '../../test';
+
+import { EuiSkipLink, POSITIONS } from './skip_link';
+
+describe('EuiSkipLink', () => {
+ test('is rendered', () => {
+ const component = render(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ describe('position', () => {
+ POSITIONS.forEach(position => {
+ test(`${position} is rendered`, () => {
+ const component = render(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+ });
+ });
+});
diff --git a/src/components/accessibility/skip_link.tsx b/src/components/accessibility/skip_link.tsx
new file mode 100644
index 000000000000..a87e2880b9c7
--- /dev/null
+++ b/src/components/accessibility/skip_link.tsx
@@ -0,0 +1,68 @@
+import React, { FunctionComponent, Ref } from 'react';
+import classNames from 'classnames';
+import { EuiButton, EuiButtonProps } from '../button/button';
+import { EuiScreenReaderOnly } from '../accessibility/screen_reader';
+import { PropsForAnchor, PropsForButton, ExclusiveUnion } from '../common';
+
+type Positions = 'static' | 'fixed' | 'absolute';
+export const POSITIONS = ['static', 'fixed', 'absolute'] as Positions[];
+
+export interface EuiSkipLinkProps extends EuiButtonProps {
+ /**
+ * If true, the link will be fixed to the top left of the viewport
+ */
+ position?: Positions;
+
+ /**
+ * Typically an anchor id (e.g. `a11yMainContent`), the value provided
+ * will be prepended with a hash `#` and used as the link `href`
+ */
+ destinationId: string;
+
+ tabIndex?: number;
+}
+
+type propsForAnchor = PropsForAnchor<
+ EuiSkipLinkProps,
+ {
+ buttonRef?: Ref;
+ }
+>;
+
+type propsForButton = PropsForButton<
+ EuiSkipLinkProps,
+ {
+ buttonRef?: Ref;
+ }
+>;
+
+export type Props = ExclusiveUnion;
+
+export const EuiSkipLink: FunctionComponent = ({
+ destinationId,
+ tabIndex,
+ position = 'static',
+ children,
+ className,
+ ...rest
+}) => {
+ const classes = classNames(
+ 'euiSkipLink',
+ [`euiSkipLink--${position}`],
+ className
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+};
diff --git a/src/components/badge/_index.scss b/src/components/badge/_index.scss
index a567f7a42de2..a271275b2dc8 100644
--- a/src/components/badge/_index.scss
+++ b/src/components/badge/_index.scss
@@ -1,3 +1,4 @@
@import 'badge';
+@import 'badge_group/index';
@import 'beta_badge/index';
@import 'notification_badge/index';
diff --git a/src/components/badge/badge_group/__snapshots__/badge_group.test.tsx.snap b/src/components/badge/badge_group/__snapshots__/badge_group.test.tsx.snap
new file mode 100644
index 000000000000..57ba7cbeb02e
--- /dev/null
+++ b/src/components/badge/badge_group/__snapshots__/badge_group.test.tsx.snap
@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiBadgeGroup gutterSize none is rendered 1`] = `
+
+`;
+
+exports[`EuiBadgeGroup gutterSize s is rendered 1`] = `
+
+`;
+
+exports[`EuiBadgeGroup gutterSize xs is rendered 1`] = `
+
+`;
+
+exports[`EuiBadgeGroup is rendered 1`] = `
+
+
+
+
+
+ Content
+
+
+
+
+
+`;
diff --git a/src/components/badge/badge_group/_badge_group.scss b/src/components/badge/badge_group/_badge_group.scss
new file mode 100644
index 000000000000..a1a0b0ec5d3c
--- /dev/null
+++ b/src/components/badge/badge_group/_badge_group.scss
@@ -0,0 +1,23 @@
+$euiBadgeGroupGutterTypes: (
+ gutterExtraSmall: $euiSizeXS,
+ gutterSmall: $euiSizeS,
+);
+
+.euiBadgeGroup__item {
+ display: inline-block;
+ max-width: 100%;
+}
+
+// Gutter Sizes
+@each $gutterName, $gutterSize in $euiBadgeGroupGutterTypes {
+ $halfGutterSize: $gutterSize * .5;
+
+ .euiBadgeGroup--#{$gutterName} {
+ margin: -$halfGutterSize;
+
+ & > .euiBadgeGroup__item {
+ margin: $halfGutterSize;
+ max-width: calc(100% - #{$gutterSize});
+ }
+ }
+}
diff --git a/src/components/badge/badge_group/_index.scss b/src/components/badge/badge_group/_index.scss
new file mode 100644
index 000000000000..21500c4a8cf7
--- /dev/null
+++ b/src/components/badge/badge_group/_index.scss
@@ -0,0 +1 @@
+@import './badge_group';
diff --git a/src/components/badge/badge_group/badge_group.test.tsx b/src/components/badge/badge_group/badge_group.test.tsx
new file mode 100644
index 000000000000..895f94e08b96
--- /dev/null
+++ b/src/components/badge/badge_group/badge_group.test.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import { render } from 'enzyme';
+import { requiredProps } from '../../../test/required_props';
+
+import { EuiBadge } from '../badge';
+import { EuiBadgeGroup, GUTTER_SIZES } from './badge_group';
+
+describe('EuiBadgeGroup', () => {
+ test('is rendered', () => {
+ const component = render(
+
+ Content
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ describe('gutterSize', () => {
+ GUTTER_SIZES.forEach(size => {
+ it(`${size} is rendered`, () => {
+ const component = render();
+
+ expect(component).toMatchSnapshot();
+ });
+ });
+ });
+});
diff --git a/src/components/badge/badge_group/badge_group.tsx b/src/components/badge/badge_group/badge_group.tsx
new file mode 100644
index 000000000000..ce3b97565c08
--- /dev/null
+++ b/src/components/badge/badge_group/badge_group.tsx
@@ -0,0 +1,48 @@
+import React, { HTMLAttributes, Ref, ReactNode } from 'react';
+import classNames from 'classnames';
+import { CommonProps, keysOf } from '../../common';
+
+const gutterSizeToClassNameMap = {
+ none: null,
+ xs: 'euiBadgeGroup--gutterExtraSmall',
+ s: 'euiBadgeGroup--gutterSmall',
+};
+
+export const GUTTER_SIZES = keysOf(gutterSizeToClassNameMap);
+type BadgeGroupGutterSize = keyof typeof gutterSizeToClassNameMap;
+
+export interface EuiBadgeGroupProps {
+ /**
+ * Space between badges
+ */
+ gutterSize?: BadgeGroupGutterSize;
+ /**
+ * Should be a list of EuiBadge's but can also be any other element
+ * Will apply an extra class to add spacing
+ */
+ children?: ReactNode;
+}
+
+export const EuiBadgeGroup = React.forwardRef<
+ HTMLDivElement,
+ CommonProps & HTMLAttributes & EuiBadgeGroupProps
+>(
+ (
+ { children, className, gutterSize = 'xs', ...rest },
+ ref: Ref
+ ) => {
+ const classes = classNames(
+ 'euiBadgeGroup',
+ gutterSizeToClassNameMap[gutterSize as BadgeGroupGutterSize],
+ className
+ );
+
+ return (
+
+`;
diff --git a/src/components/list_group/__snapshots__/list_group_item.test.tsx.snap b/src/components/list_group/__snapshots__/list_group_item.test.tsx.snap
index cc4c07759322..e110e1cebbee 100644
--- a/src/components/list_group/__snapshots__/list_group_item.test.tsx.snap
+++ b/src/components/list_group/__snapshots__/list_group_item.test.tsx.snap
@@ -17,6 +17,74 @@ exports[`EuiListGroupItem is rendered 1`] = `
`;
+exports[`EuiListGroupItem props color inherit is rendered 1`] = `
+
+
+
+ Label
+
+
+
+`;
+
+exports[`EuiListGroupItem props color primary is rendered 1`] = `
+
+
+
+ Label
+
+
+
+`;
+
+exports[`EuiListGroupItem props color subdued is rendered 1`] = `
+
+
+
+ Label
+
+
+
+`;
+
+exports[`EuiListGroupItem props color text is rendered 1`] = `
+
+
+
+ Label
+
+
+
+`;
+
exports[`EuiListGroupItem props extraAction is rendered 1`] = `
`;
-exports[`EuiListGroupItem throws an warning if both iconType and icon are provided but still renders 1`] = `
+exports[`EuiListGroupItem throws a warning if both iconType and icon are provided but still renders 1`] = `