Skip to content

Commit

Permalink
✨(react) add select multi options custom render
Browse files Browse the repository at this point in the history
We want to be able to render the options in a customized manner.
  • Loading branch information
NathanVss committed Oct 6, 2023
1 parent 11f4eae commit 71b30eb
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 8 deletions.
1 change: 1 addition & 0 deletions packages/react/src/components/Forms/Select/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,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;
Expand Down
12 changes: 8 additions & 4 deletions packages/react/src/components/Forms/Select/multi-common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -160,7 +161,10 @@ export const SelectMultiAux = ({
index,
})}
>
<span>{selectedItemForRender.label}</span>
<SelectedOption
option={selectedItemForRender}
{...props}
/>
<Button
tabIndex={-1}
color="tertiary"
Expand Down Expand Up @@ -210,7 +214,7 @@ export const SelectMultiAux = ({
index,
})}
>
<span>{option.label}</span>
<span>{renderOption(option)}</span>
</li>
);
})}
Expand Down
234 changes: 233 additions & 1 deletion packages/react/src/components/Forms/Select/multi.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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",
});
});
});
});
35 changes: 34 additions & 1 deletion packages/react/src/components/Forms/Select/multi.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -216,6 +219,36 @@ 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,
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);

Expand Down
7 changes: 5 additions & 2 deletions packages/react/src/components/Forms/Select/multi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 71b30eb

Please sign in to comment.