Skip to content

Commit d54a491

Browse files
authored
feat(CustomSelect): add selectedLabel (#1260)
Signed-off-by: Kim Anh Nguyen <4783194+kimanhou@users.noreply.github.com>
1 parent 5552db5 commit d54a491

File tree

4 files changed

+180
-2
lines changed

4 files changed

+180
-2
lines changed

src/components/CustomSelect/CustomSelect.stories.tsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,107 @@ const generateCustomOptions = (): CustomSelectOption[] => {
5050
];
5151
};
5252

53+
const generateCustomOptionsWithSelectedLabel = () => {
54+
const options = [
55+
{
56+
type: "ovn",
57+
name: "ovntest",
58+
config: {
59+
"security.acls": "foo,bar",
60+
},
61+
},
62+
{
63+
type: "bridge",
64+
name: "lxdbr0",
65+
config: {},
66+
},
67+
{
68+
type: "bridge",
69+
name: "microbr0",
70+
config: {},
71+
},
72+
{
73+
type: "macvlan",
74+
name: "macvlantest",
75+
config: {},
76+
},
77+
].map((network) => {
78+
return {
79+
label: (
80+
<div className="label" style={{ display: "flex", gap: "5px" }}>
81+
<span
82+
title={network.name}
83+
className="network-option u-truncate"
84+
style={{ width: "12rem" }}
85+
>
86+
{network.name}
87+
</span>
88+
<span
89+
title={network.type}
90+
className="network-option u-truncate"
91+
style={{ width: "8rem" }}
92+
>
93+
{network.type}
94+
</span>
95+
<span
96+
title="network ACLs"
97+
className="network-option u-truncate u-align--right"
98+
style={{ paddingRight: "8px", width: "4rem" }}
99+
>
100+
{network.config["security.acls"]?.length || "-"}
101+
</span>
102+
</div>
103+
),
104+
value: network.name,
105+
text: `${network.name} - ${network.type}`,
106+
disabled: false,
107+
selectedLabel: (
108+
<span>
109+
{network.name}&nbsp;
110+
<span className="u-text--muted">&#40;{network.type}&#41;</span>
111+
</span>
112+
),
113+
};
114+
});
115+
116+
return options;
117+
};
118+
119+
const getHeader = () => {
120+
return (
121+
<div
122+
className="header"
123+
style={{
124+
backgroundColor: "$colors--theme--background-alt",
125+
display: "flex",
126+
gap: "$sph--small",
127+
padding: "$sph--x-small $sph--small",
128+
position: "sticky",
129+
top: 0,
130+
}}
131+
>
132+
<span
133+
className="network-option u-no-margin--bottom"
134+
style={{ color: "$colors--theme--text-default", width: "12rem" }}
135+
>
136+
Name
137+
</span>
138+
<span
139+
className="network-option u-no-margin--bottom"
140+
style={{ color: "$colors--theme--text-default", width: "8rem" }}
141+
>
142+
Type
143+
</span>
144+
<span
145+
className="network-option u-no-margin--bottom"
146+
style={{ color: "$colors--theme--text-default", width: "4rem" }}
147+
>
148+
ACLs
149+
</span>
150+
</div>
151+
);
152+
};
153+
53154
const Template = ({ ...props }: StoryProps) => {
54155
const [selected, setSelected] = useState<string>(props.value || "");
55156
return (
@@ -110,6 +211,19 @@ export const CustomOptions: Story = {
110211
},
111212
};
112213

214+
/**
215+
* If `label` is of `ReactNode` type. You can render custom content.
216+
* In this case, the `selectedLabel` for each option is provided and will be displayed in the toggle instead of `text`
217+
* The `text` property for each option is still required and is used for search and sort functionalities.
218+
*/
219+
export const CustomOptionsAndSelectedLabel: Story = {
220+
args: {
221+
options: generateCustomOptionsWithSelectedLabel(),
222+
header: getHeader(),
223+
dropdownClassName: "network-select-dropdown",
224+
},
225+
};
226+
113227
/**
114228
* For each option, if `disabled` is set to `true`, the option will be disabled.
115229
*/

src/components/CustomSelect/CustomSelect.test.tsx

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render, screen, fireEvent } from "@testing-library/react";
1+
import { render, screen, fireEvent, within } from "@testing-library/react";
22
import CustomSelect from "./CustomSelect";
33
import { CustomSelectOption } from "./CustomSelectDropdown";
44
import React from "react";
@@ -11,6 +11,18 @@ describe("CustomSelect", () => {
1111
const options: CustomSelectOption[] = [
1212
{ value: "1", label: "Option 1" },
1313
{ value: "2", label: "Option 2" },
14+
{
15+
value: "3",
16+
label: "Option 3",
17+
selectedLabel: "Selected label of 3",
18+
},
19+
{
20+
value: "4",
21+
label: "Option 4",
22+
selectedLabel: (
23+
<span data-testid="selected-label-test-id">🇯🇵 Selected label of 4</span>
24+
),
25+
},
1426
];
1527

1628
afterEach(() => {
@@ -69,4 +81,52 @@ describe("CustomSelect", () => {
6981
const option = screen.queryByRole("option", { name: "Option 2" });
7082
expect(option).not.toBeInTheDocument();
7183
});
84+
85+
it("should display the default placeholder when no option is selected", () => {
86+
render(<CustomSelect options={options} value={null} onChange={() => {}} />);
87+
expect(screen.getByText("Select an option")).toBeInTheDocument();
88+
});
89+
90+
it("should display the standard label when selected option has no selectedLabel", () => {
91+
render(<CustomSelect options={options} value="1" onChange={() => {}} />);
92+
expect(screen.getByText("Option 1")).toBeInTheDocument();
93+
});
94+
95+
it("should display the selectedLabel when the selected option has one", () => {
96+
render(<CustomSelect options={options} value="3" onChange={() => {}} />);
97+
expect(screen.getByText("Selected label of 3")).toBeInTheDocument();
98+
expect(screen.queryByText("Option 3")).not.toBeInTheDocument();
99+
});
100+
101+
it("should correctly display a React element provided as a selectedLabel", () => {
102+
render(<CustomSelect options={options} value="4" onChange={() => {}} />);
103+
104+
const span = screen.getByTestId("selected-label-test-id");
105+
expect(
106+
within(span).getByText("🇯🇵 Selected label of 4"),
107+
).toBeInTheDocument();
108+
});
109+
110+
it("should update the displayed label when the selection changes", () => {
111+
const { rerender } = render(
112+
<CustomSelect
113+
options={options}
114+
value="1" // Initially selected
115+
onChange={() => {}}
116+
/>,
117+
);
118+
119+
expect(screen.getByText("Option 1")).toBeInTheDocument();
120+
121+
rerender(
122+
<CustomSelect
123+
options={options}
124+
value="3" // New selection
125+
onChange={() => {}}
126+
/>,
127+
);
128+
129+
expect(screen.getByText("Selected label of 3")).toBeInTheDocument();
130+
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
131+
});
72132
});

src/components/CustomSelect/CustomSelect.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,9 @@ const CustomSelect = ({
126126

127127
const toggleLabel = (
128128
<span className="toggle-label u-truncate">
129-
{selectedOption ? getOptionText(selectedOption) : "Select an option"}
129+
{selectedOption
130+
? selectedOption.selectedLabel || getOptionText(selectedOption)
131+
: "Select an option"}
130132
</span>
131133
);
132134

src/components/CustomSelect/CustomSelectDropdown/CustomSelectDropdown.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export type CustomSelectOption = LiHTMLAttributes<HTMLLIElement> & {
1919
// text must be provided if label is not a string
2020
text?: string;
2121
disabled?: boolean;
22+
// What is displayed in toggle button once an option is selected. If selectedLabel is not provided, fall back onto text
23+
selectedLabel?: ReactNode;
2224
};
2325

2426
export type Props = {

0 commit comments

Comments
 (0)