Lists · SelectableList: list semantics, row names, selection and place in set

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 16:43:30 -04:00
1 parent 17643893cb
commit 7df3045a82
3 files changed
+33 -2

No files matched your search

+1
View File
@@ -265,3 +265,4 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- 2026-10-05: `Select::label` and `TextInput::label` name the control for assistive technology without drawing anything; a text input with neither label nor placeholder had no name, and a select read only its value, so a field named by a description list beside it was anonymous in the AX tree.
- 2026-10-05: `NumberInput::label` names the field for assistive technology without drawing it, through `TextInput::set_label` on the field's inner input, which lives across frames; a number field named only by a description list beside it was anonymous in the AX tree.
- 2026-10-05: `TemplatePicker`'s list is one Tab stop: up, down, Home and End move the highlight, Enter uses it; rows are list items with name, selection, place and count; its search placeholder, All chip, Use button and empty line read from the builtin catalog in English and Chinese. Before, the rows were reachable by mouse alone and anonymous in the AX tree.
- 2026-10-05: `SelectableList` is a list in the AX tree, named by `label`; each `ListItem` is a list item with its title as name, its description, its selection, and, in a selectable list, its place and count (`ListItem::place`). Before, the rows had no role and were missing from the AX tree.
+19 -1
View File
@@ -2,7 +2,7 @@ use std::rc::Rc;
use gpui::{
AnyElement, App, ClickEvent, ElementId, FontWeight, InteractiveElement, IntoElement,
MouseButton, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled,
MouseButton, ParentElement, RenderOnce, Role, SharedString, StatefulInteractiveElement, Styled,
Window, div, prelude::*, relative,
};
use smallvec::SmallVec;
@@ -29,6 +29,8 @@ pub struct ListItem {
strong: bool,
quiet: bool,
struck: bool,
/// Its place among its list's rows, from one, and their count.
place: Option<(usize, usize)>,
on_click: Option<OnClick>,
}
@@ -47,6 +49,7 @@ impl ListItem {
strong: false,
quiet: false,
struck: false,
place: None,
on_click: None,
}
}
@@ -104,6 +107,12 @@ impl ListItem {
self
}
/// Tells assistive tech it is row `at` of `of`, from one.
pub fn place(mut self, at: usize, of: usize) -> Self {
self.place = Some((at, of));
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
@@ -140,6 +149,15 @@ impl RenderOnce for ListItem {
let pressable = self.on_click.is_some() && !self.disabled;
div()
.id(self.id)
.role(Role::ListItem)
.aria_label(self.title.clone())
.aria_selected(self.selected)
.when_some(self.description.clone(), |row, text| {
row.aria_description(text)
})
.when_some(self.place, |row, (at, of)| {
row.aria_position_in_set(at).aria_size_of_set(of)
})
.flex()
.items_center()
.gap_3()
+13 -1
View File
@@ -2,7 +2,8 @@ use std::{rc::Rc, time::Duration};
use gpui::{
App, Div, ElementId, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce,
ScrollHandle, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Window, div,
Role, ScrollHandle, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Window,
div, prelude::FluentBuilder as _,
};
use web_time::Instant;
@@ -86,6 +87,7 @@ pub struct SelectableList {
rows: Vec<(SharedString, ListItem)>,
selected: Vec<SharedString>,
multiple: bool,
label: Option<SharedString>,
on_change: Option<OnSelect>,
on_activate: Option<OnActivate>,
}
@@ -98,6 +100,7 @@ impl SelectableList {
rows: Vec::new(),
selected: Vec::new(),
multiple: false,
label: None,
on_change: None,
on_activate: None,
}
@@ -114,6 +117,12 @@ impl SelectableList {
self
}
/// Names the list for assistive tech.
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
self
}
pub fn multiple(mut self) -> Self {
self.multiple = true;
self
@@ -249,6 +258,7 @@ impl RenderOnce for SelectableList {
.map(|(ix, (key, item))| {
let (pick, focus, activate) = (pick.clone(), focus.clone(), activate.clone());
item.selected(selected.contains(&key))
.place(ix + 1, count)
.current(focused && ix == at)
.on_click(move |event, window, cx| {
let held = event.modifiers();
@@ -269,6 +279,8 @@ impl RenderOnce for SelectableList {
(cursor.clone(), activate.clone(), off.clone(), keys.clone());
self.base
.id(self.id)
.role(Role::List)
.when_some(self.label, |list, label| list.aria_label(label))
.track_focus(&focus)
.track_scroll(&scroll)
.overflow_y_scroll()