Skip to content

Commit 8e05fad

Browse files
feat(CustomSelect): expose searchPlaceholder and default toggle label and add async option to searchable (#1280)
1 parent 2ab3134 commit 8e05fad

File tree

5 files changed

+94
-9
lines changed

5 files changed

+94
-9
lines changed

src/components/CustomSelect/CustomSelect.stories.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,14 @@ const meta: Meta<StoryProps> = {
169169
args: {
170170
name: "customSelect",
171171
label: "Custom Select",
172+
defaultToggleLabel: "Select an option",
172173
searchable: "auto",
174+
searchPlaceholder: "Search",
173175
initialPosition: "left",
174176
},
175177
argTypes: {
176178
searchable: {
177-
options: ["auto", "always", "never"],
179+
options: ["auto", "always", "async", "never"],
178180
control: {
179181
type: "select",
180182
},
@@ -255,3 +257,14 @@ export const ManualSearchable: Story = {
255257
searchable: "always",
256258
},
257259
};
260+
261+
/**
262+
* Search can be enabled manually by setting `searchable` to `async`.
263+
* This will always show the search input regardless of the number of options.
264+
*/
265+
export const AsyncSearchable: Story = {
266+
args: {
267+
options: generateStandardOptions(0),
268+
searchable: "async",
269+
},
270+
};

src/components/CustomSelect/CustomSelect.test.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ describe("CustomSelect", () => {
8787
expect(screen.getByText("Select an option")).toBeInTheDocument();
8888
});
8989

90+
it("should allow customizing the placeholder when no option is selected", () => {
91+
render(
92+
<CustomSelect
93+
options={options}
94+
defaultToggleLabel="Custom label"
95+
value={null}
96+
onChange={() => {}}
97+
/>,
98+
);
99+
expect(screen.getByText("Custom label")).toBeInTheDocument();
100+
});
101+
90102
it("should display the standard label when selected option has no selectedLabel", () => {
91103
render(<CustomSelect options={options} value="1" onChange={() => {}} />);
92104
expect(screen.getByText("Option 1")).toBeInTheDocument();

src/components/CustomSelect/CustomSelect.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,34 @@ export type Props = PropsWithSpread<
3838
name?: string;
3939
// Whether if the select is disabled
4040
disabled?: boolean;
41+
/**
42+
* Toggle label when no option is selected
43+
*
44+
* @default - "Select an option"
45+
*/
46+
defaultToggleLabel?: string;
4147
// Styling for the wrapping Field component
4248
wrapperClassName?: ClassName;
4349
// The styling for the select toggle button
4450
toggleClassName?: ClassName;
4551
// The styling for the select dropdown
4652
dropdownClassName?: string;
47-
// Whether the select is searchable. Option "auto" is the default, the select will be searchable if it has 5 or more options.
48-
searchable?: "auto" | "always" | "never";
53+
/**
54+
* Whether the select is searchable.
55+
* - `auto`: the select will be searchable if it has 5 or more options.
56+
* - `always`: the select will always be searchable if there is at least 1 option.
57+
* - `async`: the select will always be searchable.
58+
* - `never`: the select will never be searchable.
59+
*
60+
* @default - "auto"
61+
*/
62+
searchable?: "auto" | "always" | "async" | "never";
63+
/**
64+
* Placeholder text for the search input when searchable is enabled.
65+
*
66+
* @default - "Search"
67+
*/
68+
searchPlaceholder?: string;
4969
// Whether to focus on the element on initial render.
5070
takeFocus?: boolean;
5171
// Additional component to display above the dropdown list.
@@ -76,7 +96,9 @@ const CustomSelect = ({
7696
wrapperClassName,
7797
toggleClassName,
7898
dropdownClassName,
99+
defaultToggleLabel = "Select an option",
79100
searchable = "auto",
101+
searchPlaceholder = "Search",
80102
takeFocus,
81103
header,
82104
selectRef,
@@ -128,7 +150,7 @@ const CustomSelect = ({
128150
<span className="toggle-label u-truncate">
129151
{selectedOption
130152
? selectedOption.selectedLabel || getOptionText(selectedOption)
131-
: "Select an option"}
153+
: defaultToggleLabel}
132154
</span>
133155
);
134156

@@ -188,6 +210,7 @@ const CustomSelect = ({
188210
{(close: () => void) => (
189211
<CustomSelectDropdown
190212
searchable={searchable}
213+
searchPlaceholder={searchPlaceholder}
191214
onSearch={onSearch}
192215
name={name || ""}
193216
options={options || []}

src/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,15 @@ describe("CustomSelectDropdown", () => {
141141
expect(searchBox).toBeInTheDocument();
142142
});
143143

144+
it("should render the search box when `searchable` is `async` and there are no options", () => {
145+
setup("async", []);
146+
147+
const searchBox = screen.queryByRole("textbox", {
148+
name: /search for test-dropdown/i,
149+
});
150+
expect(searchBox).toBeInTheDocument();
151+
});
152+
144153
it("should not render search box when `searchable` is `never`", () => {
145154
setup("never");
146155

src/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.tsx

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ export type CustomSelectOption = LiHTMLAttributes<HTMLLIElement> & {
2424
};
2525

2626
export type Props = {
27-
searchable?: "auto" | "always" | "never";
27+
searchable?: "auto" | "always" | "async" | "never";
28+
searchPlaceholder?: string;
2829
name: string;
2930
options: CustomSelectOption[];
3031
onSelect: (value: string) => void;
@@ -140,8 +141,36 @@ export const getOptionText = (option: CustomSelectOption): string => {
140141
);
141142
};
142143

144+
const getIsSearchable = (
145+
searchable: Props["searchable"],
146+
numberOfOptions: number,
147+
) => {
148+
if (searchable === "async") {
149+
return true;
150+
}
151+
152+
if (searchable === "never") {
153+
return false;
154+
}
155+
156+
if (numberOfOptions <= 1) {
157+
return false;
158+
}
159+
160+
if (searchable === "always") {
161+
return true;
162+
}
163+
164+
if (searchable === "auto" && numberOfOptions >= 5) {
165+
return true;
166+
}
167+
168+
return false;
169+
};
170+
143171
const CustomSelectDropdown: FC<Props> = ({
144172
searchable,
173+
searchPlaceholder,
145174
name,
146175
options,
147176
onSelect,
@@ -158,10 +187,8 @@ const CustomSelectDropdown: FC<Props> = ({
158187
const dropdownRef = useRef<HTMLDivElement>(null);
159188
const searchRef = useRef<HTMLInputElement>(null);
160189
const dropdownListRef = useRef<HTMLUListElement>(null);
161-
const isSearchable =
162-
searchable !== "never" &&
163-
options.length > 1 &&
164-
(searchable === "always" || (searchable === "auto" && options.length >= 5));
190+
191+
const isSearchable = getIsSearchable(searchable, options.length);
165192

166193
useEffect(() => {
167194
if (dropdownRef.current) {
@@ -324,6 +351,7 @@ const CustomSelectDropdown: FC<Props> = ({
324351
name={`select-search-${name}`}
325352
type="text"
326353
aria-label={`Search for ${name}`}
354+
placeholder={searchPlaceholder}
327355
className="u-no-margin--bottom"
328356
onChange={handleSearch}
329357
value={search}

0 commit comments

Comments
 (0)