diff --git a/CHANGELOG.md b/CHANGELOG.md
index d059ac22ed0..d30b4addcac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
## [`master`](https://github.com/elastic/eui/tree/master)
+- Added `sortMatchesBy` prop for `EuiComboBox` ([#3089](https://github.com/elastic/eui/pull/3089))
- Added `prepend` and `append` ability to `EuiFieldPassword` ([#3122](https://github.com/elastic/eui/pull/3122))
- Added `Enter` key press functionality to `EuiSuperDatePicker` ([#3048](https://github.com/elastic/eui/pull/3048))
- Added `title` to headers of `EuiTable` in case of truncation ([#3094](https://github.com/elastic/eui/pull/3094))
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 a3479d6ff26..0f70d299c8b 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 StartingWith from './startingWith';
+const startingWithSource = require('!!raw-loader!./startingWith');
+const startingWithHtml = renderToHtml(StartingWith);
+
export const ComboBoxExample = {
title: 'Combo Box',
intro: (
@@ -347,5 +351,29 @@ export const ComboBoxExample = {
props: { EuiComboBox },
demo: ,
},
+ {
+ title: 'Sorting matches',
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: startingWithSource,
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: startingWithHtml,
+ },
+ ],
+ text: (
+
+ By default, the matched options will keep their original sort order.
+ If you would like to prioritize those options that{' '}
+ start with the searched string, pass{' '}
+ sortMatchesBy="startsWith"
+ to display those options at the top of the list.
+
+ ),
+ props: { EuiComboBox },
+ demo: ,
+ },
],
};
diff --git a/src-docs/src/views/combo_box/startingWith.js b/src-docs/src/views/combo_box/startingWith.js
new file mode 100644
index 00000000000..95de94a4738
--- /dev/null
+++ b/src-docs/src/views/combo_box/startingWith.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/components/combo_box/combo_box.test.tsx b/src/components/combo_box/combo_box.test.tsx
index dadb0e77c85..6856b23c2bc 100644
--- a/src/components/combo_box/combo_box.test.tsx
+++ b/src/components/combo_box/combo_box.test.tsx
@@ -7,7 +7,7 @@ import {
} from '../../test';
import { comboBoxKeyCodes } from '../../services';
-import { EuiComboBox } from './combo_box';
+import { EuiComboBox, EuiComboBoxProps } from './combo_box';
jest.mock('../portal', () => ({
EuiPortal: ({ children }: { children: ReactNode }) => children,
@@ -325,4 +325,20 @@ describe('behavior', () => {
).toBe(document.activeElement);
});
});
+
+ describe('sortMatchesBy', () => {
+ test('options startsWith', () => {
+ const component = mount<
+ EuiComboBox,
+ EuiComboBoxProps,
+ { matchingOptions: TitanOption[] }
+ >();
+
+ findTestSubject(component, 'comboBoxSearchInput').simulate('change', {
+ target: { value: 'e' },
+ });
+
+ expect(component.state('matchingOptions')[0].label).toBe('Enceladus');
+ });
+ });
});
diff --git a/src/components/combo_box/combo_box.tsx b/src/components/combo_box/combo_box.tsx
index 740fff697f0..f84770a8322 100644
--- a/src/components/combo_box/combo_box.tsx
+++ b/src/components/combo_box/combo_box.tsx
@@ -110,6 +110,12 @@ interface _EuiComboBoxProps
* When `true` only allows the user to select a single option. Set to `{ asPlainText: true }` to not render input selection as pills
*/
singleSelection: boolean | EuiComboBoxSingleSelectionShape;
+ /**
+ * Display matching options by:
+ * `startsWith`: moves items that start with search value to top of the list;
+ * `none`: don't change the sort order of initial object
+ */
+ sortMatchesBy?: 'none' | 'startsWith';
/**
* Creates an input group with element(s) coming before input. It won't show if `singleSelection` is set to `false`.
* `string` | `ReactElement` or an array of these
@@ -170,6 +176,7 @@ export class EuiComboBox extends Component<
singleSelection: false,
prepend: null,
append: null,
+ sortMatchesBy: 'none',
};
state: EuiComboBoxState = {
@@ -779,6 +786,7 @@ export class EuiComboBox extends Component<
);
}
}
+
this.setState({
matchingOptions: newMatchingOptions,
activeOptionIndex: nextActiveOptionIndex,
@@ -841,6 +849,7 @@ export class EuiComboBox extends Component<
singleSelection,
prepend,
append,
+ sortMatchesBy,
...rest
} = this.props;
const {
@@ -850,8 +859,30 @@ export class EuiComboBox extends Component<
listPosition,
searchValue,
width,
+ matchingOptions,
} = this.state;
+ let newMatchingOptions = matchingOptions;
+
+ if (sortMatchesBy === 'startsWith') {
+ const refObj: {
+ startWith: Array>;
+ others: Array>;
+ } = { startWith: [], others: [] };
+
+ newMatchingOptions.forEach(object => {
+ if (
+ object.label
+ .toLowerCase()
+ .startsWith(searchValue.trim().toLowerCase())
+ ) {
+ refObj.startWith.push(object);
+ } else {
+ refObj.others.push(object);
+ }
+ });
+ newMatchingOptions = [...refObj.startWith, ...refObj.others];
+ }
// Visually indicate the combobox is in an invalid state if it has lost focus but there is text entered in the input.
// When custom options are disabled and the user leaves the combo box after entering text that does not match any
// options, this tells the user that they've entered invalid input.
@@ -887,7 +918,7 @@ export class EuiComboBox extends Component<
fullWidth={fullWidth}
isLoading={isLoading}
listRef={this.listRefCallback}
- matchingOptions={this.state.matchingOptions}
+ matchingOptions={newMatchingOptions}
onCloseList={this.closeList}
onCreateOption={onCreateOption}
onOptionClick={this.onOptionClick}