feat(a11y): announce the app's own dialogs as dialogs, named by their title

The SSH sheet, the worktree form and the in-app alert are dialogs to a
screen reader, titled with the question they ask.
This commit is contained in:
l0ng-ai
2026-09-30 02:48:54 +08:00
parent 4ae1c86290
commit 6d07ca41b0
4 changed files with 76 additions and 60 deletions
+12 -1
View File
@@ -61,8 +61,19 @@ pub(crate) fn popover_rungs(cx: &App) -> Surface {
/// The card itself, opaque and occluding. Callers add the header, body and
/// footer as children, in that order.
pub(crate) fn card(width: f32, cx: &App) -> Div {
///
/// `title` is what a screen reader announces the card as — a dialog, named
/// by the question it asks.
pub(crate) fn card(
id: impl Into<ElementId>,
title: impl Into<SharedString>,
width: f32,
cx: &App,
) -> Stateful<Div> {
v_flex()
.id(id)
.role(gpui::Role::Dialog)
.aria_label(title)
.occlude()
.w(px(width))
.map(|panel| crate::ui::theme::floating_surface(panel, cx))
+1 -1
View File
@@ -212,7 +212,7 @@ impl Render for TextPrompt {
0..=2 => ALERT_W,
_ => ALERT_W_WIDE,
};
let card = dialog::card(width, cx)
let card = dialog::card("prompt-card", self.message.clone(), width, cx)
.max_w(gpui::relative(0.9))
.gap(px(16.))
.pt(px(20.))
+1 -1
View File
@@ -809,7 +809,7 @@ impl Tty7App {
.child(text)
};
let card = dialog::card(SHEET_W, cx)
let card = dialog::card("ssh-prompt-card", title.clone(), SHEET_W, cx)
.track_focus(&self.ssh_prompt.focus_handle)
.key_context("SshPrompt")
// The changed-key sheet is the one card allowed a coloured edge:
+62 -57
View File
@@ -363,66 +363,71 @@ impl Tty7App {
let start = (!p.agents.is_empty()).then(|| self.render_worktree_start(p, cx));
let rungs = dialog::popover_rungs(cx);
let card = dialog::card(440., cx)
.child(dialog::header(t(L10nKey::WorktreePromptTitle), cx))
.child(
dialog::body()
// A click anywhere else in the form closes the Start menu.
.on_mouse_down(
gpui::MouseButton::Left,
cx.listener(|this, _: &gpui::MouseDownEvent, _, cx| {
this.close_worktree_start_menu(cx)
}),
)
// The path preview hangs off the Name field it follows,
// closer to it than the next field is.
.child(
dialog::labelled(t(L10nKey::WorktreePromptName), Input::new(&p.name), cx)
.child(meta(preview)),
)
.child(dialog::labelled(
t(L10nKey::WorktreePromptBranch),
Input::new(&p.branch),
cx,
))
.child(
dialog::labelled(t(L10nKey::WorktreePromptBase), Input::new(&p.base), cx)
.children(setup_note.map(meta)),
)
.children(start)
.when(takes_task, |body| {
body.child(dialog::labelled(
t(L10nKey::WorktreePromptTask),
Input::new(&p.task),
cx,
))
let card = dialog::card(
"worktree-prompt-card",
t(L10nKey::WorktreePromptTitle),
440.,
cx,
)
.child(dialog::header(t(L10nKey::WorktreePromptTitle), cx))
.child(
dialog::body()
// A click anywhere else in the form closes the Start menu.
.on_mouse_down(
gpui::MouseButton::Left,
cx.listener(|this, _: &gpui::MouseDownEvent, _, cx| {
this.close_worktree_start_menu(cx)
}),
)
.child(
dialog::footer(cx)
.child(dialog::button(
"worktree-cancel",
t(L10nKey::Cancel),
Tone::Secondary,
true,
rungs,
)
// The path preview hangs off the Name field it follows,
// closer to it than the next field is.
.child(
dialog::labelled(t(L10nKey::WorktreePromptName), Input::new(&p.name), cx)
.child(meta(preview)),
)
.child(dialog::labelled(
t(L10nKey::WorktreePromptBranch),
Input::new(&p.branch),
cx,
))
.child(
dialog::labelled(t(L10nKey::WorktreePromptBase), Input::new(&p.base), cx)
.children(setup_note.map(meta)),
)
.children(start)
.when(takes_task, |body| {
body.child(dialog::labelled(
t(L10nKey::WorktreePromptTask),
Input::new(&p.task),
cx,
cx.listener(|this, _, window, cx| this.cancel_worktree_prompt(window, cx)),
))
.child(dialog::button(
"worktree-create",
if p.busy {
t(L10nKey::WorktreePromptCreating)
} else {
t(L10nKey::WorktreePromptCreate)
},
Tone::Primary,
!(p.busy || nothing_to_name),
rungs,
cx,
cx.listener(|this, _, window, cx| this.submit_worktree_prompt(window, cx)),
)),
);
}),
)
.child(
dialog::footer(cx)
.child(dialog::button(
"worktree-cancel",
t(L10nKey::Cancel),
Tone::Secondary,
true,
rungs,
cx,
cx.listener(|this, _, window, cx| this.cancel_worktree_prompt(window, cx)),
))
.child(dialog::button(
"worktree-create",
if p.busy {
t(L10nKey::WorktreePromptCreating)
} else {
t(L10nKey::WorktreePromptCreate)
},
Tone::Primary,
!(p.busy || nothing_to_name),
rungs,
cx,
cx.listener(|this, _, window, cx| this.submit_worktree_prompt(window, cx)),
)),
);
Some(
div()