diff --git a/CHANGELOG.md b/CHANGELOG.md index 2111dfacce9..ad6479a8f99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## [`master`](https://github.com/elastic/eui/tree/master) +- Added `EuiSuggestItem` component ([#2090](https://github.com/elastic/eui/pull/2090)) - Added support for negated or clauses to `EuiSearchBar` ([#2140](https://github.com/elastic/eui/pull/2140)) **Bug fixes** diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index a9fc78a5964..f3c8c27dfbc 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -160,6 +160,8 @@ import { StatExample } from './views/stat/stat_example'; import { StepsExample } from './views/steps/steps_example'; +// import { SuggestExample } from './views/suggest/suggest_example'; + import { TableExample } from './views/tables/tables_example'; import { TabsExample } from './views/tabs/tabs_example'; @@ -337,6 +339,7 @@ const navigation = [ RangeControlExample, SearchBarExample, SelectableExample, + // SuggestExample, ].map(example => createExample(example)), }, { diff --git a/src-docs/src/views/suggest/suggest.js b/src-docs/src/views/suggest/suggest.js new file mode 100644 index 00000000000..3c4c903427b --- /dev/null +++ b/src-docs/src/views/suggest/suggest.js @@ -0,0 +1,3 @@ +import React from 'react'; + +export default () =>
; diff --git a/src-docs/src/views/suggest/suggest_example.js b/src-docs/src/views/suggest/suggest_example.js new file mode 100644 index 00000000000..b884f78846b --- /dev/null +++ b/src-docs/src/views/suggest/suggest_example.js @@ -0,0 +1,81 @@ +import React from 'react'; + +import { renderToHtml } from '../../services'; + +import { GuideSectionTypes } from '../../components'; + +import { EuiCode, EuiSuggestItem } from '../../../../src/components'; + +import Suggest from './suggest'; +const suggestSource = require('!!raw-loader!./suggest'); +const suggestHtml = renderToHtml(Suggest); + +import SuggestItem from './suggest_item'; +const suggestItemSource = require('!!raw-loader!./suggest_item'); +const suggestItemHtml = renderToHtml(SuggestItem); +const suggestItemSnippet = [ + ` +`, + ``, +]; + +export const SuggestExample = { + title: 'Suggest', + sections: [ + { + source: [ + { + type: GuideSectionTypes.JS, + code: suggestSource, + }, + { + type: GuideSectionTypes.HTML, + code: suggestHtml, + }, + ], + text: ( +
+

+ EuiSuggest description goes here. +

+
+ ), + }, + { + title: 'Suggest Item', + source: [ + { + type: GuideSectionTypes.JS, + code: suggestItemSource, + }, + { + type: GuideSectionTypes.HTML, + code: suggestItemHtml, + }, + ], + text: ( +
+

+ EuiSuggestItem is a list item component to + display suggestions when typing queries in{' '} + EuiSuggestInput. Use{' '} + labelDisplay to set whether the{' '} + label has a fixed width or not. +

+
+ ), + props: { EuiSuggestItem }, + snippet: suggestItemSnippet, + demo: , + }, + ], +}; diff --git a/src-docs/src/views/suggest/suggest_item.js b/src-docs/src/views/suggest/suggest_item.js new file mode 100644 index 00000000000..b3c90b18753 --- /dev/null +++ b/src-docs/src/views/suggest/suggest_item.js @@ -0,0 +1,71 @@ +import React from 'react'; + +import { EuiSuggestItem, EuiSpacer } from '../../../../src/components'; + +const shortDescription = 'This is the description'; + +const sampleItems = [ + { + type: { iconType: 'kqlField', color: 'tint4' }, + label: 'Field sample', + description: shortDescription, + }, + { + type: { iconType: 'kqlValue', color: 'tint0' }, + label: 'Value sample', + description: shortDescription, + }, + { + type: { iconType: 'kqlSelector', color: 'tint3' }, + label: 'Conjunction sample', + description: shortDescription, + }, + { + type: { iconType: 'kqlOperand', color: 'tint1' }, + label: 'Operator sample', + description: shortDescription, + }, + { + type: { iconType: 'search', color: 'tint8' }, + label: 'Recent search', + }, + { + type: { iconType: 'save', color: 'tint5' }, + label: 'Saved query', + }, +]; + +const typeObj = { iconType: 'kqlValue', color: 'tint0' }; + +const longLabel = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ut quam eget augue pulvinar.'; + +export default () => ( +
+ {sampleItems.map((item, index) => ( + + ))} + + + + + +
+); diff --git a/src/components/index.js b/src/components/index.js index 9218bd62cdc..32923f19c17 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -234,6 +234,8 @@ export { EuiStat } from './stat'; export { EuiStep, EuiSteps, EuiSubSteps, EuiStepsHorizontal } from './steps'; +export { EuiSuggestItem } from './suggest_item'; + export { EuiTable, EuiTableBody, diff --git a/src/components/index.scss b/src/components/index.scss index a46031166fa..c39303f5798 100644 --- a/src/components/index.scss +++ b/src/components/index.scss @@ -51,6 +51,7 @@ @import 'selectable/index'; @import 'stat/index'; @import 'steps/index'; +@import 'suggest_item/index'; @import 'table/index'; @import 'tabs/index'; @import 'title/index'; diff --git a/src/components/suggest_item/__snapshots__/suggest_item.test.js.snap b/src/components/suggest_item/__snapshots__/suggest_item.test.js.snap new file mode 100644 index 00000000000..a3068051a5f --- /dev/null +++ b/src/components/suggest_item/__snapshots__/suggest_item.test.js.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiSuggestItem is rendered 1`] = ` +
+ + + + + Test label + + +
+`; + +exports[`props item with no description has expanded label is rendered 1`] = ` +
+ + + + + This is the description + + +
+`; + +exports[`props labelDisplay as expand is rendered 1`] = ` +
+ + + + + This is the description + + + This is the description + +
+`; diff --git a/src/components/suggest_item/_index.scss b/src/components/suggest_item/_index.scss new file mode 100644 index 00000000000..c4dd928bb4e --- /dev/null +++ b/src/components/suggest_item/_index.scss @@ -0,0 +1,3 @@ +@import 'variables'; + +@import 'suggest_item'; diff --git a/src/components/suggest_item/_suggest_item.scss b/src/components/suggest_item/_suggest_item.scss new file mode 100644 index 00000000000..9f063b25889 --- /dev/null +++ b/src/components/suggest_item/_suggest_item.scss @@ -0,0 +1,68 @@ +.euiSuggestItem { + display: flex; + flex-grow: 1; + align-items: center; + font-size: $euiFontSizeXS; + white-space: nowrap; + + @each $name, $color in $itemColors { + .euiSuggestItem__type--#{$name} { + $backgroundColor: tintOrShade($color, 90%, 50%); + background-color: $backgroundColor; + color: makeHighContrastColor($color, $backgroundColor); + } + } + + .euiSuggestItem__label, + .euiSuggestItem__type, + .euiSuggestItem__description { + flex-grow: 0; + display: flex; + flex-direction: column; + } + + .euiSuggestItem__type { + position: relative; + flex-shrink: 0; + flex-basis: auto; + width: $euiSizeXL; + height: $euiSizeXL; + text-align: center; + overflow: hidden; + padding: $euiSizeXS; + justify-content: center; + align-items: center; + } + + .euiSuggestItem__label { + flex-basis: 50%; + min-width: 50%; + font-family: $euiCodeFontFamily; + overflow: hidden; + text-overflow: ellipsis; + padding: $euiSizeXS $euiSizeS; + color: $euiTextColor; + + &.euiSuggestItem__labelDisplay--expand { + flex-basis: auto; + flex-shrink: 1; + } + } + + .euiSuggestItem__description, + .euiSuggestItem__label { + @include euiTextTruncate; + display: block; + } + + .euiSuggestItem__description { + color: $euiColorDarkShade; + flex-basis: auto; + padding-top: $euiSizeXS * .5; + + &:empty { + flex-grow: 0; + margin-left: 0; + } + } +} diff --git a/src/components/suggest_item/_variables.scss b/src/components/suggest_item/_variables.scss new file mode 100644 index 00000000000..d90fa21260d --- /dev/null +++ b/src/components/suggest_item/_variables.scss @@ -0,0 +1,11 @@ +$itemColors: ( + tint0: $euiColorVis0, + tint1: $euiColorVis1, + tint2: $euiColorVis2, + tint3: $euiColorVis3, + tint4: $euiColorVis5, + tint5: $euiColorVis7, + tint6: $euiColorVis8, + tint7: $euiColorVis9, + tint8: $euiColorDarkShade, +); diff --git a/src/components/suggest_item/index.js b/src/components/suggest_item/index.js new file mode 100644 index 00000000000..fde82176579 --- /dev/null +++ b/src/components/suggest_item/index.js @@ -0,0 +1 @@ +export { EuiSuggestItem } from './suggest_item'; diff --git a/src/components/suggest_item/suggest_item.js b/src/components/suggest_item/suggest_item.js new file mode 100644 index 00000000000..0d396023414 --- /dev/null +++ b/src/components/suggest_item/suggest_item.js @@ -0,0 +1,91 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { EuiIcon, IconPropType } from '../icon'; + +const colorToClassNameMap = { + tint0: 'euiSuggestItem__type--tint0', + tint1: 'euiSuggestItem__type--tint1', + tint2: 'euiSuggestItem__type--tint2', + tint3: 'euiSuggestItem__type--tint3', + tint4: 'euiSuggestItem__type--tint4', + tint5: 'euiSuggestItem__type--tint5', + tint6: 'euiSuggestItem__type--tint6', + tint7: 'euiSuggestItem__type--tint7', + tint8: 'euiSuggestItem__type--tint8', + tint9: 'euiSuggestItem__type--tint9', +}; + +export const COLORS = Object.keys(colorToClassNameMap); + +const labelDisplayToClassMap = { + fixed: 'euiSuggestItem__labelDisplay--fixed', + expand: 'euiSuggestItem__labelDisplay--expand', +}; + +export const DISPLAYS = Object.keys(labelDisplayToClassMap); + +export const EuiSuggestItem = ({ + className, + label, + type, + labelDisplay, + description, + ...rest +}) => { + const classes = classNames('euiSuggestItem', className); + + let colorClass = ''; + + const labelDisplayClass = classNames( + 'euiSuggestItem__label', + labelDisplayToClassMap[labelDisplay], + { + 'euiSuggestItem__labelDisplay--expand': !description, + } + ); + + if (type && type.color) { + if (COLORS.indexOf(type.color) > -1) { + colorClass = colorToClassNameMap[type.color]; + } + } + + return ( +
+ + + + {label} + {description} +
+ ); +}; + +EuiSuggestItem.propTypes = { + className: PropTypes.string, + /** + * Takes 'iconType' for EuiIcon and 'color'. 'color' can be tint1 through tint9. + */ + type: PropTypes.shape({ + iconType: IconPropType.isRequired, + color: PropTypes.oneOfType([PropTypes.oneOf(COLORS), PropTypes.string]) + .isRequired, + }).isRequired, + /** + * Label or primary text. + */ + label: PropTypes.string.isRequired, + /** + * Description or secondary text (optional). + */ + description: PropTypes.string, + /** + * Label display is 'fixed' by default. Label will increase its width beyond 50% if needed with 'expand'. + */ + labelDisplay: PropTypes.oneOf(DISPLAYS), +}; + +EuiSuggestItem.defaultProps = { + labelDisplay: 'fixed', +}; diff --git a/src/components/suggest_item/suggest_item.test.js b/src/components/suggest_item/suggest_item.test.js new file mode 100644 index 00000000000..e6fe2664991 --- /dev/null +++ b/src/components/suggest_item/suggest_item.test.js @@ -0,0 +1,51 @@ +import React from 'react'; +import { render } from 'enzyme'; +import { requiredProps } from '../../test/required_props'; + +import { EuiSuggestItem } from './suggest_item'; + +const TYPE = { + iconType: 'search', + color: 'tint1', +}; + +describe('EuiSuggestItem', () => { + test('is rendered', () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); +}); + +describe('props', () => { + const sampleItem = { + type: { iconType: 'kqlValue', color: 'tint2' }, + label: 'Charles de Gaulle International Airport', + description: 'This is the description', + }; + + describe('labelDisplay as expand', () => { + test('is rendered', () => { + const component = render( + + ); + expect(component).toMatchSnapshot(); + }); + }); + + describe('item with no description has expanded label', () => { + test('is rendered', () => { + const component = render( + + ); + expect(component).toMatchSnapshot(); + }); + }); +});