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 `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))
Expand Down
28 changes: 28 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,29 @@ export const ComboBoxExample = {
props: { EuiComboBox },
demo: <Async />,
},
{
title: 'Sorting matches',
source: [
{
type: GuideSectionTypes.JS,
code: startingWithSource,
},
{
type: GuideSectionTypes.HTML,
code: startingWithHtml,
},
],
text: (
<p>
By default, the matched options will keep their original sort order.
If you would like to prioritize those options that{' '}
<strong>start with</strong> the searched string, pass{' '}
<EuiCode language="js">sortMatchesBy=&quot;startsWith&quot;</EuiCode>
to display those options at the top of the list.
</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"
/>
);
}
}
18 changes: 17 additions & 1 deletion src/components/combo_box/combo_box.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -325,4 +325,20 @@ describe('behavior', () => {
).toBe(document.activeElement);
});
});

describe('sortMatchesBy', () => {
test('options startsWith', () => {
const component = mount<
EuiComboBox<TitanOption>,
EuiComboBoxProps<TitanOption>,
{ matchingOptions: TitanOption[] }
>(<EuiComboBox options={options} sortMatchesBy="startsWith" />);

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

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