diff --git a/CHANGELOG.md b/CHANGELOG.md index afc59bc8836..59656fcb6bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## [`master`](https://github.com/elastic/eui/tree/master) +- Created `EuiSuggest` component ([#2270](https://github.com/elastic/eui/pull/2270)) - Added missing `compressed` styling to `EuiSwitch` ([#2327](https://github.com/elastic/eui/pull/2327)) ## [`14.0.0`](https://github.com/elastic/eui/tree/v14.0.0) diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index 4c176e213dc..3b0f5e54e9b 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -162,7 +162,7 @@ import { StatExample } from './views/stat/stat_example'; import { StepsExample } from './views/steps/steps_example'; -// import { SuggestExample } from './views/suggest/suggest_example'; +import { SuggestExample } from './views/suggest/suggest_example'; import { TableExample } from './views/tables/tables_example'; @@ -352,7 +352,7 @@ const navigation = [ RangeControlExample, SearchBarExample, SelectableExample, - // SuggestExample, + SuggestExample, ].map(example => createExample(example)), }, { diff --git a/src-docs/src/theme_dark.scss b/src-docs/src/theme_dark.scss index a096191a91e..a85342807af 100644 --- a/src-docs/src/theme_dark.scss +++ b/src-docs/src/theme_dark.scss @@ -4,8 +4,4 @@ @import '../../src/theme_dark'; @import './components/guide_components'; -@import './views/header/global_filter_group'; - -// Elastic charts -@import '~@elastic/charts/dist/theme'; -@import '../../src/themes/charts/theme'; +@import './views/suggest/global_filter_group'; diff --git a/src-docs/src/theme_light.scss b/src-docs/src/theme_light.scss index 6cde81a190a..775564c155d 100644 --- a/src-docs/src/theme_light.scss +++ b/src-docs/src/theme_light.scss @@ -4,9 +4,4 @@ @import '../../src/theme_light'; @import './components/guide_components'; -@import './views/header/global_filter_group'; - -// Elastic charts -@import '~@elastic/charts/dist/theme'; -@import '../../src/themes/charts/theme'; - +@import './views/suggest/global_filter_group'; diff --git a/src-docs/src/views/header/_global_filter_form.scss b/src-docs/src/views/header/_global_filter_form.scss deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src-docs/src/views/header/global_query.js b/src-docs/src/views/header/global_query.js deleted file mode 100644 index f8afdb43645..00000000000 --- a/src-docs/src/views/header/global_query.js +++ /dev/null @@ -1,167 +0,0 @@ -import React, { Component } from 'react'; -import classNames from 'classnames'; -import ResizeObserver from 'resize-observer-polyfill'; -import { - EuiFilterButton, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, -} from '../../../../src/components'; - -import { GlobalFilterBar } from './global_filter_bar'; -import GlobalFilterOptions from './global_filter_options'; - -export default class extends Component { - constructor(props) { - super(props); - - this.state = { - isFiltersVisible: true, - filters: [ - { - id: 'filter0', - field: '@tags.keyword', - operator: 'IS', - value: - 'This documents a visual pattern for the eventual replacement of Kibanas global query and filter bars. The filter bar has been broken down into multiple components. There are still bugs and not all the logic is well-formed.', - isDisabled: false, - isPinned: true, - isExcluded: false, - }, - { - id: 'filter1', - field: '@tags.keyword', - operator: 'IS', - value: 'value', - isDisabled: true, - isPinned: false, - isExcluded: false, - }, - { - id: 'filter2', - field: '@tags.keyword', - operator: 'IS NOT', - value: 'value', - isDisabled: false, - isPinned: true, - isExcluded: true, - }, - { - id: 'filter3', - field: '@tags.keyword', - operator: 'IS', - value: 'value', - isDisabled: false, - isPinned: false, - isExcluded: false, - }, - ], - query: '', - }; - - this.ro = new ResizeObserver(this.setFilterBarHeight); - } - - setFilterBarHeight = () => { - requestAnimationFrame(() => { - const height = - this.filterBar && this.state.isFiltersVisible - ? this.filterBar.clientHeight + 4 - : 0; - this.filterBarWrapper && - this.filterBarWrapper.setAttribute('style', `height: ${height}px`); - }); - }; - - componentDidMount() { - this.setFilterBarHeight(); - this.ro.observe(this.filterBar); - } - - componentDidUpdate() { - this.setFilterBarHeight(); - this.ro.unobserve(this.filterBar); - } - - toggleFilterVisibility = () => { - this.setState(prevState => ({ - isFiltersVisible: !prevState.isFiltersVisible, - })); - }; - - onQueryChange = e => { - this.setState({ - query: e.target.value, - }); - }; - - setFilterBarRef = node => { - this.filterBar = node; - }; - - render() { - const filterButtonTitle = `${ - this.state.filters.length - } filters applied. Select to ${ - this.state.isFiltersVisible ? 'hide' : 'show' - }.`; - - const filterTriggerButton = ( - 0 ? this.state.filters.length : null - } - aria-controls="GlobalFilterGroup" - aria-expanded={!!this.state.isFiltersVisible} - title={filterButtonTitle}> - Filters - - ); - - const classes = classNames('globalFilterGroup__wrapper', { - 'globalFilterGroup__wrapper-isVisible': this.state.isFiltersVisible, - }); - - return ( - - - -
{ - this.filterBarWrapper = node; - }} - className={classes}> -
- - - - - - - - - -
-
-
- ); - } -} diff --git a/src-docs/src/views/header/header_example.js b/src-docs/src/views/header/header_example.js index 44fe7730b3b..565aed13fc8 100644 --- a/src-docs/src/views/header/header_example.js +++ b/src-docs/src/views/header/header_example.js @@ -14,15 +14,8 @@ import { EuiCode, EuiHeaderLinks, EuiHeaderLink, - EuiCallOut, } from '../../../../src/components'; -import { GlobalFilterBar } from './global_filter_bar'; -import GlobalFilterAdd from './global_filter_add'; -import GlobalFilterOptions from './global_filter_options'; -import GlobalFilterForm from './global_filter_form'; -import { GlobalFilterItem } from './global_filter_item'; - import Header from './header'; const headerSource = require('!!raw-loader!./header'); const headerHtml = renderToHtml(Header); @@ -31,10 +24,6 @@ import HeaderLinks from './header_links'; const headerLinksSource = require('!!raw-loader!./header_links'); const headerLinksHtml = renderToHtml(HeaderLinks); -import GlobalQuery from './global_query'; -const globalQuerySource = require('!!raw-loader!./global_query'); -const globalQueryHtml = renderToHtml(GlobalQuery); - const headerSnippet = ` @@ -122,39 +111,5 @@ export const HeaderExample = { snippet: headerLinksSnippet, demo: , }, - { - title: 'Global query and filters', - source: [ - { - type: GuideSectionTypes.JS, - code: globalQuerySource, - }, - { - type: GuideSectionTypes.HTML, - code: globalQueryHtml, - }, - ], - text: ( -
- -

- This documents a visual pattern for the eventual - replacement of Kibana's global query and filter bars. The - filter bar has been broken down into multiple components. There - are still bugs and not all the logic is well-formed. -

-
-
- ), - props: { - GlobalQuery, - GlobalFilterBar, - GlobalFilterOptions, - GlobalFilterAdd, - GlobalFilterForm, - GlobalFilterItem, - }, - demo: , - }, ], }; diff --git a/src-docs/src/views/header/_global_filter_group.scss b/src-docs/src/views/suggest/_global_filter_group.scss similarity index 96% rename from src-docs/src/views/header/_global_filter_group.scss rename to src-docs/src/views/suggest/_global_filter_group.scss index 0af50dadf3a..82908a215aa 100644 --- a/src-docs/src/views/header/_global_filter_group.scss +++ b/src-docs/src/views/suggest/_global_filter_group.scss @@ -1,5 +1,5 @@ @import 'global_filter_item'; -@import 'global_filter_form'; +@import 'saved_queries'; .globalFilterGroup__filterBar { margin-top: $euiSizeM; @@ -25,4 +25,4 @@ .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/header/_global_filter_item.scss b/src-docs/src/views/suggest/_global_filter_item.scss similarity index 100% rename from src-docs/src/views/header/_global_filter_item.scss rename to src-docs/src/views/suggest/_global_filter_item.scss diff --git a/src-docs/src/views/suggest/_saved_queries.scss b/src-docs/src/views/suggest/_saved_queries.scss new file mode 100644 index 00000000000..a858e74bf36 --- /dev/null +++ b/src-docs/src/views/suggest/_saved_queries.scss @@ -0,0 +1,31 @@ +.savedQueriesInput__hideDatepicker { + .euiSuperDatePicker__flexWrapper { + width: 100%; + + > div:nth-of-type(1) { + display: none; + } + } +} + +.savedQueriesInput { + padding-bottom: $euiSizeXL * 6; +} + +.savedQueryManagement__text { + padding: $euiSizeM $euiSizeM ($euiSizeM / 2); +} + +.savedQueryManagement__listWrapper { + // Addition height will ensure one item is "cutoff" to indicate more below the scroll + max-height: $euiFormMaxWidth + $euiSize; + overflow-y: hidden; +} + +.savedQueryManagement__list { + @include euiYScrollWithShadows; + max-height: inherit; // Fixes overflow for applied max-height + // Left/Right padding is calculated to match the left alignment of the + // popover text and buttons + padding: ($euiSizeM / 2) $euiSizeXS !important; // sass-lint:disable-line no-important +} \ No newline at end of file diff --git a/src-docs/src/views/header/global_filter_add.js b/src-docs/src/views/suggest/global_filter_add.js similarity index 93% rename from src-docs/src/views/header/global_filter_add.js rename to src-docs/src/views/suggest/global_filter_add.js index 7159959b019..26239b98d50 100644 --- a/src-docs/src/views/header/global_filter_add.js +++ b/src-docs/src/views/suggest/global_filter_add.js @@ -13,13 +13,9 @@ import GlobalFilterForm from './global_filter_form'; export default class GlobalFilterAdd extends Component { static propTypes = {}; - constructor(props) { - super(props); - - this.state = { - isPopoverOpen: false, - }; - } + state = { + isPopoverOpen: false, + }; togglePopover = () => { this.setState(prevState => ({ diff --git a/src-docs/src/views/header/global_filter_bar.js b/src-docs/src/views/suggest/global_filter_bar.js similarity index 100% rename from src-docs/src/views/header/global_filter_bar.js rename to src-docs/src/views/suggest/global_filter_bar.js diff --git a/src-docs/src/views/header/global_filter_form.js b/src-docs/src/views/suggest/global_filter_form.js similarity index 92% rename from src-docs/src/views/header/global_filter_form.js rename to src-docs/src/views/suggest/global_filter_form.js index 648ae81cd15..7afebabcf94 100644 --- a/src-docs/src/views/header/global_filter_form.js +++ b/src-docs/src/views/suggest/global_filter_form.js @@ -75,26 +75,22 @@ export default class GlobalFilterForm extends Component { selectedObject: PropTypes.object, }; - constructor(props) { - super(props); - - this.state = { - fieldOptions: fieldOptions, - operandOptions: operatorOptions, - valueOptions: valueOptions, - selectedField: this.props.selectedObject - ? this.props.selectedObject.field - : [], - selectedOperand: this.props.selectedObject - ? this.props.selectedObject.operand - : [], - selectedValues: this.props.selectedObject - ? this.props.selectedObject.values - : [], - useCustomLabel: false, - customLabel: '', - }; - } + state = { + fieldOptions: fieldOptions, + operandOptions: operatorOptions, + valueOptions: valueOptions, + selectedField: this.props.selectedObject + ? this.props.selectedObject.field + : [], + selectedOperand: this.props.selectedObject + ? this.props.selectedObject.operand + : [], + selectedValues: this.props.selectedObject + ? this.props.selectedObject.values + : [], + useCustomLabel: false, + customLabel: '', + }; onFieldChange = selectedOptions => { // We should only get back either 0 or 1 options. diff --git a/src-docs/src/views/header/global_filter_item.js b/src-docs/src/views/suggest/global_filter_item.js similarity index 97% rename from src-docs/src/views/header/global_filter_item.js rename to src-docs/src/views/suggest/global_filter_item.js index 941960a72ff..a575f10bccd 100644 --- a/src-docs/src/views/header/global_filter_item.js +++ b/src-docs/src/views/suggest/global_filter_item.js @@ -36,13 +36,9 @@ export class GlobalFilterItem extends Component { isExcluded: PropTypes.bool.isRequired, }; - constructor(props) { - super(props); - - this.state = { - isPopoverOpen: false, - }; - } + state = { + isPopoverOpen: false, + }; togglePopover = () => { this.setState(prevState => ({ diff --git a/src-docs/src/views/header/global_filter_options.js b/src-docs/src/views/suggest/global_filter_options.js similarity index 95% rename from src-docs/src/views/header/global_filter_options.js rename to src-docs/src/views/suggest/global_filter_options.js index 00d5162708e..ee0e7a276af 100644 --- a/src-docs/src/views/header/global_filter_options.js +++ b/src-docs/src/views/suggest/global_filter_options.js @@ -25,13 +25,9 @@ function flattenPanelTree(tree, array = []) { export default class GlobalFilterOptions extends Component { static propTypes = {}; - constructor(props) { - super(props); - - this.state = { - isPopoverOpen: false, - }; - } + state = { + isPopoverOpen: false, + }; togglePopover = () => { this.setState(prevState => ({ @@ -109,7 +105,7 @@ export default class GlobalFilterOptions extends Component { diff --git a/src-docs/src/views/suggest/hashtag_popover.js b/src-docs/src/views/suggest/hashtag_popover.js new file mode 100644 index 00000000000..58cdce3f85c --- /dev/null +++ b/src-docs/src/views/suggest/hashtag_popover.js @@ -0,0 +1,106 @@ +import React, { Component } from 'react'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiListGroup, + EuiListGroupItem, + EuiPopover, + EuiPopoverFooter, + EuiPopoverTitle, + EuiText, +} from '../../../../src/components'; + +export default class HashtagPopover extends Component { + static propTypes = {}; + + state = { + isPopoverOpen: false, + }; + + togglePopover = () => { + this.setState(prevState => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); + }; + + closePopover = () => { + this.setState({ isPopoverOpen: false }); + }; + + onButtonClick = () => { + this.setState({ + isPopoverOpen: !this.state.isPopoverOpen, + }); + }; + + render() { + const { isPopoverOpen } = this.state; + + const hashtagButton = ( + + + + ); + + return ( + + SAVED QUERIES +
+ +

Save query text and filters that you want to use again.

+
+
+ + + + +
+ {this.props.value !== '' ? ( + + + + Save + + + + ) : ( + undefined + )} +
+
+ ); + } +} diff --git a/src-docs/src/views/suggest/saved_queries.js b/src-docs/src/views/suggest/saved_queries.js new file mode 100644 index 00000000000..353ec1f925c --- /dev/null +++ b/src-docs/src/views/suggest/saved_queries.js @@ -0,0 +1,163 @@ +import React, { Component } from 'react'; + +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiSuggest, + EuiSuperDatePicker, +} from '../../../../src/components'; + +import { GlobalFilterBar } from './global_filter_bar'; +import GlobalFilterOptions from './global_filter_options'; +import HashtagPopover from './hashtag_popover'; + +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: 'tint2' }, + 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: 'tint3' }, + label: 'Saved search', + }, +]; + +export default class extends Component { + state = { + status: 'unchanged', + value: '', + hideDatepicker: false, + filters: [ + { + id: 'filter0', + field: '@tags.keyword', + operator: 'IS', + value: 'value', + isDisabled: false, + isPinned: true, + isExcluded: false, + }, + { + id: 'filter1', + field: '@tags.keyword', + operator: 'IS', + value: 'value', + isDisabled: true, + isPinned: false, + isExcluded: false, + }, + { + id: 'filter2', + field: '@tags.keyword', + operator: 'IS NOT', + value: 'value', + isDisabled: false, + isPinned: true, + isExcluded: true, + }, + { + id: 'filter3', + field: '@tags.keyword', + operator: 'IS', + value: 'value', + isDisabled: false, + isPinned: false, + isExcluded: false, + }, + ], + }; + + onFieldFocus = () => { + this.setState({ + hideDatepicker: true, + }); + }; + + onFieldBlur = () => { + this.setState({ + hideDatepicker: false, + }); + }; + + getInputValue = val => { + this.setState({ + value: val, + }); + }; + + onItemClick = item => { + alert(`Item [${item.label}] was clicked`); + }; + + onTimeChange() { + alert('Time changed'); + } + + render() { + const append = KQL; + + return ( +
+ + + } + append={append} + suggestions={sampleItems} + onItemClick={this.onItemClick} + onInputChange={this.getInputValue} + /> + + + + + + + + + + + + + +
+ ); + } +} diff --git a/src-docs/src/views/suggest/suggest.js b/src-docs/src/views/suggest/suggest.js index 3c4c903427b..b2a0dc3d12e 100644 --- a/src-docs/src/views/suggest/suggest.js +++ b/src-docs/src/views/suggest/suggest.js @@ -1,3 +1,101 @@ -import React from 'react'; +import React, { Component } from 'react'; -export default () =>
; +import { + EuiRadioGroup, + EuiSuggest, + EuiSpacer, +} from '../../../../src/components'; + +import makeId from '../../../../src/components/form/form_row/make_id'; + +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: 'tint2' }, + 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: 'tint3' }, + label: 'Saved search', + }, +]; + +export default class extends Component { + constructor(props) { + super(props); + + const idPrefix = makeId(); + + this.radios = [ + { id: `${idPrefix}0`, value: 'unchanged', label: 'No new changes' }, + { id: `${idPrefix}1`, value: 'unsaved', label: 'Not yet saved' }, + { id: `${idPrefix}2`, value: 'saved', label: 'Saved' }, + { id: `${idPrefix}3`, value: 'loading', label: 'Loading' }, + ]; + + this.state = { + status: 'unchanged', + radioIdSelected: `${idPrefix}0`, + value: '', + tooltipContent: '', + }; + } + + onChange = optionId => { + this.setState({ + radioIdSelected: optionId, + status: this.radios.find(x => x.id === optionId).value, + }); + }; + + onItemClick(item) { + alert(`Item [${item.label}] was clicked`); + } + + getInputValue = val => { + this.setState({ + value: val, + }); + }; + + render() { + return ( +
+ + + +
+ ); + } +} diff --git a/src-docs/src/views/suggest/suggest_example.js b/src-docs/src/views/suggest/suggest_example.js index b884f78846b..3b755e465e2 100644 --- a/src-docs/src/views/suggest/suggest_example.js +++ b/src-docs/src/views/suggest/suggest_example.js @@ -4,12 +4,22 @@ import { renderToHtml } from '../../services'; import { GuideSectionTypes } from '../../components'; -import { EuiCode, EuiSuggestItem } from '../../../../src/components'; +import { + EuiCallOut, + EuiCode, + EuiSpacer, + EuiSuggest, + EuiSuggestItem, +} from '../../../../src/components'; import Suggest from './suggest'; const suggestSource = require('!!raw-loader!./suggest'); const suggestHtml = renderToHtml(Suggest); +import SavedQueries from './saved_queries'; +const savedQueriesSource = require('!!raw-loader!./saved_queries'); +const savedQueriesHtml = renderToHtml(SavedQueries); + import SuggestItem from './suggest_item'; const suggestItemSource = require('!!raw-loader!./suggest_item'); const suggestItemHtml = renderToHtml(SuggestItem); @@ -28,6 +38,27 @@ const suggestItemSnippet = [ />`, ]; +const suggestSnippet = [ + ``, +]; + export const SuggestExample = { title: 'Suggest', sections: [ @@ -45,10 +76,17 @@ export const SuggestExample = { text: (

- EuiSuggest description goes here. + EuiSuggest is a text field component used to + display suggestions. The status of the component is shown on its + right side. The available status are:{' '} + unsaved, saved, + unchanged and isLoading.

), + props: { EuiSuggest }, + snippet: suggestSnippet, + demo: , }, { title: 'Suggest Item', @@ -67,9 +105,9 @@ export const SuggestExample = {

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. + EuiSuggest. Use labelDisplay{' '} + to set whether the label has a fixed width or + not.

), @@ -77,5 +115,33 @@ export const SuggestExample = { snippet: suggestItemSnippet, demo: , }, + { + title: 'Saved queries and filters', + source: [ + { + type: GuideSectionTypes.JS, + code: savedQueriesSource, + }, + { + type: GuideSectionTypes.HTML, + code: savedQueriesHtml, + }, + ], + text: ( +
+ +

+ This documents a visual pattern for Kibana's + global query and filter bars. The filter bar has been broken down + into multiple components. There are still bugs and not all the + logic is well-formed. +

+
+ +
+ ), + props: { EuiSuggest }, + demo: , + }, ], }; diff --git a/src/components/index.js b/src/components/index.js index 85a5dd2ac8f..5d93ff1ab84 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -236,7 +236,7 @@ export { EuiStat } from './stat'; export { EuiStep, EuiSteps, EuiSubSteps, EuiStepsHorizontal } from './steps'; -export { EuiSuggestItem } from './suggest_item'; +export { EuiSuggestInput, EuiSuggestItem, EuiSuggest } from './suggest'; export { EuiTable, diff --git a/src/components/index.scss b/src/components/index.scss index c39303f5798..82fba518747 100644 --- a/src/components/index.scss +++ b/src/components/index.scss @@ -51,7 +51,7 @@ @import 'selectable/index'; @import 'stat/index'; @import 'steps/index'; -@import 'suggest_item/index'; +@import 'suggest/index'; @import 'table/index'; @import 'tabs/index'; @import 'title/index'; diff --git a/src/components/suggest/__snapshots__/suggest.test.js.snap b/src/components/suggest/__snapshots__/suggest.test.js.snap new file mode 100644 index 00000000000..f98a783343a --- /dev/null +++ b/src/components/suggest/__snapshots__/suggest.test.js.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiSuggest is rendered 1`] = ` +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+`; diff --git a/src/components/suggest/__snapshots__/suggest_input.test.js.snap b/src/components/suggest/__snapshots__/suggest_input.test.js.snap new file mode 100644 index 00000000000..a3330c560fe --- /dev/null +++ b/src/components/suggest/__snapshots__/suggest_input.test.js.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EuiSuggestInput is rendered 1`] = ` +
+
+
+
+
+
+ +
+ + + +
+
+
+
+
+`; diff --git a/src/components/suggest_item/__snapshots__/suggest_item.test.js.snap b/src/components/suggest/__snapshots__/suggest_item.test.js.snap similarity index 100% rename from src/components/suggest_item/__snapshots__/suggest_item.test.js.snap rename to src/components/suggest/__snapshots__/suggest_item.test.js.snap diff --git a/src/components/suggest_item/_index.scss b/src/components/suggest/_index.scss similarity index 63% rename from src/components/suggest_item/_index.scss rename to src/components/suggest/_index.scss index c4dd928bb4e..d31182e14d4 100644 --- a/src/components/suggest_item/_index.scss +++ b/src/components/suggest/_index.scss @@ -1,3 +1,5 @@ @import 'variables'; @import 'suggest_item'; + +@import 'suggest_input'; diff --git a/src/components/suggest/_suggest_input.scss b/src/components/suggest/_suggest_input.scss new file mode 100644 index 00000000000..ef30a41be16 --- /dev/null +++ b/src/components/suggest/_suggest_input.scss @@ -0,0 +1,14 @@ +.euiSuggestInput { + font-size: $euiFontSizeS; + color: $euiColorPrimary; + + .euiLoadingSpinner { + margin-right: $euiSizeS; + } + + .euiSuggestInput__statusIcon { + padding-left: $euiSizeS; + padding-right: $euiSizeS; + } + +} diff --git a/src/components/suggest_item/_suggest_item.scss b/src/components/suggest/_suggest_item.scss similarity index 78% rename from src/components/suggest_item/_suggest_item.scss rename to src/components/suggest/_suggest_item.scss index 9f063b25889..ac5bcc58215 100644 --- a/src/components/suggest_item/_suggest_item.scss +++ b/src/components/suggest/_suggest_item.scss @@ -5,13 +5,28 @@ font-size: $euiFontSizeXS; white-space: nowrap; - @each $name, $color in $itemColors { + &.euiSuggestItem-isClickable { + width: 100%; + text-align: left; + + &:hover, + &:focus { + cursor: pointer; + background-color: $euiColorLightestShade; + + .euiSuggestItem__type { //sass-lint:disable-line nesting-depth + color: $euiColorDarkestShade; + } + } + } + + @each $name, $color in $euiSuggestItemColors { .euiSuggestItem__type--#{$name} { $backgroundColor: tintOrShade($color, 90%, 50%); background-color: $backgroundColor; color: makeHighContrastColor($color, $backgroundColor); - } - } + } + } .euiSuggestItem__label, .euiSuggestItem__type, @@ -19,7 +34,7 @@ flex-grow: 0; display: flex; flex-direction: column; - } + } .euiSuggestItem__type { position: relative; @@ -53,7 +68,7 @@ .euiSuggestItem__label { @include euiTextTruncate; display: block; - } + } .euiSuggestItem__description { color: $euiColorDarkShade; @@ -64,5 +79,5 @@ flex-grow: 0; margin-left: 0; } - } + } } diff --git a/src/components/suggest_item/_variables.scss b/src/components/suggest/_variables.scss similarity index 89% rename from src/components/suggest_item/_variables.scss rename to src/components/suggest/_variables.scss index d90fa21260d..ff718dea7f6 100644 --- a/src/components/suggest_item/_variables.scss +++ b/src/components/suggest/_variables.scss @@ -1,4 +1,4 @@ -$itemColors: ( +$euiSuggestItemColors: ( tint0: $euiColorVis0, tint1: $euiColorVis1, tint2: $euiColorVis2, diff --git a/src/components/suggest/index.js b/src/components/suggest/index.js new file mode 100644 index 00000000000..2204a3cb211 --- /dev/null +++ b/src/components/suggest/index.js @@ -0,0 +1,5 @@ +export { EuiSuggestInput } from './suggest_input'; + +export { EuiSuggestItem } from './suggest_item'; + +export { EuiSuggest } from './suggest'; diff --git a/src/components/suggest/suggest.js b/src/components/suggest/suggest.js new file mode 100644 index 00000000000..0f657f08c6a --- /dev/null +++ b/src/components/suggest/suggest.js @@ -0,0 +1,81 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { EuiSuggestItem } from './suggest_item'; +import { EuiSuggestInput } from './suggest_input'; + +export class EuiSuggest extends Component { + state = { + value: '', + status: 'unsaved', + }; + + getValue = val => { + this.setState({ + value: val, + }); + }; + + onChange = e => { + this.props.onInputChange(e.target.value); + }; + + render() { + const { + onItemClick, + onInputChange, + status, + append, + tooltipContent, + suggestions, + ...rest + } = this.props; + + const suggestionList = suggestions.map((item, index) => ( + onItemClick(item) : null} + description={item.description} + /> + )); + + const suggestInput = ( + + ); + return
{suggestInput}
; + } +} + +EuiSuggest.propTypes = { + className: PropTypes.string, + /** + * Status of the current query 'notYetSaved', 'saved', 'unchanged' or 'loading'. + */ + status: PropTypes.oneOf(['unsaved', 'saved', 'unchanged', 'loading']), + tooltipContent: PropTypes.string, + /** + * Element to be appended to the input bar (e.g. hashtag popover). + */ + append: PropTypes.node, + /** + * List of suggestions to display using 'suggestItem'. + */ + suggestions: PropTypes.array, + /** + * Handler for click on a suggestItem. + */ + onItemClick: PropTypes.func, + onInputChange: PropTypes.func, +}; + +EuiSuggestInput.defaultProps = { + status: 'unchanged', +}; diff --git a/src/components/suggest/suggest.test.js b/src/components/suggest/suggest.test.js new file mode 100644 index 00000000000..69e5a9ccd5f --- /dev/null +++ b/src/components/suggest/suggest.test.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { render } from 'enzyme'; +import { requiredProps } from '../../test/required_props'; + +import { EuiSuggest } from './suggest'; + +const sampleItems = [ + { + type: { iconType: 'kqlField', color: 'tint4' }, + label: 'Field sample', + description: 'Description', + }, + { + type: { iconType: 'kqlValue', color: 'tint0' }, + label: 'Value sample', + description: 'Description', + }, +]; + +describe('EuiSuggest', () => { + test('is rendered', () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/components/suggest/suggest_input.js b/src/components/suggest/suggest_input.js new file mode 100644 index 00000000000..6147b5cc4d3 --- /dev/null +++ b/src/components/suggest/suggest_input.js @@ -0,0 +1,132 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { EuiFilterButton } from '../filter_group'; +import { EuiFieldText } from '../form'; +import { EuiToolTip } from '../tool_tip'; +import { EuiIcon } from '../icon'; +import { EuiPopover, EuiInputPopover } from '../popover'; + +const statusMap = { + unsaved: { + icon: 'dot', + color: 'accent', + tooltip: 'Changes have not been saved.', + }, + saved: { + icon: 'checkInCircleFilled', + color: 'secondary', + tooltip: 'Saved.', + }, + unchanged: { + icon: '', + color: 'secondary', + }, +}; + +export class EuiSuggestInput extends Component { + state = { + value: '', + isPopoverOpen: false, + }; + + onFieldChange = e => { + this.setState({ + value: e.target.value, + isPopoverOpen: e.target.value !== '' ? true : false, + }); + this.props.sendValue(e.target.value); + }; + + closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); + }; + + render() { + const { + className, + status, + append, + tooltipContent, + suggestions, + sendValue, + ...rest + } = this.props; + + let icon; + let color; + + if (statusMap[status]) { + icon = statusMap[status].icon; + color = statusMap[status].color; + } + const classes = classNames('euiSuggestInput', className); + + // EuiFieldText's append accepts an array of elements so start by creating an empty array + const appendArray = []; + + const statusElement = (status === 'saved' || status === 'unsaved') && ( + + + + ); + + // Push the status element to the array if it is not undefined + if (statusElement) appendArray.push(statusElement); + + // Check to see if consumer passed an append item and if so, add it to the array + if (append) appendArray.push(append); + + const customInput = ( + + ); + + return ( +
+ +
{suggestions}
+
+
+ ); + } +} + +EuiSuggestInput.propTypes = { + className: PropTypes.string, + /** + * Status of the current query 'unsaved', 'saved', 'unchanged' or 'loading'. + */ + status: PropTypes.oneOf(['unsaved', 'saved', 'unchanged', 'loading']), + tooltipContent: PropTypes.string, + /** + * Element to be appended to the input bar. + */ + append: PropTypes.node, + /** + * List of suggestions to display using 'suggestItem'. + */ + suggestions: PropTypes.array, +}; + +EuiSuggestInput.defaultProps = { + status: 'unchanged', +}; diff --git a/src/components/suggest/suggest_input.test.js b/src/components/suggest/suggest_input.test.js new file mode 100644 index 00000000000..d96dae5b097 --- /dev/null +++ b/src/components/suggest/suggest_input.test.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { render } from 'enzyme'; +import { requiredProps } from '../../test/required_props'; + +import { EuiSuggestInput } from './suggest_input'; + +const sampleItems = [ + { + type: { iconType: 'kqlField', color: 'tint4' }, + label: 'Field sample', + description: 'Description', + }, + { + type: { iconType: 'kqlValue', color: 'tint0' }, + label: 'Value sample', + description: 'Description', + }, +]; + +describe('EuiSuggestInput', () => { + test('is rendered', () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/components/suggest_item/suggest_item.js b/src/components/suggest/suggest_item.js similarity index 85% rename from src/components/suggest_item/suggest_item.js rename to src/components/suggest/suggest_item.js index 0d396023414..7b07559c936 100644 --- a/src/components/suggest_item/suggest_item.js +++ b/src/components/suggest/suggest_item.js @@ -31,9 +31,16 @@ export const EuiSuggestItem = ({ type, labelDisplay, description, + onClick, ...rest }) => { - const classes = classNames('euiSuggestItem', className); + const classes = classNames( + 'euiSuggestItem', + { + 'euiSuggestItem-isClickable': onClick, + }, + className + ); let colorClass = ''; @@ -51,14 +58,19 @@ export const EuiSuggestItem = ({ } } + let OuterElement = 'div'; + if (onClick) { + OuterElement = 'button'; + } + return ( -
+ {label} {description} -
+ ); }; @@ -84,6 +96,10 @@ EuiSuggestItem.propTypes = { * Label display is 'fixed' by default. Label will increase its width beyond 50% if needed with 'expand'. */ labelDisplay: PropTypes.oneOf(DISPLAYS), + /** + * Handler for click on a suggestItem. + */ + onClick: PropTypes.func, }; EuiSuggestItem.defaultProps = { diff --git a/src/components/suggest_item/suggest_item.test.js b/src/components/suggest/suggest_item.test.js similarity index 100% rename from src/components/suggest_item/suggest_item.test.js rename to src/components/suggest/suggest_item.test.js diff --git a/src/components/suggest_item/index.js b/src/components/suggest_item/index.js deleted file mode 100644 index fde82176579..00000000000 --- a/src/components/suggest_item/index.js +++ /dev/null @@ -1 +0,0 @@ -export { EuiSuggestItem } from './suggest_item';