diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 409943f75..9ea164597 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1303,16 +1303,17 @@ fn text_frame_options_dialog(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) "baseline" => { crate::rtl::label(ui, egui::RichText::new(tr("First Baseline")).font(semibold(12.0))); egui::Grid::new("tfo_fb").num_columns(4).spacing([8.0, 6.0]).show(ui, |ui| { - crate::rtl::label(ui, tr("Offset:")); + let name = crate::rtl::label(ui, tr("Offset:")); translated_combo( &lang, ui, d, "firstBaseline", &[("ascent", "Ascent"), ("capHeight", "Cap Height"), ("leading", "Leading"), ("xHeight", "x Height"), ("fixed", "Fixed")], - ); + ) + .labelled_by(name.id); crate::rtl::label(ui, tr("Min:")); - text_field(ui, d, "firstBaselineMin", 70.0); + text_field_named(ui, d, "firstBaselineMin", 70.0, tr("Min:")); ui.end_row(); }); ui.add_space(8.0); @@ -1321,19 +1322,20 @@ fn text_frame_options_dialog(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) ui.add_enabled_ui(d.b("customGrid"), |ui| { egui::Grid::new("tfo_grid").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { crate::rtl::label(ui, tr("Start:")); - text_field(ui, d, "gridStart", 70.0); + text_field_named(ui, d, "gridStart", 70.0, tr("Start:")); ui.end_row(); - crate::rtl::label(ui, tr("Relative To:")); + let name = crate::rtl::label(ui, tr("Relative To:")); translated_combo( &lang, ui, d, "gridRelativeTo", &[("page", "Top of Page"), ("margin", "Top Margin"), ("frame", "Top of Frame"), ("inset", "Top Inset")], - ); + ) + .labelled_by(name.id); ui.end_row(); crate::rtl::label(ui, tr("Increment Every:")); - text_field(ui, d, "gridIncrement", 70.0); + text_field_named(ui, d, "gridIncrement", 70.0, tr("Increment Every:")); ui.end_row(); }); }); @@ -1350,10 +1352,12 @@ fn text_frame_options_dialog(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) } } -/// A measure field of the dialog with InDesign's spinner (stored as text in the document's unit). -fn spin_measure(ui: &mut egui::Ui, d: &mut Dialog, key: &str, w: f32, enabled: bool) { +/// A measure field of the dialog with InDesign's spinner (stored as text in the document's unit), +/// named from its `caption`. +fn spin_measure(ui: &mut egui::Ui, d: &mut Dialog, key: &str, caption: &str, w: f32, enabled: bool) { let id = format!("tfo_{key}"); - if let Some(v) = crate::widgets::NumField::measure(&id, d.m(key), d.unit).width(w).spinner().range(0.0, 15552.0).enabled(enabled).show(ui) { + let field = crate::widgets::NumField::measure(&id, d.m(key), d.unit).name(caption.trim_end_matches(':')); + if let Some(v) = field.width(w).spinner().range(0.0, 15552.0).enabled(enabled).show(ui) { d.fields.insert(key.into(), json!(format_measure(v, d.unit))); } } @@ -1444,19 +1448,22 @@ pub(crate) fn general(lang: &str, ui: &mut egui::Ui, d: &mut Dialog) { framed(ui, None, |ui| { ui.horizontal(|ui| { caption_at(ui, tr("Columns:"), cap); - translated_combo(lang, ui, d, "columnsKind", &[("fixedNumber", "Fixed Number"), ("fixedWidth", "Fixed Width")]); + translated_combo(lang, ui, d, "columnsKind", &[("fixedNumber", "Fixed Number"), ("fixedWidth", "Fixed Width")]) + .accessible_name(tr("Columns:").to_owned()); }); ui.add_space(4.0); ui.horizontal(|ui| { - caption_at(ui, crate::i18n::tr_context(lang, "Number:", "count"), cap); + let number = crate::i18n::tr_context(lang, "Number:", "count"); + caption_at(ui, number, cap); let n = d.n("columns"); - if let Some(v) = crate::widgets::NumField::number("tfo_columns", n, "", 0).width(field).spinner().range(1.0, 40.0).show(ui) { + let columns = crate::widgets::NumField::number("tfo_columns", n, "", 0).name(number.trim_end_matches(':')); + if let Some(v) = columns.width(field).spinner().range(1.0, 40.0).show(ui) { d.fields.insert("columns".into(), json!(v.round().to_string())); } // Fixed Number: the width follows from the frame; Fixed Width: it's set here. caption_at(ui, tr("Width:"), cap2); if d.s("columnsKind") == "fixedWidth" { - spin_measure(ui, d, "columnWidth", field, true); + spin_measure(ui, d, "columnWidth", tr("Width:"), field, true); } else { // Fixed Number: each column's width, from the frame, the insets and the gutter. let frame = d.fields.get("_frameWidth").and_then(Value::as_f64); @@ -1467,16 +1474,18 @@ pub(crate) fn general(lang: &str, ui: &mut egui::Ui, d: &mut Dialog) { let inner = w - d.m(left).unwrap_or(0.0) - d.m(right).unwrap_or(0.0); ((inner - d.m("gutter").unwrap_or(0.0) * (n - 1.0)) / n).max(0.0) }); - let _ = crate::widgets::NumField::measure("tfo_auto_width", width, d.unit).width(field).spinner().enabled(false).show(ui); + let auto = crate::widgets::NumField::measure("tfo_auto_width", width, d.unit).name(tr("Width:").trim_end_matches(':')); + let _ = auto.width(field).spinner().enabled(false).show(ui); } }); ui.horizontal(|ui| { caption_at(ui, tr("Gutter:"), cap); - spin_measure(ui, d, "gutter", field, true); + spin_measure(ui, d, "gutter", tr("Gutter:"), field, true); // Maximum is for Flexible Width columns, which aren't in the document model yet. caption_at(ui, tr("Maximum:"), cap2); ui.add_enabled_ui(false, |ui| { - let _ = crate::widgets::NumField::number("tfo_max", None, "", 0).width(field).spinner().enabled(false).show(ui); + let max = crate::widgets::NumField::number("tfo_max", None, "", 0).name(tr("Maximum:").trim_end_matches(':')); + let _ = max.width(field).spinner().enabled(false).show(ui); }); }); check(ui, d, "balanceColumns", tr("Balance Columns")); @@ -1493,7 +1502,7 @@ pub(crate) fn general(lang: &str, ui: &mut egui::Ui, d: &mut Dialog) { { ui.horizontal(|ui| { caption_at(ui, tr(l1), cap); - spin_measure(ui, d, k1, field, !shaped); + spin_measure(ui, d, k1, tr(l1), field, !shaped); ui.add_space(8.0); if r == 0 { let linked = d.b("insetLinked"); @@ -1508,13 +1517,13 @@ pub(crate) fn general(lang: &str, ui: &mut egui::Ui, d: &mut Dialog) { ui.add_space(22.0 + ui.spacing().item_spacing.x); } caption_at(ui, tr(l2), cap2 - 30.0); - spin_measure(ui, d, k2, field, !shaped); + spin_measure(ui, d, k2, tr(l2), field, !shaped); }); } if shaped { ui.horizontal(|ui| { caption_at(ui, tr("Inset:"), cap); - spin_measure(ui, d, "inset", field, true); + spin_measure(ui, d, "inset", tr("Inset:"), field, true); }); } if !shaped @@ -1536,12 +1545,13 @@ pub(crate) fn general(lang: &str, ui: &mut egui::Ui, d: &mut Dialog) { d, "verticalJustification", &[("top", "Top"), ("center", "Center"), ("bottom", "Bottom"), ("justify", "Justify")], - ); + ) + .accessible_name(tr("Align:").to_owned()); }); ui.horizontal(|ui| { caption_at(ui, tr("Paragraph Spacing Limit:"), 170.0); let justify = d.s("verticalJustification") == "justify"; - spin_measure(ui, d, "paragraphSpacingLimit", field, justify); + spin_measure(ui, d, "paragraphSpacingLimit", tr("Paragraph Spacing Limit:"), field, justify); }); }); ui.add_space(4.0); @@ -1567,10 +1577,10 @@ fn column_rules(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { let opts: Vec<(&str, &str)> = swatches.iter().map(|n| (n.as_str(), n.as_str())).collect(); egui::Grid::new("tfo_rule").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { crate::rtl::label(ui, tr("Weight")); - text_field(ui, d, "columnRuleWeight", 70.0); + text_field_named(ui, d, "columnRuleWeight", 70.0, tr("Weight")); ui.end_row(); - crate::rtl::label(ui, tr("Color")); - combo(ui, d, "columnRuleColor", &opts); + let name = crate::rtl::label(ui, tr("Color")); + combo(ui, d, "columnRuleColor", &opts).labelled_by(name.id); ui.end_row(); for (label, key) in [ ("Tint %", "columnRuleTint"), @@ -1579,7 +1589,7 @@ fn column_rules(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { ("Horizontal Offset", "columnRuleOffset"), ] { crate::rtl::label(ui, tr(label)); - text_field(ui, d, key, 70.0); + text_field_named(ui, d, key, 70.0, tr(label)); ui.end_row(); } }); @@ -1590,7 +1600,7 @@ fn auto_size(lang: &str, ui: &mut egui::Ui, d: &mut Dialog) { let tr = |s: &'static str| crate::i18n::tr(lang, s); crate::rtl::label(ui, egui::RichText::new(tr("Auto-Size")).font(semibold(12.0))); egui::Grid::new("tfo_autosize").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { - crate::rtl::label(ui, tr("Auto-Sizing")); + let name = crate::rtl::label(ui, tr("Auto-Sizing")); translated_combo( lang, ui, @@ -1603,16 +1613,17 @@ fn auto_size(lang: &str, ui: &mut egui::Ui, d: &mut Dialog) { ("heightAndWidth", "Height and Width"), ("heightAndWidthProportionally", "Height and Width (Keep Proportions)"), ], - ); + ) + .labelled_by(name.id); ui.end_row(); crate::rtl::label(ui, tr("Reference Point")); reference_point(ui, d, "autoSizeReference"); ui.end_row(); check(ui, d, "useMinimumHeight", tr("Minimum Height")); - text_field(ui, d, "minimumHeight", 70.0); + text_field_named(ui, d, "minimumHeight", 70.0, tr("Minimum Height")); ui.end_row(); check(ui, d, "useMinimumWidth", tr("Minimum Width")); - text_field(ui, d, "minimumWidth", 70.0); + text_field_named(ui, d, "minimumWidth", 70.0, tr("Minimum Width")); ui.end_row(); }); check(ui, d, "noLineBreaks", tr("No Line Breaks")); diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index ca057856d..0309be7e8 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -1746,8 +1746,7 @@ fn text_frame_options_dims_the_sides_for_a_non_rectangular_frame() { let r = app.run("frame.create", json!({"rect": [36, 36, 336, 236], "shape": shape, "content": "text", "text": "Hello"})).unwrap(); app.run("selection.set", json!({"ids": [r["id"]]})).unwrap(); app.ui.dialog = Some(Dialog::new("textFrameOptions", json!({}))); - // The General tab's Columns and Align dropdowns have no accessible name yet. - let mut h = Harness::builder().with_size(egui::vec2(1000.0, 800.0)).with_accessibility_check(false).build_ui_state( + let mut h = Harness::builder().with_size(egui::vec2(1000.0, 800.0)).build_ui_state( |ui, app: &mut DesignApp| { let key = egui::Id::unique("text_frame_inset_test_fonts"); if ui.data(|d| d.get_temp::(key)).is_none() { @@ -1760,17 +1759,31 @@ fn text_frame_options_dims_the_sides_for_a_non_rectangular_frame() { app, ); h.run_steps(3); - for side in ["insetTop", "insetLeft", "insetBottom", "insetRight"] { - let label = format!("Increase tfo_{side}"); + // The Columns and Align dropdowns carry their captions as accessible names. + for name in ["Columns:", "Align:"] { + let _ = h.get_by_role_and_label(egui::accesskit::Role::ComboBox, name); + } + // The numeric fields and their spinners are named from the captions beside them. + for name in ["Number", "Width", "Gutter", "Maximum", "Paragraph Spacing Limit"] { + let _ = h.get_by_role_and_label(egui::accesskit::Role::TextInput, name); + let _ = h.get_by_label(&format!("Increase {name}")); + } + for side in ["Top", "Left", "Bottom", "Right"] { + let _ = h.get_by_role_and_label(egui::accesskit::Role::TextInput, side); + let label = format!("Increase {side}"); assert_eq!(h.get_by_label(&label).accesskit_node().is_disabled(), shaped, "{shape}: {side}"); } assert_eq!(h.query_by_label("Inset:").is_some(), shaped, "{shape}: the Inset caption"); - assert_eq!(h.query_by_label("Increase tfo_inset").is_some(), shaped, "{shape}: the Inset field"); + assert_eq!(h.query_by_role_and_label(egui::accesskit::Role::TextInput, "Inset").is_some(), shaped, "{shape}: the Inset editor"); + assert_eq!(h.query_by_label("Increase Inset").is_some(), shaped, "{shape}: the Inset field"); // The General tab fits in the dialog's fixed height: the buttons stay put on another tab. let (wrap, cancel) = (h.get_by_label("Ignore Text Wrap").rect().bottom(), h.get_by_label("Cancel").rect().top()); assert!(wrap < cancel, "{shape}: Ignore Text Wrap ends at {wrap}, below the top of Cancel at {cancel}"); - h.get_by_label("Auto-Size").click(); - h.run_steps(3); + // Every tab names its inputs (the harness checks after each settled run). + for tab in ["Column Rules", "Baseline Options", "Auto-Size"] { + h.get_by_label(tab).click(); + h.run(); + } assert_eq!(h.get_by_label("Cancel").rect().top(), cancel, "{shape}: the buttons stay put on another tab"); h.get_by_label("General").click(); h.run_steps(3);