feat(e10): open the app's menu with a right-click, starting with playlist rows
S2's headline was never a new Menu: `ActionMenu` already sits on `AnchoredPopover` (arrows, typeahead, Escape, anchor-flip). What was missing is an INPUT PATH into it, so this adds `useContextMenu` + `<ContextMenu>` over the same `ActionMenuItem[]` and the same `MenuItemList` — a row cannot grow a right-click menu that looks or behaves unlike its own ⋯ menu. Three ways in: right-click anywhere on the row, long-press on touch/pen, and Shift+F10 / the Menu key. The LEFT button is untouched and keeps the row's own action. One popover per list, anchored to a virtual element at the pointer (native behaviour, and a table of 200 rows would otherwise mount 200 floating contexts to keep 199 shut). Platform bits it needed: `hoverAlso` on AnchoredPopover (a submenu falls open under a mouse and is tapped by a finger — mouse-only, so a tap's synthetic mouseenter can't race the click toggle) and `elementRef` on PopoverItem (the submenu anchors to the very menuitem that opens it, where aria-haspopup/expanded belong). `ActionMenuItem` becomes a discriminated union — a row either fires or opens a submenu — plus `danger` and `separatorBefore`. First consumers, both in Playlists: - item rows: play · move to another list (a submenu of every other playlist, one gesture) · to top / up / down / bottom · remove. Reordering goes through the same undoable store as the drag handle and is disabled under a view for the same reason, with the same explanation rather than silence. - rail rows: rename (in the row that was right-clicked, keeping its cover so the rail doesn't jump) · delete. Both now run through `usePlaylistActions`, which also takes over the detail header's pencil and trash — the delete dialog's YouTube branch and the "what gets selected next" rule exist once, not twice. - the rail gains a "recently changed" sort key off the new `updated_at`. NOT the default: a rail that reshuffles itself while you look at it costs more than it saves.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import { MENU_SURFACE, MenuItemList, type ActionMenuItem } from "./ActionMenu";
|
||||
import { PopoverSurface } from "./AnchoredPopover";
|
||||
import { type ContextMenuController } from "./useContextMenu";
|
||||
|
||||
/** The menu surface for a `useContextMenu` controller. Render ONE per list, outside the rows. */
|
||||
export function ContextMenu<T>({
|
||||
menu,
|
||||
ariaLabel,
|
||||
items,
|
||||
}: {
|
||||
menu: ContextMenuController<T>;
|
||||
/** Accessible name for the panel — a context menu has no visible trigger to take one from. */
|
||||
ariaLabel: string;
|
||||
items: readonly ActionMenuItem[];
|
||||
}) {
|
||||
return (
|
||||
<PopoverSurface
|
||||
popover={menu.popover}
|
||||
ariaLabel={ariaLabel}
|
||||
className={`${MENU_SURFACE} overflow-y-auto`}
|
||||
style={{ maxHeight: "min(28rem, var(--fit-max-h))" }}
|
||||
>
|
||||
<MenuItemList items={items} onClose={menu.close} />
|
||||
</PopoverSurface>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user