diff --git a/CHANGELOG.md b/CHANGELOG.md index d8ef1a4f0e7..9f6b17fcf33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,18 @@ ## [`main`](https://github.com/elastic/eui/tree/main) +- Refactored `EuiSuggest` to use `EuiSelectable` ([#5157](https://github.com/elastic/eui/pull/5157)) + **Bug fixes** - Fixed `EuiDataGrid` to correctly remove the cell expansion action button when a column sets both `cellActions` and `isExpandable` to false ([#5592](https://github.com/elastic/eui/pull/5592)) - Fixed `EuiDataGrid` re-playing the cell actions animation when hovering over an already-focused cell ([#5592](https://github.com/elastic/eui/pull/5592)) +**Breaking changes** + +- Removed `EuiSuggestInput` ([#5157](https://github.com/elastic/eui/pull/5157)) +- Required `aria-label` or `aria-labelledby` for `EuiSuggest` ([#5157](https://github.com/elastic/eui/pull/5157)) +- Renamed `sendValue` prop to `onSearchChange` for `EuiSuggest` ([#5157](https://github.com/elastic/eui/pull/5157)) + ## [`47.0.0`](https://github.com/elastic/eui/tree/v47.0.0) - Added support for React 17 (this is a backwards compatible change - React 16.12+ is still supported for consuming applications) ([#5584](https://github.com/elastic/eui/pull/5584)) diff --git a/src-docs/src/views/suggest/suggest.js b/src-docs/src/views/suggest/suggest.tsx similarity index 66% rename from src-docs/src/views/suggest/suggest.js rename to src-docs/src/views/suggest/suggest.tsx index e233d08a0db..9aa9d4a509e 100644 --- a/src-docs/src/views/suggest/suggest.js +++ b/src-docs/src/views/suggest/suggest.tsx @@ -4,8 +4,12 @@ import { EuiRadioGroup, EuiSuggest, EuiSpacer, + EuiFormRow, + EuiSuggestionProps, } from '../../../../src/components'; +import { EuiSuggestStatus } from '../../../../src/components/suggest/types'; + import { htmlIdGenerator } from '../../../../src/services'; const shortDescription = 'This is the description'; @@ -44,21 +48,25 @@ const sampleItems = [ const idPrefix = htmlIdGenerator()(); export default () => { - const radios = [ + const radios: Array<{ + id: string; + value: EuiSuggestStatus; + label: string; + }> = [ { 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' }, ]; - const [status, setStatus] = useState('unchanged'); + const [status, setStatus] = useState('unchanged'); const [radioIdSelected, setSelectedId] = useState(`${idPrefix}0`); - const onChange = (optionId) => { + const onChange = (optionId: string) => { setSelectedId(optionId); - setStatus(radios.find((x) => x.id === optionId).value); + setStatus(radios.find((x) => x.id === optionId)!.value); }; - const onItemClick = (item) => { + const onItemClick = (item: EuiSuggestionProps) => { console.log(item); }; @@ -70,13 +78,17 @@ export default () => { onChange={(id) => onChange(id)} /> - {}} - onItemClick={onItemClick} - placeholder="Enter query to display suggestions" - suggestions={sampleItems} - /> + + {}} + onItemClick={onItemClick} + placeholder="Enter query to display suggestions" + suggestions={sampleItems} + /> + ); }; diff --git a/src-docs/src/views/suggest/suggest_example.js b/src-docs/src/views/suggest/suggest_example.js index 2b11ce4047d..7861229ec5b 100644 --- a/src-docs/src/views/suggest/suggest_example.js +++ b/src-docs/src/views/suggest/suggest_example.js @@ -1,4 +1,5 @@ import React from 'react'; +import { Link } from 'react-router-dom'; import { GuideSectionTypes } from '../../components'; @@ -7,7 +8,7 @@ import { EuiCode, EuiSpacer, EuiSuggest, - EuiSuggestItem, + EuiText, } from '../../../../src/components'; import Suggest from './suggest'; @@ -16,8 +17,7 @@ const suggestSource = require('!!raw-loader!./suggest'); import SavedQueries from './saved_queries'; const savedQueriesSource = require('!!raw-loader!./saved_queries'); -import SuggestItem from './suggest_item'; -const suggestItemSource = require('!!raw-loader!./suggest_item'); +import SuggestItem from './suggest_item_example'; const suggestItemSnippet = [ ` + <>

