The tea shop shows brewing notes for each kind of tea in tabs. Build App: it takes tabs, a list of { id, label, content }, and label, the name of the tab list. When they aren't given, as in the Preview, show a few tabs of your own and the label Brewing guide.
Render a <div role="tablist"> with aria-label set to label, holding one <button role="tab"> per item with its label as text. The selected tab has aria-selected="true", the others "false". The first tab is selected at first.
Each tab has a panel: a <div role="tabpanel"> with the item's content, tabIndex={0}, and aria-labelledby set to its tab's id. Each tab's aria-controls is its panel's id. Every panel is always in the page; all but the selected one have the hidden attribute. Make the ids unique on the page.
Clicking a tab selects it. Only the selected tab is in the Tab order (tabIndex={0}; the others get -1), so Tab from the selected tab goes straight to its panel.
With the focus on a tab, ArrowRight and ArrowLeft move the focus to the next and previous tab and select it, wrapping around at the ends. Home and End move to the first and last tab.
<App label="Brewing guide" tabs={[
{ id: "green", label: "Green", content: "Steep at 75 degrees for two minutes." },
{ id: "black", label: "Black", content: "Steep at 95 degrees for four minutes." },
]} />
// renders:
<div role="tablist" aria-label="Brewing guide">
<button role="tab" id="t-green" aria-selected="true" aria-controls="p-green" tabindex="0">Green</button>
<button role="tab" id="t-black" aria-selected="false" aria-controls="p-black" tabindex="-1">Black</button>
</div>
<div role="tabpanel" id="p-green" aria-labelledby="t-green" tabindex="0">Steep at 75 degrees for two minutes.</div>
<div role="tabpanel" id="p-black" aria-labelledby="t-black" tabindex="0" hidden>Steep at 95 degrees for four minutes.</div>
const tabs = [
{ id: "green", label: "Green", content: "Steep at 75 degrees for two minutes." },
{ id: "black", label: "Black", content: "Steep at 95 degrees for four minutes." },
{ id: "oolong", label: "Oolong", content: "Steep at 85 degrees for three minutes." },
];
render(<App tabs={tabs} label="Brewing guide" />);
const tablist = screen.getByRole("tablist", { name: "Brewing guide" });
expect(within(tablist).getAllByRole("tab").map((tab) => tab.textContent)).toEqual(["Green", "Black", "Oolong"]);
const tabs = [
{ id: "green", label: "Green", content: "Steep at 75 degrees for two minutes." },
{ id: "black", label: "Black", content: "Steep at 95 degrees for four minutes." },
{ id: "oolong", label: "Oolong", content: "Steep at 85 degrees for three minutes." },
];
render(<App tabs={tabs} label="Brewing guide" />);
expect(screen.getByRole("tab", { name: "Green" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tab", { name: "Black" })).toHaveAttribute("aria-selected", "false");
expect(screen.getByRole("tabpanel")).toHaveTextContent("Steep at 75 degrees for two minutes.");
expect(screen.getByText("Steep at 95 degrees for four minutes.")).not.toBeVisible();
const tabs = [
{ id: "green", label: "Green", content: "Steep at 75 degrees for two minutes." },
{ id: "black", label: "Black", content: "Steep at 95 degrees for four minutes." },
{ id: "oolong", label: "Oolong", content: "Steep at 85 degrees for three minutes." },
];
render(<App tabs={tabs} label="Brewing guide" />);
await user.click(screen.getByRole("tab", { name: "Oolong" }));
expect(screen.getByRole("tab", { name: "Oolong" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tab", { name: "Green" })).toHaveAttribute("aria-selected", "false");
expect(screen.getByRole("tabpanel", { name: "Oolong" })).toHaveTextContent("Steep at 85 degrees for three minutes.");
const tabs = [
{ id: "green", label: "Green", content: "Steep at 75 degrees for two minutes." },
{ id: "black", label: "Black", content: "Steep at 95 degrees for four minutes." },
{ id: "oolong", label: "Oolong", content: "Steep at 85 degrees for three minutes." },
];
render(<App tabs={tabs} label="Brewing guide" />);
for (const item of tabs) {
const tab = screen.getByRole("tab", { name: item.label });
const panel = document.getElementById(tab.getAttribute("aria-controls"));
expect(panel).toHaveAttribute("role", "tabpanel");
expect(panel).toHaveTextContent(item.content);
expect(tab.id).toBeTruthy();
expect(panel).toHaveAttribute("aria-labelledby", tab.id);
}
const tabs = [
{ id: "green", label: "Green", content: "Steep at 75 degrees for two minutes." },
{ id: "black", label: "Black", content: "Steep at 95 degrees for four minutes." },
{ id: "oolong", label: "Oolong", content: "Steep at 85 degrees for three minutes." },
];
render(<App tabs={tabs} label="Brewing guide" />);
await user.click(screen.getByRole("tab", { name: "Black" }));
expect(screen.getByRole("tab", { name: "Black" })).toHaveAttribute("tabindex", "0");
expect(screen.getByRole("tab", { name: "Green" })).toHaveAttribute("tabindex", "-1");
expect(screen.getByRole("tab", { name: "Oolong" })).toHaveAttribute("tabindex", "-1");
await user.tab();
expect(screen.getByRole("tabpanel", { name: "Black" })).toHaveFocus();
const tabs = [
{ id: "green", label: "Green", content: "Steep at 75 degrees for two minutes." },
{ id: "black", label: "Black", content: "Steep at 95 degrees for four minutes." },
{ id: "oolong", label: "Oolong", content: "Steep at 85 degrees for three minutes." },
];
render(<App tabs={tabs} label="Brewing guide" />);
await user.click(screen.getByRole("tab", { name: "Green" }));
await user.keyboard("{ArrowRight}");
expect(screen.getByRole("tab", { name: "Black" })).toHaveFocus();
expect(screen.getByRole("tab", { name: "Black" })).toHaveAttribute("aria-selected", "true");
await user.keyboard("{ArrowRight}{ArrowRight}");
expect(screen.getByRole("tab", { name: "Green" })).toHaveFocus();
expect(screen.getByRole("tab", { name: "Green" })).toHaveAttribute("aria-selected", "true");
const tabs = [
{ id: "green", label: "Green", content: "Steep at 75 degrees for two minutes." },
{ id: "black", label: "Black", content: "Steep at 95 degrees for four minutes." },
{ id: "oolong", label: "Oolong", content: "Steep at 85 degrees for three minutes." },
];
render(<App tabs={tabs} label="Brewing guide" />);
await user.click(screen.getByRole("tab", { name: "Green" }));
await user.keyboard("{ArrowLeft}");
expect(screen.getByRole("tab", { name: "Oolong" })).toHaveFocus();
expect(screen.getByRole("tab", { name: "Oolong" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tabpanel")).toHaveTextContent("Steep at 85 degrees for three minutes.");
const tabs = [
{ id: "green", label: "Green", content: "Steep at 75 degrees for two minutes." },
{ id: "black", label: "Black", content: "Steep at 95 degrees for four minutes." },
{ id: "oolong", label: "Oolong", content: "Steep at 85 degrees for three minutes." },
];
render(<App tabs={tabs} label="Brewing guide" />);
await user.click(screen.getByRole("tab", { name: "Black" }));
await user.keyboard("{End}");
expect(screen.getByRole("tab", { name: "Oolong" })).toHaveFocus();
expect(screen.getByRole("tab", { name: "Oolong" })).toHaveAttribute("aria-selected", "true");
await user.keyboard("{Home}");
expect(screen.getByRole("tab", { name: "Green" })).toHaveFocus();
expect(screen.getByRole("tab", { name: "Green" })).toHaveAttribute("aria-selected", "true");
const tabs = [
{ id: "green", label: "Green", content: "Steep at 75 degrees for two minutes." },
{ id: "black", label: "Black", content: "Steep at 95 degrees for four minutes." },
{ id: "oolong", label: "Oolong", content: "Steep at 85 degrees for three minutes." },
];
render(
<>
<App tabs={tabs} label="First" />
<App tabs={tabs} label="Second" />
</>,
);
const ids = screen.getAllByRole("tab").map((tab) => tab.id);
expect(new Set(ids).size).toBe(6);
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 Tabs with keyboard support. 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.