From 9ab870fc38174fa0215b8dbd58485a87f15b9835 Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso Date: Tue, 2 Dec 2025 12:37:51 +0100 Subject: [PATCH 1/3] Add searchable select with 'startsWith' mode and update filtering logic --- packages/lib/src/select/Select.stories.tsx | 28 ++++++++++++++++++++++ packages/lib/src/select/Select.tsx | 6 ++++- packages/lib/src/select/types.ts | 6 +++++ packages/lib/src/select/utils.ts | 21 +++++++++++----- 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/packages/lib/src/select/Select.stories.tsx b/packages/lib/src/select/Select.stories.tsx index bb15eee65..788d1250d 100644 --- a/packages/lib/src/select/Select.stories.tsx +++ b/packages/lib/src/select/Select.stories.tsx @@ -35,6 +35,13 @@ const singleOptions = [ { label: "Option 04", value: "4" }, ]; +const startsWithSingleOptions = [ + { label: "Option 01", value: "1" }, + { label: "This is option 02", value: "2" }, + { label: "Is option 03", value: "3" }, + { label: "And Option 04", value: "4" }, +]; + const single_options_virtualized = [ ...Array.from({ length: 10000 }, (_, i) => ({ label: `Option ${String(i + 1).padStart(2, "0")}`, @@ -593,6 +600,19 @@ const SearchableSelect = () => ( ); +const startsWithSearchableSelect = () => ( + + + <DxcSelect + label="Select Label" + searchable + searchMode="startsWith" + options={startsWithSingleOptions} + placeholder="Choose an option" + /> + </ExampleContainer> +); + const SearchValue = () => ( <ExampleContainer expanded> <Title title="Searchable select with value" theme="light" level={4} /> @@ -740,6 +760,14 @@ export const Searchable: Story = { }, }; +export const StartsWithSearchable: Story = { + render: startsWithSearchableSelect, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.type(await canvas.findByRole("combobox"), "t"); + }, +}; + export const SearchableWithValue: Story = { render: SearchValue, play: async ({ canvasElement }) => { diff --git a/packages/lib/src/select/Select.tsx b/packages/lib/src/select/Select.tsx index 1a044ce52..a1718a170 100644 --- a/packages/lib/src/select/Select.tsx +++ b/packages/lib/src/select/Select.tsx @@ -191,6 +191,7 @@ const DxcSelect = forwardRef<RefType, SelectPropsType>( options, placeholder = "", searchable = false, + searchMode = "contains", size = "medium", tabIndex = 0, value, @@ -217,7 +218,10 @@ const DxcSelect = forwardRef<RefType, SelectPropsType>( const translatedLabels = useContext(HalstackLanguageContext); const optionalItem = useMemo(() => ({ label: placeholder, value: "" }), [placeholder]); - const filteredOptions = useMemo(() => filterOptionsBySearchValue(options, searchValue), [options, searchValue]); + const filteredOptions = useMemo( + () => filterOptionsBySearchValue(options, searchValue, searchMode), + [options, searchValue, searchMode] + ); const lastOptionIndex = useMemo( () => getLastOptionIndex(options, filteredOptions, searchable, optional, multiple, enableSelectAll), [options, filteredOptions, searchable, optional, multiple, enableSelectAll] diff --git a/packages/lib/src/select/types.ts b/packages/lib/src/select/types.ts index 0ea85c328..a7bb09609 100644 --- a/packages/lib/src/select/types.ts +++ b/packages/lib/src/select/types.ts @@ -91,6 +91,12 @@ type CommonProps = { * If true, enables search functionality. */ searchable?: boolean; + /** + * Defines the search mode when searchable is true. + * 'contains' (default): Matches options that contain the search text anywhere in their label. + * 'startsWith': Matches options that start with the search text. + */ + searchMode?: "contains" | "startsWith"; /** * Size of the component. */ diff --git a/packages/lib/src/select/utils.ts b/packages/lib/src/select/utils.ts index 83af7d14b..77b37da63 100644 --- a/packages/lib/src/select/utils.ts +++ b/packages/lib/src/select/utils.ts @@ -51,20 +51,29 @@ export const canOpenListbox = (options: Props["options"], disabled: boolean) => /** * Filters the options by the search value. */ -export const filterOptionsBySearchValue = (options: Props["options"], searchValue: string): Props["options"] => - options.length > 0 +export const filterOptionsBySearchValue = ( + options: Props["options"], + searchValue: string, + searchMode: Props["searchMode"] = "contains" +): Props["options"] => { + const matchesSearch = (label: string, search: string, mode: Props["searchMode"]) => { + const upperLabel = label.toUpperCase(); + const upperSearch = search.toUpperCase(); + return mode === "startsWith" ? upperLabel.startsWith(upperSearch) : upperLabel.includes(upperSearch); + }; + + return options.length > 0 ? isArrayOfGroupedOptions(options) ? options.map((optionGroup) => { const group = { label: optionGroup.label, - options: optionGroup.options.filter((option) => - option.label.toUpperCase().includes(searchValue.toUpperCase()) - ), + options: optionGroup.options.filter((option) => matchesSearch(option.label, searchValue, searchMode)), }; return group; }) - : options.filter((option) => option.label.toUpperCase().includes(searchValue.toUpperCase())) + : options.filter((option) => matchesSearch(option.label, searchValue, searchMode)) : []; +}; /** * Returns the index of the last option, depending on several conditions. From 8db1390197f59daf524122dee568c1e8369a720d Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso <pfelguerosogalguera@gmail.com> Date: Tue, 2 Dec 2025 13:11:39 +0100 Subject: [PATCH 2/3] Add searchMode prop documentation to SelectCodePage --- .../components/select/code/SelectCodePage.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/website/screens/components/select/code/SelectCodePage.tsx b/apps/website/screens/components/select/code/SelectCodePage.tsx index 502d7f507..67ef6cc04 100644 --- a/apps/website/screens/components/select/code/SelectCodePage.tsx +++ b/apps/website/screens/components/select/code/SelectCodePage.tsx @@ -255,6 +255,22 @@ const sections = [ <TableCode>false</TableCode> </td> </tr> + <tr> + <td> + <DxcFlex direction="column" gap="var(--spacing-gap-xs)" alignItems="baseline"> + <StatusBadge status="new" /> + searchMode + </DxcFlex> + </td> + <td> + <TableCode>'contains' | 'startsWith'</TableCode> + </td> + <td> + Defines the search mode when searchable is true.'contains' (default): Matches options that contain the + search text anywhere in their label. 'startsWith': Matches options that start with the search text. + </td> + <td>- </td> + </tr> <tr> <td>size</td> <td> From 87f1f0dc3acb09fd90b0b2a4f4c1a48a9acf083d Mon Sep 17 00:00:00 2001 From: PelayoFelgueroso <pfelguerosogalguera@gmail.com> Date: Tue, 2 Dec 2025 14:48:12 +0100 Subject: [PATCH 3/3] refactor: rename searchMode to searchByStartsWith and update related logic --- .../components/select/code/SelectCodePage.tsx | 10 +++++----- packages/lib/src/select/Select.stories.tsx | 2 +- packages/lib/src/select/Select.tsx | 6 +++--- packages/lib/src/select/types.ts | 6 +++--- packages/lib/src/select/utils.ts | 12 +++++++----- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/apps/website/screens/components/select/code/SelectCodePage.tsx b/apps/website/screens/components/select/code/SelectCodePage.tsx index 67ef6cc04..82ed44dbb 100644 --- a/apps/website/screens/components/select/code/SelectCodePage.tsx +++ b/apps/website/screens/components/select/code/SelectCodePage.tsx @@ -259,17 +259,17 @@ const sections = [ <td> <DxcFlex direction="column" gap="var(--spacing-gap-xs)" alignItems="baseline"> <StatusBadge status="new" /> - searchMode + searchByStartsWith </DxcFlex> </td> <td> - <TableCode>'contains' | 'startsWith'</TableCode> + <TableCode>boolean</TableCode> </td> <td> - Defines the search mode when searchable is true.'contains' (default): Matches options that contain the - search text anywhere in their label. 'startsWith': Matches options that start with the search text. + Defines the search mode when searchable is true. If true, matches options that start with the search text. + If false, matches options that contain the search text anywhere in their label. </td> - <td>- </td> + <td>false</td> </tr> <tr> <td>size</td> diff --git a/packages/lib/src/select/Select.stories.tsx b/packages/lib/src/select/Select.stories.tsx index 788d1250d..097f1482c 100644 --- a/packages/lib/src/select/Select.stories.tsx +++ b/packages/lib/src/select/Select.stories.tsx @@ -606,7 +606,7 @@ const startsWithSearchableSelect = () => ( <DxcSelect label="Select Label" searchable - searchMode="startsWith" + searchByStartsWith options={startsWithSingleOptions} placeholder="Choose an option" /> diff --git a/packages/lib/src/select/Select.tsx b/packages/lib/src/select/Select.tsx index a1718a170..5fa9a0e82 100644 --- a/packages/lib/src/select/Select.tsx +++ b/packages/lib/src/select/Select.tsx @@ -191,7 +191,7 @@ const DxcSelect = forwardRef<RefType, SelectPropsType>( options, placeholder = "", searchable = false, - searchMode = "contains", + searchByStartsWith = false, size = "medium", tabIndex = 0, value, @@ -219,8 +219,8 @@ const DxcSelect = forwardRef<RefType, SelectPropsType>( const optionalItem = useMemo(() => ({ label: placeholder, value: "" }), [placeholder]); const filteredOptions = useMemo( - () => filterOptionsBySearchValue(options, searchValue, searchMode), - [options, searchValue, searchMode] + () => filterOptionsBySearchValue(options, searchValue, searchByStartsWith), + [options, searchValue, searchByStartsWith] ); const lastOptionIndex = useMemo( () => getLastOptionIndex(options, filteredOptions, searchable, optional, multiple, enableSelectAll), diff --git a/packages/lib/src/select/types.ts b/packages/lib/src/select/types.ts index a7bb09609..adcea907f 100644 --- a/packages/lib/src/select/types.ts +++ b/packages/lib/src/select/types.ts @@ -93,10 +93,10 @@ type CommonProps = { searchable?: boolean; /** * Defines the search mode when searchable is true. - * 'contains' (default): Matches options that contain the search text anywhere in their label. - * 'startsWith': Matches options that start with the search text. + * If true, matches options that start with the search text. + * If false, matches options that contain the search text anywhere in their label. */ - searchMode?: "contains" | "startsWith"; + searchByStartsWith?: boolean; /** * Size of the component. */ diff --git a/packages/lib/src/select/utils.ts b/packages/lib/src/select/utils.ts index 77b37da63..1720aa34e 100644 --- a/packages/lib/src/select/utils.ts +++ b/packages/lib/src/select/utils.ts @@ -54,12 +54,12 @@ export const canOpenListbox = (options: Props["options"], disabled: boolean) => export const filterOptionsBySearchValue = ( options: Props["options"], searchValue: string, - searchMode: Props["searchMode"] = "contains" + searchByStartsWith: Props["searchByStartsWith"] = false ): Props["options"] => { - const matchesSearch = (label: string, search: string, mode: Props["searchMode"]) => { + const matchesSearch = (label: string, search: string, searchByStartsWith: boolean) => { const upperLabel = label.toUpperCase(); const upperSearch = search.toUpperCase(); - return mode === "startsWith" ? upperLabel.startsWith(upperSearch) : upperLabel.includes(upperSearch); + return searchByStartsWith ? upperLabel.startsWith(upperSearch) : upperLabel.includes(upperSearch); }; return options.length > 0 @@ -67,11 +67,13 @@ export const filterOptionsBySearchValue = ( ? options.map((optionGroup) => { const group = { label: optionGroup.label, - options: optionGroup.options.filter((option) => matchesSearch(option.label, searchValue, searchMode)), + options: optionGroup.options.filter((option) => + matchesSearch(option.label, searchValue, searchByStartsWith) + ), }; return group; }) - : options.filter((option) => matchesSearch(option.label, searchValue, searchMode)) + : options.filter((option) => matchesSearch(option.label, searchValue, searchByStartsWith)) : []; };