From 72a3c8522f838d37c00d730ccdf7e6730bdcc812 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:37:55 +0200 Subject: [PATCH] Keep consecutive spaces in recent file accessible names Fixes #436 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/ui-egui/src/a11y.rs | 5 ++ crates/ui-egui/src/home_recents.rs | 4 +- crates/ui-egui/tests/recent_file_names.rs | 64 +++++++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 crates/ui-egui/tests/recent_file_names.rs diff --git a/crates/ui-egui/src/a11y.rs b/crates/ui-egui/src/a11y.rs index fc634243..824defd4 100644 --- a/crates/ui-egui/src/a11y.rs +++ b/crates/ui-egui/src/a11y.rs @@ -813,6 +813,11 @@ pub fn button(resp: &Response, tip: &str) { }); } +/// A button named by `label` as it is: no key is split off (a file name may hold two spaces). +pub fn named_button(resp: &Response, label: &str) { + node(resp, label, |n| n.set_role(Role::Button)); +} + /// A link, named by its text. pub fn link(resp: &Response, label: &str) { node(resp, label, |n| n.set_role(Role::Link)); diff --git a/crates/ui-egui/src/home_recents.rs b/crates/ui-egui/src/home_recents.rs index dbc58506..881a0a4e 100644 --- a/crates/ui-egui/src/home_recents.rs +++ b/crates/ui-egui/src/home_recents.rs @@ -738,7 +738,7 @@ fn grid(ui: &mut Ui, t: &Tokens, recents: &mut Recents, files: &[Shown], now: u6 for (i, file) in row.iter().enumerate() { let rect = Rect::from_min_size(pos2(r.min.x + i as f32 * (card_w + GAP), r.min.y), vec2(card_w, card_h)); let resp = ui.interact(rect, card_id(&file.path), Sense::click()); - a11y::button(&resp, &spoken(file, now)); + a11y::named_button(&resp, &spoken(file, now)); let resp = resp.on_hover_text(&file.path); let visible = ui.is_rect_visible(rect); let picture = recents.picture(&file.path, visible, time); @@ -878,7 +878,7 @@ fn list(ui: &mut Ui, t: &Tokens, recents: &mut Recents, files: &[Shown], now: u6 for file in files { let (r, _) = ui.allocate_exact_size(vec2(width, ROW_H), Sense::hover()); let resp = ui.interact(r, card_id(&file.path), Sense::click()); - a11y::button(&resp, &spoken(file, now)); + a11y::named_button(&resp, &spoken(file, now)); let resp = resp.on_hover_text(&file.path); if let Some(a) = interact(ui, &resp, recents, file) { action = Some(a); diff --git a/crates/ui-egui/tests/recent_file_names.rs b/crates/ui-egui/tests/recent_file_names.rs new file mode 100644 index 00000000..461d0c73 --- /dev/null +++ b/crates/ui-egui/tests/recent_file_names.rs @@ -0,0 +1,64 @@ +//! Recent file rows keep their full accessible name. +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +use egui::{Context, Id, Pos2, RawInput, Rect, accesskit::Role, vec2}; +use figcraft_engine::Session; +use figcraft_ui_egui::{FigApp, NoServices}; + +/// Render the home screen in `view` with `path` (and a single-space control) as recents and return +/// the row's (label, shortcut, focused name). +fn row(view: &str, path: &str, prefix: &str, exclude: &str) -> (String, Option, String) { + let mut session = Session::empty(); + session.prefs.recent_files = vec![path.into(), "Q3 report.figcraft".into()]; + session.prefs.getting_started_dismissed = true; + session.prefs.home_views.insert("recents".into(), view.into()); + let mut app = FigApp::new(session, Box::new(NoServices)); + app.ui.home = true; + let ctx = Context::default(); + ctx.enable_accesskit(); + let mut found = None; + for frame in 0..4 { + if frame >= 2 { + ctx.memory_mut(|m| m.request_focus(Id::new(("home_recent_file", path)))); + } + let mut out = ctx.run_ui( + RawInput { + screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1440.0, 900.0))), + time: Some(frame as f64 / 60.0), + ..Default::default() + }, + |ui| app.ui(ui), + ); + out.textures_delta.clear(); + if frame == 3 { + let nodes = out.platform_output.accesskit_update.unwrap().nodes; + let mut matches = nodes.iter().map(|(_, n)| n).filter(|n| { + n.role() == Role::Button && n.label().is_some_and(|s| s.starts_with(prefix) && (exclude.is_empty() || !s.starts_with(exclude))) + }); + let n = matches.next().expect("file row"); + assert!(matches.next().is_none(), "{view}: more than one row matches {prefix:?}"); + found = Some(( + n.label().unwrap_or_default().to_string(), + n.keyboard_shortcut().map(str::to_string), + figcraft_ui_egui::a11y::focused_name(&ctx), + )); + } + } + found.expect("rendered") +} + +#[test] +fn recent_file_names_preserve_spaces() { + for view in ["grid", "list"] { + for (path, prefix, full) in [("Q3 report.figcraft", "Q3", "Q3 report"), ("Notes A.figcraft", "Notes", "Notes A")] { + // The single-space control "Q3 report" also starts with "Q3"; it is not the row under test. + let (label, shortcut, focused) = row(view, path, prefix, "Q3 report"); + assert!(label.starts_with(full), "{view}: label {label:?}"); + assert_eq!(shortcut, None, "{view}: {path}"); + assert!(focused.starts_with(full), "{view}: focused {focused:?}"); + } + let (label, shortcut, _) = row(view, "Q3 report.figcraft", "Q3 report", ""); + assert!(label.starts_with("Q3 report"), "{view}: control {label:?}"); + assert_eq!(shortcut, None); + } +}