Skip to content

Commit

Permalink
feat(filterable-select): add disableDefaultFiltering prop
Browse files Browse the repository at this point in the history
  • Loading branch information
stripeyjumper committed Jan 25, 2024
1 parent 291e175 commit 3bd9903
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -125,6 +128,7 @@ export const FilterableSelect = React.forwardRef(
inputRef,
enableVirtualScroll,
virtualScrollOverscan,
disableDefaultFiltering = false,
...textboxProps
}: FilterableSelectProps,
ref
Expand Down Expand Up @@ -626,33 +630,37 @@ export const FilterableSelect = React.forwardRef(
};
}

const selectList = (
<FilterableSelectList
ref={listboxRef}
id={selectListId.current}
labelId={labelId}
anchorElement={textboxRef?.parentElement || undefined}
onSelect={onSelectOption}
onSelectListClose={onSelectListClose}
onMouseDown={handleListMouseDown}
filterText={filterText}
highlightedValue={highlightedValue}
noResultsMessage={noResultsMessage}
disablePortal={disablePortal}
listActionButton={listActionButton}
listMaxHeight={listMaxHeight}
onListAction={handleOnListAction}
isLoading={isLoading}
onListScrollBottom={onListScrollBottom}
tableHeader={tableHeader}
multiColumn={multiColumn}
loaderDataRole="filterable-select-list-loader"
listPlacement={listPlacement}
flipEnabled={flipEnabled}
isOpen={isOpen}
enableVirtualScroll={enableVirtualScroll}
virtualScrollOverscan={virtualScrollOverscan}
>
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 ? (
<SelectList {...selectListProps}>{children}</SelectList>
) : (
<FilterableSelectList {...selectListProps} filterText={filterText}>
{children}
</FilterableSelectList>
);
Expand Down
75 changes: 53 additions & 22 deletions src/components/select/filterable-select/filterable-select.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,17 @@ a `selectionConfirmed` property on the emitted event when the enter key is press
<Story name="selection confirmed" story={stories.SelectionConfirmedStory} />
</Canvas>

### 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.

<Canvas>
<Story name="custom filtering and option styles" story={stories.CustomFilterAndOptionStyle} />
</Canvas>

## Props

### Filterable Select
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useRef } from "react";
import React, { useState, useRef, useMemo, useCallback } from "react";
import {
CustomSelectChangeEvent,
FilterableSelect,
Expand Down Expand Up @@ -753,3 +753,71 @@ export const SelectionConfirmedStory = () => {
};

SelectionConfirmedStory.parameters = { chromatic: { disableSnapshot: true } };

export const CustomFilterAndOptionStyle = () => {
const [filterText, setFilterText] = useState("");
const [selectedColor, setSelectedColor] = useState<string | undefined>();

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 (
<Box p={1}>
<Typography variant="strong" mb={2} display="block">
Selected Color:{" "}
{selectedColor ? (
<Icon type="favourite" color={selectedColor} />
) : (
"[none]"
)}
</Typography>
<FilterableSelect
onChange={handleChange}
onFilterChange={setFilterText}
name="Custom filter and option styles"
id="custom-filter-and-option-styles"
label="Color"
disableDefaultFiltering
>
{data.map(({ text, color }) => (
<Option text={text} value={color} key={color}>
<Icon type="favourite" color={color} mr={1} />
{text}
</Option>
))}
</FilterableSelect>
</Box>
);
};

CustomFilterAndOptionStyle.parameters = {
chromatic: { disableSnapshot: true },
};

0 comments on commit 3bd9903

Please sign in to comment.