(react) add select multi options custom render

We want to be able to render the options in a customized manner.
This commit is contained in:
Nathan Vasse
2023-10-06 16:55:30 +02:00
committed by NathanVss
parent 48e4e56a44
commit b86ba5cc8e
6 changed files with 295 additions and 8 deletions

View File

@@ -255,6 +255,7 @@
margin-bottom: 0.25rem;
max-width: var(--c--components--forms-select--multi-pill-max-width);
font-size: var(--c--components--forms-select--multi-pill-font-size);
vertical-align: middle;
> span {
min-width: 0;

View File

@@ -5,12 +5,13 @@ import { Field } from ":/components/Forms/Field";
import { LabelledBox } from ":/components/Forms/LabelledBox";
import { Button } from ":/components/Button";
import { useCunningham } from ":/components/Provider";
import { Option } from ":/components/Forms/Select/mono";
import { SelectProps } from ":/components/Forms/Select";
import { Option, SelectProps } from ":/components/Forms/Select";
import {
getOptionsFilter,
optionToValue,
renderOption,
} from ":/components/Forms/Select/mono-common";
import { SelectedOption } from ":/components/Forms/Select/utils";
/**
* This method returns a comparator that can be used to filter out options for multi select.
@@ -160,7 +161,10 @@ export const SelectMultiAux = ({
index,
})}
>
<span>{selectedItemForRender.label}</span>
<SelectedOption
option={selectedItemForRender}
{...props}
/>
<Button
tabIndex={-1}
color="tertiary"
@@ -210,7 +214,7 @@ export const SelectMultiAux = ({
index,
})}
>
<span>{option.label}</span>
<span>{renderOption(option)}</span>
</li>
);
})}

View File

@@ -88,6 +88,19 @@ For some reasons you might want to hide the label of the Multi-Select. You can d
<Story id="components-forms-select-multi--hidden-label"/>
</Canvas>
## Custom render option
You can give customize the look of the options by providing `render` callback.
> When you provide `render` the fields `label` and `value` are mandatory.
Feel free to use the attribute `showLabelWhenSelected` to choose whether you want to display selected option with the custom
HTML or with its `label`. It is set to `true` by default.
<Canvas sourceState="shown">
<Story id="components-forms-select-multi--searchable-custom-render"/>
</Canvas>
## Controlled / Non Controlled
Like a native select, you can use the Select component in a controlled or non controlled way. You can see the example below

View File

@@ -4,7 +4,11 @@ import React, { createRef, FormEvent, useState } from "react";
import { expect } from "vitest";
import { within } from "@testing-library/dom";
import { CunninghamProvider } from ":/components/Provider";
import { Select, SelectHandle } from ":/components/Forms/Select/index";
import {
Select,
SelectHandle,
SelectProps,
} from ":/components/Forms/Select/index";
import {
expectMenuToBeClosed,
expectMenuToBeOpen,
@@ -834,6 +838,106 @@ describe("<Select multi={true} />", () => {
await waitFor(() => expectMenuToBeClosed(menu));
expect(document.activeElement?.className).toEqual("");
});
it("renders custom options", async () => {
const Wrapper = (props: SelectProps) => {
return (
<CunninghamProvider>
<Select {...props} />
</CunninghamProvider>
);
};
const props: SelectProps = {
label: "City",
multi: true,
options: [
{
label: "Paris",
value: "paris",
render: () => (
<div>
<img src="paris.png" alt="Paris flag" />
Paris
</div>
),
},
{
label: "Panama",
value: "panama",
render: () => (
<div>
<img src="panama.png" alt="Panama flag" />
Panama
</div>
),
},
{
label: "London",
value: "london",
render: () => (
<div>
<img src="london.png" alt="London flag" />
London
</div>
),
},
],
};
const { rerender } = render(<Wrapper {...props} />);
const input = screen.getByRole("combobox", {
name: "City",
});
const menu: HTMLDivElement = screen.getByRole("listbox", {
name: "City",
});
const valueRendered = document.querySelector(
".c__select__inner__value",
) as HTMLElement;
const user = userEvent.setup();
expectSelectedOptions([]);
await user.click(input);
expectMenuToBeOpen(menu);
screen.getByRole("img", { name: "Paris flag" });
screen.getByRole("img", { name: "Panama flag" });
screen.getByRole("img", { name: "London flag" });
// Select Paris
await user.click(
screen.getByRole("option", { name: "Paris flag Paris" }),
);
await user.click(
screen.getByRole("option", { name: "London flag London" }),
);
// Make sure only the label is rendered by default.
expectSelectedOptions(["Paris", "London"]);
expect(
within(valueRendered).queryByRole("img", {
name: "Paris flag",
}),
).not.toBeInTheDocument();
expect(
within(valueRendered).queryByRole("img", {
name: "London flag",
}),
).not.toBeInTheDocument();
// Now showLabelWhenSelected to false.
rerender(<Wrapper {...props} showLabelWhenSelected={false} />);
// Make sure the HTML content of the options is rendered.
expectSelectedOptions(["Paris", "London"]);
within(valueRendered).getByRole("img", {
name: "Paris flag",
});
within(valueRendered).getByRole("img", {
name: "London flag",
});
});
});
describe("Searchable", async () => {
@@ -1442,5 +1546,133 @@ describe("<Select multi={true} />", () => {
await waitFor(() => expectMenuToBeClosed(menu));
expect(document.activeElement?.tagName).toEqual("BODY");
});
it("renders custom options", async () => {
const Wrapper = (props: SelectProps) => {
return (
<CunninghamProvider>
<Select {...props} />
</CunninghamProvider>
);
};
const props: SelectProps = {
label: "City",
multi: true,
searchable: true,
options: [
{
label: "Paris",
value: "paris",
render: () => (
<div>
<img src="paris.png" alt="Paris flag" />
Paris
</div>
),
},
{
label: "Panama",
value: "panama",
render: () => (
<div>
<img src="panama.png" alt="Panama flag" />
Panama
</div>
),
},
{
label: "London",
value: "london",
render: () => (
<div>
<img src="london.png" alt="London flag" />
London
</div>
),
},
],
};
const { rerender } = render(<Wrapper {...props} />);
const input = screen.getByRole("combobox", {
name: "City",
});
const menu: HTMLDivElement = screen.getByRole("listbox", {
name: "City",
});
const valueRendered = document.querySelector(
".c__select__inner__value",
) as HTMLElement;
const user = userEvent.setup();
expectSelectedOptions([]);
await user.click(input);
expectMenuToBeOpen(menu);
screen.getByRole("img", { name: "Paris flag" });
screen.getByRole("img", { name: "Panama flag" });
screen.getByRole("img", { name: "London flag" });
// Filter options.
await user.type(input, "Pa");
screen.getByRole("img", { name: "Paris flag" });
screen.getByRole("img", { name: "Panama flag" });
expect(
screen.queryByRole("img", { name: "London flag" }),
).not.toBeInTheDocument();
// Select Paris
await user.click(
screen.getByRole("option", { name: "Paris flag Paris" }),
);
// Filter to find London.
await user.clear(input);
expect(
screen.queryByRole("img", { name: "Paris flag" }),
).not.toBeInTheDocument();
screen.getByRole("img", { name: "Panama flag" });
screen.getByRole("img", { name: "London flag" });
await user.type(input, "Lo");
expect(
screen.queryByRole("img", { name: "Paris flag" }),
).not.toBeInTheDocument();
expect(
screen.queryByRole("img", { name: "Panama flag" }),
).not.toBeInTheDocument();
screen.getByRole("img", { name: "London flag" });
// Select London.
await user.click(
screen.getByRole("option", { name: "London flag London" }),
);
// Make sure only the label is rendered by default.
expectSelectedOptions(["Paris", "London"]);
expect(
within(valueRendered).queryByRole("img", {
name: "Paris flag",
}),
).not.toBeInTheDocument();
expect(
within(valueRendered).queryByRole("img", {
name: "London flag",
}),
).not.toBeInTheDocument();
// Now showLabelWhenSelected to false.
rerender(<Wrapper {...props} showLabelWhenSelected={false} />);
// Make sure the HTML content of the options is rendered.
expectSelectedOptions(["Paris", "London"]);
within(valueRendered).getByRole("img", {
name: "Paris flag",
});
within(valueRendered).getByRole("img", {
name: "London flag",
});
});
});
});

View File

@@ -7,7 +7,10 @@ import { faker } from "@faker-js/faker";
import { onSubmit } from ":/components/Forms/Examples/ReactHookForm/reactHookFormUtils";
import { Select, SelectHandle } from ":/components/Forms/Select";
import { Button } from ":/components/Button";
import { RhfSelect } from ":/components/Forms/Select/stories-utils";
import {
getCountryOption,
RhfSelect,
} from ":/components/Forms/Select/stories-utils";
export default {
title: "Components/Forms/Select/Multi",
@@ -216,6 +219,37 @@ export const NoOptions = {
},
};
export const CustomRender = {
render: Template,
args: {
label: "Select a country",
showLabelWhenSelected: false,
options: [
getCountryOption("Germany", "DE"),
getCountryOption("France", "FR"),
getCountryOption("United States", "US"),
getCountryOption("Spain", "ES"),
getCountryOption("China", "CN"),
],
},
};
export const SearchableCustomRender = {
render: Template,
args: {
label: "Select a country",
showLabelWhenSelected: false,
searchable: true,
defaultValue: ["france", "united states"],
options: [
getCountryOption("Germany", "DE"),
getCountryOption("France", "FR"),
getCountryOption("United States", "US"),
getCountryOption("Spain", "ES"),
getCountryOption("China", "CN"),
],
},
};
export const Ref = () => {
const ref = useRef<SelectHandle>(null);

View File

@@ -3,8 +3,11 @@ import { optionToValue } from ":/components/Forms/Select/mono-common";
import { SelectMultiSearchable } from ":/components/Forms/Select/multi-searchable";
import { SelectMultiSimple } from ":/components/Forms/Select/multi-simple";
import { SubProps } from ":/components/Forms/Select/multi-common";
import { Option } from ":/components/Forms/Select/mono";
import { SelectHandle, SelectProps } from ":/components/Forms/Select/index";
import {
Option,
SelectHandle,
SelectProps,
} from ":/components/Forms/Select/index";
export type SelectMultiProps = Omit<SelectProps, "onChange"> & {
onChange?: (event: { target: { value: string[] } }) => void;