Skip to content

Commit 3385d73

Browse files
author
Hector Arce De Las Heras
committed
Fix issues with inputSearch and review recommendation logic
This commit addresses several issues with the inputSearch component and reviews the recommendation logic. 1. Fixed an issue where an option selected in the inputSearch was not marked as selected. 2. Reviewed the recommendation logic to ensure it is necessary and functioning as expected. 3. Fixed an issue where, when `noResultText` was not set and `showResultTextWrittenByUser` was set to true, no text was displayed in the visor when no option matched. These fixes improve the usability of the inputSearch component and ensure the recommendation logic is effective and necessary.
1 parent a9e887b commit 3385d73

File tree

9 files changed

+63
-133
lines changed

9 files changed

+63
-133
lines changed

src/components/inputSearch/__tests__/helpers.test.tsx

Lines changed: 21 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
1-
import {
2-
filterOptions,
3-
findBestMatch,
4-
getAriaControls,
5-
getLength,
6-
hasMatchWithOptions,
7-
} from '../helpers';
8-
import { InputSearchBestMatch } from '../types/inputSearch';
1+
import { filterOptions, getAriaControls, getLength, hasMatchWithOptions } from '../helpers';
92

103
const optionsToFilter = [
114
{
@@ -35,61 +28,44 @@ describe('Input Search Helpers', () => {
3528
];
3629
expect(filterOptions('st', optionsToFilter)).toEqual({
3730
optionsFiltered: filteredOptions,
38-
recomemededOption: undefined,
3931
});
4032
});
4133

4234
test('Should filter options without value', async () => {
4335
expect(filterOptions(undefined, optionsToFilter)).toEqual({ optionsFiltered: optionsToFilter });
4436
});
4537

46-
test('Should filter options with no options filtered', async () => {
47-
const filteredOptions = {
48-
optionsFiltered: [{ options: [] }],
49-
recommendedOption: undefined,
50-
};
51-
expect(filterOptions('ra', optionsToFilter)).toStrictEqual(filteredOptions);
52-
});
53-
5438
test('Should get options lenght', async () => {
5539
expect(getLength(options)).toEqual(4);
5640
});
5741

5842
test('Should get ariaControls', async () => {
5943
expect(getAriaControls(options, 'ariaControls')).toStrictEqual('ariaControls0 ariaControls1');
6044
});
61-
test('findBestMatch function should return 1', () => {
62-
const objA = { list: 1, bestMatch: { key1: 5 }, bestMatchkey: 'key1' };
63-
const objB = { list: 1, bestMatch: { key1: 10 }, bestMatchkey: 'key1' };
6445

65-
expect(findBestMatch(objA, objB)).toBe(1);
46+
test('should return empty array when options is empty', () => {
47+
const result = filterOptions('test', []);
48+
expect(result.optionsFiltered).toEqual([]);
6649
});
67-
test('findBestMatch function should return -1', async () => {
68-
const objA: InputSearchBestMatch = {
69-
list: 1,
70-
bestMatch: { label: 'label1', value: '2' },
71-
bestMatchkey: 'value',
72-
};
73-
const objB: InputSearchBestMatch = {
74-
list: 1,
75-
bestMatch: { label: 'label2', value: '1' },
76-
bestMatchkey: 'value',
77-
};
78-
expect(findBestMatch(objA, objB)).toBe(-1);
50+
51+
test('should return original options when value is undefined', () => {
52+
const options = [{ options: ['test1', 'test2'] }];
53+
const result = filterOptions(undefined, options);
54+
expect(result.optionsFiltered).toEqual(options);
7955
});
80-
test('findBestMatch function should return 0', async () => {
81-
const objA: InputSearchBestMatch = {
82-
list: 1,
83-
bestMatch: { label: 'label1', value: '2' },
84-
bestMatchkey: 'value',
85-
};
86-
const objB: InputSearchBestMatch = {
87-
list: 1,
88-
bestMatch: { label: 'label2', value: '2' },
89-
bestMatchkey: 'value',
90-
};
91-
expect(findBestMatch(objA, objB)).toBe(0);
56+
57+
test('should filter options based on value', () => {
58+
const options = [{ options: ['test1', 'test2', 'test3'] }];
59+
const result = filterOptions('test1', options);
60+
expect(result.optionsFiltered).toEqual([{ options: ['test1'] }]);
9261
});
62+
63+
test('should return original options when no match is found', () => {
64+
const options = [{ options: ['test1', 'test2', 'test3'] }];
65+
const result = filterOptions('test4', options);
66+
expect(result.optionsFiltered).toEqual(options);
67+
});
68+
9369
test('check match', () => {
9470
const options = ['first', 'second', 'thrid'];
9571
const correctValue = 'first';

src/components/inputSearch/components/optionsList.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export const OptionsListComponent = (
4646
{props.stylesListOption?.optionVariant && props.stylesListOption?.variant && (
4747
<ListOptions
4848
ref={sendRef}
49+
caseSensitive={props.caseSensitive}
4950
charsHighlighted={props.searchText?.toString()}
5051
dataTestId={props.dataTestId}
5152
hightlightedOptionVariant={props.stylesListOption?.hightlightedOptionVariant}

src/components/inputSearch/components/popoverSearchList.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const PopoverSearchListComponent = (
3030
const useActionBottomSheet = props.styles?.[props.state]?.useActionBottomSheet?.[props.device];
3131
const labelInResultTextWrittenByUser = useActionBottomSheet
3232
? props.inputConfiguration?.value?.toString()
33-
: props.recommendedOption?.toString() ?? props.searchText?.toString();
33+
: props.searchText?.toString();
3434
// Behavior with "use this" option
3535
const showTextWritten: InputSearchOptionType | undefined = useMemo(
3636
() =>
@@ -63,6 +63,7 @@ export const PopoverSearchListComponent = (
6363
key={index}
6464
ref={ref}
6565
aria-controls={`${props['aria-controls']}${index}`}
66+
caseSensitive={props.caseSensitive}
6667
dataTestId={`${props.dataTestId}OptionsList${index}`}
6768
hightlightedOption={showTextWritten || showHighlightedOption}
6869
index={index}
Lines changed: 18 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IOptionGroup, InputSearchBestMatch, InputSearchFilterOptionReturnValue } from '../types';
1+
import { IOptionGroup, InputSearchFilterOptionReturnValue } from '../types';
22

33
// eslint-disable-next-line complexity
44
export const filterOptions = (
@@ -10,95 +10,43 @@ export const filterOptions = (
1010
if (options.length === 0) {
1111
return { optionsFiltered: [] };
1212
}
13-
if (!value) {
13+
if (!value || String(value).length < suggestInit) {
1414
return { optionsFiltered: options };
1515
}
1616
const re = new RegExp(wordSeparator);
1717
// Cloned option list structure
1818
const optionsFiltered = structuredClone(options);
19-
// Array to save the best option from each list
20-
const betterMatches: InputSearchBestMatch[] = [];
21-
// Array to save the text when it matches
22-
const wordCount: string[] = [];
23-
// Simply condition
24-
const full = true;
2519

2620
for (let i = 0; i < optionsFiltered.length; i++) {
2721
// Array to save matches options
2822
const optionsAvailable: string[] = [];
29-
// Array to register the index of the matches options
30-
const optionsPushed: number[] = [];
31-
// Object to record how many times an option is repeated
32-
const betterMatchesForlist = {};
3323
String(value)
3424
.split(re)
3525
.map(text =>
3626
optionsFiltered[i].options.filter(opt => {
3727
if (text.length && String(opt).toLocaleLowerCase().includes(text.toLocaleLowerCase())) {
38-
//Save text when it matches
39-
if (!wordCount.includes(text)) {
40-
wordCount.push(text);
41-
}
42-
// Check if the matched option has already been stored
43-
const optionPosition = optionsFiltered[i].options.indexOf(opt);
44-
if (!optionsPushed.includes(optionPosition)) {
45-
// If the option was not registered, it saves its index, registers the option and creates an entry in the object to check if it is repeated in the future
46-
optionsPushed.push(optionPosition);
47-
optionsAvailable.push(opt);
48-
betterMatchesForlist[optionPosition] = 1;
49-
} else {
50-
// If the option exists, add the match
51-
betterMatchesForlist[optionPosition]++;
52-
}
28+
optionsAvailable.push(opt);
5329
}
5430
})
5531
);
56-
// Register the best match from each list, if exists
57-
if (Object.keys(betterMatchesForlist).length) {
58-
betterMatches.push({
59-
list: i,
60-
bestMatchkey: findTheHighestKey(betterMatchesForlist),
61-
bestMatch: findTheHighestKey(betterMatchesForlist, full),
62-
});
63-
}
64-
// Overwrites the options list with the filtered values
32+
6533
optionsFiltered[i].options = optionsAvailable;
6634
}
6735

68-
// Returns the best match from all lists
69-
const betterMatch = betterMatches.sort(findBestMatch)[0];
70-
// Returns the recommended option, based on the best match
71-
const recommendedOption =
72-
wordCount.length > suggestInit && betterMatch
73-
? options[betterMatch.list].options[betterMatch.bestMatchkey]
74-
: undefined;
75-
return { optionsFiltered, recommendedOption };
76-
};
77-
78-
// Function to return the option with the highest agreement from the list
79-
function findTheHighestKey(obj: object, full?: boolean) {
80-
const highestValue = -Infinity;
81-
let highest;
82-
for (const [key, value] of Object.entries(obj)) {
83-
// Check if the current value is a number and greater than the current highestValue
84-
if (typeof value === 'number' && value > highestValue) {
85-
highest = full ? { [key]: value } : key;
86-
}
87-
}
88-
return highest;
89-
}
36+
const hasValue = optionsFiltered.some(option => option.options.length > 0);
9037

91-
// Function to reorder (from highest to lowest) the list with the best match
92-
export function findBestMatch(objA: InputSearchBestMatch, objB: InputSearchBestMatch): number {
93-
if (objA.bestMatch?.[objA.bestMatchkey] > objB.bestMatch?.[objB.bestMatchkey]) {
94-
return -1;
95-
} else if (objA.bestMatch?.[objA.bestMatchkey] < objB.bestMatch?.[objB.bestMatchkey]) {
96-
return 1;
97-
}
98-
return 0;
99-
}
38+
return {
39+
optionsFiltered: hasValue ? optionsFiltered : options,
40+
};
41+
};
10042

101-
export const hasMatchWithOptions = (inputValue: string, options: string[]): boolean => {
102-
const hasMatch = options.find(option => option === inputValue);
103-
return !!hasMatch;
43+
export const hasMatchWithOptions = (
44+
inputValue: string,
45+
options: string[],
46+
caseSensitive?: boolean
47+
): boolean => {
48+
const hasMatch = caseSensitive
49+
? options.some(option => option === inputValue)
50+
: options.some(option => option.toLocaleLowerCase() === inputValue.toLocaleLowerCase());
51+
return hasMatch;
10452
};

src/components/inputSearch/hooks/useInputSearch.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type ParamsType = {
5454
informationAssociated?: string;
5555
maxLength?: number;
5656
searchFilterConfig?: SearchFilterConfig;
57+
caseSensitive?: boolean;
5758
onClick?: (event: React.MouseEvent<HTMLInputElement, MouseEvent>) => void;
5859
onIconClick?: React.MouseEventHandler<HTMLButtonElement>;
5960
executeInternalOpenOptions?: boolean;
@@ -75,7 +76,6 @@ type ReturnType = {
7576
searchText: string;
7677
inputPopoverText: string;
7778
optionsFiltered: IOptionGroup[];
78-
recommendedOption: string | undefined;
7979
handleOpenOptions: (open: boolean) => void;
8080
handleClickInputSearch: React.MouseEventHandler<HTMLInputElement>;
8181
handleIconClick: React.MouseEventHandler<HTMLButtonElement>;
@@ -185,7 +185,7 @@ export const useInputSearch = ({
185185
const handleInputBlur = event => {
186186
props.onBlur?.(event);
187187
const allOptions = props.options.map(option => option.options).flat();
188-
const hasMatch = hasMatchWithOptions(searchText, allOptions);
188+
const hasMatch = hasMatchWithOptions(searchText, allOptions, props.caseSensitive);
189189
// if the input loses focus and there is no valid option in the input value, show an error message
190190
if (!hasMatch && !props.hasResultTextWrittenByUser && !props.disableErrorInvalidOption) {
191191
addInternalError(InternalErrorType.INVALID_OPTION);
@@ -289,7 +289,7 @@ export const useInputSearch = ({
289289
};
290290

291291
// Filter options
292-
const { optionsFiltered, recommendedOption } = filterOptions(
292+
const { optionsFiltered } = filterOptions(
293293
useActionBottomSheet ? inputPopoverText : searchText,
294294
props.options,
295295
props.searchFilterConfig?.wordSeparator,
@@ -301,7 +301,6 @@ export const useInputSearch = ({
301301
searchText,
302302
inputPopoverText,
303303
optionsFiltered,
304-
recommendedOption,
305304
showHighlightedOption,
306305
handleOpenOptions,
307306
handleClickInputSearch,

src/components/inputSearch/inputSearch.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const InputSearchComponent = React.forwardRef(
4343
ctv,
4444
blockBackPopover = false,
4545
searchFilterConfig,
46+
caseSensitive,
4647
...props
4748
}: IInputSearch<V>,
4849
ref: React.ForwardedRef<HTMLInputElement | undefined>
@@ -53,7 +54,6 @@ const InputSearchComponent = React.forwardRef(
5354
const {
5455
openOptions,
5556
optionsFiltered,
56-
recommendedOption,
5757
searchText,
5858
inputPopoverText,
5959
valueSearchSelected,
@@ -102,6 +102,7 @@ const InputSearchComponent = React.forwardRef(
102102
onOptionClick,
103103
onInternalErrors,
104104
searchFilterConfig,
105+
caseSensitive,
105106
});
106107

107108
return (
@@ -111,6 +112,7 @@ const InputSearchComponent = React.forwardRef(
111112
innerRef as unknown as React.ForwardedRef<HTMLInputElement | undefined | null> | undefined
112113
}
113114
blockBackPopover={blockBackPopover}
115+
caseSensitive={caseSensitive}
114116
device={device}
115117
hasHighlightedOption={showHighlightedOption}
116118
hasResultTextWrittenByUser={hasResultTextWrittenByUser}
@@ -122,7 +124,6 @@ const InputSearchComponent = React.forwardRef(
122124
maxLength={maxLength}
123125
open={openOptions}
124126
optionList={optionsFiltered}
125-
recommendedOption={recommendedOption}
126127
searchText={searchText}
127128
state={state}
128129
styles={styles}

src/components/inputSearch/inputSearchStandAlone.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export const InputSearchStandAloneComponent = (
6060
ref={ref}
6161
aria-controls={ariaControls}
6262
blockBackPopover={props.blockBackPopover}
63+
caseSensitive={props.caseSensitive}
6364
closeIcon={props.closeIcon}
6465
dataTestId={props.dataTestId}
6566
device={props.device}
@@ -94,7 +95,6 @@ export const InputSearchStandAloneComponent = (
9495
(refInput as MutableRefObject<HTMLInputElement | null | undefined>)?.current,
9596
(refIcon as MutableRefObject<HTMLSpanElement | null | undefined>)?.current,
9697
]}
97-
recommendedOption={props.recommendedOption}
9898
searchText={props.searchText}
9999
state={props.state}
100100
styles={props.styles}

src/components/inputSearch/stories/argtypes.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,17 @@ export const argtypes = (variants: IThemeObjectVariants, themeSelected: string):
320320
category: CATEGORY_CONTROL.FUNCTIONS,
321321
},
322322
},
323+
caseSensitive: {
324+
description: 'Indicates if the search is case sensitive',
325+
control: { type: 'boolean' },
326+
type: { name: 'boolean' },
327+
table: {
328+
type: {
329+
summary: 'boolean',
330+
},
331+
category: CATEGORY_CONTROL.MODIFIERS,
332+
},
333+
},
323334
['aria-controls']: {
324335
table: {
325336
disable: true,

0 commit comments

Comments
 (0)