From cdd3f3c2cd68d15bcb508aa82a4f160854c54a2a Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 30 Sep 2026 02:56:02 +0800 Subject: [PATCH] feat(a11y): read Settings rows as named groups with their switches' state Each row is a group named by its title and description, so the control in it is heard with its setting's name; switches report on or off, the reset link is a button, and the Modified only switch has a name of its own. --- src/ui/settings/kit.rs | 15 +++++++++++++++ src/ui/settings/shell.rs | 13 +++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/ui/settings/kit.rs b/src/ui/settings/kit.rs index 25b979f5..55d0b2d7 100644 --- a/src/ui/settings/kit.rs +++ b/src/ui/settings/kit.rs @@ -179,6 +179,9 @@ pub(crate) struct Switch { disabled: bool, small: bool, handler: Option, + /// The name a screen reader gives the switch, where no settings row + /// around it already says what it is. + label: Option, } pub(crate) fn switch(id: impl Into) -> Switch { @@ -188,6 +191,7 @@ pub(crate) fn switch(id: impl Into) -> Switch { disabled: false, small: false, handler: None, + label: None, } } @@ -200,6 +204,10 @@ impl Switch { self.disabled = disabled; self } + pub(crate) fn label(mut self, label: impl Into) -> Self { + self.label = Some(label.into()); + self + } pub(crate) fn small(mut self) -> Self { self.small = true; self @@ -222,6 +230,13 @@ impl RenderOnce for Switch { let left = if on { w - knob - 2. } else { 2. }; div() .id(self.id) + .role(gpui::Role::Switch) + .when_some(self.label, |d, label| d.aria_label(label)) + .aria_toggled(if on { + gpui::Toggled::True + } else { + gpui::Toggled::False + }) .flex_shrink_0() .relative() .w(px(w)) diff --git a/src/ui/settings/shell.rs b/src/ui/settings/shell.rs index d16d641e..cf9e9e9d 100644 --- a/src/ui/settings/shell.rs +++ b/src/ui/settings/shell.rs @@ -433,6 +433,14 @@ impl Tty7App { let tk = Tk::of(cx); let label = label.into(); let desc = desc.into(); + // A screen reader enters the row as a group named by the setting, so + // the control inside it — a switch, a dropdown — is heard with the + // name it belongs to rather than as a bare "switch, off". + let a11y_name = match desc.is_empty() { + true => label.clone(), + false => format!("{label}. {desc}"), + }; + let reset_name = format!("{} {label}", t(L10nKey::Reset)); let entry = settings_search_entries() .iter() .find(|entry| t(entry.title) == label); @@ -483,6 +491,8 @@ impl Tty7App { line.child( div() .id(SharedString::from(format!("reset-setting-{key:?}"))) + .role(gpui::Role::Button) + .aria_label(reset_name.clone()) .flex_shrink_0() .text_size(fs(11.5)) .text_color(tk.k4) @@ -518,6 +528,8 @@ impl Tty7App { }); let row = div() .id(element_id) + .role(gpui::Role::Group) + .aria_label(a11y_name) .flex() .when(stacked, |row| row.flex_col().items_start().gap(px(8.))) .when(!stacked, |row| { @@ -1264,6 +1276,7 @@ impl Tty7App { }) .child( kit::switch("settings-modified-switch") + .label(t(L10nKey::SettingsModifiedOnly)) .small() .checked(modified_only), )