diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b399ac16db..5dd6529151c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@
- Added `left-start` popover placement to `EuiDatePicker` ([#3511](https://github.com/elastic/eui/pull/3511))
- Added `theme` prop to `EuiHeader` ([#3524](https://github.com/elastic/eui/pull/3524))
- Added `.euiHeaderLink-isActive` class to `EuiHeaderLink` when `isActive` ([#3524](https://github.com/elastic/eui/pull/3524))
+- Added `display`, `descriptionWidth`, `textWrap` and `isInvalid` props to `EuiExpression` ([#3467](https://github.com/elastic/eui/pull/3467))
**Bug Fixes**
diff --git a/src-docs/src/views/expression/columns.js b/src-docs/src/views/expression/columns.js
new file mode 100644
index 00000000000..ce8521d310a
--- /dev/null
+++ b/src-docs/src/views/expression/columns.js
@@ -0,0 +1,202 @@
+import React, { useState, Fragment } from 'react';
+
+import {
+ EuiPopoverTitle,
+ EuiPopover,
+ EuiSelect,
+ EuiComboBox,
+ EuiExpression,
+ EuiTitle,
+ EuiSpacer,
+} from '../../../../src/components';
+
+export default () => {
+ const [example1, setExample1] = useState({
+ isOpen: false,
+ value: (
+
+ .kibana_task_manager,
+ kibana_sample_data_ecommerce
+
+ ),
+ });
+
+ const [example2, setExample2] = useState({
+ isOpen: false,
+ value: 'count()',
+ });
+
+ const options = [
+ {
+ label: '.kibana_task_manager',
+ },
+ {
+ label: 'kibana_sample_data_ecommerce',
+ },
+ {
+ label: '.kibana-event-log-8.0.0-000001',
+ },
+ {
+ label: 'kibana_sample_data_flights',
+ },
+ {
+ label: '.kibana-event-log-8.0.0',
+ },
+ ];
+
+ const [selectedOptions, setSelected] = useState([options[0], options[1]]);
+
+ const openExample1 = () => {
+ setExample1({
+ ...example1,
+ isOpen: !example1.isOpen,
+ });
+ };
+
+ const closeExample1 = () => {
+ setExample1({
+ ...example1,
+ isOpen: false,
+ });
+ };
+
+ const openExample2 = () => {
+ setExample2({
+ ...example2,
+ isOpen: !example2.isOpen,
+ });
+ };
+
+ const closeExample2 = () => {
+ setExample2({
+ ...example2,
+ isOpen: false,
+ });
+ };
+
+ const changeExample2 = e => {
+ setExample2({
+ value: e.target.value,
+ isOpen: false,
+ });
+ };
+
+ const onChange = selectedOptions => {
+ setSelected(selectedOptions);
+ const indices = selectedOptions.map((s, index) => {
+ return (
+
+ {s.label}
+ {index < selectedOptions.length - 1 ? ',' : null}
+
+ );
+ });
+ setExample1({
+ ...example1,
+ value: indices,
+ });
+ };
+
+ const renderPopover1 = () => (
+
+ INDICES
+
+
+ );
+
+ const renderPopover2 = () => (
+
+ WHEN
+
+
+ );
+
+ return (
+
+ 0 ? false : true
+ }
+ isActive={example1.isOpen}
+ onClick={openExample1}
+ />
+ }
+ isOpen={example1.isOpen}
+ closePopover={closeExample1}
+ ownFocus
+ display="block"
+ panelPaddingSize="s"
+ anchorPosition="downLeft">
+ {renderPopover1()}
+
+
+
+ }
+ isOpen={example2.isOpen}
+ closePopover={closeExample2}
+ ownFocus
+ display="block"
+ anchorPosition="downLeft">
+ {renderPopover2()}
+
+
+
+
+ Description width at 50px
+
+ {}}
+ />
+
+ );
+};
diff --git a/src-docs/src/views/expression/expression_example.js b/src-docs/src/views/expression/expression_example.js
index a869f1b637c..2692e707834 100644
--- a/src-docs/src/views/expression/expression_example.js
+++ b/src-docs/src/views/expression/expression_example.js
@@ -12,7 +12,7 @@ import Expression from './expression';
const expressionSource = require('!!raw-loader!./expression');
const expressionHtml = renderToHtml(Expression);
const expressionSnippet = ``;
@@ -32,17 +32,44 @@ const stringingSource = require('!!raw-loader!./stringing');
const stringingHtml = renderToHtml(Stringing);
const stringingSnippet = `
`;
+import Columns from './columns';
+const columnsSource = require('!!raw-loader!./columns');
+const columnsHtml = renderToHtml(Columns);
+const columnsSnippet = ``;
+
+import Invalid from './invalid';
+const invalidSource = require('!!raw-loader!./invalid');
+const invalidHtml = renderToHtml(Invalid);
+const invalidSnippet = ``;
+
+import Truncate from './truncate';
+const truncateSource = require('!!raw-loader!./truncate');
+const truncateHtml = renderToHtml(Truncate);
+const truncateSnippet = ``;
+
export const ExpressionExample = {
title: 'Expression',
sections: [
@@ -114,5 +141,89 @@ export const ExpressionExample = {
snippet: stringingSnippet,
demo: ,
},
+ {
+ title: 'Column display',
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: columnsSource,
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: columnsHtml,
+ },
+ ],
+ text: (
+
+
+ There might be cases where displaying multiple{' '}
+ EuiExpressions in a paragraph is not ideal. For
+ example, when both the description and the{' '}
+ value are variable or when their text is quite
+ long. To use a column display instead, pass{' '}
+ {'display="columns"'}.
+
+
+ In column display, each expression is its own line and the{' '}
+ description column is aligned to the right. The
+ default width for the description is 20%, but you
+ can customize this with the
+ descriptionWidth prop. When displaying a group of{' '}
+ EuiExpressions, make sure to set the same width for
+ all descriptions.
+
+
+ ),
+ snippet: columnsSnippet,
+ demo: ,
+ },
+ {
+ title: 'Invalid state',
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: invalidSource,
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: invalidHtml,
+ },
+ ],
+ text: (
+
+ Set isInvalid to true to display{' '}
+ EuiExpression's error state. This state will
+ override the color prop with danger.
+
+ ),
+ snippet: invalidSnippet,
+ demo: ,
+ },
+ {
+ title: 'Truncate text',
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: truncateSource,
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: truncateHtml,
+ },
+ ],
+ text: (
+
+ To truncate EuiExpression's content, pass{' '}
+ {'textWrap="truncate"'}. Text
+ truncation only works properly if the prop types of{' '}
+ description and value are
+ strings. If you're using nodes, use the{' '}
+ .eui-textTruncate utility class on all their
+ sub-children.
+
+ ),
+ snippet: truncateSnippet,
+ demo: ,
+ },
],
};
diff --git a/src-docs/src/views/expression/invalid.js b/src-docs/src/views/expression/invalid.js
new file mode 100644
index 00000000000..bf2e09785cd
--- /dev/null
+++ b/src-docs/src/views/expression/invalid.js
@@ -0,0 +1,24 @@
+import React from 'react';
+
+import { EuiExpression, EuiSpacer } from '../../../../src/components';
+
+export default () => (
+
+
{}}
+ description="sort by"
+ value="count"
+ isInvalid
+ />
+
+
+ {}}
+ />
+
+
+);
diff --git a/src-docs/src/views/expression/stringing.tsx b/src-docs/src/views/expression/stringing.tsx
index f8cefa571bf..71acd396635 100644
--- a/src-docs/src/views/expression/stringing.tsx
+++ b/src-docs/src/views/expression/stringing.tsx
@@ -24,6 +24,8 @@ export default () => (
description="group by"
value="right.kytccountynmbr"
onClick={() => {}}
+ color="accent"
/>
+
);
diff --git a/src-docs/src/views/expression/truncate.js b/src-docs/src/views/expression/truncate.js
new file mode 100644
index 00000000000..7f46a8fcf82
--- /dev/null
+++ b/src-docs/src/views/expression/truncate.js
@@ -0,0 +1,48 @@
+import React, { Fragment } from 'react';
+
+import { EuiExpression, EuiSpacer, EuiTitle } from '../../../../src/components';
+
+const value = 'and a very long string as value';
+const description = 'some very very long description';
+const nodes = (
+
+ .kibana_task_manager
+ kibana_sample_data_ecommerce
+
+);
+
+export default () => (
+
+
+ {}}
+ description={description}
+ value={value}
+ textWrap="truncate"
+ />
+
+ {}}
+ />
+
+
+
+ eui-textTruncate applied to sub-children
+
+
+ {}}
+ />
+
+
+);
diff --git a/src/components/expression/__snapshots__/expression.test.tsx.snap b/src/components/expression/__snapshots__/expression.test.tsx.snap
index 9f91b365e54..9d97367d008 100644
--- a/src/components/expression/__snapshots__/expression.test.tsx.snap
+++ b/src/components/expression/__snapshots__/expression.test.tsx.snap
@@ -120,6 +120,49 @@ exports[`EuiExpression props color warning is rendered 1`] = `
`;
+exports[`EuiExpression props descriptionWidth changes the description's width when using columns 1`] = `
+
+
+ the answer is
+
+
+
+ 42
+
+
+
+`;
+
+exports[`EuiExpression props display can be columns 1`] = `
+
+
+ the answer is
+
+
+
+ 42
+
+
+`;
+
exports[`EuiExpression props isActive false renders inactive 1`] = `
`;
-exports[`EuiExpression props uppercase false renders inherted case 1`] = `
+exports[`EuiExpression props isInvalid renders error state 1`] = `
+
+
+ the answer is
+
+
+
+ 42
+
+
+
+`;
+
+exports[`EuiExpression props textWrap can truncate text 1`] = `
+
+
+ the answer is
+
+
+
+ 42
+
+
+`;
+
+exports[`EuiExpression props uppercase false renders inherited case 1`] = `
diff --git a/src/components/expression/_expression.scss b/src/components/expression/_expression.scss
index 3797018cc0b..b97a51c05b0 100644
--- a/src/components/expression/_expression.scss
+++ b/src/components/expression/_expression.scss
@@ -7,12 +7,12 @@
@include euiFontSizeS;
@include euiCodeFont;
+ border-bottom: $euiBorderWidthThick solid transparent;
display: inline-block; /* 1 */
text-align: left;
padding: ($euiSizeXS / 2) 0;
- transition: all $euiAnimSpeedNormal $euiAnimSlightBounce;
+ transition: all $euiAnimSpeedNormal ease-in-out;
color: $euiTextColor;
- border-bottom: 2px solid transparent;
&:focus {
border-bottom-style: solid;
@@ -21,6 +21,25 @@
& + .euiExpression {
margin-left: $euiSizeS;
}
+
+ &.euiExpression--columns {
+ border-color: transparent;
+ // Ensures there's no flash of the dashed style before turning solid for the active state
+ border-bottom-style: solid;
+ margin-bottom: $euiSizeXS;
+ }
+
+ &.euiExpression--truncate {
+ max-width: 100%;
+
+ .euiExpression__description,
+ .euiExpression__value {
+ @include euiTextTruncate;
+ display: inline-block;
+ vertical-align: bottom;
+ }
+
+ }
}
.euiExpression-isUppercase .euiExpression__description {
@@ -37,11 +56,48 @@
}
}
+.euiExpression__icon {
+ margin-left: $euiSizeXS;
+}
+
.euiExpression-isActive {
border-bottom-style: solid;
}
+.euiExpression--columns {
+ width: 100%;
+ display: flex;
+ padding: $euiSizeXS;
+ border-radius: $euiSizeXS;
+ &.euiExpression-isClickable {
+ background-color: $euiColorLightestShade;
+
+ // sass-lint:disable-block nesting-depth
+ &:focus,
+ &:hover:not(:disabled) {
+ .euiExpression__description,
+ .euiExpression__value {
+ // inner child specificity so it inherits underline color from text color
+ text-decoration: underline;
+ }
+ }
+ }
+
+ .euiExpression__description {
+ text-align: right;
+ margin-right: $euiSizeS;
+ flex-shrink: 0; // Ensures it doesn't get smaller in case the value is really long
+ }
+
+ .euiExpression__value {
+ flex-grow: 1;
+ }
+
+ .euiExpression__icon {
+ margin-top: $euiSizeXS;
+ }
+}
@each $name, $color in $euiExpressionColors {
.euiExpression--#{$name} {
@@ -51,6 +107,7 @@
&.euiExpression-isActive {
border-bottom-color: $color;
+ border-color: $color;
}
.euiExpression__description {
diff --git a/src/components/expression/expression.test.tsx b/src/components/expression/expression.test.tsx
index f334919d9d4..3a80cb64d80 100644
--- a/src/components/expression/expression.test.tsx
+++ b/src/components/expression/expression.test.tsx
@@ -70,7 +70,7 @@ describe('EuiExpression', () => {
expect(render(component)).toMatchSnapshot();
});
- test('false renders inherted case', () => {
+ test('false renders inherited case', () => {
const component = (
{
});
});
+ describe('display', () => {
+ test('can be columns', () => {
+ const component = (
+
+ );
+
+ expect(render(component)).toMatchSnapshot();
+ });
+ });
+
+ describe('isInvalid', () => {
+ test('renders error state', () => {
+ const component = (
+
+ );
+
+ expect(render(component)).toMatchSnapshot();
+ });
+ });
+
+ describe('descriptionWidth', () => {
+ test('changes the description's width when using columns', () => {
+ const component = (
+
+ );
+
+ expect(render(component)).toMatchSnapshot();
+ });
+ });
+
+ describe('textWrap', () => {
+ test('can truncate text', () => {
+ const component = (
+
+ );
+
+ expect(render(component)).toMatchSnapshot();
+ });
+ });
+
describe('isActive', () => {
test('true renders active', () => {
const component = (
diff --git a/src/components/expression/expression.tsx b/src/components/expression/expression.tsx
index 7de53d0b346..b0d15d056bc 100644
--- a/src/components/expression/expression.tsx
+++ b/src/components/expression/expression.tsx
@@ -25,6 +25,7 @@ import React, {
} from 'react';
import classNames from 'classnames';
import { CommonProps, keysOf, ExclusiveUnion } from '../common';
+import { EuiIcon } from '../icon';
const colorToClassNameMap = {
subdued: 'euiExpression--subdued',
@@ -35,10 +36,20 @@ const colorToClassNameMap = {
danger: 'euiExpression--danger',
};
+const textWrapToClassNameMap = {
+ 'break-word': null,
+ truncate: 'euiExpression--truncate',
+};
+
export const COLORS = keysOf(colorToClassNameMap);
export type ExpressionColor = keyof typeof colorToClassNameMap;
+const displayToClassNameMap = {
+ inline: null,
+ columns: 'euiExpression--columns',
+};
+
export type EuiExpressionProps = CommonProps & {
/**
* First part of the expression
@@ -66,6 +77,25 @@ export type EuiExpressionProps = CommonProps & {
* Turns the component into a button and adds an editable style border at the bottom
*/
onClick?: MouseEventHandler;
+ /**
+ * Sets the display style for the expression. Defaults to `inline`
+ */
+ display?: keyof typeof displayToClassNameMap;
+ /**
+ * Forces color to display as `danger` and shows an `alert` icon
+ */
+ isInvalid?: boolean;
+ /**
+ * Sets a custom width for the description when using the columns layout.
+ * Set to a number for a custom width in `px`.
+ * Set to a string for a custom width in custom measurement.
+ * Defaults to `20%`
+ */
+ descriptionWidth?: number | string;
+ /**
+ * Sets how to handle the wrapping of long text.
+ */
+ textWrap?: keyof typeof textWrapToClassNameMap;
};
type Buttonlike = EuiExpressionProps &
@@ -86,9 +116,15 @@ export const EuiExpression: React.FunctionComponent<
color = 'secondary',
uppercase = true,
isActive = false,
+ display = 'inline',
+ descriptionWidth = '20%',
onClick,
+ isInvalid = false,
+ textWrap = 'break-word',
...rest
}) => {
+ const calculatedColor = isInvalid ? 'danger' : color;
+
const classes = classNames(
'euiExpression',
className,
@@ -97,19 +133,44 @@ export const EuiExpression: React.FunctionComponent<
'euiExpression-isClickable': onClick,
'euiExpression-isUppercase': uppercase,
},
- colorToClassNameMap[color]
+ displayToClassNameMap[display],
+ colorToClassNameMap[calculatedColor],
+ textWrapToClassNameMap[textWrap]
);
const Component = onClick ? 'button' : 'span';
+ const descriptionStyle = descriptionProps && descriptionProps.style;
+ const customWidth =
+ display === 'columns' && descriptionWidth
+ ? {
+ flexBasis: descriptionWidth,
+ ...descriptionStyle,
+ }
+ : undefined;
+
+ const invalidIcon = isInvalid ? (
+
+ ) : (
+ undefined
+ );
+
return (
-
+
{description}
{' '}
{value}
+ {invalidIcon}
);
};