✨(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:
@@ -255,6 +255,7 @@
|
|||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
max-width: var(--c--components--forms-select--multi-pill-max-width);
|
max-width: var(--c--components--forms-select--multi-pill-max-width);
|
||||||
font-size: var(--c--components--forms-select--multi-pill-font-size);
|
font-size: var(--c--components--forms-select--multi-pill-font-size);
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
> span {
|
> span {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import { Field } from ":/components/Forms/Field";
|
|||||||
import { LabelledBox } from ":/components/Forms/LabelledBox";
|
import { LabelledBox } from ":/components/Forms/LabelledBox";
|
||||||
import { Button } from ":/components/Button";
|
import { Button } from ":/components/Button";
|
||||||
import { useCunningham } from ":/components/Provider";
|
import { useCunningham } from ":/components/Provider";
|
||||||
import { Option } from ":/components/Forms/Select/mono";
|
import { Option, SelectProps } from ":/components/Forms/Select";
|
||||||
import { SelectProps } from ":/components/Forms/Select";
|
|
||||||
import {
|
import {
|
||||||
getOptionsFilter,
|
getOptionsFilter,
|
||||||
optionToValue,
|
optionToValue,
|
||||||
|
renderOption,
|
||||||
} from ":/components/Forms/Select/mono-common";
|
} 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.
|
* This method returns a comparator that can be used to filter out options for multi select.
|
||||||
@@ -160,7 +161,10 @@ export const SelectMultiAux = ({
|
|||||||
index,
|
index,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span>{selectedItemForRender.label}</span>
|
<SelectedOption
|
||||||
|
option={selectedItemForRender}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
color="tertiary"
|
color="tertiary"
|
||||||
@@ -210,7 +214,7 @@ export const SelectMultiAux = ({
|
|||||||
index,
|
index,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span>{option.label}</span>
|
<span>{renderOption(option)}</span>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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"/>
|
<Story id="components-forms-select-multi--hidden-label"/>
|
||||||
</Canvas>
|
</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
|
## 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
|
Like a native select, you can use the Select component in a controlled or non controlled way. You can see the example below
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import React, { createRef, FormEvent, useState } from "react";
|
|||||||
import { expect } from "vitest";
|
import { expect } from "vitest";
|
||||||
import { within } from "@testing-library/dom";
|
import { within } from "@testing-library/dom";
|
||||||
import { CunninghamProvider } from ":/components/Provider";
|
import { CunninghamProvider } from ":/components/Provider";
|
||||||
import { Select, SelectHandle } from ":/components/Forms/Select/index";
|
import {
|
||||||
|
Select,
|
||||||
|
SelectHandle,
|
||||||
|
SelectProps,
|
||||||
|
} from ":/components/Forms/Select/index";
|
||||||
import {
|
import {
|
||||||
expectMenuToBeClosed,
|
expectMenuToBeClosed,
|
||||||
expectMenuToBeOpen,
|
expectMenuToBeOpen,
|
||||||
@@ -834,6 +838,106 @@ describe("<Select multi={true} />", () => {
|
|||||||
await waitFor(() => expectMenuToBeClosed(menu));
|
await waitFor(() => expectMenuToBeClosed(menu));
|
||||||
expect(document.activeElement?.className).toEqual("");
|
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 () => {
|
describe("Searchable", async () => {
|
||||||
@@ -1442,5 +1546,133 @@ describe("<Select multi={true} />", () => {
|
|||||||
await waitFor(() => expectMenuToBeClosed(menu));
|
await waitFor(() => expectMenuToBeClosed(menu));
|
||||||
expect(document.activeElement?.tagName).toEqual("BODY");
|
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",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import { faker } from "@faker-js/faker";
|
|||||||
import { onSubmit } from ":/components/Forms/Examples/ReactHookForm/reactHookFormUtils";
|
import { onSubmit } from ":/components/Forms/Examples/ReactHookForm/reactHookFormUtils";
|
||||||
import { Select, SelectHandle } from ":/components/Forms/Select";
|
import { Select, SelectHandle } from ":/components/Forms/Select";
|
||||||
import { Button } from ":/components/Button";
|
import { Button } from ":/components/Button";
|
||||||
import { RhfSelect } from ":/components/Forms/Select/stories-utils";
|
import {
|
||||||
|
getCountryOption,
|
||||||
|
RhfSelect,
|
||||||
|
} from ":/components/Forms/Select/stories-utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Components/Forms/Select/Multi",
|
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 = () => {
|
export const Ref = () => {
|
||||||
const ref = useRef<SelectHandle>(null);
|
const ref = useRef<SelectHandle>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ import { optionToValue } from ":/components/Forms/Select/mono-common";
|
|||||||
import { SelectMultiSearchable } from ":/components/Forms/Select/multi-searchable";
|
import { SelectMultiSearchable } from ":/components/Forms/Select/multi-searchable";
|
||||||
import { SelectMultiSimple } from ":/components/Forms/Select/multi-simple";
|
import { SelectMultiSimple } from ":/components/Forms/Select/multi-simple";
|
||||||
import { SubProps } from ":/components/Forms/Select/multi-common";
|
import { SubProps } from ":/components/Forms/Select/multi-common";
|
||||||
import { Option } from ":/components/Forms/Select/mono";
|
import {
|
||||||
import { SelectHandle, SelectProps } from ":/components/Forms/Select/index";
|
Option,
|
||||||
|
SelectHandle,
|
||||||
|
SelectProps,
|
||||||
|
} from ":/components/Forms/Select/index";
|
||||||
|
|
||||||
export type SelectMultiProps = Omit<SelectProps, "onChange"> & {
|
export type SelectMultiProps = Omit<SelectProps, "onChange"> & {
|
||||||
onChange?: (event: { target: { value: string[] } }) => void;
|
onChange?: (event: { target: { value: string[] } }) => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user