Menus
CdxMenu is the one menu: a trigger that opens a panel of rows. It replaced the dropdown, the multi-select dropdown, the combobox, the action menu and the split button, which are removed.
The three modes
| Constructor | Does | Value |
|---|---|---|
CdxMenu.actions | Runs commands | none |
CdxMenu<T>.single | Picks one value | T? |
CdxMenu<T>.multi | Picks any number of values | Set<T> |
CdxMenu<String>.single(
trigger: const CdxMenuTrigger.field(label: 'Country'),
items: const [
CdxMenuItem.option(value: 'in', title: 'India'),
CdxMenuItem.option(value: 'us', title: 'United States'),
],
value: country,
onChanged: (value) => setState(() => country = value),
)
CdxMenu.actions(
trigger: const CdxMenuTrigger.icon(
icon: CdxIcons.more,
tooltip: 'More actions',
),
items: [
CdxMenuItem.action(title: 'Rename', onTap: rename),
const CdxMenuItem.divider(),
CdxMenuItem.action(title: 'Delete', destructive: true, onTap: delete),
],
)In a Form, CdxMenuFormField<T> binds CdxMenu.single and CdxMenuMultiFormField<T> binds CdxMenu.multi: validator, onSaved, autovalidateMode, initialValue. A field or input trigger draws the validator's message under itself.
Triggers
| Trigger | Looks like | Modes |
|---|---|---|
CdxMenuTrigger.field | The form-field box; leading puts a control at its start | all |
CdxMenuTrigger.fieldEnd | A title and a chevron set into the end of another field | all |
CdxMenuTrigger.button | A button with a caret | all |
CdxMenuTrigger.icon | An icon button; tooltip is required | all |
CdxMenuTrigger.split | A primary action joined to a caret | all |
CdxMenuTrigger.custom | What the builder draws | all |
CdxMenuTrigger.input | A text field that filters as you type | single |
CdxMenuTrigger.region | Right-click or long-press on a child | actions |
A field trigger's leading sits inside the outline at the start, keeps the field's height and takes its own gestures: a tap on it never opens the menu. It is muted with the field.
CdxMenu<String>.single(
trigger: CdxMenuTrigger.field(
label: 'Depends on',
leading: PickHandle(pick: pick), // the host's own control
),
value: value,
items: items,
onChanged: onChanged,
)A fieldEnd trigger goes in another field's suffixIcon, such as the unit of a duration. It fills the field's height inside the outline, and its hover and focus fill is flat where it meets the text and rounded at the field's end corners. The field it sits in mutes it when disabled.
CdxTextField(
controller: amount,
suffixIcon: CdxMenu<DurationUnit>.single(
trigger: const CdxMenuTrigger.fieldEnd(tooltip: 'Unit'),
width: const CdxPopoverWidth.trigger(),
value: unit,
items: [for (final u in DurationUnit.values) CdxMenuOption(value: u, title: u.label)],
onChanged: (u) => setState(() => unit = u!),
),
)Items
CdxMenuItem.option, .action, .submenu, .header, .divider, .custom. A value menu may hold actions. A menu of commands holds no options. leading and trailing are widgets; a command also takes a shortcut hint.
A command or a submenu given selected is one of a set of choices, and the selected row shows the check, so a submenu marks the scope a menu of nested scopes is on. The selection slot belongs to a group: the rows between two headers or dividers keep it when one of them is a choice (an option, or a command or submenu given selected), so their titles line up after the check. The other rows of the panel have no selection slot and start at the row's padding: a "Theme" group of three choices does not push Settings or Logout in. A value menu keeps the slot on every row. The leading slot (icons) and the trailing slot (shortcuts, chevrons) are the whole panel's, so icons and shortcuts line up from top to bottom. A submenu's panel applies the same rule to its own rows.
A header names the rows under it up to the next header or divider. A header directly followed by a divider keeps both: the divider is the header's own rule, and the header names the rows after it.
Keep the items list the same object while its items are the same (a field, a constant), not a list built in build().
Sources
items: is shorthand for CdxMenuSource.items. To load: .future (once), .query (for every search, debounced), .listenable (live), .builder (worked out only while the panel is open).
A single-select menu names its value by the title of a loaded option. When the option may not be loaded, give it labelOf or resolveSelectedLabel. A single-select menu on CdxMenuSource.builder that holds a value must have one of them.
Keyboard
Arrow keys, Home and End move the highlight. Enter and Space choose. Escape closes and returns the focus to the trigger; in a search field the first Escape clears the search. Tab and Shift+Tab leave the menu for the control after or before the trigger. The arrow towards the end of the text opens a submenu and the other closes it. Typing letters jumps to the row they spell.
Moving from the removed widgets
dart run vyuh_cdx_ui:migrate_cdx_menu # rewrites lib and test
dart format <the files it changed> ; dart fix --apply ; dart analyze
dart run vyuh_cdx_ui:migrate_cdx_menu --check # proves nothing is leftThe tool rewrites what is mechanical and lists, by file and line, what is left for a person. The full mapping (every old constructor and parameter, what has no equivalent, what behaves differently, and before and after examples) ships with the package: packages/vyuh_cdx_ui/doc/menu-migration.md.
dart fix --apply alone renames the types and parameters that have a one-for-one replacement; the tool does those and the constructors too. The migration page says which of the two does what.