From 6d07ca41b04f697380ef7a0bcc0286665269e0ce Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 30 Sep 2026 02:48:54 +0800 Subject: [PATCH] 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. --- src/ui/dialog.rs | 13 ++++- src/ui/prompt.rs | 2 +- src/ui/ssh_prompt.rs | 2 +- src/ui/worktree_prompt.rs | 119 ++++++++++++++++++++------------------ 4 files changed, 76 insertions(+), 60 deletions(-) diff --git a/src/ui/dialog.rs b/src/ui/dialog.rs index 7b2febe2..b76b2e5e 100644 --- a/src/ui/dialog.rs +++ b/src/ui/dialog.rs @@ -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, + title: impl Into, + width: f32, + cx: &App, +) -> Stateful
{ v_flex() + .id(id) + .role(gpui::Role::Dialog) + .aria_label(title) .occlude() .w(px(width)) .map(|panel| crate::ui::theme::floating_surface(panel, cx)) diff --git a/src/ui/prompt.rs b/src/ui/prompt.rs index f4302d91..3faaa0cf 100644 --- a/src/ui/prompt.rs +++ b/src/ui/prompt.rs @@ -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.)) diff --git a/src/ui/ssh_prompt.rs b/src/ui/ssh_prompt.rs index df02f617..89de66bb 100644 --- a/src/ui/ssh_prompt.rs +++ b/src/ui/ssh_prompt.rs @@ -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: diff --git a/src/ui/worktree_prompt.rs b/src/ui/worktree_prompt.rs index 350e3acf..0ea2e211 100644 --- a/src/ui/worktree_prompt.rs +++ b/src/ui/worktree_prompt.rs @@ -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()