feat(a11y): screen reader name and description support for button widgets

This commit is contained in:
Michael Aaron Murphy 2026-01-09 23:03:09 +01:00 committed by Michael Murphy
parent f6039597b7
commit b9c24d2421
6 changed files with 122 additions and 19 deletions

View file

@ -38,6 +38,10 @@ impl<Message> Button<'_, Message> {
Self { Self {
id: Id::unique(), id: Id::unique(),
label: Cow::Borrowed(""), label: Cow::Borrowed(""),
#[cfg(feature = "a11y")]
name: Cow::Borrowed(""),
#[cfg(feature = "a11y")]
description: Cow::Borrowed(""),
tooltip: Cow::Borrowed(""), tooltip: Cow::Borrowed(""),
on_press: None, on_press: None,
width: Length::Shrink, width: Length::Shrink,
@ -151,7 +155,7 @@ impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Mes
); );
} }
let button = if builder.variant.vertical { let mut button = if builder.variant.vertical {
crate::widget::column::with_children(content) crate::widget::column::with_children(content)
.padding(builder.padding) .padding(builder.padding)
.spacing(builder.spacing) .spacing(builder.spacing)
@ -167,6 +171,11 @@ impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Mes
.apply(super::custom) .apply(super::custom)
}; };
#[cfg(feature = "a11y")]
{
button = button.name(builder.name).description(builder.description);
}
let button = button let button = button
.padding(0) .padding(0)
.id(builder.id) .id(builder.id)

View file

@ -33,6 +33,10 @@ impl<'a, Message> Button<'a, Message> {
Self { Self {
id: Id::unique(), id: Id::unique(),
label: Cow::Borrowed(""), label: Cow::Borrowed(""),
#[cfg(feature = "a11y")]
name: Cow::Borrowed(""),
#[cfg(feature = "a11y")]
description: Cow::Borrowed(""),
tooltip: Cow::Borrowed(""), tooltip: Cow::Borrowed(""),
on_press: None, on_press: None,
width: Length::Shrink, width: Length::Shrink,
@ -79,12 +83,18 @@ where
.width(builder.width) .width(builder.width)
.height(builder.height); .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) .padding(0)
.selected(builder.variant.selected) .selected(builder.variant.selected)
.id(builder.id) .id(builder.id)
.on_press_maybe(builder.on_press) .on_press_maybe(builder.on_press)
.class(builder.class) .class(builder.class);
.into()
#[cfg(feature = "a11y")]
{
button = button.name(builder.name).description(builder.description);
}
button.into()
} }
} }

View file

