From 2bc99172c1dc32f3c6ced352aaac4dc1d8f0b0e8 Mon Sep 17 00:00:00 2001 From: Anish Aggarwal <43617894+anishagg17@users.noreply.github.com> Date: Thu, 26 Mar 2020 01:37:12 +0530 Subject: [PATCH] Added `delimiter` prop to `EuiComboBox` (#3104) * added delimenator prop * Entering delimeator hits enter functionality added * added docs example, CL * prettified * removed console.log() * added copy functionality * added test,snap * removed copy button * removed copy button * Update src-docs/src/views/combo_box/combo_box_example.js Co-Authored-By: Caroline Horn <549577+cchaos@users.noreply.github.com> * Update src/components/combo_box/combo_box.tsx Co-Authored-By: Caroline Horn <549577+cchaos@users.noreply.github.com> * fixed only first gets entered * updated text * indent fix * Update combo_box_options_list.tsx * Update src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx Co-Authored-By: Caroline Horn <549577+cchaos@users.noreply.github.com> * removed empty string * removed redundant code Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com> --- CHANGELOG.md | 2 +- .../views/combo_box/combo_box_delimiter.js | 97 +++++++++++++++++++ .../src/views/combo_box/combo_box_example.js | 26 +++++ .../__snapshots__/combo_box.test.tsx.snap | 43 ++++++++ src/components/combo_box/combo_box.test.tsx | 12 +++ src/components/combo_box/combo_box.tsx | 36 +++++-- .../combo_box_options_list.tsx | 32 ++++-- 7 files changed, 232 insertions(+), 16 deletions(-) create mode 100644 src-docs/src/views/combo_box/combo_box_delimiter.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b8cabd2e08..e5e2f5b5ded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## [`master`](https://github.com/elastic/eui/tree/master) -No public interface changes since `22.0.0`. +- Added `delimiter` prop to `EuiComboBox` ([#3104](https://github.com/elastic/eui/pull/3104)) ## [`22.0.0`](https://github.com/elastic/eui/tree/v22.0.0) diff --git a/src-docs/src/views/combo_box/combo_box_delimiter.js b/src-docs/src/views/combo_box/combo_box_delimiter.js new file mode 100644 index 00000000000..3bc601cfd1e --- /dev/null +++ b/src-docs/src/views/combo_box/combo_box_delimiter.js @@ -0,0 +1,97 @@ +import React, { Component } from 'react'; + +import { EuiComboBox } from '../../../../src/components'; + +export default class extends Component { + constructor(props) { + super(props); + + this.options = [ + { + label: 'Titan', + 'data-test-subj': 'titanOption', + }, + { + label: 'Enceladus is disabled', + disabled: true, + }, + { + label: 'Mimas', + }, + { + label: 'Dione', + }, + { + label: 'Iapetus', + }, + { + label: 'Phoebe', + }, + { + label: 'Rhea', + }, + { + label: + "Pandora is one of Saturn's moons, named for a Titaness of Greek mythology", + }, + { + label: 'Tethys', + }, + { + label: 'Hyperion', + }, + ]; + + this.state = { + selectedOptions: [this.options[2], this.options[4]], + }; + } + + onChange = selectedOptions => { + this.setState({ + selectedOptions, + }); + }; + + onCreateOption = (searchValue, flattenedOptions) => { + const normalizedSearchValue = searchValue.trim().toLowerCase(); + + if (!normalizedSearchValue) { + return; + } + + const newOption = { + label: searchValue, + }; + + // Create the option if it doesn't exist. + if ( + flattenedOptions.findIndex( + option => option.label.trim().toLowerCase() === normalizedSearchValue + ) === -1 + ) { + this.options.push(newOption); + } + + // Select the option. + this.setState(prevState => ({ + selectedOptions: prevState.selectedOptions.concat(newOption), + })); + }; + + render() { + const { selectedOptions } = this.state; + return ( + + ); + } +} diff --git a/src-docs/src/views/combo_box/combo_box_example.js b/src-docs/src/views/combo_box/combo_box_example.js index 0f70d299c8b..556924d3ae6 100644 --- a/src-docs/src/views/combo_box/combo_box_example.js +++ b/src-docs/src/views/combo_box/combo_box_example.js @@ -59,6 +59,10 @@ import Disabled from './disabled'; const disabledSource = require('!!raw-loader!./disabled'); const disabledHtml = renderToHtml(Disabled); +import Delimiter from './combo_box_delimiter'; +const delimiterSource = require('!!raw-loader!./combo_box_delimiter'); +const delimiterHtml = renderToHtml(Delimiter); + import StartingWith from './startingWith'; const startingWithSource = require('!!raw-loader!./startingWith'); const startingWithHtml = renderToHtml(StartingWith); @@ -351,6 +355,28 @@ export const ComboBoxExample = { props: { EuiComboBox }, demo: , }, + { + title: 'With delimiter', + source: [ + { + type: GuideSectionTypes.JS, + code: delimiterSource, + }, + { + type: GuideSectionTypes.HTML, + code: delimiterHtml, + }, + ], + text: ( +

+ Pass a unique character to the delimiter prop to + aid in option creation. This is best used when knowing that content + may be pasted from elsewhere such as a comma separated list. +

+ ), + props: { EuiComboBox }, + demo: , + }, { title: 'Sorting matches', source: [ diff --git a/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap b/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap index b4138517cf4..1fbb44b9711 100644 --- a/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap +++ b/src/components/combo_box/__snapshots__/combo_box.test.tsx.snap @@ -57,6 +57,49 @@ exports[`EuiComboBox is rendered 1`] = ` `; +exports[`props delimiter is rendered 1`] = ` +
+ +
+`; + exports[`props full width is rendered 1`] = `
{ expect(component).toMatchSnapshot(); }); + + test('delimiter is rendered', () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); }); describe('behavior', () => { diff --git a/src/components/combo_box/combo_box.tsx b/src/components/combo_box/combo_box.tsx index f84770a8322..7c84e40a528 100644 --- a/src/components/combo_box/combo_box.tsx +++ b/src/components/combo_box/combo_box.tsx @@ -126,6 +126,10 @@ interface _EuiComboBoxProps * `string` | `ReactElement` or an array of these */ append?: EuiFormControlLayoutProps['append']; + /** + * A special character to use as a value separator. Typically a comma `,` + */ + delimiter?: string; } /** @@ -400,7 +404,7 @@ export class EuiComboBox extends Component< } }; - addCustomOption = (isContainerBlur: boolean) => { + addCustomOption = (isContainerBlur: boolean, searchValue: string) => { const { onCreateOption, options, @@ -408,7 +412,7 @@ export class EuiComboBox extends Component< singleSelection, } = this.props; - const { searchValue, matchingOptions } = this.state; + const { matchingOptions } = this.state; if (this.doesSearchMatchOnlyOption()) { this.onAddOption(matchingOptions[0], isContainerBlur); @@ -497,6 +501,18 @@ export class EuiComboBox extends Component< this.setState({ hasFocus: true }); }; + setCustomOptions = (isContainerBlur: boolean) => { + const { searchValue } = this.state; + const { delimiter } = this.props; + if (delimiter) { + searchValue.split(delimiter).forEach((option: string) => { + if (option.length > 0) this.addCustomOption(true, option); + }); + } else { + this.addCustomOption(isContainerBlur, searchValue); + } + }; + onContainerBlur: EventListener = event => { // close the options list, unless the use clicked on an option @@ -531,7 +547,7 @@ export class EuiComboBox extends Component< // If the user tabs away or changes focus to another element, take whatever input they've // typed and convert it into a pill, to prevent the combo box from looking like a text input. if (!this.hasActiveOption()) { - this.addCustomOption(true); + this.setCustomOptions(true); } } }; @@ -576,7 +592,7 @@ export class EuiComboBox extends Component< this.state.matchingOptions[this.state.activeOptionIndex] ); } else { - this.addCustomOption(false); + this.setCustomOptions(false); } break; @@ -708,7 +724,8 @@ export class EuiComboBox extends Component< onSearchChange: NonNullable< EuiComboBoxInputProps['onChange'] > = searchValue => { - const { onSearchChange } = this.props; + const { onSearchChange, delimiter } = this.props; + if (onSearchChange) { const hasMatchingOptions = this.state.matchingOptions.length > 0; onSearchChange(searchValue, hasMatchingOptions); @@ -717,6 +734,11 @@ export class EuiComboBox extends Component< this.setState({ searchValue }, () => { if (searchValue && this.state.isListOpen === false) this.openList(); }); + if (delimiter && searchValue.endsWith(delimiter)) { + searchValue.split(delimiter).forEach(value => { + if (value.length > 0) this.addCustomOption(false, value); + }); + } }; componentDidMount() { @@ -848,8 +870,9 @@ export class EuiComboBox extends Component< selectedOptions, singleSelection, prepend, - append, sortMatchesBy, + delimiter, + append, ...rest } = this.props; const { @@ -936,6 +959,7 @@ export class EuiComboBox extends Component< selectedOptions={selectedOptions} updatePosition={this.updatePosition} width={width} + delimiter={delimiter} /> ); diff --git a/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx b/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx index 04967ce18b0..8d230a1e82b 100644 --- a/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx +++ b/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx @@ -76,6 +76,7 @@ export type EuiComboBoxOptionsListProps = CommonProps & updatePosition: UpdatePositionHandler; width: number; singleSelection?: boolean | EuiComboBoxSingleSelectionShape; + delimiter?: string; }; export class EuiComboBoxOptionsList extends Component< @@ -179,6 +180,7 @@ export class EuiComboBoxOptionsList extends Component< singleSelection, updatePosition, width, + delimiter, ...rest } = this.props; @@ -232,15 +234,27 @@ export class EuiComboBoxOptionsList extends Component< ); } } else { - emptyStateContent = ( -

- {searchValue} }} - /> -

- ); + if (delimiter && searchValue.includes(delimiter)) { + emptyStateContent = ( +

+ {delimiter} }} + /> +

+ ); + } else { + emptyStateContent = ( +

+ {searchValue} }} + /> +

+ ); + } } } else if (!options.length) { emptyStateContent = (