Skip to the code
cf.

Keyboard shortcuts help

The tea shop's order page has keyboard shortcuts, and pressing ? should show them. Build App: the page, with a help panel that opens on ? and closes on Escape, wherever the focus is on the page.

The page has a text box labelled Search teas (an <input>), a text box labelled Order note (a <textarea>), and a line that says Press ? to see the keyboard shortcuts. When the help is open, it's a <section> labelled Keyboard shortcuts, with a heading and a short list. When it's closed, it isn't on the page at all.

Pressing ? opens the help and pressing Escape closes it. Both keys are ignored while the focus is in an <input> or a <textarea>, so people can type a question mark in their note. Once the focus leaves the text box, the keys work again. Other keys do nothing.

App takes an optional onOpenHelp prop and calls it each time the help opens (pressing ? while it's already open doesn't count). The parent may pass a different function on any render, and the latest one is the one called.

Listen on window, and stop listening when App leaves the page.

<App onOpenHelp={() => console.log("help opened")} />
 
// renders, after pressing ?:
<label>Search teas <input /></label>
<label>Order note <textarea /></label>
<p>Press ? to see the keyboard shortcuts.</p>
<section aria-label="Keyboard shortcuts">
  <h2>Keyboard shortcuts</h2>
  ...
</section>

What the tests check

  1. keeps the help closed at first
    render(<App />);
    expect(screen.queryByRole("region", { name: "Keyboard shortcuts" })).not.toBeInTheDocument();
    expect(screen.getByLabelText("Search teas")).toBeInTheDocument();
    expect(screen.getByLabelText("Order note")).toBeInTheDocument();
    
  2. opens the help on ?
    render(<App />);
    await user.keyboard("?");
    expect(screen.getByRole("region", { name: "Keyboard shortcuts" })).toBeInTheDocument();
    
  3. closes the help on Escape
    render(<App />);
    await user.keyboard("?");
    await user.keyboard("{Escape}");
    expect(screen.queryByRole("region", { name: "Keyboard shortcuts" })).not.toBeInTheDocument();
    
  4. ignores ? while typing in the search box
    render(<App />);
    await user.type(screen.getByLabelText("Search teas"), "green?");
    expect(screen.getByLabelText("Search teas")).toHaveValue("green?");
    expect(screen.queryByRole("region", { name: "Keyboard shortcuts" })).not.toBeInTheDocument();
    
  5. ignores Escape while typing in the note
    render(<App />);
    await user.keyboard("?");
    await user.click(screen.getByLabelText("Order note"));
    await user.keyboard("{Escape}");
    expect(screen.getByRole("region", { name: "Keyboard shortcuts" })).toBeInTheDocument();
    
  6. works again once the focus leaves the text box
    render(<App />);
    const search = screen.getByLabelText("Search teas");
    await user.type(search, "oolong");
    act(() => search.blur());
    await user.keyboard("?");
    expect(screen.getByRole("region", { name: "Keyboard shortcuts" })).toBeInTheDocument();
    
  7. calls onOpenHelp each time the help opens
    const onOpenHelp = spy();
    render(<App onOpenHelp={onOpenHelp} />);
    await user.keyboard("?");
    expect(onOpenHelp.callCount).toBe(1);
    await user.keyboard("?");
    expect(onOpenHelp.callCount).toBe(1);
    await user.keyboard("{Escape}");
    await user.keyboard("?");
    expect(onOpenHelp.callCount).toBe(2);
    
  8. calls the latest onOpenHelp
    const first = spy();
    const second = spy();
    const { rerender } = render(<App onOpenHelp={first} />);
    rerender(<App onOpenHelp={second} />);
    await user.keyboard("?");
    expect(first.callCount).toBe(0);
    expect(second.callCount).toBe(1);
    
  9. stops listening when it leaves the page
    const onOpenHelp = spy();
    const { unmount } = render(<App onOpenHelp={onOpenHelp} />);
    unmount();
    await user.keyboard("?");
    expect(onOpenHelp.callCount).toBe(0);
    

Learn more

App.jsxSaved in this browser

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).