From c6760516414914a48892f0279c032a6ae9e8c24f Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 10 Nov 2026 20:53:24 -0800 Subject: [PATCH] Character Styles: the style dialogs' fields have accessible names The text fields and menus of the Basic Character Formats, Advanced Character Formats, Character Color and OpenType Features sections, and the Style Name field of Character Style Options, are now named by the label beside them, so screen readers announce them. This covers both Character Style Options and New Character Style, which share the sections. The New Character Style UI test runs with the accessibility check on again, and a new test opens every section of both dialogs and checks that no input is unnamed. --- .../ui-egui/src/dialogs/char_style_options.rs | 110 ++++++++++++------ 1 file changed, 73 insertions(+), 37 deletions(-) diff --git a/crates/ui-egui/src/dialogs/char_style_options.rs b/crates/ui-egui/src/dialogs/char_style_options.rs index 86f8fe453..e1f564d08 100644 --- a/crates/ui-egui/src/dialogs/char_style_options.rs +++ b/crates/ui-egui/src/dialogs/char_style_options.rs @@ -661,18 +661,22 @@ pub(crate) fn basic_body(ui: &mut egui::Ui, d: &mut Dialog) { ("kerning", tl!("Kerning")), ("tracking", tl!("Tracking")), ] { - ui.label(caption); + let row = ui.label(caption); if d.fields.get(key).is_some_and(Value::is_null) { d.fields.insert(key.into(), json!("")); } - form::text(ui, d, key, 190.0); + ui.label_inputs_by(row.id, |ui| form::text(ui, d, key, 190.0)); ui.end_row(); } - ui.label(tl!("Case")); - choice(ui, d, "case", &[("normal", tl!("Normal")), ("allCaps", tl!("All Caps")), ("smallCaps", tl!("Small Caps"))]); + let row = ui.label(tl!("Case")); + ui.label_inputs_by(row.id, |ui| { + choice(ui, d, "case", &[("normal", tl!("Normal")), ("allCaps", tl!("All Caps")), ("smallCaps", tl!("Small Caps"))]); + }); ui.end_row(); - ui.label(tl!("Position")); - choice(ui, d, "position", &[("normal", tl!("Normal")), ("superscript", tl!("Superscript")), ("subscript", tl!("Subscript"))]); + let row = ui.label(tl!("Position")); + ui.label_inputs_by(row.id, |ui| { + choice(ui, d, "position", &[("normal", tl!("Normal")), ("superscript", tl!("Superscript")), ("subscript", tl!("Subscript"))]); + }); ui.end_row(); for (key, caption) in [ ("underline", tl!("Underline")), @@ -693,20 +697,22 @@ pub(crate) fn basic_body(ui: &mut egui::Ui, d: &mut Dialog) { } ui.end_row(); } - ui.label(tl!("Character Alignment")); - choice( - ui, - d, - "charAlign", - &[ - ("romanBaseline", tl!("Roman Baseline")), - ("emBoxTop", tl!("Em Box Top/Right")), - ("emBoxCenter", tl!("Em Box Center")), - ("emBoxBottom", tl!("Em Box Bottom/Left")), - ("icfTop", tl!("ICF Top/Right")), - ("icfBottom", tl!("ICF Bottom/Left")), - ], - ); + let row = ui.label(tl!("Character Alignment")); + ui.label_inputs_by(row.id, |ui| { + choice( + ui, + d, + "charAlign", + &[ + ("romanBaseline", tl!("Roman Baseline")), + ("emBoxTop", tl!("Em Box Top/Right")), + ("emBoxCenter", tl!("Em Box Center")), + ("emBoxBottom", tl!("Em Box Bottom/Left")), + ("icfTop", tl!("ICF Top/Right")), + ("icfBottom", tl!("ICF Bottom/Left")), + ], + ); + }); ui.end_row(); }); } @@ -721,20 +727,20 @@ pub(crate) fn advanced_body(ui: &mut egui::Ui, d: &mut Dialog) { ("baseline_shift", tl!("Baseline Shift"), "pt"), ("rotation", tl!("Character Rotation"), "°"), ] { - ui.label(caption); + let row = ui.label(caption); if d.fields.get(key).is_some_and(Value::is_null) { d.fields.insert(key.into(), json!("")); } ui.horizontal(|ui| { - form::text(ui, d, key, 160.0); + ui.label_inputs_by(row.id, |ui| form::text(ui, d, key, 160.0)); ui.label(unit); }); ui.end_row(); } - ui.label(tl!("Language:")); + let row = ui.label(tl!("Language:")); let names: Vec<&str> = std::iter::once(" ").chain(vectorcraft_doc::setup::LANGUAGES.iter().map(|(name, _, _)| *name)).collect(); let current = d.fields.get("language").and_then(Value::as_str).unwrap_or("").to_string(); - if let Some(i) = crate::widgets::dropdown(ui, (KIND, "language"), ¤t, &names, 190.0) { + if let Some(i) = ui.label_inputs_by(row.id, |ui| crate::widgets::dropdown(ui, (KIND, "language"), ¤t, &names, 190.0)) { d.fields.insert("language".into(), if i == 0 { Value::Null } else { names.get(i).map_or(Value::Null, |name| json!(name)) }); } ui.end_row(); @@ -799,31 +805,31 @@ fn paint_choice(ui: &mut egui::Ui, d: &mut Dialog, key: &str) { pub(crate) fn color_body(ui: &mut egui::Ui, d: &mut Dialog) { egui::Grid::new("character-style-color").num_columns(2).spacing([18.0, 8.0]).show(ui, |ui| { for (key, caption) in [("fill", tl!("Fill")), ("stroke", tl!("Stroke"))] { - ui.label(caption); - paint_choice(ui, d, key); + let row = ui.label(caption); + ui.label_inputs_by(row.id, |ui| paint_choice(ui, d, key)); ui.end_row(); let tint_key = format!("{key}_tint"); - ui.label(tl!("Tint")); + let row = ui.label(tl!("Tint")); if d.fields.get(&tint_key).is_none_or(Value::is_null) { d.fields.insert(tint_key.clone(), json!("")); } let linked = d.fields.get(key).and_then(|p| p.get("swatch")).is_some_and(Value::is_string); ui.add_enabled_ui(linked, |ui| { ui.horizontal(|ui| { - form::text(ui, d, &tint_key, 160.0); + ui.label_inputs_by(row.id, |ui| form::text(ui, d, &tint_key, 160.0)); ui.label("%"); }); }); ui.end_row(); } - ui.label(tl!("Weight")); + let row = ui.label(tl!("Weight")); if d.fields.get("stroke_width").is_none_or(Value::is_null) { d.fields.insert("stroke_width".into(), json!("")); } let stroked = d.fields.get("stroke").and_then(|p| p.get("type")).and_then(Value::as_str) != Some("none"); ui.add_enabled_ui(stroked, |ui| { ui.horizontal(|ui| { - form::text(ui, d, "stroke_width", 160.0); + ui.label_inputs_by(row.id, |ui| form::text(ui, d, "stroke_width", 160.0)); ui.label("pt"); }); }); @@ -839,11 +845,11 @@ pub(crate) fn opentype_body(ui: &mut egui::Ui, d: &mut Dialog) { let figures: Vec<(&str, &str)> = FIGURE_VALUES.into_iter().zip(crate::panels::opentype::FIGURES).collect(); let positions: Vec<(&str, &str)> = POSITION_VALUES.into_iter().zip(crate::panels::opentype::POSITIONS).collect(); egui::Grid::new("character-style-opentype").num_columns(2).spacing([18.0, 8.0]).show(ui, |ui| { - ui.label(tl!("Figure:")); - choice(ui, d, "ot_figure", &figures); + let row = ui.label(tl!("Figure:")); + ui.label_inputs_by(row.id, |ui| choice(ui, d, "ot_figure", &figures)); ui.end_row(); - ui.label(tl!("Position:")); - choice(ui, d, "ot_position", &positions); + let row = ui.label(tl!("Position:")); + ui.label_inputs_by(row.id, |ui| choice(ui, d, "ot_position", &positions)); ui.end_row(); for (key, caption) in [ ("ot_liga", tl!("Standard Ligatures")), @@ -929,8 +935,8 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { opentype_body(ui, d); } else { ui.horizontal(|ui| { - ui.label(tl!("Style Name:")); - form::text(ui, d, "name", 190.0); + let row = ui.label(tl!("Style Name:")); + ui.label_inputs_by(row.id, |ui| form::text(ui, d, "name", 190.0)); }); ui.separator(); if let Ok(attrs) = edited_attrs(d) { @@ -1837,7 +1843,7 @@ mod tests { }; let shown = crate::i18n::message(crate::i18n::current(), &why); let mut fonts = false; - let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1100.0, 900.0)).with_accessibility_check(false).build_ui_state( + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1100.0, 900.0)).build_ui_state( move |ui, app: &mut VectorcraftApp| { if !std::mem::replace(&mut fonts, true) { crate::theme::install_fonts(ui.ctx()); @@ -1857,6 +1863,36 @@ mod tests { } } + #[test] + fn every_section_names_its_inputs_in_both_dialogs() { + for new_style in [false, true] { + for section in ["general", "basic", "advanced", "color", "opentype"] { + let mut app = app(); + if new_style { + app.run("ui.newCharStyle", json!({})).unwrap(); + } else { + app.run("charStyle.new", json!({"name": "Body", "attrs": {}})).unwrap(); + open(&mut app, "Body").unwrap(); + } + app.ui.dialog.as_mut().unwrap().fields.insert("section".into(), json!(section)); + let mut fonts = false; + // The harness fails a frame that leaves an input without an accessible name. + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1100.0, 900.0)).build_ui_state( + move |ui, app: &mut VectorcraftApp| { + if !std::mem::replace(&mut fonts, true) { + crate::theme::install_fonts(ui.ctx()); + return; + } + super::super::show(app, ui.ctx()); + }, + app, + ); + h.run_steps(4); + assert!(h.unnamed_widgets().is_empty(), "new {new_style}, {section}: {:?}", h.unnamed_widgets()); + } + } + } + #[test] fn invalid_number_keeps_last_valid_preview() { let mut app = app();