From 1418af673bf0c2e8e8ce9d0c9c70b9cb08e1e305 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:26:52 +0100 Subject: [PATCH] sidebar: Improve keyboard navigation (#50938) We know outline the focus sidebar entry similar to the project panel. This allows users to see what they have selected vs active Before you mark this PR as ready for review, make sure that you have: - [ ] Added a solid test coverage and/or screenshots from doing manual testing - [x] Done a self-review taking into account security and performance aspects - [x] Aligned any UI changes with the [UI checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) Release Notes: - N/A --- crates/sidebar/src/sidebar.rs | 19 +++++++++---- crates/ui/src/components/ai/thread_item.rs | 33 ++++++++++++++++++++++ crates/ui/src/components/list/list_item.rs | 14 +++++++++ 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 3bb3ea9ea44efe2cf57a4d021b0a1755ac3b3681..eec55f16af8cf7deefdb8adeddeac5b6b4fb4ea9 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -677,9 +677,15 @@ impl Sidebar { label, workspace, highlight_positions, - } => { - self.render_project_header(ix, path_list, label, workspace, highlight_positions, cx) - } + } => self.render_project_header( + ix, + path_list, + label, + workspace, + highlight_positions, + is_selected, + cx, + ), ListEntry::Thread { session_info, icon, @@ -730,6 +736,7 @@ impl Sidebar { label: &SharedString, workspace: &Entity, highlight_positions: &[usize], + is_selected: bool, cx: &mut Context, ) -> AnyElement { let id = SharedString::from(format!("project-header-{}", ix)); @@ -770,6 +777,7 @@ impl Sidebar { // TODO: if is_selected, draw a blue border around the item. ListItem::new(id) + .selection_outlined(is_selected) .group_name(&group) .toggle_state(is_active_workspace) .child( @@ -1092,7 +1100,7 @@ impl Sidebar { status: AgentThreadStatus, workspace: &Entity, highlight_positions: &[usize], - _is_selected: bool, + is_selected: bool, cx: &mut Context, ) -> AnyElement { let has_notification = self.contents.is_thread_notified(&session_info.session_id); @@ -1114,6 +1122,7 @@ impl Sidebar { .status(status) .notified(has_notification) .selected(self.focused_thread.as_ref() == Some(&session_info.session_id)) + .outlined(is_selected) .on_click(cx.listener(move |this, _, window, cx| { this.selection = None; this.activate_thread(session_info.clone(), &workspace, window, cx); @@ -1159,7 +1168,7 @@ impl Sidebar { let count = format!("({})", remaining_count); ListItem::new(id) - .toggle_state(is_selected) + .selection_outlined(is_selected) .child( h_flex() .px_1() diff --git a/crates/ui/src/components/ai/thread_item.rs b/crates/ui/src/components/ai/thread_item.rs index 52d91e09824077738bde6be75122b0bf7b9e3d52..c8f5c8a41cdf74dae16a411b4fe3170b2be04bf3 100644 --- a/crates/ui/src/components/ai/thread_item.rs +++ b/crates/ui/src/components/ai/thread_item.rs @@ -24,6 +24,7 @@ pub struct ThreadItem { notified: bool, status: AgentThreadStatus, selected: bool, + outlined: bool, hovered: bool, added: Option, removed: Option, @@ -47,6 +48,7 @@ impl ThreadItem { notified: false, status: AgentThreadStatus::default(), selected: false, + outlined: false, hovered: false, added: None, removed: None, @@ -90,6 +92,11 @@ impl ThreadItem { self } + pub fn outlined(mut self, outlined: bool) -> Self { + self.outlined = outlined; + self + } + pub fn added(mut self, added: usize) -> Self { self.added = Some(added); self @@ -221,6 +228,9 @@ impl RenderOnce for ThreadItem { } }) .when(self.selected, |s| s.bg(clr.element_active)) + .border_1() + .border_color(gpui::transparent_black()) + .when(self.outlined, |s| s.border_color(clr.panel_focused_border)) .hover(|s| s.bg(clr.element_hover)) .on_hover(self.on_hover) .child( @@ -409,6 +419,29 @@ impl Component for ThreadItem { ) .into_any_element(), ), + single_example( + "Outlined Item (Keyboard Selection)", + container() + .child( + ThreadItem::new("ti-7", "Implement keyboard navigation") + .icon(IconName::AiClaude) + .timestamp("4:00 PM") + .outlined(true), + ) + .into_any_element(), + ), + single_example( + "Selected + Outlined", + container() + .child( + ThreadItem::new("ti-8", "Active and keyboard-focused thread") + .icon(IconName::AiGemini) + .timestamp("5:00 PM") + .selected(true) + .outlined(true), + ) + .into_any_element(), + ), ]; Some( diff --git a/crates/ui/src/components/list/list_item.rs b/crates/ui/src/components/list/list_item.rs index d581fad9453d9812f17b7bc9e0297fb9927c8188..cc9c955fd35aa33355be84f9ee3f17f27995ffaf 100644 --- a/crates/ui/src/components/list/list_item.rs +++ b/crates/ui/src/components/list/list_item.rs @@ -42,6 +42,7 @@ pub struct ListItem { selectable: bool, always_show_disclosure_icon: bool, outlined: bool, + selection_outlined: Option, rounded: bool, overflow_x: bool, focused: Option, @@ -71,6 +72,7 @@ impl ListItem { selectable: true, always_show_disclosure_icon: false, outlined: false, + selection_outlined: None, rounded: false, overflow_x: false, focused: None, @@ -171,6 +173,11 @@ impl ListItem { self } + pub fn selection_outlined(mut self, outlined: bool) -> Self { + self.selection_outlined = Some(outlined); + self + } + pub fn rounded(mut self) -> Self { self.rounded = true; self @@ -241,6 +248,13 @@ impl RenderOnce for ListItem { }) }) .when(self.rounded, |this| this.rounded_sm()) + .when_some(self.selection_outlined, |this, outlined| { + this.border_1() + .border_color(gpui::transparent_black()) + .when(outlined, |this| { + this.border_color(cx.theme().colors().panel_focused_border) + }) + }) .when_some(self.on_hover, |this, on_hover| this.on_hover(on_hover)) .child( h_flex()