From b9c24d24212a865977db4871efc13ff890055648 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 9 Jan 2026 23:03:09 +0100 Subject: [PATCH] feat(a11y): screen reader name and description support for button widgets --- src/widget/button/icon.rs | 11 +++++- src/widget/button/image.rs | 16 ++++++-- src/widget/button/link.rs | 15 ++++++- src/widget/button/mod.rs | 10 +++++ src/widget/button/text.rs | 8 +++- src/widget/spin_button.rs | 81 ++++++++++++++++++++++++++++++++------ 6 files changed, 122 insertions(+), 19 deletions(-) diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs index 754bc433..edb54272 100644 --- a/src/widget/button/icon.rs +++ b/src/widget/button/icon.rs @@ -38,6 +38,10 @@ impl Button<'_, Message> { Self { id: Id::unique(), label: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + name: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + description: Cow::Borrowed(""), tooltip: Cow::Borrowed(""), on_press: None, width: Length::Shrink, @@ -151,7 +155,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes ); } - let button = if builder.variant.vertical { + let mut button = if builder.variant.vertical { crate::widget::column::with_children(content) .padding(builder.padding) .spacing(builder.spacing) @@ -167,6 +171,11 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .apply(super::custom) }; + #[cfg(feature = "a11y")] + { + button = button.name(builder.name).description(builder.description); + } + let button = button .padding(0) .id(builder.id) diff --git a/src/widget/button/image.rs b/src/widget/button/image.rs index 6a5c47b1..ab51e667 100644 --- a/src/widget/button/image.rs +++ b/src/widget/button/image.rs @@ -33,6 +33,10 @@ impl<'a, Message> Button<'a, Message> { Self { id: Id::unique(), label: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + name: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + description: Cow::Borrowed(""), tooltip: Cow::Borrowed(""), on_press: None, width: Length::Shrink, @@ -79,12 +83,18 @@ where .width(builder.width) .height(builder.height); - super::custom_image_button(content, builder.variant.on_remove) + let mut button = super::custom_image_button(content, builder.variant.on_remove) .padding(0) .selected(builder.variant.selected) .id(builder.id) .on_press_maybe(builder.on_press) - .class(builder.class) - .into() + .class(builder.class); + + #[cfg(feature = "a11y")] + { + button = button.name(builder.name).description(builder.description); + } + + button.into() } } diff --git a/src/widget/button/link.rs b/src/widget/button/link.rs index b86ef1a3..9ce81268 100644 --- a/src/widget/button/link.rs +++ b/src/widget/button/link.rs @@ -34,6 +34,10 @@ impl<'a, Message> Button<'a, Message> { Self { id: Id::unique(), label: label.into(), + #[cfg(feature = "a11y")] + name: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + description: Cow::Borrowed(""), tooltip: Cow::Borrowed(""), on_press: None, width: Length::Shrink, @@ -62,7 +66,7 @@ pub fn icon() -> Handle { impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { fn from(mut builder: Button<'a, Message>) -> Element<'a, Message> { - let button: super::Button<'a, Message> = row::with_capacity(2) + let mut button: super::Button<'a, Message> = row::with_capacity(2) .push({ // TODO: Avoid allocation crate::widget::text(builder.label.to_string()) @@ -89,6 +93,15 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .on_press_maybe(builder.on_press.take()) .class(builder.class); + #[cfg(feature = "a11y")] + { + if !builder.label.is_empty() { + button = button.name(builder.label); + } + + button = button.description(builder.description); + } + if builder.tooltip.is_empty() { button.into() } else { diff --git a/src/widget/button/mod.rs b/src/widget/button/mod.rs index d9a4df94..f5975d39 100644 --- a/src/widget/button/mod.rs +++ b/src/widget/button/mod.rs @@ -69,6 +69,16 @@ pub struct Builder<'a, Message, Variant> { #[setters(into)] label: Cow<'a, str>, + /// A name for screen reader support + #[cfg(feature = "a11y")] + #[setters(into)] + name: Cow<'a, str>, + + /// A description for screen reader support + #[cfg(feature = "a11y")] + #[setters(into)] + description: Cow<'a, str>, + // Adds a tooltip to the button. #[setters(into)] tooltip: Cow<'a, str>, diff --git a/src/widget/button/text.rs b/src/widget/button/text.rs index 3f58c932..bcdd02ba 100644 --- a/src/widget/button/text.rs +++ b/src/widget/button/text.rs @@ -63,6 +63,10 @@ impl Button<'_, Message> { Self { id: Id::unique(), label: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + name: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + description: Cow::Borrowed(""), tooltip: Cow::Borrowed(""), on_press: None, width: Length::Shrink, @@ -136,8 +140,10 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes #[cfg(feature = "a11y")] { if !builder.label.is_empty() { - button = button.name(builder.label); + button = button.name(builder.label) } + + button = button.description(builder.description); } if builder.tooltip.is_empty() { diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index 6f4a4de2..db90a000 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -16,6 +16,7 @@ use std::ops::{Add, Sub}; /// Horizontal spin button widget. pub fn spin_button<'a, T, M>( label: impl Into>, + #[cfg(feature = "a11y")] name: impl Into>, value: T, step: T, min: T, @@ -25,7 +26,7 @@ pub fn spin_button<'a, T, M>( where T: Copy + Sub + Add + PartialOrd, { - SpinButton::new( + let mut button = SpinButton::new( label, value, step, @@ -33,12 +34,20 @@ where max, Orientation::Horizontal, on_press, - ) + ); + + #[cfg(feature = "a11y")] + { + button = button.name(name.into()); + } + + button } /// Vertical spin button widget. pub fn vertical<'a, T, M>( label: impl Into>, + #[cfg(feature = "a11y")] name: impl Into>, value: T, step: T, min: T, @@ -48,15 +57,22 @@ pub fn vertical<'a, T, M>( where T: Copy + Sub + Add + PartialOrd, { - SpinButton::new( + let mut button = SpinButton::new( label, value, step, min, max, - Orientation::Vertical, + Orientation::Horizontal, on_press, - ) + ); + + #[cfg(feature = "a11y")] + { + button = button.name(name.into()); + } + + button } #[derive(Clone, Copy)] @@ -71,6 +87,9 @@ where { /// The formatted value of the spin button. label: Cow<'a, str>, + /// A name for screen reader support. + #[cfg(feature = "a11y")] + name: Cow<'a, str>, /// The current value of the spin button. value: T, /// The amount to increment or decrement the value. @@ -99,6 +118,8 @@ where ) -> Self { Self { label: label.into(), + #[cfg(feature = "a11y")] + name: Cow::Borrowed(""), step, value: if value < min { min @@ -113,6 +134,12 @@ where on_press: Box::from(on_press), } } + + #[cfg(feature = "a11y")] + pub(self) fn name(mut self, name: Cow<'a, str>) -> Self { + self.name = name; + self + } } fn increment(value: T, step: T, _min: T, max: T) -> T @@ -153,21 +180,28 @@ where fn make_button<'a, T, Message>( spin_button: &SpinButton<'a, T, Message>, icon: &'static str, + #[cfg(feature = "a11y")] name: String, operation: fn(T, T, T, T) -> T, ) -> Element<'a, Message> where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, { - icon::from_name(icon) + let mut button = icon::from_name(icon) .apply(button::icon) .on_press((spin_button.on_press)(operation( spin_button.value, spin_button.step, spin_button.min, spin_button.max, - ))) - .into() + ))); + + #[cfg(feature = "a11y")] + { + button = button.name(name.clone()); + } + + button.into() } fn horizontal_variant(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message> @@ -175,9 +209,20 @@ where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, { - let decrement_button = make_button(&spin_button, "list-remove-symbolic", decrement); - let increment_button = make_button(&spin_button, "list-add-symbolic", increment); - + let decrement_button = make_button( + &spin_button, + "list-remove-symbolic", + #[cfg(feature = "a11y")] + [&spin_button.name, " decrease"].concat(), + decrement, + ); + let increment_button = make_button( + &spin_button, + "list-add-symbolic", + #[cfg(feature = "a11y")] + [&spin_button.name, " increase"].concat(), + increment, + ); let label = text::body(spin_button.label) .apply(container) .center_x(Length::Fixed(48.0)) @@ -198,8 +243,18 @@ where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, { - let decrement_button = make_button(&spin_button, "list-remove-symbolic", decrement); - let increment_button = make_button(&spin_button, "list-add-symbolic", increment); + let decrement_button = make_button( + &spin_button, + "list-remove-symbolic", + [&spin_button.label, " decrease"].concat(), + decrement, + ); + let increment_button = make_button( + &spin_button, + "list-add-symbolic", + [&spin_button.label, " increase"].concat(), + increment, + ); let label = text::body(spin_button.label) .apply(container)