Skip to content
84 changes: 84 additions & 0 deletions src-docs/src/views/combo_box/case_sensitive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, { useState } from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[extremely optional] it might be nice to potentially make this a .tsx file instead of JS to dogfood/catch any type friction points, feel free to skip though


import { EuiComboBox } from '../../../../src/components';

export default () => {
const [options, updateOptions] = useState([
{
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',
},
]);

const [selectedOptions, setSelected] = useState([]);

const onChange = (selectedOptions) => {
setSelected(selectedOptions);
};

const 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
) {
updateOptions([...options, newOption]);
}

// Select the option.
setSelected((prevSelected) => [...prevSelected, newOption]);
};

return (
<EuiComboBox
aria-label="Accessible screen reader label"
placeholder="Select or create options"
options={options}
selectedOptions={selectedOptions}
onChange={onChange}
onCreateOption={onCreateOption}
isClearable={true}
isCaseSensitive
/>
);
};
29 changes: 29 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 @@ -150,6 +150,17 @@ const virtualizedSnippet = `<EuiComboBox
onChange={onChange}
/>`;

import CaseSensitive from './case_sensitive';
const caseSensitiveSource = require('!!raw-loader!./case_sensitive');
const caseSensitiveSnippet = `<EuiComboBox
aria-label="Accessible screen reader label"
placeholder="Select or create options"
options={options}
onChange={onChange}
onCreateOption={onCreateOption}
isCaseSensitive
/>`;

import Disabled from './disabled';
const disabledSource = require('!!raw-loader!./disabled');
const disabledSnippet = `<EuiComboBox
Expand Down Expand Up @@ -269,6 +280,24 @@ export const ComboBoxExample = {
snippet: disabledSnippet,
demo: <Disabled />,
},
{
title: 'Case-sensitive matching',
source: [
{
type: GuideSectionTypes.JS,
code: caseSensitiveSource,
},
],
text: (
<p>
Set the prop <EuiCode>isCaseSensitive</EuiCode> to make the combo box
option matching case sensitive.
</p>
),
props: { EuiComboBox, EuiComboBoxOptionOption },
snippet: caseSensitiveSnippet,
demo: <CaseSensitive />,
},
{
title: 'Virtualized',
source: [
Expand Down
46 changes: 46 additions & 0 deletions src/components/combo_box/combo_box.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,52 @@ describe('behavior', () => {
});
});

describe('isCaseSensitive', () => {
const sortMatchesByOptions = [
{
label: 'Case sensitivity',
},
...options,
];
test('options "false"', () => {
const component = mount<
EuiComboBox<TitanOption>,
EuiComboBoxProps<TitanOption>,
{ matchingOptions: TitanOption[] }
>(<EuiComboBox options={sortMatchesByOptions} isCaseSensitive={false} />);

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

expect(component.state('matchingOptions')[0].label).toBe(
'Case sensitivity'
);
});

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

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

expect(component.state('matchingOptions').length).toBe(0);

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

expect(component.state('matchingOptions')[0].label).toBe(
'Case sensitivity'
);
});
});

it('calls the inputRef prop with the input element', () => {
const inputRefCallback = jest.fn();

Expand Down
59 changes: 39 additions & 20 deletions src/components/combo_box/combo_box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
getMatchingOptions,
flattenOptionGroups,
getSelectedOptionForSearchValue,
SortMatchesBy,
} from './matching_options';
import {
EuiComboBoxInputProps,
Expand Down Expand Up @@ -122,7 +123,11 @@ export interface _EuiComboBoxProps<T>
* `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';
sortMatchesBy: SortMatchesBy;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch!

