diff --git a/AGENTS.md b/AGENTS.md index ba423b4..25b37ed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -261,3 +261,4 @@ Ely GPUI Component. A component library for GPUI, in light and dark. - 2026-10-04: the Showcase follows gpui-kit.com/apps: apps live in their own public repo and arrive by pull request. The site pins a commit of it in `frontend/showcase.lock` and moves only when someone bumps the pin, so stars refresh on the site with each bump. READMEs take Markdown and PNG pictures alone, no HTML or video, so the site needs no sanitizer. The catalog started empty. - 2026-10-04: chapter 44, Rendering, holds `RenderSurface` alone. gpui at 1a28cff paints `RenderImage`s everywhere, `CVPixelBuffer`s (NV12) on macOS's Metal renderer alone, and shares no device or shader hook, so shader, camera and GPU views are the host's, feeding the surface, and zero-copy or Metal, OpenGL, Vulkan and Direct3D surfaces are blocked (`tasks/ch44.md`). CI tests the crate and builds the gallery on Linux, Windows and macOS, arm64 and x86_64. - 2026-10-05: an overlay its owner removes without calling `Close` still hands focus back: `take_focus` records each holder and what it took focus from, `give_back` forgets it, and when the focused element leaves the tree the root scope walks back past every gone holder to the focus before them all, else takes it; nested overlays removed together return to what the outer one took focus from. Before, a dialog whose action set its owner's state to closed left focus on the root, and keys reached nothing until a click. +- 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. diff --git a/src/forms/select.rs b/src/forms/select.rs index df0c1ba..f3ea7bc 100644 --- a/src/forms/select.rs +++ b/src/forms/select.rs @@ -252,6 +252,7 @@ pub struct Select { choices: Vec, selected: Option, placeholder: SharedString, + label: Option, size: ControlSize, disabled: bool, on_change: Option, @@ -264,12 +265,19 @@ impl Select { choices: choices.into_iter().collect(), selected: None, placeholder: SharedString::from("Choose…"), + label: None, size: ControlSize::default(), disabled: false, on_change: None, } } + /// The name assistive technology reads; nothing is drawn. + pub fn label(mut self, text: impl Into) -> Self { + self.label = Some(text.into()); + self + } + pub fn selected(mut self, value: impl Into) -> Self { self.selected = Some(value.into()); self @@ -329,6 +337,7 @@ impl RenderOnce for Select { ) .role(Role::ComboBox) .aria_placeholder(self.placeholder.clone()) + .when_some(self.label.clone(), |trigger, label| trigger.aria_label(label)) .when_some(shown.as_ref(), |trigger, choice| { trigger.aria_value(choice.label.clone()) }) diff --git a/src/forms/text/actions.rs b/src/forms/text/actions.rs index ddddd03..e9ca77c 100644 --- a/src/forms/text/actions.rs +++ b/src/forms/text/actions.rs @@ -360,9 +360,13 @@ impl Render for TextInput { div() .id("ely-text-input") .role(Role::TextInput) - .when(!self.placeholder.is_empty(), |el| { - el.aria_label(self.placeholder.clone()) - }) + .when_some( + [&self.label, &self.placeholder] + .into_iter() + .find(|name| !name.is_empty()) + .cloned(), + |el, name| el.aria_label(name), + ) .when(!self.masked, |el| el.aria_value(self.text.clone())) .key_context(CONTEXT) .track_focus(&self.focus) diff --git a/src/forms/text/mod.rs b/src/forms/text/mod.rs index f4cf52b..ee3dde7 100644 --- a/src/forms/text/mod.rs +++ b/src/forms/text/mod.rs @@ -51,6 +51,7 @@ pub struct TextInput { focus: FocusHandle, text: String, placeholder: SharedString, + label: SharedString, selection: Range, reversed: bool, marked: Option>, @@ -105,6 +106,7 @@ impl TextInput { focus, text: String::new(), placeholder: SharedString::default(), + label: SharedString::default(), selection: 0..0, reversed: false, marked: None, @@ -141,6 +143,12 @@ impl TextInput { self } + /// The name assistive technology reads; the placeholder otherwise. + pub fn label(mut self, text: impl Into) -> Self { + self.label = text.into(); + self + } + /// Shows bullets instead of the text, and keeps it off the clipboard. pub fn masked(mut self) -> Self { self.masked = true;