a11y: a skip link, color-blind safe charts

i18n::SkipLink holds a Tab stop in flow at zero size, first where it
sits, and while focused raises a pill over the page; Enter, Space or a
press hands focus to its target. gpui orders stops by tab_index before
paint and registers none for an invisible element, so the stop stays in
flow and only the pill is raised. Theme::set_color_blind_safe swaps Ely's
chart hues for a set fitted to stay apart under Machado's protanopia,
deuteranopia and tritanopia; a test holds every pair 0.10 apart in Oklab
and each hue 3:1 on its page, and red against green shows the measure
bites. KeyboardNavigationHelper points to interaction::RovingFocus. The
announcer, labels and screen-reader text stay blocked: gpui 0.2.2 has no
accessibility tree, and nothing here is trusted to observe VoiceOver.
The i18n tests move under src/i18n/tests.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-27 20:00:26 -04:00
1 parent aa39fba87f
commit 1ac5c04fb0
10 files changed
+463 -7

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
use gpui::{
App, ElementId, FocusHandle, InteractiveElement, IntoElement, ParentElement, RenderOnce,
SharedString, StatefulInteractiveElement, Styled, Window, anchored, div, prelude::*,
};
use crate::{
primitives::{raise, tab_stop},
theme::{ActiveTheme, Elevation, Radius, TextSize},
};
/// A link that shows only while focused and hands focus to `target`. Put it before what a keyboard would otherwise tab through, such as a long nav; it takes no room at rest and lies over the page while shown.
#[derive(IntoElement)]
pub struct SkipLink {
id: ElementId,
label: SharedString,
target: FocusHandle,
}
impl SkipLink {
pub fn new(
id: impl Into<ElementId>,
label: impl Into<SharedString>,
target: &FocusHandle,
) -> Self {
Self {
id: id.into(),
label: label.into(),
target: target.clone(),
}
}
}
impl RenderOnce for SkipLink {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let handle = tab_stop(self.id.clone(), true, window, cx);
let shown = handle.is_focused(window);
let theme = cx.theme();
let (target, label) = (self.target, self.label.clone());
let skip = move |window: &mut Window| {
log::info!("skip link {label:?}: focus to its target");
window.focus(&target);
};
let press = skip.clone();
let pill = div()
.id((self.id.clone(), "pill"))
.occlude()
.px_3()
.py_1p5()
.rounded(theme.radius(Radius::Md))
.bg(theme.colors.overlay)
.border_1()
.border_color(theme.colors.focus)
.shadow(theme.elevation(Elevation::Floating))
.text_size(theme.text_size(TextSize::Sm))
.text_color(theme.colors.fg)
.cursor_pointer()
.child(self.label)
.on_click(move |_, window, _| press(window));
div()
.id(self.id.clone())
.track_focus(&handle)
.on_click(move |_, window, _| skip(window))
.when(shown, |link| {
link.child(raise(
self.id.clone(),
anchored().snap_to_window().child(div().p_2().child(pill)),
))
})
}
}