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 = (