Build a right-click context menu
mediumUI Components+190 XP
Build an area that, when right-clicked, opens a small context menu at the cursor position. Clicking an item invokes a handler. Clicking anywhere else, or pressing Escape, closes the menu.
Prevent the browser's default context menu from appearing.
Examples
// Right-click at (100, 120) inside the area
// → <ul role="menu" style="left:100px; top:120px">
// <li role="menuitem">Open</li>
// <li role="menuitem">Rename</li>
// <li role="menuitem">Delete</li>
// </ul>
// Click "Rename"
// → menu disappears; last-action reads "Rename".
// Press Escape (while open) → menu closes, no action.The menu is a transient portal — right-click opens, anything outside or Escape closes.
function openMenu(e) {
e.preventDefault(); // suppress the browser's own menu
setMenu({ open: true, x: e.clientX, y: e.clientY });
}
// On the <div>:
<div onContextMenu={openMenu}>...</div>`preventDefault` on the `contextmenu` event is what hides the browser's native menu.
What it has to do
- The area is `<div data-testid="menu-area" />` filling at least 300×200 of visible space.
- The menu is `<ul data-testid="context-menu" role="menu">` — only present in the DOM while open.
- Each menu item is `<li data-testid="menu-item-{action}" role="menuitem">` with text matching the action name.
- Right-click on the area opens the menu. The menu's inline `left` and `top` styles reflect the event's `clientX` / `clientY` so it appears at the cursor.
- A left-click anywhere outside the menu closes it. Escape closes it. Clicking a menu item closes it after invoking.
- An element `<p data-testid="last-action">` reflects the most recently clicked item name (or stays empty before any invocation).
Notes
- Attach the outside-click + Escape listeners inside an effect gated on `menu.open`, so they're not running while the menu is closed.
- A production menu should clamp `x`/`y` so the menu doesn't spill off the viewport right/bottom edges — swap to right/bottom-anchored positioning when it would overflow.
- Give the menu `role="menu"` and items `role="menuitem"`, and support arrow-key navigation with focus for keyboard users; the grader checks structure, not keyboard focus movement.
- Capture the click on the window so a click inside the menu doesn't bubble up and immediately close it — use a `ref` to the menu and bail out when `menu.contains(e.target)`.
Stuck? The react course covers the ideas this problem is built on.
import { useState, useEffect } from "react"; const ACTIONS = ["Open", "Rename", "Delete"]; export default function App() { const [menu, setMenu] = useState({ open: false, x: 0, y: 0 }); const [lastAction, setLastAction] = useState(""); // TODO: implement onContextMenu (preventDefault, open at cursor); // outside-click close (window click listener while open); // Escape close; item click invokes + closes. return ( <div data-testid="menu-area" style={{ fontFamily: "sans-serif", padding: 16, width: 400, height: 240, border: "1px solid #ddd", position: "relative" }} > <p>Right-click here.</p> <p data-testid="last-action">{lastAction}</p> {menu.open ? ( <ul data-testid="context-menu" role="menu" style={{ position: "fixed", left: menu.x, top: menu.y, background: "white", border: "1px solid #ddd", listStyle: "none", padding: 4, margin: 0, }} > {ACTIONS.map((action) => ( <li key={action} role="menuitem" data-testid={`menu-item-${action}`} style={{ padding: "4px 12px", cursor: "pointer" }} > {action} </li> ))} </ul> ) : null} </div> ); }
- 1.The area is `<div data-testid="menu-area" />` filling at least 300×200 of visible space.
- 2.The menu is `<ul data-testid="context-menu" role="menu">` — only present in the DOM while open.
- 3.Each menu item is `<li data-testid="menu-item-{action}" role="menuitem">` with text matching the action name.
- 4.Right-click on the area opens the menu. The menu's inline `left` and `top` styles reflect the event's `clientX` / `clientY` so it appears at the cursor.
- 5.A left-click anywhere outside the menu closes it. Escape closes it. Clicking a menu item closes it after invoking.
- 6.An element `<p data-testid="last-action">` reflects the most recently clicked item name (or stays empty before any invocation).
5 automated assertions run against your rendered component. The exact assertion names are hidden so you build to the behaviour, not to the test.
5 tests · +190 XP
Discussion
Sign in to postSign in to join the conversation.Sign in
No comments yet. Be the first to say something.