Onboarding · Help: articles in a list the keyboard walks, a long address cut

HelpPanel lists its articles in lists::SelectableList: the list is one
stop whose cursor scrolls into view, and Enter or a double press opens
an article, focus going to All articles as before. SelectableList now
activates on Enter's release, armed by its own press, as the palette
and menus pick: an activation that moves focus left the release to
press what took it, here the way back. ContactSupport cuts a long
address in the middle and puts a Copy beside it. The lists tests move
into a folder, the release tests on their own.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-27 15:28:49 -04:00
1 parent fe1b00d414
commit 3bd4b63d91
9 files changed
+257 -70

No files matched your search

+68 -3
View File
@@ -48,16 +48,16 @@ fn panel(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
.into_any_element()
}
/// Stops on the list: close, the search, each article, then the foot; on an article: close, All articles, then the foot. An article opened by key takes focus to All articles, and the way back takes it to the search, where words narrow the list.
/// Stops on the list: close, the search, the list, then the foot; on an article: close, All articles, then the foot. Enter on the list's cursor, on the first row at first, opens an article and takes focus to All articles, which the release leaves alone; the way back takes it to the search, where words narrow the list.
#[gpui::test]
fn an_article_opens_from_the_list_and_focus_follows(cx: &mut TestAppContext) {
let (host, cx) = bench(panel, cx);
tab(3, cx);
tap("space", cx);
tap("enter", cx);
tap("space", cx);
write("email", cx);
tab(3, cx);
tap("space", cx);
tap("enter", cx);
tab(3, cx);
tap("space", cx);
tab(4, cx);
@@ -77,6 +77,47 @@ fn an_article_opens_from_the_list_and_focus_follows(cx: &mut TestAppContext) {
);
}
fn many(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
let articles = (1..=12).map(|n| article(&format!("a{n}"), &format!("Article {n}"), "Words."));
let (opener, contact) = (owner.clone(), owner);
div()
.w(px(280.0))
.h(px(300.0))
.child(
HelpPanel::new("help", articles, &bench.search, bench.open.clone())
.on_open(move |key, _, cx| {
let key = key.cloned();
opener.update(cx, |bench, cx| {
bench.said.push(format!(
"open {}",
key.as_ref().map_or("none", |key| key.as_ref())
));
bench.open = key;
cx.notify();
})
})
.on_contact(move |_, cx| say(&contact, "contact".into(), cx)),
)
.into_any_element()
}
/// Twelve articles in a 300px panel scroll inside the list, so the foot stays in the box, and the cursor walks down to the eighth.
#[gpui::test]
fn a_long_list_scrolls_inside_the_panel(cx: &mut TestAppContext) {
let (host, cx) = bench(many, cx);
let foot = cx.debug_bounds("help-foot").expect("the foot");
assert!(
foot.bottom() <= px(316.0),
"the foot ends inside the 300px panel, which starts 16px down: {foot:?}"
);
tab(2, cx);
for _ in 0..7 {
tap("down", cx);
}
tap("enter", cx);
assert_eq!(said(&host, cx), ["open a8"]);
}
fn cheatsheet(bench: &Bench, owner: Entity<Bench>) -> AnyElement {
let shortcut = |name: &str, keys: &str| Shortcut {
group: "General".into(),
@@ -216,6 +257,30 @@ fn a_message_waits_while_busy(cx: &mut TestAppContext) {
assert_eq!(said(&host, cx), ["bug: It froze"]);
}
fn long_address(_: &Bench, owner: Entity<Bench>) -> AnyElement {
div()
.w(px(280.0))
.child(
ContactSupport::new("support", [Choice::new("bug", "Something broke")])
.address("support-and-billing-questions@example-company.com")
.on_send(move |topic, message, _, cx| {
say(&owner, format!("{topic}: {message}"), cx)
}),
)
.into_any_element()
}
/// A long address gives way in the middle, so it ends inside a 280px box, which starts 16px in.
#[gpui::test]
fn a_long_address_is_cut_inside_a_narrow_box(cx: &mut TestAppContext) {
let (_, cx) = bench(long_address, cx);
let address = cx.debug_bounds("support-address").expect("the address");
assert!(
address.right() <= px(296.0),
"the address ends inside the box: {address:?}"
);
}
fn inline(_: &Bench, owner: Entity<Bench>) -> AnyElement {
InlineHelp::new("hint", "Only admins can change billing.")
.on_more(move |_, cx| say(&owner, "more".into(), cx))