keymap: name the selected row to screen readers, announce conflicts and messages, and make group headers and key caps Tab stops

This commit is contained in:
aiongg committed 2026-10-20 15:46:56 +00:00
1 parent ad79cbcf31
commit 60daca3770
3 files changed
+57 -9

No files matched your search

+3 -1
View File
@@ -559,7 +559,7 @@ impl Draw<'_, '_> {
}
let a11y = if names.is_empty() { spoken } else { fill(self.ui("{chord}: {label}"), &[("chord", &spoken), ("label", &names.join(", "))]) };
let selected = self.state.keyboard.key == Some(key);
let resp = ui.interact(r, self.base.with(("key", key.name())), Sense::CLICK);
let resp = ui.interact(r, self.base.with(("key", key.name())), Sense::click());
resp.widget_info(|| WidgetInfo::selected(WidgetType::Button, true, selected, &a11y));
let description = match class(holders) {
Class::Free => self.ui("Not assigned"),
@@ -572,6 +572,8 @@ impl Draw<'_, '_> {
let v = ui.visuals();
if selected {
ui.painter().rect_stroke(r, CornerRadius::same(3), Stroke::new(2.0, v.strong_text_color()), StrokeKind::Outside);
} else if resp.has_focus() {
ui.painter().rect_stroke(r, CornerRadius::same(3), v.selection.stroke, StrokeKind::Outside);
} else if resp.hovered() {
ui.painter().rect_stroke(r, CornerRadius::same(3), Stroke::new(1.5, ink), StrokeKind::Inside);
}
+24 -5
View File
@@ -12,6 +12,7 @@ use craft_ui::buttons::{IconButton, IconButtonStyle};
use craft_ui::layout::{ResizeHandle, SplitAxis, SplitSize, split_rect};
use craft_ui::tabs::{self, Tab};
use egui::Galley;
use egui::accesskit::Live;
use egui::text::{LayoutJob, TextFormat, TextWrapping};
use egui::{
Align, Button, Color32, ComboBox, EventFilter, Id, Key as EguiKey, Label, Layout, Modifiers, Painter, Rect, Response, RichText, ScrollArea,
@@ -686,13 +687,24 @@ impl Draw<'_, '_> {
action = self.rows(ui, range, shortcut_w, has_focus);
});
});
// Screen readers follow the selected row while the table has focus.
let row_id = position.filter(|p| shown.contains(p)).and_then(|p| match self.state.lines_cached().get(p) {
Some(Line::Row(r)) => Some(self.base.with(("row", &r.key))),
_ => None,
});
if let Some(id) = row_id {
ui.ctx().accesskit_node_builder(table_id, |node| node.set_active_descendant(id.accesskit_id()));
}
self.state.visible = shown;
if has_focus {
ui.painter().rect_stroke(rect, 2.0, ui.visuals().selection.stroke, StrokeKind::Inside);
}
if let Some(a) = action {
focus.request_focus();
// A group header is a Tab stop of its own and keeps focus.
if !matches!(a, RowAction::Toggle(_)) {
focus.request_focus();
}
match a {
RowAction::Select(c) => self.state.select_row(&c),
RowAction::Record(c, old, chip) => {
@@ -788,9 +800,14 @@ impl Draw<'_, '_> {
action
}
/// A group's header; a Tab stop, toggled with Enter or Space.
fn group_header(&self, ui: &mut Ui, rect: Rect, name: &str, collapsed: bool) -> Option<RowAction> {
let resp = ui.interact(rect, self.base.with(("group", name)), Sense::CLICK);
resp.widget_info(|| WidgetInfo::labeled(WidgetType::Label, true, name));
let resp = ui.interact(rect, self.base.with(("group", name)), Sense::click());
resp.widget_info(|| WidgetInfo::labeled(WidgetType::CollapsingHeader, true, name));
ui.ctx().accesskit_node_builder(resp.id, |node| node.set_expanded(!collapsed));
if resp.has_focus() {
ui.painter().rect_stroke(rect, 2.0, ui.visuals().selection.stroke, StrokeKind::Inside);
}
let v = ui.visuals();
let color = v.strong_text_color();
let m = self.mirror(rect);
@@ -1254,7 +1271,8 @@ impl Draw<'_, '_> {
fn conflict_bar(&mut self, ui: &mut Ui) {
let Some(c) = self.state.conflict.clone() else { return };
let text = self.vis_chord(&c.text, &self.chord(&c.binding.trigger));
ui.add(Label::new(RichText::new(text).color(self.style.warning)).wrap());
let r = ui.add(Label::new(RichText::new(text).color(self.style.warning)).wrap());
ui.ctx().accesskit_node_builder(r.id, |node| node.set_live(Live::Polite));
let mut act = 0;
ui.horizontal_wrapped(|ui| {
if self.button(ui, true, &self.ui("Take It"), "shortcuts.conflict.take") {
@@ -1289,7 +1307,8 @@ impl Draw<'_, '_> {
let text = self.vis_chord(&m.text, m.chord.as_deref().unwrap_or_default());
let text = if m.refused { self.warned(text) } else { text };
let color = if m.refused { self.style.warning } else { ui.visuals().text_color() };
ui.add(Label::new(RichText::new(text).color(color)).wrap());
let r = ui.add(Label::new(RichText::new(text).color(color)).wrap());
ui.ctx().accesskit_node_builder(r.id, |node| node.set_live(Live::Polite));
if m.goto.is_some() && self.button(ui, true, &self.ui("Go to Conflict"), "shortcuts.message.goto") {
act = 1;
}
+30 -3
View File
@@ -114,8 +114,9 @@ fn has(h: &Harness<'_, Fx>, label: &str) -> bool {
h.query_by_label(label).is_some()
}
/// The table itself has focus (a screen reader sees its selected row, the active descendant).
fn table_focused(h: &Harness<'_, Fx>) -> bool {
h.get_by_label("Commands").is_focused()
h.get_by_label("Commands").accesskit_node().is_focused_in_tree()
}
fn chord(text: &str, platform: Platform) -> String {
@@ -359,7 +360,8 @@ fn search(h: &mut Harness<'_, Fx>, text: &str) {
fn choose_area(h: &mut Harness<'_, Fx>, area: &str) {
h.get_by_role_and_label(Role::ComboBox, "Show:").click();
h.run();
h.get_by_role_and_label(Role::Button, area).click();
// The popup's item, after a group header of the same name.
h.get_all_by_role_and_label(Role::Button, area).last().unwrap_or_else(|| panic!("{area}")).click();
h.run();
}
@@ -409,7 +411,7 @@ fn areas_follow_paths_kind_and_scope() {
for area in ["All Commands", "File Menu", "Object Menu", "Tools", "Timeline Panel", "Other"] {
assert!(h.get_all_by_label(area).count() >= 1, "{area}");
}
h.get_by_role_and_label(Role::Button, "Tools").click();
h.get_all_by_role_and_label(Role::Button, "Tools").last().expect("the Tools item").click();
h.run();
assert!(has(&h, "Marquee Tool: M") && has(&h, "Elliptical Marquee Tool: M"));
assert!(!has(&h, "Save: ⌘S"));
@@ -463,6 +465,31 @@ fn table_keys_move_record_and_delete() {
assert_eq!(h.state().bindings("file.saveAs"), ["F7"]);
}
#[test]
fn group_headers_take_focus_and_toggle_with_enter() {
let mut h = mac();
h.get_by_label("Save").click();
h.run();
assert!(tab_to(&mut h, "File", 3));
press(&mut h, Modifiers::NONE, EguiKey::Enter);
assert!(!has(&h, "Export…: ⌘E"), "File is collapsed");
assert!(h.get_by_label("File").is_focused());
press(&mut h, Modifiers::NONE, EguiKey::Enter);
assert!(has(&h, "Export…: ⌘E"));
}
#[test]
fn table_names_the_selected_row_and_messages_are_live() {
let mut h = mac();
h.get_by_label("Save").click();
h.run();
let row = h.get_by_role_and_label(Role::Button, "Save").accesskit_node().id();
assert_eq!(h.get_by_label("Commands").accesskit_node().active_descendant().map(|n| n.id()), Some(row));
click(&mut h, "Export…: ⌘E");
press(&mut h, Modifiers::COMMAND, EguiKey::S);
assert_eq!(h.get_by_label("⚠ ⌘S is used by Save").accesskit_node().live(), egui::accesskit::Live::Polite);
}
#[test]
fn small_window_keeps_ok_and_long_labels_truncate() {
let long = "A very long command label ".repeat(12);