diff --git a/AGENTS.md b/AGENTS.md index e39f9a8..d1eb36e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,7 +73,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark. - gpui focuses the nearest focusable ancestor on mouse down, and apps wrap everything in a `FocusScope`. A press that must leave focus alone calls `prevent_default`, as buttons and context regions do. - gpui runs the click of every pressed element under the pointer, a row's with its button's. A pressable row that holds buttons stops their press at their box on mouse down (`files::transfer`), as `overlays::media_button` stops its own. - gpui draws a window's first frame inside `open_window`. State the first frame reads, such as `cx.set_menus`, is set before it opens. -- gpui clicks a focused element when Enter or Space is released. An overlay that hands focus back picks on release too; a pick on press returns focus first, and the release clicks the opener again. An edit in place that hands focus back keeps its text on Enter's release (`forms::InlineEdit`). The press itself bubbles, so a container that takes Space leaves a focused button its own: `overlays::media_button` stops it. +- gpui clicks a focused element when Enter or Space is released. An overlay that hands focus back picks on release too; a pick on press returns focus first, and the release clicks the opener again. An edit in place that hands focus back keeps its text on Enter's release (`forms::InlineEdit`). The press itself bubbles, so a container that takes Space leaves a focused button its own: `overlays::media_button` stops it, and a button in a `SelectableList` row stops Space and Enter at its box (`mail::MailItem`'s star, `messaging::PinnedMessages`' Unpin). - A focus handle a component hands back lives across all its modes: take it with `tab_stop` before any early return, or keyed state drops it while the other mode draws and the handed-back focus lands nowhere. - A dialog goes through `overlays::Dialog`: scrim, focus trap, Escape and focus return live there. Its close runs on every way out, so a dialog with two answers marks the chosen one before it closes (`messaging::IncomingCallDialog`). AlertDialog, ConfirmDialog and PromptDialog build on it; a fullscreen dialog fills the window below its title bar. A dialog's card fits the window, a title bar's height clear at each end; its body scrolls and its actions stay. - Controls on a dark layer over pictures are `overlays::media_button`, light icons that dim when they have nowhere to go, for the lightbox and a video player's bar alike. diff --git a/src/mail/list.rs b/src/mail/list.rs index 2513274..386bf65 100644 --- a/src/mail/list.rs +++ b/src/mail/list.rs @@ -166,6 +166,14 @@ impl MailItem { window.prevent_default(); cx.stop_propagation(); }) + .on_key_down(|event, _, cx| { + let key = event.keystroke.key.as_str(); + if matches!(key, "space" | "enter") + && !event.keystroke.modifiers.modified() + { + cx.stop_propagation(); + } + }) .on_click(move |_, window, cx| { log::info!("mail {key}: star {}", !starred); set(!starred, window, cx) diff --git a/src/mail/tests/mod.rs b/src/mail/tests/mod.rs index 91c0626..8a29980 100644 --- a/src/mail/tests/mod.rs +++ b/src/mail/tests/mod.rs @@ -187,6 +187,16 @@ fn a_star_toggles_and_leaves_the_selection(cx: &mut TestAppContext) { assert_eq!(heard(&host, cx), ["star m3 false"]); } +#[gpui::test] +fn space_on_a_star_stars_and_leaves_the_selection(cx: &mut TestAppContext) { + let (host, cx) = mailing(mails, cx); + tab_to(2, cx); + press("space", cx); + tab_to(2, cx); + press("enter", cx); + assert_eq!(heard(&host, cx), ["star m1 true", "star m1 true"]); +} + #[gpui::test] fn no_mail_says_so_and_takes_no_tab_stop(cx: &mut TestAppContext) { let (_, cx) = mailing( diff --git a/src/messaging/pins.rs b/src/messaging/pins.rs index db94a4d..e5ce17d 100644 --- a/src/messaging/pins.rs +++ b/src/messaging/pins.rs @@ -90,6 +90,14 @@ impl RenderOnce for PinnedMessages { div() .debug_selector(move || format!("unpin {named}")) .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .on_key_down(|event, _, cx| { + let key = event.keystroke.key.as_str(); + if matches!(key, "space" | "enter") + && !event.keystroke.modifiers.modified() + { + cx.stop_propagation(); + } + }) .child( IconButton::new( (self.id.clone(), format!("unpin-{key}")), diff --git a/src/messaging/tests/mod.rs b/src/messaging/tests/mod.rs index a639867..88cb1aa 100644 --- a/src/messaging/tests/mod.rs +++ b/src/messaging/tests/mod.rs @@ -221,6 +221,17 @@ fn a_press_on_unpin_unpins_and_opens_nothing(cx: &mut TestAppContext) { ); } +#[gpui::test] +fn space_on_unpin_unpins_and_opens_nothing(cx: &mut TestAppContext) { + let (host, cx) = pinning(cx); + tab_to(2, cx); + press("space", cx); + assert_eq!( + host.read_with(cx, |host, _| host.asked.clone()), + ["unpin newer"] + ); +} + #[test] #[should_panic(expected = "pin a twice")] fn a_message_is_pinned_once() {