From b7c876284385fd20c3f30e4fa00a93b17108b4b0 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Tue, 27 Sep 2022 13:21:59 -0500 Subject: [PATCH 1/6] refactor API and introduce isCaseSensitive --- .../combo_box/matching_options.test.ts | 63 +++++++++++--- src/components/combo_box/matching_options.ts | 87 +++++++++++++------ 2 files changed, 111 insertions(+), 39 deletions(-) diff --git a/src/components/combo_box/matching_options.test.ts b/src/components/combo_box/matching_options.test.ts index 77f077792b0..03cfe386f24 100644 --- a/src/components/combo_box/matching_options.test.ts +++ b/src/components/combo_box/matching_options.test.ts @@ -8,6 +8,7 @@ import { EuiComboBoxOptionOption } from './types'; import { + SortMatchesBy, flattenOptionGroups, getMatchingOptions, getSelectedOptionForSearchValue, @@ -92,12 +93,13 @@ describe('getSelectedOptionForSearchValue', () => { interface GetMatchingOptionsTestCase { expected: EuiComboBoxOptionOption[]; + isCaseSensitive: boolean; isPreFiltered: boolean; options: EuiComboBoxOptionOption[]; searchValue: string; selectedOptions: EuiComboBoxOptionOption[]; showPrevSelected: boolean; - sortMatchesBy: string; + sortMatchesBy: SortMatchesBy; } const testCases: GetMatchingOptionsTestCase[] = [ @@ -110,6 +112,7 @@ const testCases: GetMatchingOptionsTestCase[] = [ }, ], searchValue: 'saturn', + isCaseSensitive: false, isPreFiltered: false, showPrevSelected: false, expected: [], @@ -124,6 +127,7 @@ const testCases: GetMatchingOptionsTestCase[] = [ }, ], searchValue: 'saturn', + isCaseSensitive: false, isPreFiltered: true, showPrevSelected: false, expected: [ @@ -141,6 +145,7 @@ const testCases: GetMatchingOptionsTestCase[] = [ }, ], searchValue: 'saturn', + isCaseSensitive: false, isPreFiltered: false, showPrevSelected: true, expected: [{ 'data-test-subj': 'saturnOption', label: 'Saturn' }], @@ -155,6 +160,7 @@ const testCases: GetMatchingOptionsTestCase[] = [ }, ], searchValue: 'saturn', + isCaseSensitive: false, isPreFiltered: true, showPrevSelected: true, expected: [ @@ -172,6 +178,7 @@ const testCases: GetMatchingOptionsTestCase[] = [ }, ], searchValue: 'titan', + isCaseSensitive: false, isPreFiltered: true, showPrevSelected: false, expected: [ @@ -191,6 +198,7 @@ const testCases: GetMatchingOptionsTestCase[] = [ }, ], searchValue: 'titan', + isCaseSensitive: false, isPreFiltered: true, showPrevSelected: false, expected: [ @@ -199,22 +207,55 @@ const testCases: GetMatchingOptionsTestCase[] = [ ], sortMatchesBy: 'none', }, + // Case sensitivity + { + options, + selectedOptions: [], + searchValue: 'saturn', + isCaseSensitive: false, + isPreFiltered: false, + showPrevSelected: false, + expected: [ + { + label: 'Saturn', + 'data-test-subj': 'saturnOption', + }, + ], + sortMatchesBy: 'none', + }, + { + options, + selectedOptions: [], + searchValue: 'saturn', + isCaseSensitive: true, + isPreFiltered: false, + showPrevSelected: false, + expected: [], + sortMatchesBy: 'none', + }, + { + options, + selectedOptions: [], + searchValue: 'Saturn', + isCaseSensitive: true, + isPreFiltered: false, + showPrevSelected: false, + expected: [ + { + label: 'Saturn', + 'data-test-subj': 'saturnOption', + }, + ], + sortMatchesBy: 'none', + }, ]; describe('getMatchingOptions', () => { test.each(testCases)( '.getMatchingOptions(%o)', (testCase: typeof testCases[number]) => { - expect( - getMatchingOptions( - testCase.options, - testCase.selectedOptions, - testCase.searchValue, - testCase.isPreFiltered, - testCase.showPrevSelected, - testCase.sortMatchesBy - ) - ).toMatchObject(testCase.expected); + const { expected, ...rest } = testCase; + expect(getMatchingOptions(rest)).toMatchObject(expected); } ); }); diff --git a/src/components/combo_box/matching_options.ts b/src/components/combo_box/matching_options.ts index 38c690f7d7e..b0b3832ef9a 100644 --- a/src/components/combo_box/matching_options.ts +++ b/src/components/combo_box/matching_options.ts @@ -8,6 +8,27 @@ import { EuiComboBoxOptionOption } from './types'; +export type SortMatchesBy = 'none' | 'startsWith'; +interface GetMatchingOptions { + options: Array>; + selectedOptions: Array>; + searchValue: string; + isCaseSensitive?: boolean; + isPreFiltered?: boolean; + showPrevSelected?: boolean; + sortMatchesBy?: SortMatchesBy; +} +interface CollectMatchingOption + extends Pick< + GetMatchingOptions, + 'isCaseSensitive' | 'isPreFiltered' | 'showPrevSelected' + > { + accumulator: Array>; + option: EuiComboBoxOptionOption; + selectedOptions: Array>; + normalizedSearchValue: string; +} + export const flattenOptionGroups = ( optionsOrGroups: Array> ) => { @@ -40,14 +61,15 @@ export const getSelectedOptionForSearchValue = ( ); }; -const collectMatchingOption = ( - accumulator: Array>, - option: EuiComboBoxOptionOption, - selectedOptions: Array>, - normalizedSearchValue: string, - isPreFiltered: boolean, - showPrevSelected: boolean -) => { +const collectMatchingOption = ({ + accumulator, + option, + selectedOptions, + normalizedSearchValue, + isCaseSensitive, + isPreFiltered, + showPrevSelected, +}: CollectMatchingOption) => { // Only show options which haven't yet been selected unless requested. const selectedOption = getSelectedOptionForSearchValue( option.label, @@ -69,35 +91,40 @@ const collectMatchingOption = ( return; } - const normalizedOption = option.label.trim().toLowerCase(); + let normalizedOption = option.label.trim(); + if (!isCaseSensitive) normalizedOption = normalizedOption.toLowerCase(); if (normalizedOption.includes(normalizedSearchValue)) { accumulator.push(option); } }; -export const getMatchingOptions = ( - options: Array>, - selectedOptions: Array>, - searchValue: string, - isPreFiltered: boolean, - showPrevSelected: boolean, - sortMatchesBy: string -) => { - const normalizedSearchValue = searchValue.trim().toLowerCase(); +export const getMatchingOptions = ({ + options, + selectedOptions, + searchValue, + isCaseSensitive = false, + isPreFiltered = false, + showPrevSelected = false, + sortMatchesBy = 'none', +}: GetMatchingOptions) => { + let normalizedSearchValue = searchValue.trim(); + if (!isCaseSensitive) + normalizedSearchValue = normalizedSearchValue.toLocaleLowerCase(); let matchingOptions: Array> = []; options.forEach((option) => { if (option.options) { const matchingOptionsForGroup: Array> = []; option.options.forEach((groupOption: EuiComboBoxOptionOption) => { - collectMatchingOption( - matchingOptionsForGroup, - groupOption, + collectMatchingOption({ + accumulator: matchingOptionsForGroup, + option: groupOption, selectedOptions, normalizedSearchValue, + isCaseSensitive, isPreFiltered, - showPrevSelected - ); + showPrevSelected, + }); }); if (matchingOptionsForGroup.length > 0) { // Add option for group label @@ -111,14 +138,15 @@ export const getMatchingOptions = ( matchingOptions = matchingOptions.concat(matchingOptionsForGroup); } } else { - collectMatchingOption( - matchingOptions, + collectMatchingOption({ + accumulator: matchingOptions, option, selectedOptions, normalizedSearchValue, + isCaseSensitive, isPreFiltered, - showPrevSelected - ); + showPrevSelected, + }); } }); @@ -129,7 +157,10 @@ export const getMatchingOptions = ( } = { startWith: [], others: [] }; matchingOptions.forEach((object) => { - if (object.label.toLowerCase().startsWith(normalizedSearchValue)) { + const normalizedLabel = isCaseSensitive + ? object.label + : object.label.toLowerCase(); + if (normalizedLabel.startsWith(normalizedSearchValue)) { refObj.startWith.push(object); } else { refObj.others.push(object); From 1dd56349e8cb4c541c2556354e4e5fd542fb666f Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Tue, 27 Sep 2022 13:22:24 -0500 Subject: [PATCH 2/6] refactor utils; add isCaseSensitiveProp --- src/components/combo_box/combo_box.test.tsx | 46 +++++++++++++++++++ src/components/combo_box/combo_box.tsx | 49 +++++++++++++-------- 2 files changed, 76 insertions(+), 19 deletions(-) diff --git a/src/components/combo_box/combo_box.test.tsx b/src/components/combo_box/combo_box.test.tsx index c0c30df8bb4..64c236617b8 100644 --- a/src/components/combo_box/combo_box.test.tsx +++ b/src/components/combo_box/combo_box.test.tsx @@ -461,6 +461,52 @@ describe('behavior', () => { }); }); + describe('isCaseSensitive', () => { + const sortMatchesByOptions = [ + { + label: 'Case sensitivity', + }, + ...options, + ]; + test('options "false"', () => { + const component = mount< + EuiComboBox, + EuiComboBoxProps, + { matchingOptions: TitanOption[] } + >(); + + findTestSubject(component, 'comboBoxSearchInput').simulate('change', { + target: { value: 'case' }, + }); + + expect(component.state('matchingOptions')[0].label).toBe( + 'Case sensitivity' + ); + }); + + test('options "true"', () => { + const component = mount< + EuiComboBox, + EuiComboBoxProps, + { matchingOptions: TitanOption[] } + >(); + + 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(); diff --git a/src/components/combo_box/combo_box.tsx b/src/components/combo_box/combo_box.tsx index 99d13d10855..68edd33e845 100644 --- a/src/components/combo_box/combo_box.tsx +++ b/src/components/combo_box/combo_box.tsx @@ -28,6 +28,7 @@ import { getMatchingOptions, flattenOptionGroups, getSelectedOptionForSearchValue, + SortMatchesBy, } from './matching_options'; import { EuiComboBoxInputProps, @@ -122,7 +123,11 @@ export interface _EuiComboBoxProps * `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; + /** + * 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 @@ -211,14 +216,15 @@ export class EuiComboBox extends Component< listElement: null, listPosition: 'bottom', listZIndex: undefined, - matchingOptions: getMatchingOptions( - this.props.options, - this.props.selectedOptions, - initialSearchValue, - this.props.async, - Boolean(this.props.singleSelection), - this.props.sortMatchesBy - ), + matchingOptions: getMatchingOptions({ + 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, + }), searchValue: initialSearchValue, width: 0, }; @@ -788,6 +794,8 @@ export class EuiComboBox extends Component< prevState: EuiComboBoxState ) { const { + async, + isCaseSensitive, options, selectedOptions, singleSelection, @@ -797,14 +805,15 @@ export class EuiComboBox 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> = { matchingOptions }; @@ -873,14 +882,15 @@ export class EuiComboBox 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, + }) ); } @@ -898,6 +908,7 @@ export class EuiComboBox extends Component< fullWidth, id, inputRef, + isCaseSensitive, isClearable, isDisabled, isInvalid, From 0a43b83ad6e1dcc88d42e8b0cdf33371a13d5b45 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Tue, 27 Sep 2022 13:32:59 -0500 Subject: [PATCH 3/6] CL --- upcoming_changelogs/6268.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 upcoming_changelogs/6268.md diff --git a/upcoming_changelogs/6268.md b/upcoming_changelogs/6268.md new file mode 100644 index 00000000000..a3041b03747 --- /dev/null +++ b/upcoming_changelogs/6268.md @@ -0,0 +1,2 @@ +- Added optional case sensitive option matching to `EuiComboBox` with the `isCaseSensitive` prop + From d86033698e9dbfd0dccec0943484566203535cef Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Wed, 28 Sep 2022 11:14:59 -0500 Subject: [PATCH 4/6] docs --- .../src/views/combo_box/case_sensitive.js | 84 +++++++++++++++++++ .../src/views/combo_box/combo_box_example.js | 29 +++++++ 2 files changed, 113 insertions(+) create mode 100644 src-docs/src/views/combo_box/case_sensitive.js diff --git a/src-docs/src/views/combo_box/case_sensitive.js b/src-docs/src/views/combo_box/case_sensitive.js new file mode 100644 index 00000000000..06d48a5a338 --- /dev/null +++ b/src-docs/src/views/combo_box/case_sensitive.js @@ -0,0 +1,84 @@ +import React, { useState } from 'react'; + +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 ( + + ); +}; diff --git a/src-docs/src/views/combo_box/combo_box_example.js b/src-docs/src/views/combo_box/combo_box_example.js index 9bf03d5f9ff..71b72e196f0 100644 --- a/src-docs/src/views/combo_box/combo_box_example.js +++ b/src-docs/src/views/combo_box/combo_box_example.js @@ -150,6 +150,17 @@ const virtualizedSnippet = ``; +import CaseSensitive from './case_sensitive'; +const caseSensitiveSource = require('!!raw-loader!./case_sensitive'); +const caseSensitiveSnippet = ``; + import Disabled from './disabled'; const disabledSource = require('!!raw-loader!./disabled'); const disabledSnippet = `, }, + { + title: 'Case-sensitive matching', + source: [ + { + type: GuideSectionTypes.JS, + code: caseSensitiveSource, + }, + ], + text: ( +

+ Set the prop isCaseSensitive to make the combo box + option matching case sensitive. +

+ ), + props: { EuiComboBox, EuiComboBoxOptionOption }, + snippet: caseSensitiveSnippet, + demo: , + }, { title: 'Virtualized', source: [ From c028af43185e5b0f4e99932dd152529eb8ab7f73 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Fri, 30 Sep 2022 12:10:12 -0500 Subject: [PATCH 5/6] enforce highlight case sensitivity --- src/components/combo_box/combo_box.tsx | 10 ++++- .../combo_box_options_list.tsx | 20 ++++++--- .../combo_box/matching_options.test.ts | 15 +++++-- src/components/combo_box/matching_options.ts | 43 +++++++++++++------ 4 files changed, 63 insertions(+), 25 deletions(-) diff --git a/src/components/combo_box/combo_box.tsx b/src/components/combo_box/combo_box.tsx index 68edd33e845..9b298c91dba 100644 --- a/src/components/combo_box/combo_box.tsx +++ b/src/components/combo_box/combo_box.tsx @@ -439,6 +439,7 @@ export class EuiComboBox extends Component< addCustomOption = (isContainerBlur: boolean, searchValue: string) => { const { + isCaseSensitive, onCreateOption, options, selectedOptions, @@ -462,7 +463,13 @@ export class EuiComboBox extends Component< } // Don't create the value if it's already been selected. - if (getSelectedOptionForSearchValue(searchValue, selectedOptions)) { + if ( + getSelectedOptionForSearchValue({ + isCaseSensitive, + searchValue, + selectedOptions, + }) + ) { return; } @@ -988,6 +995,7 @@ export class EuiComboBox extends Component< customOptionText={customOptionText} data-test-subj={optionsListDataTestSubj} fullWidth={fullWidth} + isCaseSensitive={isCaseSensitive} isLoading={isLoading} listRef={this.listRefCallback} matchingOptions={matchingOptions} diff --git a/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx b/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx index 50e4c6f960d..ed03565f960 100644 --- a/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx +++ b/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx @@ -56,10 +56,12 @@ export type EuiComboBoxOptionsListProps = CommonProps & */ customOptionText?: string; fullWidth?: boolean; - getSelectedOptionForSearchValue?: ( - searchValue: string, - selectedOptions: any[] - ) => EuiComboBoxOptionOption | undefined; + getSelectedOptionForSearchValue?: (params: { + isCaseSensitive?: boolean; + searchValue: string; + selectedOptions: any[]; + }) => EuiComboBoxOptionOption | undefined; + isCaseSensitive?: boolean; isLoading?: boolean; listRef: RefCallback; matchingOptions: Array>; @@ -113,6 +115,7 @@ export class EuiComboBoxOptionsList extends Component< static defaultProps = { 'data-test-subj': '', rowHeight: 29, // row height of default option renderer + isCaseSensitive: false, }; updatePosition = () => { @@ -267,6 +270,7 @@ export class EuiComboBoxOptionsList extends Component< ) : ( {label} @@ -286,6 +290,7 @@ export class EuiComboBoxOptionsList extends Component< customOptionText, fullWidth, getSelectedOptionForSearchValue, + isCaseSensitive, isLoading, listRef, matchingOptions, @@ -345,10 +350,11 @@ export class EuiComboBoxOptionsList extends Component< ); } else { - const selectedOptionForValue = getSelectedOptionForSearchValue( + const selectedOptionForValue = getSelectedOptionForSearchValue({ + isCaseSensitive, searchValue, - selectedOptions - ); + selectedOptions, + }); if (selectedOptionForValue) { // Disallow duplicate custom options. emptyStateContent = ( diff --git a/src/components/combo_box/matching_options.test.ts b/src/components/combo_box/matching_options.test.ts index 03cfe386f24..29d2dad2367 100644 --- a/src/components/combo_box/matching_options.test.ts +++ b/src/components/combo_box/matching_options.test.ts @@ -65,7 +65,10 @@ describe('getSelectedOptionForSearchValue', () => { 'data-test-subj': 'saturnOption', }; // Act - const got = getSelectedOptionForSearchValue('saturn', options); + const got = getSelectedOptionForSearchValue({ + searchValue: 'saturn', + selectedOptions: options, + }); // Assert expect(got).toMatchObject(expected); }); @@ -74,7 +77,10 @@ describe('getSelectedOptionForSearchValue', () => { describe('getSelectedOptionForSearchValue', () => { test('returns undefined when no matching option found for search value', () => { // Act - const got = getSelectedOptionForSearchValue('Pluto', options); + const got = getSelectedOptionForSearchValue({ + searchValue: 'Pluto', + selectedOptions: options, + }); // Assert expect(got).toBeUndefined(); }); @@ -85,7 +91,10 @@ describe('getSelectedOptionForSearchValue', () => { 'data-test-subj': 'saturnOption', }; // Act - const got = getSelectedOptionForSearchValue('saturn', options); + const got = getSelectedOptionForSearchValue({ + searchValue: 'saturn', + selectedOptions: options, + }); // Assert expect(got).toMatchObject(expected); }); diff --git a/src/components/combo_box/matching_options.ts b/src/components/combo_box/matching_options.ts index b0b3832ef9a..5f03ced4e1c 100644 --- a/src/components/combo_box/matching_options.ts +++ b/src/components/combo_box/matching_options.ts @@ -28,6 +28,13 @@ interface CollectMatchingOption selectedOptions: Array>; normalizedSearchValue: string; } +interface GetSelectedOptionForSearchValue + extends Pick< + GetMatchingOptions, + 'isCaseSensitive' | 'searchValue' | 'selectedOptions' + > { + optionKey?: string; +} export const flattenOptionGroups = ( optionsOrGroups: Array> @@ -48,17 +55,24 @@ export const flattenOptionGroups = ( ); }; -export const getSelectedOptionForSearchValue = ( - searchValue: string, - selectedOptions: Array>, - optionKey?: string -) => { - const normalizedSearchValue = searchValue.toLowerCase(); - return selectedOptions.find( - (option) => - option.label.toLowerCase() === normalizedSearchValue && +export const getSelectedOptionForSearchValue = ({ + isCaseSensitive, + searchValue, + selectedOptions, + optionKey, +}: GetSelectedOptionForSearchValue) => { + const normalizedSearchValue = isCaseSensitive + ? searchValue + : searchValue.toLowerCase(); + return selectedOptions.find((option) => { + const normalizedOption = isCaseSensitive + ? option.label + : option.label.toLowerCase(); + return ( + normalizedOption === normalizedSearchValue && (!optionKey || option.key === optionKey) - ); + ); + }); }; const collectMatchingOption = ({ @@ -71,11 +85,12 @@ const collectMatchingOption = ({ showPrevSelected, }: CollectMatchingOption) => { // Only show options which haven't yet been selected unless requested. - const selectedOption = getSelectedOptionForSearchValue( - option.label, + const selectedOption = getSelectedOptionForSearchValue({ + isCaseSensitive, + searchValue: option.label, selectedOptions, - option.key - ); + optionKey: option.key, + }); if (selectedOption && !showPrevSelected) { return false; } From afc26ff429c9f5b932e441db761b8b748c88e3fd Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Fri, 30 Sep 2022 15:57:27 -0500 Subject: [PATCH 6/6] account for more toLowerCase; new transform util --- src/components/combo_box/combo_box.test.tsx | 12 ++++-- src/components/combo_box/combo_box.tsx | 27 ++++++++++--- .../combo_box_options_list.tsx | 4 +- src/components/combo_box/matching_options.ts | 39 ++++++++++++------- 4 files changed, 56 insertions(+), 26 deletions(-) diff --git a/src/components/combo_box/combo_box.test.tsx b/src/components/combo_box/combo_box.test.tsx index 64c236617b8..3c702beb65c 100644 --- a/src/components/combo_box/combo_box.test.tsx +++ b/src/components/combo_box/combo_box.test.tsx @@ -462,18 +462,20 @@ describe('behavior', () => { }); describe('isCaseSensitive', () => { - const sortMatchesByOptions = [ + const isCaseSensitiveOptions = [ { label: 'Case sensitivity', }, - ...options, ]; + test('options "false"', () => { const component = mount< EuiComboBox, EuiComboBoxProps, { matchingOptions: TitanOption[] } - >(); + >( + + ); findTestSubject(component, 'comboBoxSearchInput').simulate('change', { target: { value: 'case' }, @@ -489,7 +491,9 @@ describe('behavior', () => { EuiComboBox, EuiComboBoxProps, { matchingOptions: TitanOption[] } - >(); + >( + + ); findTestSubject(component, 'comboBoxSearchInput').simulate('change', { target: { value: 'case' }, diff --git a/src/components/combo_box/combo_box.tsx b/src/components/combo_box/combo_box.tsx index 9b298c91dba..4b6803b7321 100644 --- a/src/components/combo_box/combo_box.tsx +++ b/src/components/combo_box/combo_box.tsx @@ -28,6 +28,7 @@ import { getMatchingOptions, flattenOptionGroups, getSelectedOptionForSearchValue, + transformForCaseSensitivity, SortMatchesBy, } from './matching_options'; import { @@ -497,26 +498,40 @@ export class EuiComboBox extends Component< if (this.state.matchingOptions.length !== 1) { return false; } - return ( - this.state.matchingOptions[0].label.toLowerCase() === - searchValue.toLowerCase() + const normalizedSearchSubject = transformForCaseSensitivity( + this.state.matchingOptions[0].label, + this.props.isCaseSensitive + ); + const normalizedSearchValue = transformForCaseSensitivity( + searchValue, + this.props.isCaseSensitive ); + return normalizedSearchSubject === normalizedSearchValue; }; areAllOptionsSelected = () => { - const { options, selectedOptions, async } = this.props; + const { options, selectedOptions, async, isCaseSensitive } = this.props; // Assume if this is async then there could be infinite options. if (async) { return false; } const flattenOptions = flattenOptionGroups(options).map((option) => { - return { ...option, label: option.label.trim().toLowerCase() }; + return { + ...option, + label: transformForCaseSensitivity( + option.label.trim(), + isCaseSensitive + ), + }; }); let numberOfSelectedOptions = 0; selectedOptions.forEach(({ label }) => { - const trimmedLabel = label.trim().toLowerCase(); + const trimmedLabel = transformForCaseSensitivity( + label.trim(), + isCaseSensitive + ); if ( flattenOptions.findIndex((option) => option.label === trimmedLabel) !== -1 diff --git a/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx b/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx index ed03565f960..d187538d50d 100644 --- a/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx +++ b/src/components/combo_box/combo_box_options_list/combo_box_options_list.tsx @@ -221,7 +221,7 @@ export class EuiComboBoxOptionsList extends Component< if (isGroupLabelOption) { return ( -
+
{label}
); @@ -244,7 +244,7 @@ export class EuiComboBoxOptionsList extends Component< return ( { if (onOptionClick) { onOptionClick(option); diff --git a/src/components/combo_box/matching_options.ts b/src/components/combo_box/matching_options.ts index 5f03ced4e1c..2ef3095994a 100644 --- a/src/components/combo_box/matching_options.ts +++ b/src/components/combo_box/matching_options.ts @@ -36,6 +36,11 @@ interface GetSelectedOptionForSearchValue optionKey?: string; } +export const transformForCaseSensitivity = ( + string: string, + isCaseSensitive?: boolean +) => (isCaseSensitive ? string : string.toLowerCase()); + export const flattenOptionGroups = ( optionsOrGroups: Array> ) => { @@ -61,13 +66,15 @@ export const getSelectedOptionForSearchValue = ({ selectedOptions, optionKey, }: GetSelectedOptionForSearchValue) => { - const normalizedSearchValue = isCaseSensitive - ? searchValue - : searchValue.toLowerCase(); + const normalizedSearchValue = transformForCaseSensitivity( + searchValue, + isCaseSensitive + ); return selectedOptions.find((option) => { - const normalizedOption = isCaseSensitive - ? option.label - : option.label.toLowerCase(); + const normalizedOption = transformForCaseSensitivity( + option.label, + isCaseSensitive + ); return ( normalizedOption === normalizedSearchValue && (!optionKey || option.key === optionKey) @@ -106,8 +113,10 @@ const collectMatchingOption = ({ return; } - let normalizedOption = option.label.trim(); - if (!isCaseSensitive) normalizedOption = normalizedOption.toLowerCase(); + const normalizedOption = transformForCaseSensitivity( + option.label.trim(), + isCaseSensitive + ); if (normalizedOption.includes(normalizedSearchValue)) { accumulator.push(option); } @@ -122,9 +131,10 @@ export const getMatchingOptions = ({ showPrevSelected = false, sortMatchesBy = 'none', }: GetMatchingOptions) => { - let normalizedSearchValue = searchValue.trim(); - if (!isCaseSensitive) - normalizedSearchValue = normalizedSearchValue.toLocaleLowerCase(); + const normalizedSearchValue = transformForCaseSensitivity( + searchValue.trim(), + isCaseSensitive + ); let matchingOptions: Array> = []; options.forEach((option) => { @@ -172,9 +182,10 @@ export const getMatchingOptions = ({ } = { startWith: [], others: [] }; matchingOptions.forEach((object) => { - const normalizedLabel = isCaseSensitive - ? object.label - : object.label.toLowerCase(); + const normalizedLabel = transformForCaseSensitivity( + object.label, + isCaseSensitive + ); if (normalizedLabel.startsWith(normalizedSearchValue)) { refObj.startWith.push(object); } else {