diff --git a/src/git/blame.rs b/src/git/blame.rs index aeba889..f9b8047 100644 --- a/src/git/blame.rs +++ b/src/git/blame.rs @@ -231,6 +231,9 @@ impl RenderOnce for BlameView { let text = lines[line].clone(); let styles = code_colors(&text, cx); div() + .id((id.clone(), format!("line-{line}"))) + .role(Role::Label) + .aria_label(SharedString::from(format!("{} {text}", line + 1))) .w_full() .flex() .items_center() diff --git a/src/git/rows.rs b/src/git/rows.rs index 9123b1b..f0a0ac5 100644 --- a/src/git/rows.rs +++ b/src/git/rows.rs @@ -94,3 +94,30 @@ pub(super) fn code(line: &DiffLine, colors: &Palette, cx: &App) -> StyledText { styles.sort_by_key(|(range, _)| range.start); StyledText::new(line.text.clone()).with_highlights(stack(styles)) } + +/// A line as assistive tech reads it: number, change, text. +pub(super) fn spoken(line: &DiffLine) -> SharedString { + let (number, sign) = match line.kind { + LineKind::Same => (line.new, ""), + LineKind::Added => (line.new, "+ "), + LineKind::Removed => (line.old, "− "), + }; + let number = number.map_or_else(String::new, |n| format!("{n} ")); + format!("{number}{sign}{}", line.text).into() +} + +#[cfg(test)] +mod tests { + #[test] + fn a_line_speaks_its_number_change_and_text() { + let stretches = super::super::diff::diff("a\nb\n", "a\nc\n", 1); + let super::Stretch::Hunk { lines, .. } = &stretches[0] else { + panic!("a hunk") + }; + let said: Vec = lines + .iter() + .map(|line| super::spoken(line).to_string()) + .collect(); + assert_eq!(said, ["1 a", "2 − b", "2 + c"]); + } +} diff --git a/src/git/viewer.rs b/src/git/viewer.rs index 3bea678..04f4a8a 100644 --- a/src/git/viewer.rs +++ b/src/git/viewer.rs @@ -9,7 +9,7 @@ use gpui::{ use super::{ badges::DiffStat, diff::{DiffLine, LineKind, Stretch, diff, side_numbers}, - rows::{Place, Shown, code, shown, washes}, + rows::{Place, Shown, code, shown, spoken, washes}, }; use crate::{ buttons::{Button, ButtonVariant, SegmentedControl}, @@ -228,15 +228,10 @@ impl RenderOnce for DiffViewer { match (&on_line, line.kind) { (Some(on_line), LineKind::Added | LineKind::Removed) => { let on_line = on_line.clone(); - let sign = if line.kind == LineKind::Added { - "+" - } else { - "−" - }; element .id((id.clone(), format!("line-{}-{}", place.0, place.1))) .role(Role::ListItem) - .aria_label(SharedString::from(format!("{sign} {}", line.text))) + .aria_label(spoken(line)) .aria_selected(picked) .tab_index(0) .focus_ring(cx) @@ -248,7 +243,7 @@ impl RenderOnce for DiffViewer { on_line(place, event.modifiers().shift, window, cx) }) } - _ => element.id(own), + _ => element.id(own).role(Role::Label).aria_label(spoken(line)), } }; let side = |line: Option<&(Place, DiffLine)>, @@ -303,6 +298,8 @@ impl RenderOnce for DiffViewer { .on_click(move |_, window, cx| run(stretch, window, cx)) }); row.id(ix) + .role(Role::Heading) + .aria_label(header.clone()) .bg(colors.hover) .items_center() .text_color(colors.fg_muted) @@ -346,7 +343,23 @@ impl RenderOnce for DiffViewer { } Shown::Fold(stretch, count) => { let (open, stretch) = (on_open.clone(), *stretch); + let said = i18n::text( + cx, + if *count == 1 { + "git.diff.unchanged.one" + } else { + "git.diff.unchanged.other" + }, + &[("n", &count.to_string())], + ); + let role = if open.is_some() { + Role::Button + } else { + Role::Label + }; row.id((id.clone(), format!("fold-{stretch}"))) + .role(role) + .aria_label(said.clone()) .justify_center() .bg(colors.hover.opacity(0.5)) .text_color(colors.fg_subtle) @@ -362,18 +375,7 @@ impl RenderOnce for DiffViewer { open(stretch, window, cx) }) }) - .child(format!( - "⋯ {}", - i18n::text( - cx, - if *count == 1 { - "git.diff.unchanged.one" - } else { - "git.diff.unchanged.other" - }, - &[("n", &count.to_string())], - ) - )) + .child(format!("⋯ {said}")) .into_any_element() } }