feat(a11y): label the remaining lists and let VoiceOver open files from the tree

Editor tabs are tabs named by file (and unsaved state) with a named close
button; Info rows read as 'label, value'; keyboard shortcut rows as
'action, keys'; SSH hosts as 'name, address' with their expanded state.
File-tree rows open on the press, which assistive tech cannot send, so
they take its press action directly: a screen reader could select a file
but never open it.
This commit is contained in:
l0ng-ai
2026-09-30 03:09:48 +08:00
parent cdd3f3c2cd
commit bb0089f43d
5 changed files with 53 additions and 0 deletions
+8
View File
@@ -3443,6 +3443,8 @@ impl Tty7App {
let muted = cx.theme().muted_foreground;
let close = div()
.id(("editor-file-tab-close", pos))
.role(gpui::Role::Button)
.aria_label(format!("{} {name}", t(L10nKey::Close)))
.flex_none()
.size(px(slot))
.flex()
@@ -3474,6 +3476,12 @@ impl Tty7App {
});
div()
.id(("editor-file-tab", pos))
.role(gpui::Role::Tab)
.aria_label(match dirty {
true => format!("{name}, {}", t(L10nKey::SettingsUnsaved)),
false => name.to_string(),
})
.aria_selected(active)
.group(group.clone())
.occlude()
.flex_none()
+14
View File
@@ -2084,6 +2084,20 @@ impl Tty7App {
}
}),
)
// A row opens on the press, not the click, so it has no click
// handler for assistive technology's "press" to reach. Give it
// the same act directly, or VoiceOver could select a file and
// never open it.
.on_a11y_action(gpui::AccessibleAction::Click, {
let path = path.clone();
let app = cx.entity().downgrade();
move |_, window, cx| {
let _ = app.update(cx, |this, cx| {
this.file_tree.focus_handle.focus(window, cx);
this.file_tree_activate(&path, is_dir, window, cx);
});
}
})
// The menu acts on the row it was opened on, so that row is the
// one lit while it is up (#942) — selected without being opened,
// the way Explorer and every editor's tree do it.
+12
View File
@@ -1072,6 +1072,16 @@ impl Tty7App {
_ => t(L10nKey::CmdCopy),
};
// "cwd, /tmp/project": the row as one line. The path drawn is split
// and may be elided; this is the whole of it.
let a11y_name = format!(
"{}, {}",
row.label,
match &row.value {
InfoValue::Text(v) | InfoValue::Path(v) => v.clone(),
InfoValue::Diff { added, removed, .. } => format!("+{added} −{removed}"),
}
);
let value = match row.value {
// A path identifies a pane by its last segment, and plain
// truncation eats exactly that: a deep checkout read
@@ -1212,6 +1222,8 @@ impl Tty7App {
h_flex()
.id(id.clone())
.role(gpui::Role::Label)
.aria_label(a11y_name)
.group(id)
.relative()
.items_center()
+6
View File
@@ -447,6 +447,12 @@ impl Tty7App {
let head = div()
.id(SharedString::from(format!("ssh-host-{}", id.as_u128())))
.role(gpui::Role::Button)
.aria_label(match address.is_empty() {
true => title.clone(),
false => format!("{title}, {address}"),
})
.aria_expanded(open)
.h(px(40.))
.px(px(10.))
.flex()
+13
View File
@@ -159,6 +159,17 @@ impl Tty7App {
(_, Some((_, spec, _))) => vec![spec.clone()],
_ => keys,
};
// "New Tab, ⌘T": the action and what it is bound to, the way
// the row reads — a screen reader says ⌘ as "command".
let a11y_keys = match shown.is_empty() {
true => "—".to_string(),
false => shown
.iter()
.map(|spec| crate::ui::keymap::key_tokens(spec).join(""))
.collect::<Vec<_>>()
.join(" "),
};
let a11y_name = format!("{label}, {a11y_keys}");
let modified = overridden.contains(&action) && !is_recording;
let ring = if is_recording {
vec![kit::ring(tk.fg, 1., true)]
@@ -175,6 +186,8 @@ impl Tty7App {
let record_action = action.clone();
let keys_button = h_flex()
.id(SharedString::from(format!("kb-{action}")))
.role(gpui::Role::Button)
.aria_label(a11y_name)
.h(px(26.))
.min_w(px(72.))
.pl(px(8.))