Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EuiComboBox] Added sortMatchesBy prop #3089

Merged
merged 13 commits into from
Mar 19, 2020
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 `Enter` key press functionality to `EuiSuperDatePicker` ([#3048](https://github.com/elastic/eui/pull/3048))

## [`21.1.0`](https://github.com/elastic/eui/tree/v21.1.0)
Expand Down
26 changes: 26 additions & 0 deletions src-docs/src/views/combo_box/combo_box_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: (
Expand Down Expand Up @@ -347,5 +351,27 @@ export const ComboBoxExample = {
props: { EuiComboBox },
demo: <Async />,
},
{
title: 'Starting With',
anishagg17 marked this conversation as resolved.
Show resolved Hide resolved
source: [
{
type: GuideSectionTypes.JS,
code: startingWithSource,
},
{
type: GuideSectionTypes.HTML,
code: startingWithHtml,
},
],
text: (
<p>
Use the
<EuiCode>startingWith</EuiCode> prop to let the options that start
with the query be displayed on the top of the list.
anishagg17 marked this conversation as resolved.
Show resolved Hide resolved
</p>
),
props: { EuiComboBox },
demo: <StartingWith />,
},
],
};
97 changes: 97 additions & 0 deletions src-docs/src/views/combo_box/startingWith.js
Original file line number Diff line number Diff line change
@@ -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 (
<EuiComboBox
sortMatchesBy="startsWith"
placeholder="Select or create options"
options={this.options}
selectedOptions={selectedOptions}
onChange={this.onChange}
onCreateOption={this.onCreateOption}
isClearable={true}
data-test-subj="demoComboBox"
/>
);
}
}
17 changes: 17 additions & 0 deletions src/components/combo_box/combo_box.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { comboBoxKeyCodes } from '../../services';

import { EuiComboBox } from './combo_box';
anishagg17 marked this conversation as resolved.
Show resolved Hide resolved
import T from 'tabbable';
anishagg17 marked this conversation as resolved.
Show resolved Hide resolved

jest.mock('../portal', () => ({
EuiPortal: ({ children }: { children: ReactNode }) => children,
Expand Down Expand Up @@ -325,4 +326,20 @@ describe('behavior', () => {
).toBe(document.activeElement);
});
});

describe('sortMatchesBy', () => {
test('options startsWith', () => {
const component = mount(
<EuiComboBox options={options} sortMatchesBy="startsWith" />
) as EuiComboBox;
anishagg17 marked this conversation as resolved.
Show resolved Hide resolved

findTestSubject(component, 'comboBoxSearchInput').simulate('change', {
target: { value: 'e' },
});
const instance = component.instance();

expect(instance.state.matchingOptions[0].label).toBe('Enceladus');
expect(component.state('matchingOptions')[0].label).toBe('Enceladus');
});
});
});
33 changes: 32 additions & 1 deletion src/components/combo_box/combo_box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ interface _EuiComboBoxProps<T>
* 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
Expand Down Expand Up @@ -170,6 +176,7 @@ export class EuiComboBox<T> extends Component<
singleSelection: false,
prepend: null,
append: null,
sortMatchesBy: 'none',
};

state: EuiComboBoxState<T> = {
Expand Down Expand Up @@ -779,6 +786,7 @@ export class EuiComboBox<T> extends Component<
);
}
}

this.setState({
matchingOptions: newMatchingOptions,
activeOptionIndex: nextActiveOptionIndex,
Expand Down Expand Up @@ -841,6 +849,7 @@ export class EuiComboBox<T> extends Component<
singleSelection,
prepend,
append,
sortMatchesBy,
...rest
} = this.props;
const {
Expand All @@ -850,8 +859,30 @@ export class EuiComboBox<T> extends Component<
listPosition,
searchValue,
width,
matchingOptions,
} = this.state;

let newMatchingOptions = matchingOptions;

if (sortMatchesBy === 'startsWith') {
const refObj: {
startWith: Array<EuiComboBoxOptionOption<T>>;
others: Array<EuiComboBoxOptionOption<T>>;
} = { 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.
Expand Down Expand Up @@ -887,7 +918,7 @@ export class EuiComboBox<T> extends Component<
fullWidth={fullWidth}
isLoading={isLoading}
listRef={this.listRefCallback}
matchingOptions={this.state.matchingOptions}
matchingOptions={newMatchingOptions}
onCloseList={this.closeList}
onCreateOption={onCreateOption}
onOptionClick={this.onOptionClick}
Expand Down