From 3bd9903bc1a0575da917534f1d46af35fe8058be Mon Sep 17 00:00:00 2001 From: Andrew Bain Date: Thu, 25 Jan 2024 11:42:30 +0000 Subject: [PATCH] feat(filterable-select): add disableDefaultFiltering prop --- .../filterable-select.component.tsx | 62 ++++++++------- .../filterable-select.spec.tsx | 75 +++++++++++++------ .../filterable-select.stories.mdx | 11 +++ .../filterable-select.stories.tsx | 70 ++++++++++++++++- 4 files changed, 168 insertions(+), 50 deletions(-) diff --git a/src/components/select/filterable-select/filterable-select.component.tsx b/src/components/select/filterable-select/filterable-select.component.tsx index 64bdb73242..68978d8090 100644 --- a/src/components/select/filterable-select/filterable-select.component.tsx +++ b/src/components/select/filterable-select/filterable-select.component.tsx @@ -84,6 +84,9 @@ export interface FilterableSelectProps * Higher values make for smoother scrolling but may impact performance. * Only used if the `enableVirtualScroll` prop is set. */ virtualScrollOverscan?: number; + /** Boolean to disable automatic filtering and highlighting of options. + * This allows custom filtering and option styling to be performed outside of the component when the filter text changes. */ + disableDefaultFiltering?: boolean; } export const FilterableSelect = React.forwardRef( @@ -125,6 +128,7 @@ export const FilterableSelect = React.forwardRef( inputRef, enableVirtualScroll, virtualScrollOverscan, + disableDefaultFiltering = false, ...textboxProps }: FilterableSelectProps, ref @@ -626,33 +630,37 @@ export const FilterableSelect = React.forwardRef( }; } - const selectList = ( - + const selectListProps = { + ref: listboxRef, + id: selectListId.current, + labelId, + anchorElement: textboxRef?.parentElement || undefined, + onSelect: onSelectOption, + onSelectListClose, + onMouseDown: handleListMouseDown, + filterText, + highlightedValue, + noResultsMessage, + disablePortal, + listActionButton, + listMaxHeight, + onListAction: handleOnListAction, + isLoading, + onListScrollBottom, + tableHeader, + multiColumn, + loaderDataRole: "filterable-select-list-loader", + listPlacement, + flipEnabled, + isOpen, + enableVirtualScroll, + virtualScrollOverscan, + }; + + const selectList = disableDefaultFiltering ? ( + {children} + ) : ( + {children} ); diff --git a/src/components/select/filterable-select/filterable-select.spec.tsx b/src/components/select/filterable-select/filterable-select.spec.tsx index 9603cff3c1..691c4c5e7a 100644 --- a/src/components/select/filterable-select/filterable-select.spec.tsx +++ b/src/components/select/filterable-select/filterable-select.spec.tsx @@ -1334,33 +1334,64 @@ describe("FilterableSelect", () => { wrapper.find(StyledSelectListContainer) ); }); -}); -describe("coverage filler for else path", () => { - const wrapper = renderSelect(); - simulateSelectTextboxEvent(wrapper, "blur"); -}); + describe("coverage filler for else path", () => { + const wrapper = renderSelect(); + simulateSelectTextboxEvent(wrapper, "blur"); + }); -describe("when maxWidth is passed", () => { - it("should be passed to InputPresentation", () => { - const wrapper = renderSelect({ maxWidth: "67%" }); + describe("when maxWidth is passed", () => { + it("should be passed to InputPresentation", () => { + const wrapper = renderSelect({ maxWidth: "67%" }); - assertStyleMatch( - { - maxWidth: "67%", - }, - wrapper.find(InputPresentation) - ); + assertStyleMatch( + { + maxWidth: "67%", + }, + wrapper.find(InputPresentation) + ); + }); + + it("renders with maxWidth as 100% when no maxWidth is specified", () => { + const wrapper = renderSelect({ maxWidth: "" }); + + assertStyleMatch( + { + maxWidth: "100%", + }, + wrapper.find(InputPresentation) + ); + }); }); + describe("when the disableDefaultFiltering prop is set", () => { + it('shows all options when "disableDefaultFiltering" is true', () => { + const wrapper = renderSelect({ disableDefaultFiltering: true }); - it("renders with maxWidth as 100% when no maxWidth is specified", () => { - const wrapper = renderSelect({ maxWidth: "" }); + simulateSelectTextboxEvent(wrapper, "change", { + target: { value: "red" }, + }); - assertStyleMatch( - { - maxWidth: "100%", - }, - wrapper.find(InputPresentation) - ); + expect(wrapper.find(Option)).toHaveLength(4); + }); + + it('hides filtered options when "disableDefaultFiltering" is false', () => { + const wrapper = renderSelect({ disableDefaultFiltering: false }); + + simulateSelectTextboxEvent(wrapper, "change", { + target: { value: "red" }, + }); + + expect(wrapper.find(Option)).toHaveLength(1); + }); + + it('hides filtered options when "disableDefaultFiltering" is unspecified', () => { + const wrapper = renderSelect(); + + simulateSelectTextboxEvent(wrapper, "change", { + target: { value: "red" }, + }); + + expect(wrapper.find(Option)).toHaveLength(1); + }); }); }); diff --git a/src/components/select/filterable-select/filterable-select.stories.mdx b/src/components/select/filterable-select/filterable-select.stories.mdx index 63c4047edd..c3710afc46 100644 --- a/src/components/select/filterable-select/filterable-select.stories.mdx +++ b/src/components/select/filterable-select/filterable-select.stories.mdx @@ -177,6 +177,17 @@ a `selectionConfirmed` property on the emitted event when the enter key is press +### Custom filtering and option styles + +By default, filtering and highlighting of options is handled by the component itself. In order to use custom filtering behaviour, or to use custom styling of option values, the +default filtering can be disabled using the `disableDefaultFiltering` prop. + +This allows use-cases like server-side filtering of options, or rich formatting of options. + + + + + ## Props ### Filterable Select diff --git a/src/components/select/filterable-select/filterable-select.stories.tsx b/src/components/select/filterable-select/filterable-select.stories.tsx index 2156c13099..8cf26b3e3c 100644 --- a/src/components/select/filterable-select/filterable-select.stories.tsx +++ b/src/components/select/filterable-select/filterable-select.stories.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from "react"; +import React, { useState, useRef, useMemo, useCallback } from "react"; import { CustomSelectChangeEvent, FilterableSelect, @@ -753,3 +753,71 @@ export const SelectionConfirmedStory = () => { }; SelectionConfirmedStory.parameters = { chromatic: { disableSnapshot: true } }; + +export const CustomFilterAndOptionStyle = () => { + const [filterText, setFilterText] = useState(""); + const [selectedColor, setSelectedColor] = useState(); + + const data = useMemo( + () => + [ + { text: "Amber", color: "#FFBF00" }, + { text: "Black", color: "#000000" }, + { text: "Blue", color: "#0000FF" }, + { text: "Brown", color: "#A52A2A" }, + { text: "Green", color: "#008000" }, + { text: "Orange", color: "#FFA500" }, + { text: "Pink", color: "#FFC0CB" }, + { text: "Purple", color: "#800080" }, + { text: "Red", color: "#FF0000" }, + { text: "White", color: "#FFFFFF" }, + { text: "Yellow", color: "#FFFF00" }, + ].filter( + ({ text }) => + !filterText || + (filterText.trim().length && + text.toLowerCase().includes(filterText.trim().toLowerCase())) + ), + [filterText] + ); + + const handleChange = useCallback((e: CustomSelectChangeEvent) => { + if (e.selectionConfirmed && e.target?.value) { + setSelectedColor(e.target.value as string); + } else { + setSelectedColor(undefined); + } + }, []); + + return ( + + + Selected Color:{" "} + {selectedColor ? ( + + ) : ( + "[none]" + )} + + + {data.map(({ text, color }) => ( + + ))} + + + ); +}; + +CustomFilterAndOptionStyle.parameters = { + chromatic: { disableSnapshot: true }, +};