@ -34,6 +34,10 @@ impl<'a, Message> Button<'a, Message> {
Self { Self {
id: Id::unique(), id: Id::unique(),
label: label.into(), label: label.into(),
#[cfg(feature = "a11y")]
name: Cow::Borrowed(""),
#[cfg(feature = "a11y")]
description: Cow::Borrowed(""),
tooltip: Cow::Borrowed(""), tooltip: Cow::Borrowed(""),
on_press: None, on_press: None,
width: Length::Shrink, width: Length::Shrink,
@ -62,7 +66,7 @@ pub fn icon() -> Handle {
impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Message> { impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Message> {
fn from(mut builder: Button<'a, Message>) -> 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({ .push({
// TODO: Avoid allocation // TODO: Avoid allocation
crate::widget::text(builder.label.to_string()) crate::widget::text(builder.label.to_string())
@ -89,6 +93,15 @@ impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Mes
.on_press_maybe(builder.on_press.take()) .on_press_maybe(builder.on_press.take())
.class(builder.class); .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() { if builder.tooltip.is_empty() {
button.into() button.into()
} else { } else {

View file

@ -69,6 +69,16 @@ pub struct Builder<'a, Message, Variant> {
#[setters(into)] #[setters(into)]
label: Cow<'a, str>, 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. // Adds a tooltip to the button.
#[setters(into)] #[setters(into)]
tooltip: Cow<'a, str>, tooltip: Cow<'a, str>,

View file

@ -63,6 +63,10 @@ impl<Message> Button<'_, Message> {
Self { Self {
id: Id::unique(), id: Id::unique(),
label: Cow::Borrowed(""), label: Cow::Borrowed(""),
#[cfg(feature = "a11y")]
name: Cow::Borrowed(""),
#[cfg(feature = "a11y")]
description: Cow::Borrowed(""),
tooltip: Cow::Borrowed(""), tooltip: Cow::Borrowed(""),
on_press: None, on_press: None,
width: Length::Shrink, width: Length::Shrink,
@ -136,8 +140,10 @@ impl<'a, Message: Clone + 'static> From<Button<'a, Message>> for Element<'a, Mes
#[cfg(feature = "a11y")] #[cfg(feature = "a11y")]
{ {
if !builder.label.is_empty() { 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() { if builder.tooltip.is_empty() {

View file

@ -16,6 +16,7 @@ use std::ops::{Add, Sub};
/// Horizontal spin button widget. /// Horizontal spin button widget.
pub fn spin_button<'a, T, M>( pub fn spin_button<'a, T, M>(
label: impl Into<Cow<'a, str>>, label: impl Into<Cow<'a, str>>,
#[cfg(feature = "a11y")] name: impl Into<Cow<'a, str>>,
value: T, value: T,
step: T, step: T,
min: T, min: T,
@ -25,7 +26,7 @@ pub fn spin_button<'a, T, M>(
where where
T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd, T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
{ {
SpinButton::new( let mut button = SpinButton::new(
label, label,
value, value,
step, step,
@ -33,12 +34,20 @@ where
max, max,
Orientation::Horizontal, Orientation::Horizontal,
on_press, on_press,
) );
#[cfg(feature = "a11y")]
{
button = button.name(name.into());
}
button
} }
/// Vertical spin button widget. /// Vertical spin button widget.
pub fn vertical<'a, T, M>( pub fn vertical<'a, T, M>(
label: impl Into<Cow<'a, str>>, label: impl Into<Cow<'a, str>>,
#[cfg(feature = "a11y")] name: impl Into<Cow<'a, str>>,
value: T, value: T,
step: T, step: T,
min: T, min: T,
@ -48,15 +57,22 @@ pub fn vertical<'a, T, M>(
where where
T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd, T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
{ {
SpinButton::new( let mut button = SpinButton::new(
label, label,
value, value,
step, step,
min, min,
max, max,
Orientation::Vertical, Orientation::Horizontal,
on_press, on_press,
) );
#[cfg(feature = "a11y")]
{
button = button.name(name.into());
}
button
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@ -71,6 +87,9 @@ where
{ {
/// The formatted value of the spin button. /// The formatted value of the spin button.
label: Cow<'a, str>, label: Cow<'a, str>,
/// A name for screen reader support.
#[cfg(feature = "a11y")]
name: Cow<'a, str>,
/// The current value of the spin button. /// The current value of the spin button.
value: T, value: T,
/// The amount to increment or decrement the value. /// The amount to increment or decrement the value.
@ -99,6 +118,8 @@ where
) -> Self { ) -> Self {
Self { Self {
label: label.into(), label: label.into(),
#[cfg(feature = "a11y")]
name: Cow::Borrowed(""),
step, step,
value: if value < min { value: if value < min {
min min
@ -113,6 +134,12 @@ where
on_press: Box::from(on_press), 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<T>(value: T, step: T, _min: T, max: T) -> T fn increment<T>(value: T, step: T, _min: T, max: T) -> T
@ -153,21 +180,28 @@ where
fn make_button<'a, T, Message>( fn make_button<'a, T, Message>(
spin_button: &SpinButton<'a, T, Message>, spin_button: &SpinButton<'a, T, Message>,
icon: &'static str, icon: &'static str,
#[cfg(feature = "a11y")] name: String,
operation: fn(T, T, T, T) -> T, operation: fn(T, T, T, T) -> T,
) -> Element<'a, Message> ) -> Element<'a, Message>
where where
Message: Clone + 'static, Message: Clone + 'static,
T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd, T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
{ {
icon::from_name(icon) let mut button = icon::from_name(icon)
.apply(button::icon) .apply(button::icon)
.on_press((spin_button.on_press)(operation( .on_press((spin_button.on_press)(operation(
spin_button.value, spin_button.value,
spin_button.step, spin_button.step,
spin_button.min, spin_button.min,
spin_button.max, spin_button.max,
))) )));
.into()
#[cfg(feature = "a11y")]
{
button = button.name(name.clone());
}
button.into()
} }
fn horizontal_variant<T, Message>(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message> fn horizontal_variant<T, Message>(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message>
@ -175,9 +209,20 @@ where
Message: Clone + 'static, Message: Clone + 'static,
T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd, T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
{ {
let decrement_button = make_button(&spin_button, "list-remove-symbolic", decrement); let decrement_button = make_button(
let increment_button = make_button(&spin_button, "list-add-symbolic", increment); &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) let label = text::body(spin_button.label)
.apply(container) .apply(container)
.center_x(Length::Fixed(48.0)) .center_x(Length::Fixed(48.0))
@ -198,8 +243,18 @@ where
Message: Clone + 'static, Message: Clone + 'static,
T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd, T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
{ {
let decrement_button = make_button(&spin_button, "list-remove-symbolic", decrement); let decrement_button = make_button(
let increment_button = make_button(&spin_button, "list-add-symbolic", increment); &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) let label = text::body(spin_button.label)
.apply(container) .apply(container)