# Menus, the command menu, and keyboard shortcuts

> The kit's menus grow out of the control that opened them and keep a notch pointing back at it, carry icons, key hints and a checked mark, and are driven from the keyboard by the platform (arrows, Home/End, type-ahead). `CommandMenu` is ⌘K. `shortcut:` on any pressable element makes a key combo press it from anywhere on the page, and every hint of that key — a menu row's chip, a button's tip, the `?` sheet — is derived from that one binding.

<!-- id: ui-menus · area: ui · stability: preview · html: https://osysharp.com/reference/ui/menus/ -->

## Summary        {#summary}
A menu in an all-day tool has three jobs: show which control opened it, be fast from the keyboard, and teach the
shortcuts. The kit's `Menu` does all three — it grows from its trigger's corner with a notch pointing back at it, the
platform moves through its items with the arrow keys and type-ahead, and a `MenuItem` shows its shortcut in a keycap.
`CommandMenu` is the launcher (⌘K) every such tool has, and `shortcut:` binds a key combo to any button. A key is
written ONCE, on the control it presses; every hint of it is derived from that binding, so no hint can advertise a key
that does nothing.

## Signature      {#signature}
```osy syntax
Box(position: Position.Relative) {
  IconButton("Actions", ToggleMenu) { Icon(Icons.Dots); }
  if (open) {
    Backdrop(CloseMenu);                                   // click away, or Escape
    Menu(from: MenuFrom.BelowEnd, label: "Ticket actions") {
      MenuItem("Assign to me", AssignMe, icon: Icons.User);       // its key chip comes from AssignMe's binding
      MenuItem("Waiting on customer", SetPending, checked: status == Status.Pending);
      MenuItem("Delete", Delete, tone: Tone.Danger);
    }
  }
}

CommandMenu(commands, onDismiss: ClosePalette)            // ⌘K — a Command[] of Label/OnPress/Icon/Group
Button("Send", onPress: Send, shortcut: "Mod+Enter")       // the combo presses the button from anywhere
Button("Send", onPress: Send, shortcut: "Mod+Enter", showShortcut: true)   // "Send ⌘↵" on its face
KeyHint("Mod+K")                                           // a binding as keycaps: ⌘K on a Mac, Ctrl+K elsewhere
KeyHintOf(AssignMe)                                        // the key bound on this page to AssignMe, or nothing
KeyboardSheet()                                            // `?` lists every key on the page (the shells mount one)
```

## Description    {#description}

### A menu grows from the thing that opened it   {#connected}
`from:` says where the menu hangs, and so which corner it grows out of: `MenuFrom.BelowStart` (the default),
`BelowEnd` for a trigger at the right edge, `AboveStart` for one at the foot of a panel, `Beside` for a rail. The
menu is placed against its nearest positioned ancestor, so put the trigger and the menu together in a
`Box(position: Position.Relative)`. The opening is a short scale-and-slide from that corner (160ms), with a notch on
the edge facing the trigger; under reduced motion it simply fades.

### The keyboard is the platform's   {#keyboard}
Any element with `role: UiRole.Menu` — every kit menu is one — gets the keyboard the ARIA menu pattern describes,
with nothing written: when it opens, its first item takes focus; ↓/↑ move between items and wrap; Home/End jump;
typing a letter moves to the next item starting with it (a quick run of letters is one word); Enter presses;
Escape closes it through its `Backdrop`. An item is anything with `role: UiRole.MenuItem` (as `MenuItem` is) that is
not disabled; a menu nested inside keeps its own items. A field inside a menu keeps its letters — only ↓/↑ leave it.

**And when it closes, the keyboard goes back to what opened it** — after Escape, a picked item, or a click away, focus
is on the trigger again, so the next Tab carries on from there rather than from the top of the page. Two exceptions,
both deliberate: an item that opens a DIALOG hands the keyboard into the dialog (and it comes back to the trigger when
the dialog closes), and focus the person has already put somewhere else — a field they clicked into — stays there. In a
test: `Ui.Click("Actions"); Ui.Press("Escape"); Assert.Focused("Actions");`.

### ⌘K — the command menu   {#command-menu}
`CommandMenu(commands, onDismiss)` is a filter field over every `Command` the app offers, grouped under each one's
`Group`, each row showing the key bound on the page to its `OnPress` — derived, so a command whose action no control
binds shows none. Typing narrows it, ↓ moves into the results, Enter runs the first match, and
a command runs with the menu already closed. Open it from `ShellSearch`, whose box ⌘K (Ctrl+K elsewhere) presses
from anywhere on the page.

```osy title="a command menu behind a button that ⌘K presses" test app=ui-menus-command
[Route("/")]
[AllowAnonymous]
component Desk() {
  bool palette = false;
  string last = "";
  action Open() { palette = true; }
  action Close() { palette = false; }
  action Assign() { last = "assigned"; }
  action Snooze() { last = "snoozed"; }

  render {
    Stack(gap: 3, p: 6) {
      Button("Search", onPress: Open, shortcut: "Mod+K");
      // The command menu's "Assign to me" row shows I, because this button runs the same action and binds it.
      Button("Assign to me", onPress: Assign, shortcut: "I");
      Text(last);
      if (palette) {
        CommandMenu([
          new Command { Label = "Assign to me", Group = "Ticket", Icon = Icons.User, OnPress = Assign },
          new Command { Label = "Snooze until tomorrow", Group = "Ticket", Icon = Icons.Clock, OnPress = Snooze },
        ], onDismiss: Close);
      }
    }
  }
}
```

