mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-02 16:55:24 +00:00
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:
1 parent
ad79cbcf31
commit
60daca3770
3 files changed
+57
-9
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user