Summary#
onEscape binds an action to the Escape key, for the universal "get me out of this" gesture:
component Dropdown() {
bool open = false;
action Close() { open = false; }
render {
Pressable(onClick: () => open = true) { Text("Options"); }
if (open) {
Box(onEscape: Close) {
Text("…menu…");
}
}
}
}Escape closes the menu whether or not anything inside it has focus.
Signature#
onEscape: <action>Description#
It is NOT onEnter's mirror image, and that is the whole design#
The two look like a pair and behave differently, because they answer different questions:
| what it acts on | so it listens | |
|---|---|---|
| onEnter | the thing you are on — confirm this field | on the focused element |
onEscape | the thing that is open — dismiss this overlay | page-wide, while mounted |
A popover, a drawer, a lightbox, a dropdown — almost none of them hold focus, because the pointer opened them.
Scoped to the element, onEscape would compile, wire up correctly, and then never fire for the single case it exists
to serve. So it listens for as long as the element is on screen, and stops the moment it leaves.
Nested overlays close one layer per keystroke#
Only the innermost live handler runs. A dialog opened over a drawer closes the dialog; pressing Escape again closes the drawer. Every overlay on the page does not collapse at once, which is what a page-wide listener would do if each one answered independently.
It is not blocked by a busy control#
Deliberately, onEscape is not treated as an activation: it has no in-flight guard and no busy spinner. Dismissing
is local — an overlay you cannot close because something else on it is still loading is a worse failure than a
double dismissal, which does nothing anyway.
An IME keystroke is not yours#
Escape also cancels an input-method candidate list. That keystroke belongs to the IME, so it does not run your action.
Examples#
component Shell() {
bool drawer = false;
action Open() { drawer = true; }
action Close() { drawer = false; }
render {
Pressable(onClick: Open) { Text("Menu"); }
if (drawer) {
Box(onEscape: Close, p: 4) {
Pressable(onClick: Close) { Text("Close"); }
}
}
}
}Note that the button and the key run the same action. That is the pattern to keep: a dismissal reachable only by pointer is unreachable for anyone not using one, and two separate code paths drift.