-
Notifications
You must be signed in to change notification settings - Fork 844
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] Optional case sensitive option matching #6268
Merged
Merged
Changes from 10 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
b7c8762
refactor API and introduce isCaseSensitive
thompsongl 1dd5634
refactor utils; add isCaseSensitiveProp
thompsongl 0a43b83
CL
thompsongl 33a509b
Merge branch 'main' into 1997-case
thompsongl 60bf62d
Merge branch 'main' into 1997-case
thompsongl d860336
docs
thompsongl c849cd5
Merge branch 'main' into 1997-case
thompsongl c028af4
enforce highlight case sensitivity
thompsongl 66e08de
Merge branch 'main' into 1997-case
thompsongl afc26ff
account for more toLowerCase; new transform util
thompsongl b0cc13f
Merge branch 'main' into 1997-case
thompsongl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<EuiComboBox | ||
aria-label="Accessible screen reader label" | ||
placeholder="Select or create options" | ||
options={options} | ||
selectedOptions={selectedOptions} | ||
onChange={onChange} | ||
onCreateOption={onCreateOption} | ||
isClearable={true} | ||
isCaseSensitive | ||
/> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,8 @@ import { | |
getMatchingOptions, | ||
flattenOptionGroups, | ||
getSelectedOptionForSearchValue, | ||
transformForCaseSensitivity, | ||
SortMatchesBy, | ||
} from './matching_options'; | ||
import { | ||
EuiComboBoxInputProps, | ||
|
@@ -122,7 +124,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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -211,14 +217,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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
}; | ||
|
@@ -433,6 +440,7 @@ export class EuiComboBox<T> extends Component< | |
|
||
addCustomOption = (isContainerBlur: boolean, searchValue: string) => { | ||
const { | ||
isCaseSensitive, | ||
onCreateOption, | ||
options, | ||
selectedOptions, | ||
|
@@ -456,7 +464,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; | ||
} | ||
|
||
cee-chen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
@@ -484,26 +498,40 @@ export class EuiComboBox<T> 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 | ||
|
@@ -788,6 +816,8 @@ export class EuiComboBox<T> extends Component< | |
prevState: EuiComboBoxState<T> | ||
) { | ||
const { | ||
async, | ||
isCaseSensitive, | ||
options, | ||
selectedOptions, | ||
singleSelection, | ||
|
@@ -797,14 +827,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 }; | ||
|
||
|
@@ -873,14 +904,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, | ||
}) | ||
); | ||
} | ||
|
||
|
@@ -898,6 +930,7 @@ export class EuiComboBox<T> extends Component< | |
fullWidth, | ||
id, | ||
inputRef, | ||
isCaseSensitive, | ||
isClearable, | ||
isDisabled, | ||
isInvalid, | ||
|
@@ -977,6 +1010,7 @@ export class EuiComboBox<T> extends Component< | |
customOptionText={customOptionText} | ||
data-test-subj={optionsListDataTestSubj} | ||
fullWidth={fullWidth} | ||
isCaseSensitive={isCaseSensitive} | ||
isLoading={isLoading} | ||
listRef={this.listRefCallback} | ||
matchingOptions={matchingOptions} | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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