Primitives · Focus: an overlay dropped without Close hands focus back to what it took it from

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 03:39:25 -04:00
1 parent b4716bb09b
commit 38deadde89
3 files changed
+52 -4

No files matched your search

+14
View File
@@ -136,6 +136,7 @@ impl Render for Stage {
.into_any_element(),
});
FocusScope::new(&self.root)
.root()
.size_full()
.child(popover)
.child(div().h(px(52.0)))
@@ -260,6 +261,19 @@ fn escape_closes_a_dialog_and_hands_focus_back(cx: &mut TestAppContext) {
assert!(cx.update(|window, _| before.is_focused(window)));
}
#[gpui::test]
fn a_dialog_its_owner_drops_hands_focus_back(cx: &mut TestAppContext) {
let (view, cx) = stage(cx);
show(&view, Open::Plain, cx);
assert!(!before_focused(&view, cx), "the dialog holds focus");
view.update(cx, |stage, cx| {
stage.open = None;
cx.notify();
});
settle(cx);
assert!(before_focused(&view, cx), "focus went back");
}
#[gpui::test]
fn the_scrim_closes_a_dialog_but_not_an_alert(cx: &mut TestAppContext) {
let (view, cx) = stage(cx);
+37 -4
View File
@@ -1,6 +1,7 @@
use gpui::{
AnyElement, App, Div, ElementId, Entity, FocusHandle, InteractiveElement, IntoElement,
ParentElement, RenderOnce, StyleRefinement, Styled, Subscription, Window, actions, div,
AnyElement, AnyWindowHandle, App, Div, ElementId, Entity, FocusHandle, Global,
InteractiveElement, IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled,
Subscription, WeakEntity, Window, actions, div,
};
use crate::theme::ActiveTheme;
@@ -71,8 +72,16 @@ impl RenderOnce for FocusScope {
let handle = self.handle.clone();
window.use_keyed_state("focus-root", cx, |window, cx| Root {
_lost: cx.on_focus_lost(window, move |_: &mut Root, window, cx| {
log::info!("focus: its element left the tree; the root takes it");
window.focus(&handle, cx);
match dropped_overlay(window, cx) {
Some(previous) => {
log::info!("focus: an overlay left unclosed; focus goes back");
window.focus(&previous, cx);
}
None => {
log::info!("focus: its element left the tree; the root takes it");
window.focus(&handle, cx);
}
}
}),
});
}
@@ -117,6 +126,26 @@ fn step(scope: &FocusHandle, trap: bool, forward: bool, window: &mut Window, cx:
}
}
/// Open overlays holding focus, oldest first, with what they took it from.
#[derive(Default)]
struct Holders(Vec<(WeakEntity<Takeover>, AnyWindowHandle, Option<FocusHandle>)>);
impl Global for Holders {}
/// Forgets overlays gone without closing; the newest one's prior focus.
fn dropped_overlay(window: &Window, cx: &mut App) -> Option<FocusHandle> {
let here = window.window_handle();
let mut back = None;
cx.default_global::<Holders>().0.retain(|(holder, at, previous)| {
let gone = holder.upgrade().is_none();
if gone && *at == here {
back = previous.clone();
}
!gone
});
back
}
/// Focus an overlay holds while open, and what it held before.
pub(crate) struct Takeover {
pub focus: FocusHandle,
@@ -147,6 +176,8 @@ pub(crate) fn take_focus(
if !state.read(cx).taken {
let previous = window.focused(cx);
window.focus(&state.read(cx).focus.clone(), cx);
let holder = (state.downgrade(), window.window_handle(), previous.clone());
cx.default_global::<Holders>().0.push(holder);
state.update(cx, |takeover, _| {
takeover.previous = previous;
takeover.taken = true;
@@ -157,6 +188,8 @@ pub(crate) fn take_focus(
/// Returns focus to whatever held it before the overlay opened.
pub(crate) fn give_back(state: &Entity<Takeover>, window: &mut Window, cx: &mut App) {
let closed = state.downgrade();
cx.default_global::<Holders>().0.retain(|(holder, ..)| *holder != closed);
let previous = state.update(cx, |takeover, _| {
takeover.returned = true;
takeover.previous.clone()