EuiSuggest is a text field component used to display suggestions. The status of the component is shown on its @@ -85,7 +85,12 @@ export const SuggestExample = { unsaved, saved, unchanged and isLoading.

- +

+ Note that EuiSuggest does not maintain internal + selection state. Use the onChange callback to + update your application state with the desired selection changes. +

+ ), props: { EuiSuggest }, snippet: suggestSnippet, @@ -93,29 +98,40 @@ export const SuggestExample = { }, { title: 'Suggest item', - source: [ - { - type: GuideSectionTypes.JS, - code: suggestItemSource, - }, - ], + wrapText: false, text: ( -
-

- EuiSuggestItem is a list item component to display - suggestions when typing queries in EuiSuggest. Use{' '} - labelDisplay to set whether the{' '} - label has a fixed width or not. By default, fixed - labels will have a width of 50%, you can adjust this by setting{' '} - labelWidth. Use{' '} - descriptionDisplay to set whether the{' '} - description truncates or wraps. -

-
+ <> + +

+ EuiSuggestItem is a list item component to + display suggestions when typing queries into{' '} + EuiSuggest. Each item requires a{' '} + label and type and can + optionally display a description. By default, + labels will have a width of 50% which you can adjust by setting{' '} + labelWidth to a multiple of{' '} + 10. +

+

+ Suggest item types are an object that requires an{' '} + iconType and color. +

+

+ Set truncate to false to force line-wrapping of + both the label and description. Note that wrapping text is not + compatible with the virtualized setting on{' '} + EuiSuggest. See{' '} + + EuiSelectable + {' '} + for more information on rendering items virtually. +

+
+ + + ), - props: { EuiSuggestItem }, snippet: suggestItemSnippet, - demo: , }, { title: 'Saved queries and filters', @@ -126,7 +142,7 @@ export const SuggestExample = { }, ], text: ( -
+ <>

This documents a visual pattern for Kibana's @@ -135,8 +151,7 @@ export const SuggestExample = { logic is well-formed.

- -
+ ), props: { EuiSuggest }, demo: , diff --git a/src-docs/src/views/suggest/suggest_item.js b/src-docs/src/views/suggest/suggest_item.js deleted file mode 100644 index 0da4ca28d59..00000000000 --- a/src-docs/src/views/suggest/suggest_item.js +++ /dev/null @@ -1,117 +0,0 @@ -import React from 'react'; - -import { EuiSuggestItem, EuiSpacer } from '../../../../src/components'; - -const shortDescription = 'This is the description'; - -const longDescription = - 'This is a long description. Fusce euismod dui eu metus sagittis molestie.'; - -const sampleItems = [ - { - type: { iconType: 'kqlField', color: 'tint5' }, - 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: 'tint10' }, - label: 'Recent search', - }, - { - type: { iconType: 'save', color: 'tint7' }, - label: 'Saved query', - }, -]; - -const sampleItems2 = [ - { - type: { iconType: 'kqlField', color: 'tint5' }, - label: 'Field sample with label at 30%', - labelWidth: '30', - description: shortDescription, - }, - { - type: { iconType: 'kqlField', color: 'tint5' }, - label: 'Field sample with label at 50%', - labelWidth: '50', - description: shortDescription, - }, - { - type: { iconType: 'kqlField', color: 'tint5' }, - label: 'Field sample with label at 80%', - labelWidth: '80', - description: shortDescription, - }, -]; - -const typeObj = { iconType: 'kqlValue', color: 'tint0' }; - -const longLabel = - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ut quam.'; - -export default () => ( -
- {sampleItems.map((item, index) => ( - - ))} - - - - - - - {sampleItems2.map((item, index) => ( - - ))} - - - -
-); diff --git a/src-docs/src/views/suggest/suggest_item.tsx b/src-docs/src/views/suggest/suggest_item.tsx new file mode 100644 index 00000000000..0a95bd364cb --- /dev/null +++ b/src-docs/src/views/suggest/suggest_item.tsx @@ -0,0 +1,102 @@ +import React from 'react'; + +import { EuiSuggestItem, EuiSuggest } from '../../../../src/components'; + +const shortDescription = 'This is the description'; + +const sampleItems = [ + { + type: { iconType: 'kqlField', color: 'tint5' }, + 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: 'tint10' }, + label: 'Recent search', + }, + { + type: { iconType: 'save', color: 'tint7' }, + label: 'Saved query', + }, +]; + +const customWidthItems = [ + { + type: { iconType: 'kqlField', color: 'tint5' }, + label: 'Field sample with label at 30%', + labelWidth: '30', + description: shortDescription, + }, + { + type: { iconType: 'kqlField', color: 'tint5' }, + label: 'Field sample with label at 50%', + labelWidth: '50', + description: shortDescription, + }, + { + type: { iconType: 'kqlField', color: 'tint5' }, + label: 'Field sample with label at 80%', + labelWidth: '80', + description: shortDescription, + }, +]; + +const moreItems = [ + { + type: { iconType: 'kqlValue', color: 'tint0' }, + label: 'Items with no description will expand their label', + }, + { + type: { iconType: 'kqlValue', color: 'tint0' }, + label: 'Item with a description that wraps', + description: + 'This is a long description. Fusce euismod dui eu metus sagittis molestie.', + truncate: false, + }, +]; + +const allItems = sampleItems.concat(customWidthItems).concat(moreItems); + +export default ({ + withInput, + fullWidth, + virtualized, +}: { + withInput: boolean; + fullWidth: boolean; + virtualized: boolean; +}) => ( + <> + {withInput ? ( + {}} + placeholder="Enter query to display suggestions" + isVirtualized={virtualized} + suggestions={allItems} + /> + ) : ( +
+ {allItems.map((item, index) => ( + + ))} +
+ )} + +); diff --git a/src-docs/src/views/suggest/suggest_item_example.tsx b/src-docs/src/views/suggest/suggest_item_example.tsx new file mode 100644 index 00000000000..c684443065c --- /dev/null +++ b/src-docs/src/views/suggest/suggest_item_example.tsx @@ -0,0 +1,59 @@ +/* eslint-disable import/no-unresolved */ +import React, { useState } from 'react'; + +import { EuiSwitch, EuiSuggestItem } from '../../../../src/components'; +import { GuideSection } from '../../components/guide_section/guide_section'; +import { GuideSectionTypes } from '../../components/guide_section/guide_section_types'; + +import SuggestItem from './suggest_item'; +const suggestItemSource = require('!!raw-loader!./suggest_item'); + +export default () => { + const [fullWidth, setFullWidth] = useState(false); + const [withInput, setWithInput] = useState(false); + const [virtualized, setVirtualized] = useState(false); + + return ( + <> + setFullWidth(e.target.checked)} + />{' '} +   + { + const checked = e.target.checked; + setWithInput(checked); + setVirtualized((isSet) => (checked ? isSet : false)); + }} + />{' '} +   + setVirtualized(e.target.checked)} + />{' '} +   + + } + source={[ + { + type: GuideSectionTypes.JS, + code: suggestItemSource, + }, + ]} + props={{ EuiSuggestItem }} + /> + + ); +}; diff --git a/src/components/suggest/__snapshots__/suggest.test.tsx.snap b/src/components/suggest/__snapshots__/suggest.test.tsx.snap index 18c66bb4159..fb39cbc3596 100644 --- a/src/components/suggest/__snapshots__/suggest.test.tsx.snap +++ b/src/components/suggest/__snapshots__/suggest.test.tsx.snap @@ -1,31 +1,1363 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EuiSuggest is rendered 1`] = ` -
+Array [
-
-
+
+
+
+ +
+ + +
+
+
+
+
+
+
, +

+ State: unchanged. +

, +] +`; + +exports[`EuiSuggest props append 1`] = ` +Array [ +
+
+
+
+
+
+ +
+ + +
+
+ + Appended + +
+
+
+
+
, +

+ State: unchanged. +

, +] +`; + +exports[`EuiSuggest props isVirtualized 1`] = ` + + +
+ + + [Function] + + + } + isOpen={false} + panelPaddingSize="none" + panelProps={ + Object { + "aria-live": undefined, + "aria-modal": false, + "role": undefined, + } + } + panelRef={[Function]} + > + + [Function] + + } + buttonRef={[Function]} + className="euiInputPopover euiInputPopover--fullWidth euiInputPopover euiInputPopover--fullWidth" + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + panelProps={ + Object { + "aria-live": undefined, + "aria-modal": false, + "role": undefined, + } + } + panelRef={[Function]} + > + +
+
+ +
+ + + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +

