1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 14:49:06 -04:00

Announce pressed state of selectable buttons to screen readers (#8130)

A `Button` used as a toggle (via `Button::selected(true)`,
`Button::selectable(...)`, or `Ui::selectable_label`) now announces its
pressed / not-pressed state to screen readers. Plain buttons that never
call `.selected(...)` stay un-toggled, so their announcement doesn't
change. `Checkbox` and the pre-existing selectable-label code path
already did this; `Button` was the odd one out.

No public API change: the field is private, `Button::selected(bool)`
keeps its signature, and visuals are identical. Internally the field
becomes `Option<bool>` so we can distinguish "plain button" from "toggle
button currently off".

Regression test added in `regression_tests.rs`, do let me know if some
other file would be a better location.

### Note for manual testing

`egui_demo_app` pulls in `eframe` with `default-features = false` and
doesn't re-enable `accesskit`, so `cargo run -p egui_demo_app` publishes
no AccessKit tree at all. To verify manually:
`cargo run -p egui_demo_app --features accessibility_inspector`
Happy to send a small follow-up PR enabling `accesskit` in the demo
app's defaults if that's desirable, since that makes a11y work much
easier to smoke-test locally.

### Use of AI

This PR was drafted with Claude Code. I understand different projects
have different policies regarding AI generated code. Do let me know if
this is not acceptable here. Also happy to take any other feedback.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Antoine Cellerier
2026-06-24 13:58:30 +02:00
committed by GitHub
parent 080ce81c8b
commit 5a49d895bf
2 changed files with 47 additions and 11 deletions

View File

@@ -35,7 +35,7 @@ pub struct Button<'a> {
frame_when_inactive: bool,
min_size: Vec2,
corner_radius: Option<CornerRadius>,
selected: bool,
selected: Option<bool>,
image_tint_follows_text_color: bool,
limit_image_size: bool,
classes: Classes,
@@ -54,7 +54,7 @@ impl<'a> Button<'a> {
frame_when_inactive: true,
min_size: Vec2::ZERO,
corner_radius: None,
selected: false,
selected: None,
image_tint_follows_text_color: false,
limit_image_size: false,
classes: Classes::default(),
@@ -261,9 +261,14 @@ impl<'a> Button<'a> {
}
/// If `true`, mark this button as "selected".
///
/// Calling this method opts the button into toggle semantics and the
/// current pressed/not-pressed state will be reported to assistive
/// technologies (e.g. screen readers). Plain buttons that never call
/// `selected` are not announced as toggles.
#[inline]
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self.selected = Some(selected);
self
}
@@ -314,7 +319,7 @@ impl<'a> Button<'a> {
let response: Option<Response> = ui.ctx().read_response(id);
let state = response.map(|r| r.widget_state()).unwrap_or_default();
classes.add_class_if(SELECTED_CLASS, selected);
classes.add_class_if(SELECTED_CLASS, selected.unwrap_or(false));
let ButtonStyle { frame, text_style } = ui.style().button_style(&classes, state);
@@ -376,12 +381,18 @@ impl<'a> Button<'a> {
ui.ctx().set_cursor_icon(cursor);
}
response.response.widget_info(|| {
if let Some(text) = &text {
WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text)
} else {
WidgetInfo::new(WidgetType::Button)
response.response.widget_info(|| match (selected, &text) {
(Some(selected), Some(text)) => {
WidgetInfo::selected(WidgetType::Button, ui.is_enabled(), selected, text)
}
(Some(selected), None) => {
let mut info = WidgetInfo::new(WidgetType::Button);
info.enabled = ui.is_enabled();
info.selected = Some(selected);
info
}
(None, Some(text)) => WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text),
(None, None) => WidgetInfo::new(WidgetType::Button),
});
response