The tea shop's order form has a box where you start typing a tea's name and pick it from a list. Build App: it takes options (a list of strings), label (the box's label), and onSelect. When they aren't given, as in the Preview, use a few tea names of your own, the label Tea, and an onSelect that logs the pick.
Render a <label> and a text <input role="combobox" aria-autocomplete="list"> it labels. As you type, show the options that contain the text, ignoring case and the spaces around it, in their original order: a <ul role="listbox"> with one <li role="option"> each. When the box is empty (or only spaces), there's no list. When the text matches nothing, show No matches instead of the list.
The input's aria-expanded is "true" while the listbox is showing and "false" otherwise (also while No matches shows). While it shows, the input's aria-controls is the listbox's id.
The focus always stays in the box. ArrowDown and ArrowUp move a highlight through the options instead: the highlighted option has aria-selected="true" (the others "false"), and the input's aria-activedescendant is that option's id. With nothing highlighted, the input has no aria-activedescendant. ArrowDown from nothing goes to the first option and ArrowUp to the last; both wrap around at the ends. While no list shows, the arrows do nothing.
Typing clears the highlight. Enter picks the highlighted option: the box's text becomes the option, the list closes, and onSelect is called with it. Enter with nothing highlighted does nothing. Clicking an option picks it the same way, and the focus stays in the box. Escape closes the list but keeps the text; typing again opens it.
<App label="Tea" options={["Earl Grey", "Green", "Sencha"]} onSelect={(tea) => addToOrder(tea)} />
// after typing "gr" and pressing ArrowDown:
<label for="tea">Tea</label>
<input id="tea" role="combobox" aria-autocomplete="list" aria-expanded="true"
aria-controls="tea-list" aria-activedescendant="tea-option-0" value="gr" />
<ul id="tea-list" role="listbox">
<li id="tea-option-0" role="option" aria-selected="true">Earl Grey</li>
<li id="tea-option-1" role="option" aria-selected="false">Green</li>
</ul>
const teas = ["Assam", "Darjeeling", "Earl Grey", "Genmaicha", "Green", "Jasmine", "Oolong", "Rooibos", "Sencha"];
const onSelect = spy();
render(<App options={teas} label="Tea" onSelect={onSelect} />);
const box = screen.getByRole("combobox", { name: "Tea" });
expect(box).toHaveAttribute("aria-autocomplete", "list");
expect(box).toHaveAttribute("aria-expanded", "false");
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
const teas = ["Assam", "Darjeeling", "Earl Grey", "Genmaicha", "Green", "Jasmine", "Oolong", "Rooibos", "Sencha"];
const onSelect = spy();
render(<App options={teas} label="Tea" onSelect={onSelect} />);
const box = screen.getByRole("combobox", { name: "Tea" });
await user.type(box, "GR");
const list = screen.getByRole("listbox");
expect(within(list).getAllByRole("option").map((option) => option.textContent)).toEqual(["Earl Grey", "Green"]);
expect(box).toHaveAttribute("aria-expanded", "true");
expect(box).toHaveAttribute("aria-controls", list.id);
await user.clear(box);
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
const teas = ["Assam", "Darjeeling", "Earl Grey", "Genmaicha", "Green", "Jasmine", "Oolong", "Rooibos", "Sencha"];
const onSelect = spy();
render(<App options={teas} label="Tea" onSelect={onSelect} />);
const box = screen.getByRole("combobox", { name: "Tea" });
await user.type(box, "xyz");
expect(screen.getByText("No matches")).toBeInTheDocument();
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
expect(box).toHaveAttribute("aria-expanded", "false");
const teas = ["Assam", "Darjeeling", "Earl Grey", "Genmaicha", "Green", "Jasmine", "Oolong", "Rooibos", "Sencha"];
const onSelect = spy();
render(<App options={teas} label="Tea" onSelect={onSelect} />);
const box = screen.getByRole("combobox", { name: "Tea" });
await user.type(box, "en");
expect(box).not.toHaveAttribute("aria-activedescendant");
await user.keyboard("{ArrowDown}");
const first = screen.getByRole("option", { name: "Genmaicha" });
expect(first.id).toBeTruthy();
expect(box).toHaveAttribute("aria-activedescendant", first.id);
expect(first).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("option", { name: "Green" })).toHaveAttribute("aria-selected", "false");
expect(box).toHaveFocus();
const teas = ["Assam", "Darjeeling", "Earl Grey", "Genmaicha", "Green", "Jasmine", "Oolong", "Rooibos", "Sencha"];
const onSelect = spy();
render(<App options={teas} label="Tea" onSelect={onSelect} />);
const box = screen.getByRole("combobox", { name: "Tea" });
await user.type(box, "en");
await user.keyboard("{ArrowUp}");
expect(box).toHaveAttribute("aria-activedescendant", screen.getByRole("option", { name: "Sencha" }).id);
await user.keyboard("{ArrowDown}");
expect(box).toHaveAttribute("aria-activedescendant", screen.getByRole("option", { name: "Genmaicha" }).id);
await user.keyboard("{ArrowDown}");
expect(box).toHaveAttribute("aria-activedescendant", screen.getByRole("option", { name: "Green" }).id);
const teas = ["Assam", "Darjeeling", "Earl Grey", "Genmaicha", "Green", "Jasmine", "Oolong", "Rooibos", "Sencha"];
const onSelect = spy();
render(<App options={teas} label="Tea" onSelect={onSelect} />);
const box = screen.getByRole("combobox", { name: "Tea" });
await user.type(box, "en");
await user.keyboard("{ArrowDown}{ArrowDown}{Enter}");
expect(box).toHaveValue("Green");
expect(onSelect).toHaveBeenCalledTimes(1);
expect(onSelect).toHaveBeenCalledWith("Green");
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
expect(box).toHaveAttribute("aria-expanded", "false");
expect(box).not.toHaveAttribute("aria-activedescendant");
const teas = ["Assam", "Darjeeling", "Earl Grey", "Genmaicha", "Green", "Jasmine", "Oolong", "Rooibos", "Sencha"];
const onSelect = spy();
render(<App options={teas} label="Tea" onSelect={onSelect} />);
const box = screen.getByRole("combobox", { name: "Tea" });
await user.type(box, "en{Enter}");
expect(onSelect).not.toHaveBeenCalled();
expect(box).toHaveValue("en");
expect(screen.getByRole("listbox")).toBeInTheDocument();
const teas = ["Assam", "Darjeeling", "Earl Grey", "Genmaicha", "Green", "Jasmine", "Oolong", "Rooibos", "Sencha"];
const onSelect = spy();
render(<App options={teas} label="Tea" onSelect={onSelect} />);
const box = screen.getByRole("combobox", { name: "Tea" });
await user.type(box, "s");
await user.click(screen.getByRole("option", { name: "Sencha" }));
expect(box).toHaveValue("Sencha");
expect(onSelect).toHaveBeenCalledWith("Sencha");
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
expect(box).toHaveFocus();
const teas = ["Assam", "Darjeeling", "Earl Grey", "Genmaicha", "Green", "Jasmine", "Oolong", "Rooibos", "Sencha"];
const onSelect = spy();
render(<App options={teas} label="Tea" onSelect={onSelect} />);
const box = screen.getByRole("combobox", { name: "Tea" });
await user.type(box, "en");
await user.keyboard("{Escape}");
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
expect(box).toHaveValue("en");
expect(box).toHaveAttribute("aria-expanded", "false");
await user.type(box, "c");
expect(screen.getAllByRole("option").map((option) => option.textContent)).toEqual(["Sencha"]);
const teas = ["Assam", "Darjeeling", "Earl Grey", "Genmaicha", "Green", "Jasmine", "Oolong", "Rooibos", "Sencha"];
const onSelect = spy();
render(<App options={teas} label="Tea" onSelect={onSelect} />);
const box = screen.getByRole("combobox", { name: "Tea" });
await user.type(box, "e");
await user.keyboard("{ArrowDown}{ArrowDown}");
await user.type(box, "n");
expect(box).not.toHaveAttribute("aria-activedescendant");
expect(screen.getAllByRole("option").every((option) => option.getAttribute("aria-selected") === "false")).toBe(true);
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 Autocomplete. 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.