PhiaUi.Components.ContextMenu (phia_ui v0.1.17)

Copy Markdown View Source

Context menu component triggered by right-click (contextmenu browser event).

A context menu provides a contextual set of actions for a specific element — just like the browser's native right-click menu, but fully customisable and integrated with your LiveView. Common use cases include file managers, spreadsheets, canvas editors, data grids, and any UI where per-item actions should be discoverable on right-click.

Requires the PhiaContextMenu JavaScript hook registered in app.js. The hook intercepts the contextmenu event on the trigger area, prevents the native browser menu, and positions the custom panel at the exact cursor coordinates.

Hook Registration

Copy the hook via mix phia.add context_menu, then register it:

# assets/js/app.js
import PhiaContextMenu from "./hooks/context_menu"

let liveSocket = new LiveSocket("/live", Socket, {
  hooks: { PhiaContextMenu }
})

Hook Behaviour

  • Right-clicking inside context_menu_trigger/1 opens the menu at the cursor
  • The native browser context menu is suppressed
  • Smart viewport-aware positioning: the panel flips if it would overflow the viewport edge (right→left, bottom→top)
  • Clicking outside the open panel or pressing Escape closes it
  • ArrowUp / ArrowDown navigate between items; Enter activates the focused item
  • Only one context menu can be open at a time — opening a new one closes the previous

Sub-components

FunctionElementPurpose
context_menu/1divRoot container
context_menu_trigger/1divRight-clickable area with hook anchor
context_menu_content/1divFloating panel (positioned fixed by hook)
context_menu_item/1divClickable item (role="menuitem")
context_menu_separator/1hrVisual divider between item groups
context_menu_checkbox_item/1divToggleable item (role="menuitemcheckbox")
context_menu_label/1divNon-interactive section heading

Example — File Manager

A classic context menu for file operations:

<.context_menu id="file-ctx">
  <.context_menu_trigger context_menu_id="file-ctx">
    <div class="flex items-center gap-2 p-3 rounded-md hover:bg-muted cursor-default">
      <.icon name="file" />
      {file.name}
    </div>
  </.context_menu_trigger>
  <.context_menu_content id="file-ctx-content">
    <.context_menu_label>{@file.name}</.context_menu_label>
    <.context_menu_separator />
    <.context_menu_item phx-click="open_file" phx-value-id={@file.id}>
      Open
    </.context_menu_item>
    <.context_menu_item phx-click="rename_file" phx-value-id={@file.id}>
      Rename...
    </.context_menu_item>
    <.context_menu_item phx-click="duplicate_file" phx-value-id={@file.id}>
      Duplicate
    </.context_menu_item>
    <.context_menu_separator />
    <.context_menu_checkbox_item
      checked={@file.starred}
      phx-click="toggle_star"
      phx-value-id={@file.id}>
      Starred
    </.context_menu_checkbox_item>
    <.context_menu_separator />
    <.context_menu_item phx-click="delete_file" phx-value-id={@file.id}
      class="text-destructive focus:text-destructive">
      Move to Trash
    </.context_menu_item>
  </.context_menu_content>
</.context_menu>

Example — Data Grid Row

Context menus work well on table rows in data grids:

<tr :for={row <- @rows}>
  <.context_menu id={"row-ctx-#{row.id}"}>
    <.context_menu_trigger context_menu_id={"row-ctx-#{row.id}"}>
      <td class="px-4 py-2">{row.name}</td>
      <td class="px-4 py-2">{row.status}</td>
    </.context_menu_trigger>
    <.context_menu_content id={"row-ctx-#{row.id}-content"}>
      <.context_menu_item phx-click="view_row" phx-value-id={row.id}>View</.context_menu_item>
      <.context_menu_item phx-click="edit_row" phx-value-id={row.id}>Edit</.context_menu_item>
      <.context_menu_separator />
      <.context_menu_item phx-click="archive_row" phx-value-id={row.id}>Archive</.context_menu_item>
    </.context_menu_content>
  </.context_menu>
</tr>

Example — Canvas / Drawing Area

For design tools where the trigger is the entire canvas:

<.context_menu id="canvas-ctx">
  <.context_menu_trigger context_menu_id="canvas-ctx">
    <div id="drawing-canvas" class="w-full h-96 bg-muted rounded border">
      <%# canvas content %>
    </div>
  </.context_menu_trigger>
  <.context_menu_content id="canvas-ctx-content">
    <.context_menu_item phx-click="paste">Paste</.context_menu_item>
    <.context_menu_item phx-click="select_all">Select All</.context_menu_item>
    <.context_menu_separator />
    <.context_menu_label>View</.context_menu_label>
    <.context_menu_checkbox_item checked={@show_grid} phx-click="toggle_grid">
      Show Grid
    </.context_menu_checkbox_item>
    <.context_menu_checkbox_item checked={@snap_to_grid} phx-click="toggle_snap">
      Snap to Grid
    </.context_menu_checkbox_item>
  </.context_menu_content>
</.context_menu>

Accessibility

  • context_menu_trigger/1 has aria-haspopup="menu" to signal the right-click behaviour to assistive technology
  • context_menu_content/1 has role="menu" and aria-orientation="vertical"
  • Items have role="menuitem" (or menuitemcheckbox for toggleable items)
  • All items have tabindex="-1" so keyboard navigation is hook-managed via [data-disabled] and focus() calls, matching the WAI-ARIA menu pattern
  • The panel is position: fixed (not absolute) so it appears at the cursor coordinates regardless of the containing element's scroll position or overflow setting

Summary

Functions

Renders the context menu root container.

Renders a toggleable context menu item with a check mark indicator.

Renders the context menu content panel.

Renders a context menu item with role="menuitem".

Renders a non-interactive section heading inside the context menu.

