Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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