Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve accessibility of tab widgets #7270

Merged
merged 9 commits into from
Jan 6, 2025
3 changes: 3 additions & 0 deletions examples/gallery/ui/pages/page.slint
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export component Page inherits VerticalBox {
in property<string> description: "description";
in property<bool> show-enable-switch: true;

accessible-role: tab-panel;
accessible-label: root.title;

HorizontalBox {
Text {
font-size: 20px;
Expand Down
15 changes: 13 additions & 2 deletions examples/gallery/ui/side_bar.slint
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@
import { HorizontalBox, VerticalBox, Palette } from "std-widgets.slint";

component SideBarItem inherits Rectangle {
in property <int> tab-index;
in property <bool> selected;
in property <bool> has-focus;
in-out property <string> text <=> label.text;

callback clicked <=> touch.clicked;

min-height: l.preferred-height;
accessible-role: tab;
accessible-label: root.text;
accessible-item-index: root.tab-index;
accessible-item-selectable: true;
accessible-item-selected: root.selected;
accessible-action-default => { self.clicked(); }

states [
pressed when touch.pressed : {
Expand Down Expand Up @@ -40,7 +47,8 @@ component SideBarItem inherits Rectangle {

label := Text {
vertical-alignment: center;
}
accessible-role: none;
}
}

touch := TouchArea {
Expand All @@ -57,8 +65,10 @@ export component SideBar inherits Rectangle {

width: 180px;
forward-focus: fs;
accessible-role: tab;
accessible-role: tab-list;
accessible-delegate-focus: root.current-focused >= 0 ? root.current-focused : root.current-item;
accessible-label: root.title;
accessible-item-count: root.model.length;

Rectangle {
background: Palette.background.darker(0.2);
Expand Down Expand Up @@ -111,6 +121,7 @@ export component SideBar inherits Rectangle {
for item[index] in root.model : SideBarItem {
clicked => { root.current-item = index; }

tab-index: index;
has-focus: index == root.current-focused;
text: item;
selected: index == root.current-item;
Expand Down
1 change: 1 addition & 0 deletions internal/backends/qt/qt_accessible.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ cpp! {{
i_slint_core::items::AccessibleRole::TextInput => QAccessible_Role_EditableText,
i_slint_core::items::AccessibleRole::Switch => QAccessible_Role_CheckBox,
i_slint_core::items::AccessibleRole::ListItem => QAccessible_Role_ListItem,
i_slint_core::items::AccessibleRole::TabPanel => QAccessible_Role_Pane,
_ => QAccessible_Role_NoRole,
}
});
Expand Down
1 change: 1 addition & 0 deletions internal/backends/testing/slint_systest.proto
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ enum AccessibleRole {
TextInput = 13;
Switch = 14;
ListItem = 15;
TabPanel = 16;
}

message ElementQueryInstruction {
Expand Down
2 changes: 2 additions & 0 deletions internal/backends/testing/systest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ fn convert_to_proto_accessible_role(
i_slint_core::items::AccessibleRole::TextInput => proto::AccessibleRole::TextInput,
i_slint_core::items::AccessibleRole::Switch => proto::AccessibleRole::Switch,
i_slint_core::items::AccessibleRole::ListItem => proto::AccessibleRole::ListItem,
i_slint_core::items::AccessibleRole::TabPanel => proto::AccessibleRole::TabPanel,
_ => return None,
})
}
Expand All @@ -554,6 +555,7 @@ fn convert_from_proto_accessible_role(
proto::AccessibleRole::TextInput => i_slint_core::items::AccessibleRole::TextInput,
proto::AccessibleRole::Switch => i_slint_core::items::AccessibleRole::Switch,
proto::AccessibleRole::ListItem => i_slint_core::items::AccessibleRole::ListItem,
proto::AccessibleRole::TabPanel => i_slint_core::items::AccessibleRole::TabPanel,
})
}

Expand Down
5 changes: 5 additions & 0 deletions internal/backends/winit/accesskit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ impl NodeCollection {
i_slint_core::items::AccessibleRole::Spinbox => Role::SpinButton,
i_slint_core::items::AccessibleRole::Tab => Role::Tab,
i_slint_core::items::AccessibleRole::TabList => Role::TabList,
i_slint_core::items::AccessibleRole::TabPanel => Role::TabPanel,
i_slint_core::items::AccessibleRole::Text => Role::Label,
i_slint_core::items::AccessibleRole::Table => Role::Table,
i_slint_core::items::AccessibleRole::Tree => Role::Tree,
Expand Down Expand Up @@ -483,6 +484,10 @@ impl NodeCollection {
node.set_disabled();
}

if !item.is_visible() {
node.set_hidden();
}

let geometry = item.geometry();
let absolute_origin = item.map_to_window(geometry.origin) + window_position.to_vector();
let physical_origin = (absolute_origin * scale_factor).cast::<f64>();
Expand Down
2 changes: 2 additions & 0 deletions internal/common/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ macro_rules! for_each_enums {
Tab,
/// The element is similar to the tab bar in a `TabWidget`.
TabList,
/// The element is a container for tab content.
TabPanel,
/// The role for a `Text` element. It's automatically applied.
Text,
/// The role for a `TableView` or behaves like one.
Expand Down
26 changes: 26 additions & 0 deletions internal/compiler/passes/lower_tabwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,32 @@ fn process_tabwidget(
&old.into_inner(),
);
}
let role = crate::typeregister::BUILTIN
.with(|e| e.enums.AccessibleRole.clone())
.try_value_from_string("tab-panel")
.unwrap();
let old = child.borrow_mut().bindings.insert(
SmolStr::new_static("accessible-role"),
RefCell::new(Expression::EnumerationValue(role).into()),
);
if let Some(old) = old {
diag.push_error(
"The property 'accessible-role' cannot be set for Tabs inside a TabWidget"
.to_owned(),
&old.into_inner(),
);
}
let title_ref = RefCell::new(
Expression::PropertyReference(NamedReference::new(child, "title".into())).into(),
);
let old = child.borrow_mut().bindings.insert("accessible-label".into(), title_ref);
if let Some(old) = old {
diag.push_error(
"The property 'accessible-label' cannot be set for Tabs inside a TabWidget"
.to_owned(),
&old.into_inner(),
);
}

let mut tab = Element {
id: format_smolstr!("{}-tab{}", elem.borrow().id, index),
Expand Down
11 changes: 11 additions & 0 deletions internal/compiler/tests/syntax/elements/tabwidget2.slint
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,16 @@ export Test2 := Rectangle {
// ^error{dynamic tabs \('if' or 'for'\) are currently not supported}
title: "hello";
}

Tab {
accessible-role: tab-panel;
// ^error{The property 'accessible-role' cannot be set for Tabs inside a TabWidget}
Rectangle { }
}
Tab {
accessible-label: "Tab Panel";
// ^error{The property 'accessible-label' cannot be set for Tabs inside a TabWidget}
Rectangle { }
}
}
}
6 changes: 6 additions & 0 deletions internal/compiler/widgets/cosmic/tabwidget.slint
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export component TabImpl inherits Rectangle {
accessible-role: tab;
accessible-enabled: root.enabled;
accessible-label: root.title;
accessible-item-index: root.tab-index;
accessible-item-selectable: true;
accessible-item-selected: root.is-current;
accessible-action-default => { touch-area.clicked(); }

Rectangle {
y: 0;
Expand Down Expand Up @@ -128,6 +132,7 @@ export component TabImpl inherits Rectangle {
font-size: CosmicFontSettings.body.font-size;
font-weight: root.is-current ? CosmicFontSettings.body-strong.font-weight : CosmicFontSettings.body.font-weight;
color: root.is-current ? CosmicPalette.accent-background : CosmicPalette.control-foreground;
accessible-role: none;
}
}
}
Expand All @@ -139,6 +144,7 @@ export component TabBarImpl inherits TabBarBase {

accessible-role: tab-list;
accessible-delegate-focus: root.current-focused >= 0 ? root.current-focused : root.current;
accessible-item-count: root.num-tabs;
preferred-height: 44px;

Flickable {
Expand Down
6 changes: 6 additions & 0 deletions internal/compiler/widgets/cupertino/tabwidget.slint
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export component TabImpl inherits Rectangle {
accessible-role: tab;
accessible-enabled: root.enabled;
accessible-label: root.title;
accessible-item-index: root.tab-index;
accessible-item-selectable: true;
accessible-item-selected: root.is-current;
accessible-action-default => { i-touch-area.clicked(); }

if (root.is-current || i-touch-area.pressed): Rectangle {
width: 100%;
Expand Down Expand Up @@ -94,6 +98,7 @@ export component TabImpl inherits Rectangle {
font-size: CupertinoFontSettings.body-strong.font-size;
font-weight: CupertinoFontSettings.body-strong.font-weight;
color: CupertinoPalette.foreground;
accessible-role: none;
}
}
}
Expand All @@ -105,6 +110,7 @@ export component TabBarImpl inherits TabBarBase {

accessible-role: tab-list;
accessible-delegate-focus: root.current-focused >= 0 ? root.current-focused : root.current;
accessible-item-count: root.num-tabs;

Rectangle {
border-radius: 7px;
Expand Down
6 changes: 6 additions & 0 deletions internal/compiler/widgets/fluent/tabwidget.slint
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export component TabImpl inherits Rectangle {
accessible-role: tab;
accessible-enabled: root.enabled;
accessible-label: root.title;
accessible-item-index: root.tab-index;
accessible-item-selectable: true;
accessible-item-selected: root.is-current;
accessible-action-default => { i-touch-area.clicked(); }

Rectangle {
clip: true;
Expand Down Expand Up @@ -85,6 +89,7 @@ export component TabImpl inherits Rectangle {
font-size: root.is-current ? FluentFontSettings.body-strong.font-size : FluentFontSettings.body.font-size;
font-weight: root.is-current ? FluentFontSettings.body-strong.font-weight : FluentFontSettings.body.font-weight;
color: root.is-current ? FluentPalette.control-foreground : FluentPalette.text-secondary;
accessible-role: none;
}
}

Expand Down Expand Up @@ -121,6 +126,7 @@ export component TabBarImpl inherits TabBarBase {

accessible-role: tab-list;
accessible-delegate-focus: root.current-focused >= 0 ? root.current-focused : root.current;
accessible-item-count: root.num-tabs;

i-focus-scope := FocusScope {
property <int> focused-tab: 0;
Expand Down
6 changes: 6 additions & 0 deletions internal/compiler/widgets/material/tabwidget.slint
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export component TabImpl inherits Rectangle {
accessible-role: tab;
accessible-enabled: root.enabled;
accessible-label: root.title;
accessible-item-index: root.tab-index;
accessible-item-selectable: true;
accessible-item-selected: root.active;
accessible-action-default => { i-touch-area.clicked(); }

i-container := Rectangle {
background: MaterialPalette.alternate-background;
Expand Down Expand Up @@ -76,6 +80,7 @@ export component TabImpl inherits Rectangle {
//font-family: MaterialFontSettings.title-small.font;
font-size: MaterialFontSettings.title-small.font-size;
font-weight: MaterialFontSettings.title-small.font-weight;
accessible-role: none;

animate color {
duration: 250ms;
Expand Down Expand Up @@ -113,6 +118,7 @@ export component TabBarImpl inherits TabBarBase {

accessible-role: tab-list;
accessible-delegate-focus: root.current-focused >= 0 ? root.current-focused : root.current;
accessible-item-count: root.num-tabs;

Flickable {
HorizontalLayout {
Expand Down
8 changes: 8 additions & 0 deletions internal/compiler/widgets/qt/tabwidget.slint
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ export component TabImpl inherits NativeTab {
accessible-role: tab;
accessible-enabled: root.enabled;
accessible-label <=> root.title;
accessible-item-index: root.tab-index;
accessible-item-selectable: true;
accessible-item-selected: root.tab-index == root.current;
accessible-action-default => {
root.current = root.tab-index;
root.current-focused = root.tab-index;
}
}

export component TabBarImpl inherits TabBarBase {
Expand All @@ -18,6 +25,7 @@ export component TabBarImpl inherits TabBarBase {

accessible-role: tab-list;
accessible-delegate-focus: root.current;
accessible-item-count: root.num-tabs;

Rectangle {
// The breeze style draws outside of the tab bar, which is clip by default with Qt
Expand Down
Loading
Loading