Onboarding · Flows and highlights: focus that holds as the flow turns, a tip that closes

navigation::Wizard keeps one focus handle for Next and Finish, so a
second press on the step before last finishes; Back that reaches the
first step, and a finished step picked by key, hand focus to Next.
Popover::with_opener hands its content a close that gives focus back:
Got it closes the hotspot's tip before the owner hides it, and the
event details close before Edit and Delete run, so focus never lands
on a panel that left the tree. The wizard's count and the setup
checklist's tally name their selectors after their words, and the bar
takes the count's own value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-27 14:48:20 -04:00
1 parent 996daed6e4
commit 973f10a6dc
15 files changed
+180 -54

No files matched your search

+14 -7
View File
@@ -12,7 +12,7 @@ use crate::{
theme::ControlSize,
};
type Content = Box<dyn FnOnce(&mut Window, &mut App) -> AnyElement>;
type Content = Box<dyn FnOnce(Run, &mut Window, &mut App) -> AnyElement>;
type Own = Box<dyn FnOnce(Run) -> AnyElement>;
/// A popover's own button: its label and look.
@@ -73,20 +73,22 @@ impl Popover {
variant: ButtonVariant::Secondary,
size: ControlSize::default(),
}),
content: Box::new(move |window, cx| content(window, cx).into_any_element()),
content: Box::new(move |_, window, cx| content(window, cx).into_any_element()),
}
}
/// A panel that opens from an element of the owner's: `opener` builds it with the run that opens and closes the panel.
/// A panel that opens from an element of the owner's: `opener` builds it with the run that opens and closes the panel, and `content` gets the run that closes it and hands focus back, for a press that ends the panel's work.
pub fn with_opener<T: IntoElement, E: IntoElement>(
id: impl Into<ElementId>,
opener: impl FnOnce(Run) -> T + 'static,
content: impl FnOnce(&mut Window, &mut App) -> E + 'static,
content: impl FnOnce(Run, &mut Window, &mut App) -> E + 'static,
) -> Self {
Self {
id: id.into(),
opener: Opener::Own(Box::new(move |toggle| opener(toggle).into_any_element())),
content: Box::new(move |window, cx| content(window, cx).into_any_element()),
content: Box::new(move |close, window, cx| {
content(close, window, cx).into_any_element()
}),
}
}
@@ -179,9 +181,14 @@ impl RenderOnce for Popover {
close(&state, window, cx);
return host;
}
let (escape, out, tall) = (state.clone(), state.clone(), state.clone());
let (escape, out, tall, done) =
(state.clone(), state.clone(), state.clone(), state.clone());
let (anchor, height) = (state.read(cx).host, state.read(cx).height);
let body = (self.content)(window, cx);
let body = (self.content)(
Rc::new(move |window, cx| close(&done, window, cx)),
window,
cx,
);
let panel = div()
.id((self.id.clone(), "panel"))
.relative()
+1 -1
View File
@@ -430,5 +430,5 @@ fn a_tall_dialog_body_keeps_its_height_and_scrolls(cx: &mut TestAppContext) {
#[test]
#[should_panic(expected = "an icon is for its own button")]
fn a_popover_opened_by_its_owner_takes_no_icon() {
let _ = Popover::with_opener("own", |_| div(), |_, _| div()).icon(IconName::Clock);
let _ = Popover::with_opener("own", |_| div(), |_, _, _| div()).icon(IconName::Clock);
}