/**
* Whether to match options with case sensitivity.
*/
isCaseSensitive?: boolean;
/**
* 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 @@ -211,14 +216,15 @@ export class EuiComboBox<T> extends Component<
listElement: null,
listPosition: 'bottom',
listZIndex: undefined,
matchingOptions: getMatchingOptions<T>(
this.props.options,
this.props.selectedOptions,
initialSearchValue,
this.props.async,
Boolean(this.props.singleSelection),
this.props.sortMatchesBy
),
matchingOptions: getMatchingOptions<T>({
options: this.props.options,
selectedOptions: this.props.selectedOptions,
searchValue: initialSearchValue,
isCaseSensitive: this.props.isCaseSensitive,
isPreFiltered: this.props.async,
showPrevSelected: Boolean(this.props.singleSelection),
sortMatchesBy: this.props.sortMatchesBy,
}),
Comment on lines +220 to +228
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙏 thanks so much for this cleanup, objects are so much nicer than multiple args once you get over like 3

searchValue: initialSearchValue,
width: 0,
};
Expand Down Expand Up @@ -433,6 +439,7 @@ export class EuiComboBox<T> extends Component<

addCustomOption = (isContainerBlur: boolean, searchValue: string) => {
const {
isCaseSensitive,
onCreateOption,
options,
selectedOptions,
Expand All @@ -456,7 +463,13 @@ export class EuiComboBox<T> extends Component<
}

// Don't create the value if it's already been selected.
if (getSelectedOptionForSearchValue(searchValue, selectedOptions)) {
if (
getSelectedOptionForSearchValue({
isCaseSensitive,
searchValue,
selectedOptions,
})
) {
return;
}

Expand Down Expand Up @@ -788,6 +801,8 @@ export class EuiComboBox<T> extends Component<
prevState: EuiComboBoxState<T>
) {
const {
async,
isCaseSensitive,
options,
selectedOptions,
singleSelection,
Expand All @@ -797,14 +812,15 @@ export class EuiComboBox<T> extends Component<

// Calculate and cache the options which match the searchValue, because we use this information
// in multiple places and it would be expensive to calculate repeatedly.
const matchingOptions = getMatchingOptions(
const matchingOptions = getMatchingOptions({
options,
selectedOptions,
searchValue,
nextProps.async,
Boolean(singleSelection),
sortMatchesBy
);
isCaseSensitive,
isPreFiltered: async,
showPrevSelected: Boolean(singleSelection),
sortMatchesBy,
});

const stateUpdate: Partial<EuiComboBoxState<T>> = { matchingOptions };

Expand Down Expand Up @@ -873,14 +889,15 @@ export class EuiComboBox<T> extends Component<
// isn't called after a state change, and we track `searchValue` in state
// instead we need to react to a change in searchValue here
this.updateMatchingOptionsIfDifferent(
getMatchingOptions(
getMatchingOptions({
options,
selectedOptions,
searchValue,
this.props.async,
Boolean(singleSelection),
sortMatchesBy
)
isCaseSensitive: this.props.isCaseSensitive,
isPreFiltered: this.props.async,
showPrevSelected: Boolean(singleSelection),
sortMatchesBy,
})
);
}

Expand All @@ -898,6 +915,7 @@ export class EuiComboBox<T> extends Component<
fullWidth,
id,
inputRef,
isCaseSensitive,
isClearable,
isDisabled,
isInvalid,
Expand Down Expand Up @@ -977,6 +995,7 @@ export class EuiComboBox<T> extends Component<
customOptionText={customOptionText}
data-test-subj={optionsListDataTestSubj}
fullWidth={fullWidth}
isCaseSensitive={isCaseSensitive}
isLoading={isLoading}
listRef={this.listRefCallback}
matchingOptions={matchingOptions}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ export type EuiComboBoxOptionsListProps<T> = CommonProps &
*/
customOptionText?: string;
fullWidth?: boolean;
getSelectedOptionForSearchValue?: (
searchValue: string,
selectedOptions: any[]
) => EuiComboBoxOptionOption<T> | undefined;
getSelectedOptionForSearchValue?: (params: {
isCaseSensitive?: boolean;
searchValue: string;
selectedOptions: any[];
}) => EuiComboBoxOptionOption<T> | undefined;
isCaseSensitive?: boolean;
isLoading?: boolean;
listRef: RefCallback<HTMLDivElement>;
matchingOptions: Array<EuiComboBoxOptionOption<T>>;
Expand Down Expand Up @@ -113,6 +115,7 @@ export class EuiComboBoxOptionsList<T> extends Component<
static defaultProps = {
'data-test-subj': '',
rowHeight: 29, // row height of default option renderer
isCaseSensitive: false,
};

updatePosition = () => {
Expand Down Expand Up @@ -267,6 +270,7 @@ export class EuiComboBoxOptionsList<T> extends Component<
) : (
<EuiHighlight
search={searchValue}
strict={this.props.isCaseSensitive}
className={OPTION_CONTENT_CLASSNAME}
>
{label}
Expand All @@ -286,6 +290,7 @@ export class EuiComboBoxOptionsList<T> extends Component<
customOptionText,
fullWidth,
getSelectedOptionForSearchValue,
isCaseSensitive,
isLoading,
listRef,
matchingOptions,
Expand Down Expand Up @@ -345,10 +350,11 @@ export class EuiComboBoxOptionsList<T> extends Component<
</div>
);
} else {
const selectedOptionForValue = getSelectedOptionForSearchValue(
const selectedOptionForValue = getSelectedOptionForSearchValue({
isCaseSensitive,
searchValue,
selectedOptions
);
selectedOptions,
});
if (selectedOptionForValue) {
// Disallow duplicate custom options.
emptyStateContent = (
Expand Down
Loading