AniUI Academy

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)`.
Asked atMicrosoftFigmaLinearNotionAtlassian

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>
  );
}

5 tests · +190 XP

Discussion

Sign in to post
Sign in to join the conversation.Sign in

No comments yet. Be the first to say something.