Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/components/MultiSelect/MultiSelect.scss
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ $dropdown-max-height: 20rem;
}
}

.multi-select__empty-state {
color: $colors--theme--text-muted;
margin: 0;
padding: $spv--small $sph--large;
}

.multi-select__dropdown-button {
border: 0;
margin-bottom: 0;
Expand Down
74 changes: 74 additions & 0 deletions src/components/MultiSelect/MultiSelect.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React from "react";
import { useState } from "react";
import { Formik } from "formik";
import { Meta, StoryObj } from "@storybook/react";

import { FormikField } from "../../index";
import { MultiSelect, MultiSelectItem, MultiSelectProps } from "./MultiSelect";

const Template = (props: MultiSelectProps) => {
Expand All @@ -28,6 +30,13 @@ export default meta;

type Story = StoryObj<typeof MultiSelect>;

const groupedItems = [
{ label: "Almond", value: "almond", group: "Nuts" },
{ label: "Cashew", value: "cashew", group: "Nuts" },
{ label: "Mango", value: "mango", group: "Fruit" },
{ label: "Peach", value: "peach", group: "Fruit" },
];

export const CondensedExample: Story = {
args: {
items: [
Expand Down Expand Up @@ -142,3 +151,68 @@ export const HelpText: Story = {
),
},
};

const FormikCallbacksAndEmptyStateTemplate = () => {
const [selectedItems, setSelectedItems] = useState<MultiSelectItem[]>([]);
const [events, setEvents] = useState<string[]>([]);

const addEvent = (eventName: string) => {
setEvents((previousEvents) => [eventName, ...previousEvents].slice(0, 6));
};

return (
<div style={{ maxWidth: "28rem" }}>
<Formik initialValues={{ ingredients: "" }} onSubmit={() => {}}>
<FormikField
name="ingredients"
component={MultiSelect}
label="Ingredients"
variant="search"
placeholder="Search ingredients"
items={groupedItems}
selectedItems={selectedItems}
onItemsUpdate={setSelectedItems}
onSearchChange={(value: string) => {
addEvent(`onSearchChange("${value}")`);
}}
onOpen={() => addEvent("onOpen()")}
onClose={() => addEvent("onClose()")}
emptyMessage="No ingredients found"
/>
</Formik>
<p style={{ marginBottom: "0.5rem" }}>Callback log:</p>
<ul style={{ margin: 0, paddingLeft: "1.25rem" }}>
{events.map((event, index) => (
<li key={`${event}-${index}`}>{event}</li>
))}
</ul>
</div>
);
};

export const FormikCallbacksAndEmptyState: Story = {
render: FormikCallbacksAndEmptyStateTemplate,
};

export const EmptyMessage: Story = {
args: {
variant: "search",
items: groupedItems,
emptyMessage: "No matching ingredients.",
placeholder: "Try typing kiwi",
},
};

export const EmptyStateNode: Story = {
args: {
variant: "search",
items: groupedItems,
placeholder: "Try typing kiwi",
emptyState: (
<div className="u-align--center" style={{ padding: "0.75rem 1rem" }}>
<strong>No ingredient matches.</strong>
<p style={{ margin: "0.25rem 0 0" }}>Use a broader term.</p>
</div>
),
},
};
74 changes: 74 additions & 0 deletions src/components/MultiSelect/MultiSelect.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,80 @@ it("can filter option list", async () => {
await waitFor(() => expect(screen.getAllByRole("listitem")).toHaveLength(2));
});

it("tracks search changes via onSearchChange callback", async () => {
const onSearchChange = jest.fn();
render(
<MultiSelect
variant="search"
items={items}
onSearchChange={onSearchChange}
/>,
);

await userEvent.click(screen.getByRole("combobox"));
await userEvent.type(screen.getByRole("combobox"), "item");

expect(screen.getByRole("combobox")).toHaveValue("item");
expect(onSearchChange).toHaveBeenCalledWith("i");
expect(onSearchChange).toHaveBeenCalledWith("it");
expect(onSearchChange).toHaveBeenCalledWith("ite");
expect(onSearchChange).toHaveBeenCalledWith("item");
});

it("calls lifecycle callbacks", async () => {
const onOpen = jest.fn();
const onClose = jest.fn();

render(
<MultiSelect
variant="search"
items={items}
onOpen={onOpen}
onClose={onClose}
/>,
);

await userEvent.click(screen.getByRole("combobox"));
expect(onOpen).toHaveBeenCalledTimes(1);

await userEvent.type(screen.getByRole("combobox"), "item");
await userEvent.click(document.body);

await waitFor(() => expect(onClose).toHaveBeenCalled());
expect(screen.getByRole("combobox")).toHaveValue("");
});

it("renders emptyMessage when no items match", async () => {
render(
<MultiSelect
variant="search"
items={items}
emptyMessage="No results found"
/>,
);

await userEvent.click(screen.getByRole("combobox"));
await userEvent.type(screen.getByRole("combobox"), "does not exist");

expect(screen.queryAllByRole("listitem")).toHaveLength(0);
expect(screen.getByText("No results found")).toBeInTheDocument();
});

it("renders emptyState when no items match", async () => {
render(
<MultiSelect
variant="search"
items={items}
emptyState={<div>No custom items</div>}
/>,
);

await userEvent.click(screen.getByRole("combobox"));
await userEvent.type(screen.getByRole("combobox"), "does not exist");

expect(screen.getByText("No custom items")).toBeInTheDocument();
});

it("can display a custom dropdown header and footer", async () => {
render(
<MultiSelect
Expand Down
Loading
Loading