From 83aef8d48b8b3a494ef4f0b16201fe38791d25d5 Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Wed, 28 Oct 2026 15:37:24 -0400 Subject: [PATCH] LR-BEHAV-ACCESS: an alert dialog carries what it says as its accessible description --- crates/ui-egui/src/panels/library_problem.rs | 20 +++++++++------ crates/ui-egui/src/panels/notices.rs | 1 + crates/ui-egui/src/widgets.rs | 7 +++++ crates/ui-egui/tests/it/dialog_role.rs | 27 ++++++++++++++++++++ 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/crates/ui-egui/src/panels/library_problem.rs b/crates/ui-egui/src/panels/library_problem.rs index 83d55a914..f5b9c25ba 100644 --- a/crates/ui-egui/src/panels/library_problem.rs +++ b/crates/ui-egui/src/panels/library_problem.rs @@ -28,6 +28,10 @@ use crate::widgets::register; /// windows and their dim. const DIM_ID: &str = "library-problem-dim"; const ERROR_WINDOW_ID: &str = "library-problem"; +/// What the error window says about the library's safety and the way on (drawn as it is, not translated). +const EXPLANATION: &str = "Your photos and edits in it are untouched. You can try again (e.g. after closing another program that has it open \ + or reconnecting its drive), open a different library or create a new one, or continue without a library — \ + then nothing you do is saved."; const CHOOSER_WINDOW_ID: &str = "library-chooser"; /// The named `Foreground` layer used by the dim and the two blocking library windows. @@ -1016,6 +1020,12 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context, blocked: bool) { dim(ctx); let mut choice = None; let frame = egui::Frame::window(&ctx.global_style()).inner_margin(egui::Margin::symmetric(18, 14)); + // what the window says, in one piece, for a screen reader: the labels below are separate nodes + let description = if problem.path.is_empty() { + format!("{} {} {EXPLANATION}", crate::i18n::tr("LightCraft couldn't find where to keep your library."), problem.error) + } else { + format!("{} {}. {} {EXPLANATION}", crate::i18n::tr("LightCraft couldn't open the library at"), problem.path, problem.error) + }; let shown = egui::Window::new(crate::i18n::tr("Your library couldn't be opened")) .id(egui::Id::unique(ERROR_WINDOW_ID)) .order(egui::Order::Foreground) @@ -1035,14 +1045,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context, blocked: bool) { ui.label(RichText::new(&problem.path).font(t.semibold(12.5)).color(t.text)); } ui.add(egui::Label::new(RichText::new(&problem.error).color(t.caution)).wrap()); - ui.label( - RichText::new( - "Your photos and edits in it are untouched. You can try again (e.g. after closing another program that has it open \ - or reconnecting its drive), open a different library or create a new one, or continue without a library — \ - then nothing you do is saved.", - ) - .color(t.text_dim), - ); + ui.label(RichText::new(EXPLANATION).color(t.text_dim)); ui.add_space(4.0); ui.horizontal(|ui| { let mut button = |ui: &mut egui::Ui, id: &str, label: &str, enabled: bool, c: Choice| { @@ -1060,6 +1063,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context, blocked: bool) { }); if let Some(w) = &shown { crate::widgets::announce_dialog(ctx, &w.response, crate::widgets::Modal::Alert); + crate::widgets::describe_dialog(ctx, &w.response, &description); // A press on the dim raises its area; keep this window (and its own buttons) above it. ctx.move_to_top(w.response.layer_id); } diff --git a/crates/ui-egui/src/panels/notices.rs b/crates/ui-egui/src/panels/notices.rs index e2ded68ee..1d6067f0a 100644 --- a/crates/ui-egui/src/panels/notices.rs +++ b/crates/ui-egui/src/panels/notices.rs @@ -513,6 +513,7 @@ fn window(ctx: &egui::Context, id: &str, title: &str, text: &str, buttons: &[(&s // A backdrop press can raise its area; keep the prompt buttons above it. if let Some(window) = shown { crate::widgets::announce_dialog(ctx, &window.response, crate::widgets::Modal::Alert); + crate::widgets::describe_dialog(ctx, &window.response, text); ctx.move_to_top(window.response.layer_id); } input.button_ids = seen; diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index d9658b097..bf3f914c9 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -1186,6 +1186,13 @@ pub(crate) fn announce_dialog(ctx: &egui::Context, response: &Response, kind: Mo }); } +/// Gives the dialog `response` drew the words it says as its accessible description, for an alert +/// whose content (the task names, the error) is drawn as separate labels: a screen reader then +/// has what the alert is about when it announces the alert, not only its title. +pub(crate) fn describe_dialog(ctx: &egui::Context, response: &Response, text: &str) { + ctx.accesskit_node_builder(response.id, |node| node.set_description(text)); +} + /// The same for one that is on or off (a toggle, a selected row or swatch). pub(crate) fn announce_selected(resp: &egui::Response, name: impl Into, selected: bool) { let name = name.into(); diff --git a/crates/ui-egui/tests/it/dialog_role.rs b/crates/ui-egui/tests/it/dialog_role.rs index 48b1f0751..a36cc96fe 100644 --- a/crates/ui-egui/tests/it/dialog_role.rs +++ b/crates/ui-egui/tests/it/dialog_role.rs @@ -85,3 +85,30 @@ fn a_library_that_could_not_be_opened_is_a_modal_alert_dialog() { assert_eq!((role, modal), (Role::AlertDialog, true), "{title:?} is a modal alert dialog"); } } + +/// The description of every alert dialog in the tree. +fn alert_descriptions(h: &Harness<'_, LightcraftApp>) -> Vec { + h.root() + .children_recursive() + .map(|n| n.accesskit_node()) + .filter(|n| n.role() == Role::AlertDialog) + .map(|n| n.data().description().unwrap_or_default().to_string()) + .collect() +} + +#[test] +fn an_alert_carries_what_it_says_as_its_description() { + let mut h = harness(); + h.state_mut().quit_prompt = Some(lightcraft_ui_egui::QuitPrompt::Tasks("1 task is still running: Import. Quitting stops it.".into())); + h.run_steps(6); + let said = alert_descriptions(&h); + assert!(said.iter().any(|d| d.contains("1 task is still running: Import")), "the quit prompt names the task: {said:?}"); + let mut h = harness(); + h.state_mut().library_problem = Some(lightcraft_ui_egui::panels::library_problem::LibraryProblem::new("/no/such/library", "No catalog here.")); + h.run_steps(6); + let said = alert_descriptions(&h); + assert!( + said.iter().any(|d| d.contains("/no/such/library") && d.contains("No catalog here.") && d.contains("untouched")), + "the library alert has its path, error and explanation: {said:?}" + ); +}