fix(ui): let escape close an open menu before its dialog

Escape closed the whole Preferences dialog even while a menu was open over
it. That held while Preferences had no menu of its own; the Interface size
menu is the first, so one Escape threw away the dialog with the menu.
Dialog keys now leave Escape to an open menu, and the next Escape closes the
dialog as before.

Adds a keyboard test of the Interface size menu: Tab reaches it, Enter opens
it, the arrows move through the sizes, Enter picks one and closes the menu,
and Escape closes only the menu.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
leandrodslv
2026-10-18 14:39:58 +02:00
co-authored by Claude Sonnet 5.5
parent a6659dcdfc
commit 4d9d12ea13
2 changed files with 68 additions and 1 deletions
+5 -1
View File
@@ -143,8 +143,12 @@ fn about_body(ui: &mut egui::Ui, t: &Tokens) {
}
/// Escape closes the topmost simple dialog (one without menus of its own), before the canvas's
/// shortcuts see it (it doesn't deselect), like a system dialog: from one of its fields too.
/// shortcuts see it (it doesn't deselect), like a system dialog: from one of its fields too. A menu
/// open over the dialog (Preferences ▸ Interface size) takes the first Escape and closes alone.
pub fn keys(app: &mut FigApp, ctx: &Context) {
if egui::Popup::is_any_open(ctx) {
return;
}
let u = &mut app.ui;
let open = u.color_profile.is_some()
|| u.menu_input.is_some()
+63
View File
@@ -155,3 +155,66 @@ fn every_window_size_system_scale_and_interface_size_lays_out_inside_the_window(
}
}
}
/// The interface size is reachable and changeable without a pointer: Tab reaches the menu, Enter
/// opens it, the arrows and Tab move through its entries, Enter picks one and closes the menu, and
/// Escape closes it alone, without changing anything and without closing Preferences (a second
/// Escape does).
#[test]
fn the_interface_size_menu_works_from_the_keyboard() {
use crate::a11y::focused_name;
use egui::{Event, Key, Modifiers, Popup};
// A key press, then two quiet frames for the focus (and the name a screen reader reads) to settle.
let press = |u: &mut Ui, key: Key| {
u.frame(vec![
Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE },
Event::Key { key, physical_key: None, pressed: false, repeat: false, modifiers: Modifiers::NONE },
]);
u.frame(vec![]);
u.frame(vec![]);
};
let open = |u: &mut Ui| {
// The Preferences dialog is not modal for the keyboard: Tab crosses the main window first.
let mut tabs = 0;
while focused_name(&u.ctx) != "Interface size" {
assert!(tabs < 140, "Tab never reaches the Interface size menu (focus: {:?})", focused_name(&u.ctx));
press(u, Key::Tab);
tabs += 1;
}
press(u, Key::Enter);
assert!(Popup::is_any_open(&u.ctx), "Enter opens the menu");
};
let chosen = |u: &Ui| u.app.inspect()["uiScale"]["choice"].clone();
let mut u = Ui::new(Session::new());
u.app.ui.preferences = true;
u.ctx.enable_accesskit();
settle(&mut u, 1.0);
assert_eq!(chosen(&u), json!("auto"));
// Escape closes the menu and nothing else: the size is unchanged and Preferences stays open.
open(&mut u);
press(&mut u, Key::Escape);
u.frame(vec![]);
assert!(!Popup::is_any_open(&u.ctx));
assert!(u.app.ui.preferences, "the first Escape closes the menu only");
assert_eq!(chosen(&u), json!("auto"));
// The arrows enter the menu on Auto (which says the size it draws at) and go down through the
// sizes (75, 100, 125); Enter picks the one that has the keyboard and closes the menu.
open(&mut u);
press(&mut u, Key::ArrowDown);
assert_eq!(focused_name(&u.ctx), "Auto (100%)");
for _ in 0..3 {
press(&mut u, Key::ArrowDown);
}
assert_eq!(focused_name(&u.ctx), "125%");
press(&mut u, Key::Enter);
assert_eq!(chosen(&u), json!(1.25));
assert!(!Popup::is_any_open(&u.ctx), "picking a size closes the menu");
assert!(u.app.ui.preferences, "picking a size leaves Preferences open");
// With no menu open, Escape closes Preferences as before.
press(&mut u, Key::Escape);
assert!(!u.app.ui.preferences);
}