+ State: unchanged. +

+
+
+`; + +exports[`EuiSuggest props maxHeight 1`] = ` + + +
+ + + [Function] + + + } + isOpen={false} + panelPaddingSize="none" + panelProps={ + Object { + "aria-live": undefined, + "aria-modal": false, + "role": undefined, + } + } + panelRef={[Function]} + > + + [Function] + + } + buttonRef={[Function]} + className="euiInputPopover euiInputPopover--fullWidth euiInputPopover euiInputPopover--fullWidth" + closePopover={[Function]} + display="block" + hasArrow={true} + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + panelProps={ + Object { + "aria-live": undefined, + "aria-modal": false, + "role": undefined, + } + } + panelRef={[Function]} > + +
+
+ +
+ + + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +

+ State: unchanged. +

+
+
+`; + +exports[`EuiSuggest props options common 1`] = ` +Array [ +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
, +

+ State: unchanged. +

, +] +`; + +exports[`EuiSuggest props options standard 1`] = ` +Array [ +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
, +

+ State: unchanged. +

, +] +`; + +exports[`EuiSuggest props status status: loading is rendered 1`] = ` +Array [ +
+
+
+
- +
+ +
+ + +
+
+ +
+
-
-
+
, +

+ State: loading. +

, +] +`; + +exports[`EuiSuggest props status status: saved is rendered 1`] = ` +Array [ +
+
+
+
+
+
+ +
+ + +
+
+ + + +
+
+
+
+
, +

+ State: saved. +

, +] +`; + +exports[`EuiSuggest props status status: unchanged is rendered 1`] = ` +Array [ +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
, +

+ State: unchanged. +

, +] +`; + +exports[`EuiSuggest props status status: unsaved is rendered 1`] = ` +Array [ +
+
+
+
+
+
+ +
+ + +
+
+ + + +
+
+
+
+
, +

+ State: unsaved. +

, +] +`; + +exports[`EuiSuggest props tooltipContent tooltipContent for status: loading is rendered 1`] = ` +Array [ +
+
+
+
+
+
+ +
+ + +
+
+ +
+
+
+
+
+
+
, +

+ State: loading. +

, +] +`; + +exports[`EuiSuggest props tooltipContent tooltipContent for status: saved is rendered 1`] = ` +Array [ +
+
+
+
+
+
+ +
+ + +
+
+ + + +
+
+
+
+
, +

+ State: saved. +

, +] +`; + +exports[`EuiSuggest props tooltipContent tooltipContent for status: unchanged is rendered 1`] = ` +Array [ +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
, +

+ State: unchanged. +

, +] +`; + +exports[`EuiSuggest props tooltipContent tooltipContent for status: unsaved is rendered 1`] = ` +Array [ +
+
+
+
+
+
+ +
+ + +
+
+ + + +
+
+
+
+
, +

+ State: unsaved. +

, +] `; diff --git a/src/components/suggest/__snapshots__/suggest_input.test.tsx.snap b/src/components/suggest/__snapshots__/suggest_input.test.tsx.snap deleted file mode 100644 index d41a307ea1e..00000000000 --- a/src/components/suggest/__snapshots__/suggest_input.test.tsx.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EuiSuggestInput is rendered 1`] = ` -
-
-
-
-
- -
- - - -
-
-
-
-`; diff --git a/src/components/suggest/__snapshots__/suggest_item.test.tsx.snap b/src/components/suggest/__snapshots__/suggest_item.test.tsx.snap index 8b3e44d1143..a80cf9cf854 100644 --- a/src/components/suggest/__snapshots__/suggest_item.test.tsx.snap +++ b/src/components/suggest/__snapshots__/suggest_item.test.tsx.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EuiSuggestItem is rendered 1`] = ` -
Test label -
+ `; exports[`props descriptionDisplay as wrap is rendered 1`] = ` -
This is the description This is the description -
+ `; exports[`props item with no description has expanded label is rendered 1`] = ` -
Charles de Gaulle International Airport -
+ `; exports[`props labelDisplay as expand is rendered 1`] = ` -
This is the description This is the description -
+ `; exports[`props labelWidth is 30% is rendered 1`] = ` -
This is the description This is the description -
+ +`; + +exports[`props truncate is rendered 1`] = ` + + + + + + This is the description + + + This is the description + + `; diff --git a/src/components/suggest/_suggest_item.scss b/src/components/suggest/_suggest_item.scss index 16769a6aa54..7ba8e8cd54e 100644 --- a/src/components/suggest/_suggest_item.scss +++ b/src/components/suggest/_suggest_item.scss @@ -1,98 +1,102 @@ .euiSuggestItem { display: flex; - flex-grow: 1; - align-items: center; + align-items: flex-start; font-size: $euiFontSizeXS; - white-space: nowrap; - - &.euiSuggestItem-isClickable { - width: 100%; - text-align: left; + line-height: $euiSize; + color: $euiTextColor; + flex: 1 1 100%; + max-width: 100%; - &:hover, - &:focus { - cursor: pointer; - background-color: $euiColorLightestShade; - - .euiSuggestItem__type { //sass-lint:disable-line nesting-depth - color: $euiColorDarkestShade; - } + &--truncate { + .euiSuggestItem__label, + .euiSuggestItem__description { + @include euiTextTruncate; } } +} + +// When `onClick` is provided, EuiSuggestItem renders as a