Skip to content

Commit

Permalink
[EuiComboBox] Added sortMatchesBy prop (#3089)
Browse files Browse the repository at this point in the history
* added startingWith prop to combobox

* Update CHANGELOG.md

* Update src/components/combo_box/combo_box.tsx

Co-Authored-By: Caroline Horn <549577+cchaos@users.noreply.github.com>

* updated prop

* added test

* Update src-docs/src/views/combo_box/combo_box_example.js

Co-Authored-By: Caroline Horn <549577+cchaos@users.noreply.github.com>

* 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.test.tsx

Co-Authored-By: Greg Thompson <thompsongl@users.noreply.github.com>

* Update src/components/combo_box/combo_box.test.tsx

Co-Authored-By: Greg Thompson <thompsongl@users.noreply.github.com>

* resolved eslint errors

Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com>
Co-authored-by: Greg Thompson <thompsongl@users.noreply.github.com>
  • Loading branch information
3 people authored Mar 19, 2020
1 parent 08e84c6 commit 93ca8ef
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 2 deletions.
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

0 comments on commit 93ca8ef

Please sign in to comment.