Context Menu

@sushindustries/ui

One menu, reachable by right-click, by long press, and by a button that is always there.

4 min read
View as Markdown

Keyboard

KeyDoes
ArrowDownthe next item, wrapping round to the first
ArrowUpthe previous item, wrapping round to the last
Homethe first item
Endthe last item
Escapecloses the menu

Disabled items are skipped, because the walk is over [role='menuitem']:not(:disabled). The first item is focused when the menu opens - not the container, because a menu that opens with nothing focused costs an extra keypress before the arrows do anything.

That roving focus is fifteen lines of local onKeyDown rather than a hotkey library. A menu's arrow keys are scoped to the menu; a global hotkey manager would fire while focus was anywhere on the page, which is the wrong shape and a dependency in every consumer's install.

Placing it

position: fixed at the pointer, clamped to the viewport with an 8px margin so it never opens off-screen.

const margin = 8;

const x = Math.max(
	margin,
	Math.min(state.x, window.innerWidth - box.width - margin),
);
const y = Math.max(
	margin,
	Math.min(state.y, window.innerHeight - box.height - margin),
);
ts

The clamp is why the coordinates live in state rather than being written as a custom property on the trigger: the menu has to know its own width and height before it can decide where it fits, and it only knows those after it renders. The first frame at the raw point is the only one that can be wrong, and it is never seen.

Closing

Global listeners for pointerdown, scroll, resize and Escape. A menu that only closes when you click the thing that opened it is a menu you have to remember how to dismiss.

Presses inside the menu stop propagating, or the away-click listener would close it before the item's own click ever fired.

Used byFolder ShelfFolder ShelfA desktop of folders that open into draggable windows, several at once, remembered between visits.@sushindustries/ui · layout