Menubar

v0.0.10

v0.0.10StableMobStatelessApache-2.0
Installation mix mishka.ui.gen.mob menubar
Source View Source

The Mishka Chelekom Mob Menubar is a bar of menus for iOS and Android screens built with Mob, where at most one menu is open at a time. Tapping a trigger opens its panel under the bar. It is the native port of the headless Menubar.

Overview of the Mob Menubar component for iOS and Android

A menubar is defined by two behaviors. One is exclusivity: opening a menu closes the one before it, so the bar never shows two popups. That is state, and it ports exactly. Your screen owns open, and open_menu/2 computes the next value.

The other is hover switching, where moving the pointer to another trigger switches menus without a click. That has no meaning on a touch screen, because a finger is either down or not. Every menu opens on tap instead.

The panel is drawn in the layout flow, directly under the bar, and not as an overlay. A tap on a trigger sends on_open to your screen, and the panel appears when your screen passes that menu's value back as open.

Each trigger hugs its label, so every menu in the bar is drawn side by side.

Menus and items

A menu is a map with label, value, items and an optional disabled. An item is a map with label, value and an optional disabled, or the atom :separator. An item can also carry a shortcut, which the panel draws as small muted text at the end of the row.

elixir
@menus [
  %{
    label: "File",
    value: :file,
    items: [
      %{label: "New", value: :new, shortcut: "⌘N"},
      %{label: "Open…", value: :open, shortcut: "⌘O"},
      :separator,
      %{label: "Quit", value: :quit, disabled: true}
    ]
  },
  %{
    label: "Edit",
    value: :edit,
    items: [%{label: "Undo", value: :undo}, %{label: "Redo", value: :redo}]
  },
  %{label: "View", value: :view, items: [%{label: "Zoom in", value: :zoom}]}
]

Generate the menubar component

Run this from your Mob app's root to add the component to lib/<app>/components/:

bash
mix mishka.ui.gen.mob menubar

The generator registers the tag, so you can use it in ~MOB markup. It also copies the shared Event and Anchored helper modules. The examples on this page use the mishka_ prefix of the showcase app (--module-prefix mishka_), so the tag is <MishkaMenubar>. Without a prefix, the tag is <Menubar>.

Helper functions

open_menu(value, current) returns the value that should be open after a tap on value. Tapping the open menu closes it, and tapping any other menu switches straight to it.

elixir
MishkaMenubar.open_menu(:file, nil)    #=> :file
MishkaMenubar.open_menu(:file, :file)  #=> nil
MishkaMenubar.open_menu(:edit, :file)  #=> :edit

Props

PropTypeDefaultDescription
menuslist of menu maps[]The bar.
openvaluenilWhich menu is open.
disabledbooleanfalseDisables the whole bar.
on_openevent tag (atom)noneSends {:tap, {tag, menu_value}} when a trigger is tapped.
on_selectevent tag (atom)noneSends {:tap, {tag, item_value}} when an item is tapped.

Events

A tap on a trigger sends {:tap, {tag, menu_value}} for on_open. A tap on an item sends {:tap, {tag, item_value}} for on_select. A disabled menu or item sends nothing. Your screen receives both messages in handle_info/2.

elixir
def handle_info({:tap, {:open, value}}, socket) do
  open = MishkaMenubar.open_menu(value, socket.assigns.open)
  {:noreply, Mob.Socket.assign(socket, :open, open)}
end

def handle_info({:tap, {:pick, value}}, socket) do
  socket =
    socket
    |> Mob.Socket.assign(:last, to_string(value))
    |> Mob.Socket.assign(:open, nil)

  {:noreply, socket}
end

Differences from the web component

The Mob Menubar does not port these parts of the headless component:

  • Roving tabindex and the arrow key, Home and End navigation.
  • loop and modal.
  • orientation: vertical. A vertical menubar is a Menu.
  • Hover switching. Every menu opens on tap.