mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
Text Frame Options: every dropdown and field has an accessible name (#2733)
* Text Frame Options: every dropdown and field has an accessible name The Columns and Align dropdowns on the General tab, and the dropdowns and text fields on the Column Rules, Baseline Options and Auto-Size tabs, had no accessible name, so a screen reader announced them without a caption and kittest's accessibility check failed on the dialog. Each one now takes the caption shown next to it. The non-rectangular frame test runs with the check on and opens every tab. * Text Frame Options: numeric fields take their captions as names The Number, Width, Gutter, Maximum, inset and Paragraph Spacing Limit fields on the General tab were named by their internal ids, so a screen reader announced tfo_columnWidth and its spinner read as Increase tfo_columnWidth. Each field and its spinner buttons now take the caption shown beside it. * Text Frame Options: test the numeric editors' accessible names The test checked only the spinner buttons' names. It now also finds each numeric editor of the General tab (Number, Width, Gutter, Maximum, Paragraph Spacing Limit, the four sides and Inset) by the text-input role and its caption.
This commit is contained in:
1 parent
f24fc1e72b
commit
cf4efa1833
2 files changed
+61
-37
No files matched your search
@@ -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"));
|
||||
|
||||
@@ -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::<bool>(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);
|
||||
|
||||
Reference in new issue
Block a user