### `shortcut:` — a combo that presses an element   {#shortcut}
`shortcut: "Mod+K"` on any element a reader presses makes that key combo press it from anywhere on the page — through
its own `onClick`, so a shortcut can never do something its button does not. `Mod` is ⌘ on a Mac and Ctrl
elsewhere (both work everywhere); write other modifiers as `Shift`, `Alt`, `Control`, `Meta`, and several combos
separated by a space. A **bare** key (`"J"`, `"?"`) is ignored while the reader is typing in a field, so a list's
`J`/`K` never fires from inside a reply box. When two mounted elements of ONE owner share a combo, the one mounted
last wins — a dialog's `Mod+Enter` beats the page's while the dialog is open (two owners: see [[#conflicts]]). It is announced to a screen reader as
`aria-keyshortcuts`. A shifted symbol is its own key: `"?"` and `"Shift+?"` are the same binding, while
`"Shift+A"` and `"A"` are two. A control the reader cannot reach — on a retained route that is hidden, or inside
`inert` — never takes a key.

### One key, two owners   {#conflicts}
A page shows controls written by different code — the app's own and each kit's. When two OWNERS bind the same key
(the app and a kit, or two kits), the platform resolves it rather than letting whichever mounted last take it:

- **the app's own binding keeps the key**;
- otherwise the binding that comes **first on the page** keeps it;
- the other is pressed with **Shift added** (`M` → `Shift+M`, `Mod+K` → `Mod+Shift+K`) — and its hint chip, its tip,
  `aria-keyshortcuts` and the `?` sheet all say so.

A key that cannot take Shift (it already has it, a symbol such as `?`, or a sequence) stays as it is, and the last one
shown wins. The owner of a binding is the code that declares the action it runs (the kit's list, the app's page).
The compiler collects every `shortcut:` a page can show — through the components it renders, a kit's included — and
warns `SHORTCUT_CONFLICT`, naming both owners and the key the platform moves to; the browser console says the same at
run time for a key set in a setting (a kit's key handed in through its setup), which the compiler cannot see. To choose
yourself, bind a different key — an app's own assignment always wins.

### A sequence — `"G then M"`   {#sequence}
`shortcut: "G then M"` is two presses: G, let go, then M within a second and a half — the "go to" keys of an inbox. The
first press waits for the second instead of firing; a pair that binds nothing is forgotten and the second key is read
on its own (so `G` then `J` still moves down a list). A key that begins a sequence belongs to the sequence. A
`NavItem` takes one too (`Shortcut = "G then M"`), so a rail row is reached from anywhere. The `?` sheet and a control's
tip show it as it is pressed, a keycap per step; ARIA has no spelling for a sequence, so it is not announced as
`aria-keyshortcuts`. In a test, press it the way a person does: `Ui.Press("G then M")`.

### Where the hints come from   {#hints}
Every hint is DERIVED from the `shortcut:` on the control the key presses; none is typed a second time.

| hint | how | derived from |
|---|---|---|
| a menu row's chip | `MenuItem(label, onPress)` — automatic | the key bound on the page to `onPress`; nothing when none. A row that runs a wrapper (`() => Run(c)`) names the action with `hintFor: c.OnPress` |
| a ⌘K row's chip | `Command { OnPress = Assign }` — automatic | the key bound to `OnPress` |
| a tip on hover and keyboard focus | `Button`/`IconButton` with `shortcut:` — automatic | the button's own name and binding |
| the key on a button's face | `Button(…, shortcut: "Mod+Enter", showShortcut: true)` → "Send ⌘↵" | the button's own binding |
| a chip anywhere | `KeyHintOf(action)`, or `KeyHint("Mod+K")` for a binding constant | the live binding / the binding as written |
| the `?` sheet | `KeyboardSheet()` — every work shell mounts one | every key a press would reach on the page |

**One spelling, every platform.** Write `Mod` — never ⌘ or Ctrl — and the platform draws it for the reader: `⌘K`,
`⇧⌘P`, `⌘↵` on a Mac; `Ctrl+K`, `Ctrl+Shift+P`, `Ctrl+Enter` elsewhere. The atom props underneath are ambient and
usable on any element: `keycap: "Mod+K"`, `keycapOf: Assign` (hidden when nothing binds the action), and
`shortcutList: true` (the sheet's list).

### `?` — every key on the page   {#sheet}
`KeyboardSheet()` binds `?`: from anywhere (except while typing in a field) it opens a dialog listing every key a press
would reach, each row the control's name and its key, grouped by the labelled region the control lives in (the page
header, the main navigation, a ticket, a dialog). `?` again, or Escape, closes it. It is generated from the live
bindings, so it never lists a key that does nothing and never misses one that works. The sheet's own control is
invisible until it takes keyboard focus, so a keyboard reader can find it too. An app with no shell renders
`KeyboardSheet()` once in its layout.

### Testing them   {#testing}
Open a menu by its trigger and press an item by its label: `Ui.Click("Priya Nandan"); Ui.Click("Account")`. Press
a shortcut with a chord: `Ui.Press("Mod+K")`, `Ui.Press("Shift+Tab")`. Narrow a command menu with
`Ui.Fill("Type a command or search", "stock")`. Open the sheet with `Ui.Press("?")` and read it with
`Assert.Visible("New order", within: "Keyboard shortcuts")`.

## See also   {#see-also}
- [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/) — every control the kit ships
- [keys](https://osysharp.com/reference/ui/keys/) — `keys:`, a surface that owns keys while it has focus (a board, a canvas)
- [Dialog.Open / Dialog.Ask / Dialog.Confirm / Dialog.Discard](https://osysharp.com/reference/ui/dialogs/) — overlays that own a unit of work
- [App shells](https://osysharp.com/reference/ui/shell/) — the shells whose identity menus and search box use all of this
- [Ui — drive the app's UI from a test](https://osysharp.com/reference/testing/ui/) — locating, clicking and pressing in a test
