Menu

v0.0.10

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

The Mob Menu in Mishka Chelekom shows a list of actions on iOS and Android screens built with Mob. It is the native port of the headless Menu and draws items, separators, section labels, checkboxes, radio rows and nested submenus as native views.

Overview of the Mob Menu component for iOS and Android

The menu is a panel of tappable rows. It builds on the Popover panel, so a menu and a popover share the same radius, border and padding. The menu adds the item list, separators and section labels.

Rows are left-aligned boxes, not buttons. A Material button centers its label, and a menu reads from the leading edge. Rows keep the order you write them in.

You place the menu yourself. Put it under a trigger in a column for a dropdown, or inside a Drawer for a bottom sheet, which is often the better pattern on a phone. The menu does not float over the page, so it has no side or alignment to set.

The screen owns the state. A closed menu renders nothing. When a user taps an enabled row, the screen receives {:tap, {tag, item_id}} in handle_info/2 and decides what happens next: toggle a tick, change a radio choice, open a submenu or close the menu. The menu never changes open itself.

Each row kind looks different:

  • A danger item uses danger_color, so a destructive action stands out from the rest.
  • A disabled item is muted and sends no event.
  • A checkbox row shows a tick when checked. A radio row shows a filled circle when checked and an empty one otherwise.
  • A submenu row shows ▸ when closed and ▾ when open. Its rows appear indented below it. A phone has no hover and no room beside the panel, so the submenu does not fly out sideways.

Generate the menu component

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

bash
mix mishka.ui.gen.mob menu

The generator registers the tag, so you can use it in ~MOB markup. It also generates the Popover that the menu builds on, and 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 <MishkaMenu>. Without a prefix, the tag is <Menu>.

Props

PropTypeDefaultDescription
openbooleanfalseWhether the menu is shown. The screen owns it, and a closed menu renders nothing.
on_selectevent tagnoneSent as {:tap, {tag, item_id}} when a user taps an enabled row.
widthnumbernilPanel width. Without it the panel fills the available width.
danger_colorcolor token or ARGB integer:errorColor of items marked danger.
backgroundcolor token or ARGB integer:surfacePanel fill, from the Popover panel.
corner_radiusradius token or number:radius_mdPanel corners.
paddingspacing token or number6Padding inside the panel.
border_colorcolor token or ARGB integer:borderPanel border color.
border_widthnumber1Panel border width.

Row tags

Write rows as child tags. Each tag has the attributes below.

TagAttributesDescription
<MishkaMenuItem>id, label, icon, disabled, danger, test_idAn action row. id is the value on_select reports, and test_id becomes the row's native test tag.
<MishkaMenuSeparator>noneA divider between groups.
<MishkaMenuLabel>textA small heading above a group.
<MishkaMenuCheckbox>id, label, checked, disabled, test_idA row with a tick. When test_id is set, the test tag gains -checked or -unchecked.
<MishkaMenuRadio>id, label, name, checked, disabled, test_idOne choice in a named group. It reports like any row, and the screen sets which row is checked.
<MishkaMenuSubmenu>id, label, open, disabled, test_idA row that reveals nested rows inline. When test_id is set, the test tag gains -open or -closed.

Each tag has a builder function that returns the same node, for rows that come from data: item/3, separator/0, label/1, checkbox/3, radio/4 and submenu/3. For example, item(:delete, "Delete", danger: true).

Events

Set on_select to an atom. When a user taps an enabled row, your screen receives {:tap, {tag, item_id}} in handle_info/2, where tag is your atom and item_id is the row's id. Disabled rows send nothing.

elixir
def handle_info({:tap, {:pick, :grid}}, socket) do
  {:noreply, Mob.Socket.assign(socket, :grid?, not socket.assigns.grid?)}
end

def handle_info({:tap, {:pick, id}}, socket) do
  socket =
    socket
    |> Mob.Socket.assign(:last, id)
    |> Mob.Socket.assign(:open?, false)

  {:noreply, socket}
end

The first clause keeps the menu open while a checkbox toggles. The second treats every other row as a command and closes the menu.

Differences from the web component

The Mob Menu does not port these parts of the headless Menu:

  • side, align and the offsets. You place the menu, so it has no side to take.
  • open_on_hover and delay. A touch screen has no hover.
  • The web component's id and *_class attributes. Style the panel with the props above.
  • The sideways submenu. A submenu opens inline, with its rows indented below the trigger row.