Renders a horizontal visual separator between groups of context menu items.

Renders the context menu trigger area.

Functions

context_menu(assigns)

Renders the context menu root container.

The root is relative so child elements can be positioned within it. The trigger and content are referenced by ID — the root itself is a layout-only wrapper.

Attributes

  • id (:string) (required) - Unique context menu ID. Used by context_menu_trigger/1 to build the data-content-id attribute that tells the hook which panel to position. Must be unique on the page. When rendering in lists, use per-item IDs: "file-ctx-#{file.id}".

  • class (:string) - Additional CSS classes. Defaults to nil.

  • Global attributes are accepted. Extra HTML attributes forwarded to the root <div>.

Slots

context_menu_checkbox_item(assigns)

Renders a toggleable context menu item with a check mark indicator.

Uses role="menuitemcheckbox" and aria-checked so assistive technology announces the current state. Toggle the state in your LiveView:

# LiveView
def handle_event("toggle_grid", _params, socket) do
  {:noreply, update(socket, :show_grid, &(!&1))}
end

# Template
<.context_menu_checkbox_item
  checked={@show_grid}
  phx-click="toggle_grid">
  Show Grid
</.context_menu_checkbox_item>

The check mark (&#10003;) is rendered only when :checked is true, keeping the item label left-aligned whether checked or not.

Attributes

  • checked (:boolean) - Whether the checkbox item is currently checked. Drives aria-checked and the visible check mark indicator. Manage state in your LiveView and pass the current value here.

    Defaults to false.

  • class (:string) - Additional CSS classes. Defaults to nil.

  • Global attributes are accepted. Extra HTML attributes forwarded to the item <div> (e.g. phx-click, phx-value-*).

Slots

  • inner_block (required) - Item label text.

context_menu_content(assigns)

Renders the context menu content panel.

Initially hidden via style="display:none; position:fixed;". Using position: fixed (not absolute) is critical — it ensures the panel appears at the cursor's viewport coordinates regardless of the page scroll position or the trigger's containing block.

The hook reveals the panel and sets top/left inline styles to the right-click cursor position, then adjusts if the panel would overflow the viewport edge.

tabindex="-1" allows the panel to receive programmatic focus from the hook after opening, enabling keyboard navigation to work immediately without the user needing to Tab into the panel.

Attributes

  • id (:string) (required) - Element ID for the content panel. Must follow the pattern "{context_menu_id}-content" — this is the ID the hook looks up via data-content-id on the trigger to position the panel.

  • class (:string) - Additional CSS classes. Defaults to nil.

  • Global attributes are accepted. Extra HTML attributes forwarded to the panel <div>.

Slots

  • inner_block (required) - Menu items, labels, separators, and checkbox items.

context_menu_item(assigns)

Renders a context menu item with role="menuitem".

Wire LiveView actions using phx-click and pass data via phx-value-*:

<.context_menu_item phx-click="rename" phx-value-id={@file.id}>
  Rename
</.context_menu_item>

The hook handles ArrowUp / ArrowDown focus management between items (via programmatic focus()) and Enter to activate the focused item.

Disabled items should use data-disabled attribute — the hook checks for this and skips them during keyboard navigation. The data-[disabled] CSS selector applies the visual disabled style.

Attributes

  • class (:string) - Additional CSS classes. Defaults to nil.
  • Global attributes are accepted. Extra HTML attributes forwarded to the item <div> (e.g. phx-click, phx-value-*).

Slots

  • inner_block (required) - Menu item content.

context_menu_label(assigns)

Renders a non-interactive section heading inside the context menu.

Use labels to give context to a group of related items — for example, showing the name of the right-clicked item at the top of the menu:

<.context_menu_content id="file-ctx-content">
  <.context_menu_label>{@file.name}</.context_menu_label>
  <.context_menu_separator />
  <.context_menu_item phx-click="open">Open</.context_menu_item>
</.context_menu_content>

Labels are not focusable and are skipped by the hook's keyboard navigation.

Attributes

  • class (:string) - Additional CSS classes. Defaults to nil.
  • Global attributes are accepted. Extra HTML attributes forwarded to the label <div>.

Slots

  • inner_block (required) - Label text content.

context_menu_separator(assigns)

Renders a horizontal visual separator between groups of context menu items.

Use separators to create clear visual groupings between related and unrelated actions. For example, separate "Open", "Rename", "Duplicate" from destructive actions like "Delete" or "Archive".

<.context_menu_item phx-click="copy">Copy</.context_menu_item>
<.context_menu_item phx-click="paste">Paste</.context_menu_item>
<.context_menu_separator />
<.context_menu_item phx-click="delete" class="text-destructive">
  Delete
</.context_menu_item>

Attributes

  • class (:string) - Additional CSS classes. Defaults to nil.
  • Global attributes are accepted. Extra HTML attributes forwarded to the <hr>.

context_menu_trigger(assigns)

Renders the context menu trigger area.

The PhiaContextMenu hook mounts on this element (via phx-hook) and intercepts contextmenu events (right-click / two-finger tap on macOS). It prevents the native browser context menu and shows the custom panel at the exact cursor (clientX, clientY) coordinates.

data-content-id tells the hook which panel element to position and show. This must equal "{context_menu_id}-content".

The trigger can wrap any content — a div, a table row, a canvas — as long as it is a valid container for the right-click area you want to cover.

Attributes

  • context_menu_id (:string) (required) - ID of the parent context_menu/1. The hook uses this to build the content panel ID (context_menu_id <> "-content") and show it at the cursor position on right-click.

  • class (:string) - Additional CSS classes. Defaults to nil.

  • Global attributes are accepted. Extra HTML attributes forwarded to the trigger <div>.

Slots

  • inner_block (required) - The right-clickable area — any content or element.