The tea shop's list has a Sort teas button that opens a short menu of ways to sort. Build App: a menu button that takes label (the button's text), items (a list of strings), and onSelect. When they aren't given, as in the Preview, use Sort teas, a few items of your own, and an onSelect that logs the choice.
The button has aria-haspopup="menu" and aria-expanded ("false" while closed, "true" while open). The menu is a <ul role="menu">, labelled by the button through aria-labelledby, with one <li role="menuitem" tabIndex={-1}> per item. While closed, the menu isn't in the page.
Clicking the button opens the menu, and so do Enter, Space, and ArrowDown while the button has the focus. Opening it moves the focus to the first item. Clicking the button again closes it.
In the open menu, ArrowDown and ArrowUp move the focus to the next and previous item, wrapping around at the ends; Home and End go to the first and last.
Clicking an item, or pressing Enter on it, calls onSelect with the item's string, closes the menu, and puts the focus back on the button. Escape closes the menu and puts the focus back on the button too. Clicking anywhere outside the menu and its button closes it.
<App label="Sort teas" items={["Name", "Price", "Newest"]} onSelect={(item) => setSort(item)} />
// renders, while open:
<button id="b" aria-haspopup="menu" aria-expanded="true">Sort teas</button>
<ul role="menu" aria-labelledby="b">
<li role="menuitem" tabindex="-1">Name</li>
<li role="menuitem" tabindex="-1">Price</li>
<li role="menuitem" tabindex="-1">Newest</li>
</ul>
const onSelect = spy();
const items = ["Name", "Price", "Newest"];
render(<App label="Sort teas" items={items} onSelect={onSelect} />);
const button = screen.getByRole("button", { name: "Sort teas" });
expect(button).toHaveAttribute("aria-haspopup", "menu");
expect(button).toHaveAttribute("aria-expanded", "false");
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
const onSelect = spy();
const items = ["Name", "Price", "Newest"];
render(<App label="Sort teas" items={items} onSelect={onSelect} />);
await user.click(screen.getByRole("button", { name: "Sort teas" }));
expect(screen.getByRole("button", { name: "Sort teas" })).toHaveAttribute("aria-expanded", "true");
const menu = screen.getByRole("menu", { name: "Sort teas" });
expect(within(menu).getAllByRole("menuitem").map((item) => item.textContent)).toEqual(items);
expect(screen.getByRole("menuitem", { name: "Name" })).toHaveFocus();
const onSelect = spy();
const items = ["Name", "Price", "Newest"];
render(<App label="Sort teas" items={items} onSelect={onSelect} />);
await user.tab();
await user.keyboard("{ArrowDown}");
expect(screen.getByRole("menuitem", { name: "Name" })).toHaveFocus();
await user.keyboard("{Escape}");
await user.keyboard("{Enter}");
expect(screen.getByRole("menu")).toBeInTheDocument();
expect(screen.getByRole("menuitem", { name: "Name" })).toHaveFocus();
await user.keyboard("{Escape}");
await user.keyboard(" ");
expect(screen.getByRole("menu")).toBeInTheDocument();
expect(screen.getByRole("menuitem", { name: "Name" })).toHaveFocus();
const onSelect = spy();
const items = ["Name", "Price", "Newest"];
render(<App label="Sort teas" items={items} onSelect={onSelect} />);
await user.click(screen.getByRole("button", { name: "Sort teas" }));
await user.keyboard("{ArrowDown}");
expect(screen.getByRole("menuitem", { name: "Price" })).toHaveFocus();
await user.keyboard("{ArrowDown}{ArrowDown}");
expect(screen.getByRole("menuitem", { name: "Name" })).toHaveFocus();
await user.keyboard("{ArrowUp}");
expect(screen.getByRole("menuitem", { name: "Newest" })).toHaveFocus();
const onSelect = spy();
const items = ["Name", "Price", "Newest"];
render(<App label="Sort teas" items={items} onSelect={onSelect} />);
await user.click(screen.getByRole("button", { name: "Sort teas" }));
await user.keyboard("{End}");
expect(screen.getByRole("menuitem", { name: "Newest" })).toHaveFocus();
await user.keyboard("{Home}");
expect(screen.getByRole("menuitem", { name: "Name" })).toHaveFocus();
const onSelect = spy();
const items = ["Name", "Price", "Newest"];
render(<App label="Sort teas" items={items} onSelect={onSelect} />);
await user.click(screen.getByRole("button", { name: "Sort teas" }));
await user.keyboard("{ArrowDown}{Enter}");
expect(onSelect).toHaveBeenCalledTimes(1);
expect(onSelect).toHaveBeenCalledWith("Price");
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Sort teas" })).toHaveFocus();
expect(screen.getByRole("button", { name: "Sort teas" })).toHaveAttribute("aria-expanded", "false");
const onSelect = spy();
const items = ["Name", "Price", "Newest"];
render(<App label="Sort teas" items={items} onSelect={onSelect} />);
await user.click(screen.getByRole("button", { name: "Sort teas" }));
await user.click(screen.getByRole("menuitem", { name: "Newest" }));
expect(onSelect).toHaveBeenCalledWith("Newest");
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Sort teas" })).toHaveFocus();
const onSelect = spy();
const items = ["Name", "Price", "Newest"];
render(<App label="Sort teas" items={items} onSelect={onSelect} />);
await user.click(screen.getByRole("button", { name: "Sort teas" }));
await user.keyboard("{ArrowDown}{Escape}");
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Sort teas" })).toHaveFocus();
expect(onSelect).not.toHaveBeenCalled();
const onSelect = spy();
const items = ["Name", "Price", "Newest"];
render(
<div>
<App label="Sort teas" items={items} onSelect={onSelect} />
<p>Our teas</p>
</div>,
);
const button = screen.getByRole("button", { name: "Sort teas" });
await user.click(button);
await user.click(button);
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
await user.click(button);
await user.click(screen.getByText("Our teas"));
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
expect(button).toHaveAttribute("aria-expanded", "false");
expect(onSelect).not.toHaveBeenCalled();
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Dropdown menu. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.