Menu

A drop-down menu for selecting actions to be performed, including keyboard navigation.

Basic Example

A drop-down menu is created using the menu() factory function.

By clicking on the module created by menuButton or Space if the menuButton focused, the selection list is displayed.

You can navigate within the selection list using the keyboard. By Enter, Space or a mouse click an item can be selected. If the menu looses focus or the user clicks outside the selection list, the box is hidden.

The selection list is generated by the menuItems building block factory. Within this scope, menuItem defines the individual options defined.

Beware: Entries cannot be removed from the menu. Once an entry is added to the menu by menuItem, it will remain inside forever. So never use some reactive pattern for populating the menu as some Flow<List<T>> combined with some call to renderEach or alike!

If the entries change, you must re-render the whole component.

data class MenuEntry(val label: String, val icon: String)

val entries = listOf(
MenuEntry("Duplicate", HeroIcons.duplicate),
MenuEntry("Archive", HeroIcons.archive),
MenuEntry("Move", HeroIcons.share),
)

val action = storeOf("", job = Job())

menu {
menuButton {
+"Open Menu"
}

menuItems {
entries.forEach { entry ->
menuItem {
svg { content(entry.icon) }
+entry.label
selected.map { entry.label } handledBy action.update
}
}
}
}

Do not forget to include a portalRoot()-call at the end of your initial RenderContext as explained here!

Styling the active Item

If a menuItem is navigated to by keyboard or mouse movement, it is active.

In the scope of a menuItem, active is available as Flow<Boolean> to apply styling or to render or hide certain elements accordingly to the state:

menuItem {
className(active.map {
if (it) "bg-violet-500 text-white"
else "text-gray-900"
})

svg { content(entry.icon) }
+entry.label
selected.map { entry.label } handledBy action.update
}

Disable Items

The individual items in the selection list can be disabled statically or dynamically. In the scope of a menuItem the Handler<Boolean> disable is available for this. It can be called up directly or used to dynamically set the disabled status of an item depending on an external data flow. The disabled state of the item is also available as Flow<Boolean> to style the item depending on it, for example.

data class MenuEntry(val label: String, val icon: String, val disabled: Boolean = false)

val entries = listOf(
MenuEntry("Duplicate", HeroIcons.duplicate, true),
MenuEntry("Archive", HeroIcons.archive),
MenuEntry("Move", HeroIcons.share),
MenuEntry("Delete", HeroIcons.trash),
MenuEntry("Edit", HeroIcons.pencil),
MenuEntry("Copy", HeroIcons.clipboard_copy, true),
MenuEntry("Encrypt", HeroIcons.key)
)

val action = storeOf("", job = Job())

menu {
menuButton { /* ... */ }

menuItems {
entries.forEach { entry ->
menuItem {
className(active.combine(disabled) { a, d ->
if (a && !d) "bg-violet-500 text-white"
else {
if (d) "text-gray-300" else "text-gray-900"
}
})

svg { content(entry.icon) }
+entry.label

if (entry.disabled) disable(true)

selected.map { entry.label } handledBy action.update
}
}
}
}

Open State

Menu is an OpenClose component. There are different Flows and Handler like opened in its scope available to control the open state of a menu based on state changes.

The opening state of the menu can be bound to an external store via data binding, e.g. to always display the menu list.

Transitions

Showing and hiding the menu can be easily animated with the help of transition:

menuItems {
transition(opened,
enter = "transition duration-100 ease-out",
enterStart = "opacity-0 scale-95",
enterEnd = "opacity-100 scale-100",
leave = "transition duration-100 ease-in",
leaveStart = "opacity-100 scale-100",
leaveEnde = "opacity-0 scale-95"
)

characters.forEach { (entry, disabledState) ->
listboxItem(entry) {
//...
}
}
}

Positioning

menuItems is a PopUpPanel and therefore provides in its scope some configuration options, e.g. to control the position or distance of the list box from the menuButton as a reference element:

menuItems {
placement = PlacementValues.top
addMiddleware(offset(20))

menuItem {
//...
}

//...
}

Focus Management

If the selection list of the menu is open, the focus is on the menuItems element. If it loses focus again, the select list is closed and the focus returns to the menuButton.

Mouse Interaction

A click on the menuButton toggles the state of the selection list. A click outside the opened selection list closes this. If the mouse is moved over an item in the open list, it becomes active marked. Clicking on an item when the list is open selects it and closes the list.

Keyboard Interaction

Command Description
Enter Space when menuButton is focused Opens menu and activates first not disabled item
︎when menu is open Activates previous / next item
Home End ︎when menu is open Activates first / last item
A-Z a-z when menu is open Activates first item beginning with according letter
Esc when menu is open Closes menu
Enter Space when menu is open Selects active item

API

Summary / Sketch

menu {
// inherited by `OpenClose`
val openState: DatabindingProperty<Boolean>
val opened: Flow<Boolean>
val close: SimpleHandler<Unit>
val open: SimpleHandler<Unit>
val toggle: SimpleHandler<Unit>

menuButton() { }
menuItems() {
// inherited by `PopUpPanel`
var placement: Placement
var strategy: Strategy
var flip: Boolean
var skidding: Int
var distance: int

// for each T {
MenuItem {
val index: Int
val selected: Flow<Boolean>
val active: Flow<Boolean>
val disabled: Flow<Boolean>
val disable: SimpleHandler<Boolean>
}
// }
}
}

Parameters: classes, id, scope, tag, initialize

Default-Tag: div

Scope property Typ Description
openState DatabindingProperty<Boolean> Optional (two-way) data binding for opening and closing.
opened Flow<Boolean> Data stream that provides Boolean values related to the "open" state. Quite useless within a menu, as it is always true
close SimpleHandler<Unit> Handler to close the list box from inside. Should not be used, as the component handles this internally.
open SimpleHandler<Unit> handler to open; does not make sense to use within a menu!
toggle SimpleHandler<Unit> handler for switching between open and closed; does not make sense to use within a menu.

Available in the scope of: menu

Parameters: classes, scope, tag, initialize

Default-Tag: button

Available in the scope of: menu

Parameters: classes, scope, tag, initialize

Default-Tag: div

Scope property Typ Description
placement Placement Defines the position of the building block, e.g. Placement.top, Placement.bottomRight, etc. Default is Placement.auto. The presumably best position is determined automatically based on the available visible space.
strategy Strategy Determines whether the block should be positioned absolute (default) or fixed.
flip Boolean If the block comes too close to the edge of the visible area, the position automatically changes to the other side if more space is available there.
skidding Int Defines the shifting of the menu along the reference element in pixels. The default value is 0.
distance Int Defines the distance of the menu from the reference element in pixels. The default value is 10.

Available in the scope of: menuItems

Parameters: classes, scope, tag, initialize

Default-Tag: div

Scope property Typ Description
index Int The index within the items.
selected Flow<Boolean> This data stream provides the selection state of the managed option: true the option is selected, false if not. Quite useless within a menu.
active Flow<Boolean> This data stream indicates whether an item has focus: true the option has focus, false if not. Only one option can have focus at a time.
disabled Flow<Boolean> This data stream indicates whether an entry is active (false) or inactive (true).
disable SimpleHandler<Boolean> This handler enables or disables an entry.
Edit this page on Github