Overlays
Popovers, tooltips, menus, dialogs, selects, and toasts render at the end of <body>, so a pane's overflow: hidden never clips them. The ones that open from a trigger take the trigger element and add aria-expanded, aria-controls, and the toggle to it.
scala
import wvlet.uni.dom.all.*
import wvlet.uni.dom.all.given
import wvlet.uni.rx.Rx
import wvlet.uni.ui.*
import scala.language.implicitConversions
import wvlet.uni.ui.base.{Align, Side}
val filterOpen = Rx.variable(false)
val menuOpen = Rx.variable(false)
val confirm = Rx.variable(false)
div(
Popover(filterOpen)(Button("Filter"))(Input(query).withLabel("Filter")).withAlign(Align.End),
Tooltip("Close dock")(IconButton("Close dock", closeIcon)).withSide(Side.Left),
Menu(menuOpen)(IconButton("More", moreIcon))(
MenuItem("Rename").onSelect(() => rename()),
MenuSeparator,
MenuItem("Delete").onSelect(() => confirm := true)
),
AlertDialog(confirm)("Delete this session?")
.withDescription("Its messages can't be restored.")
.withConfirmLabel("Delete")
.danger
.onConfirm(() => delete())
)| Component | What it does |
|---|---|
Popover(open)(trigger)(content*) | A non-modal panel next to trigger. .withSide, .withAlign, .withLabel |
Tooltip(text)(trigger) | A text label that shows on hover (after .withDelay(ms), 500 by default) and on keyboard focus; text can be an Rx[String] |
Menu(open)(trigger)(entries*) | A list of MenuItems and MenuSeparators. Items take .onSelect, .withShortcut, .withDisabled, and .withChecked(rx) |
ContextMenu(target)(entries*) | A menu that opens at the pointer when target is right-clicked |
Dialog(open)(content*) | A modal dialog with a backdrop. .withTitle, .withDescription, .withActions(buttons*), .noDismissOnBackdrop() |
AlertDialog(open)(title) | A confirm-or-cancel dialog: .onConfirm, .onCancel, .withConfirmLabel, .danger |
Select(selected, options)(label) | A button with a list of options, bound to an RxVar[A]; options can be an Rx[Seq[A]] |
Toaster() | Mount one per app, then call .info, .success, or .error on it to show a message |
They all behave the same way:
- Closing. Escape, a press outside, or setting
open := falsecloses an overlay. When overlays are nested, such as a menu inside a dialog, Escape closes only the top one. - Focus. Opening moves focus into the overlay, and closing moves it back to the trigger. A dialog keeps Tab and Shift+Tab inside it and focuses the element with
autofocusfirst. - Keyboard. In menus and selects, the arrow keys, Home, End, and typed letters move between items, and Enter or Space chooses one.
- Placement. An anchored overlay flips to the other side of its trigger when there's no room, and shifts to stay inside the window. Its final side is in
data-side. - Animation. Overlays fade in on open and out on close. A closing overlay gets
data-state="closed"andinert, and stays in the document until its exit animation ends. Focus and Escape have already moved on by then. Style.uni-popover[data-state='closed'](and the other overlay classes) to change the exit; withanimation: none, or underprefers-reduced-motion, overlays unmount at once.
wvlet.uni.ui.base holds the pieces they share, for building your own overlay: Anchored.place (placement math), Dismiss (the Escape and outside-press stack), FocusTrap, and FocusReturn.