From e86f90d3093ec8619310f5b27f8fc7fb12ed1b92 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Fri, 16 Aug 2024 14:03:45 +0800 Subject: [PATCH] Close Dropdown menu on blur. (#157) Closes #156 --- crates/story/src/dropdown_story.rs | 44 ++++++++++++++++++++++++++++-- crates/story/src/input_story.rs | 8 ++++-- crates/story/src/lib.rs | 1 + crates/ui/locales/ui.yml | 5 ++++ crates/ui/src/dropdown.rs | 41 +++++++++++++++++++++------- 5 files changed, 83 insertions(+), 16 deletions(-) diff --git a/crates/story/src/dropdown_story.rs b/crates/story/src/dropdown_story.rs index b1d0d403..a836597c 100644 --- a/crates/story/src/dropdown_story.rs +++ b/crates/story/src/dropdown_story.rs @@ -1,15 +1,25 @@ use gpui::{ - px, IntoElement, ParentElement, Render, SharedString, Styled, View, ViewContext, VisualContext, - WindowContext, + actions, px, AppContext, InteractiveElement, IntoElement, KeyBinding, ParentElement, Render, + SharedString, Styled, View, ViewContext, VisualContext, WindowContext, }; use ui::{ dropdown::{Dropdown, DropdownEvent, DropdownItem, SearchableVec}, h_flex, theme::ActiveTheme, - v_flex, IconName, Sizable, + v_flex, FocusableCycle, IconName, Sizable, }; +actions!(dropdown_story, [Tab, TabPrev]); + +const CONTEXT: &str = "DropdownStory"; +pub fn init(cx: &mut AppContext) { + cx.bind_keys([ + KeyBinding::new("shift-tab", TabPrev, Some(CONTEXT)), + KeyBinding::new("tab", Tab, Some(CONTEXT)), + ]) +} + struct Country { name: SharedString, code: SharedString, @@ -138,11 +148,39 @@ impl DropdownStory { DropdownEvent::Confirm(value) => println!("Selected country: {:?}", value), } } + + fn on_key_tab(&mut self, _: &Tab, cx: &mut ViewContext) { + self.cycle_focus(true, cx); + cx.notify(); + } + + fn on_key_shift_tab(&mut self, _: &TabPrev, cx: &mut ViewContext) { + self.cycle_focus(false, cx); + cx.notify(); + } +} + +impl FocusableCycle for DropdownStory { + fn cycle_focus_handles(&self, cx: &mut ViewContext) -> Vec + where + Self: Sized, + { + vec![ + self.country_dropdown.focus_handle(cx), + self.fruit_dropdown.focus_handle(cx), + self.simple_dropdown1.focus_handle(cx), + self.simple_dropdown2.focus_handle(cx), + self.simple_dropdown3.focus_handle(cx), + ] + } } impl Render for DropdownStory { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { v_flex() + .key_context(CONTEXT) + .on_action(cx.listener(Self::on_key_tab)) + .on_action(cx.listener(Self::on_key_shift_tab)) .size_full() .gap_4() .child( diff --git a/crates/story/src/input_story.rs b/crates/story/src/input_story.rs index 5698ea18..e58c9488 100644 --- a/crates/story/src/input_story.rs +++ b/crates/story/src/input_story.rs @@ -17,10 +17,12 @@ use crate::section; actions!(input_story, [Tab, TabPrev]); +const CONTEXT: &str = "InputStory"; + pub fn init(cx: &mut AppContext) { cx.bind_keys([ - KeyBinding::new("shift-tab", TabPrev, Some("InputStory")), - KeyBinding::new("tab", Tab, Some("InputStory")), + KeyBinding::new("shift-tab", TabPrev, Some(CONTEXT)), + KeyBinding::new("tab", Tab, Some(CONTEXT)), ]) } @@ -204,7 +206,7 @@ impl FocusableCycle for InputStory { impl Render for InputStory { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { v_flex() - .key_context("InputStory") + .key_context(CONTEXT) .on_action(cx.listener(Self::tab)) .on_action(cx.listener(Self::tab_prev)) .size_full() diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs index 4b8dedbf..8152f9d1 100644 --- a/crates/story/src/lib.rs +++ b/crates/story/src/lib.rs @@ -50,6 +50,7 @@ use ui::{divider::Divider, h_flex, label::Label, v_flex}; pub fn init(cx: &mut AppContext) { input_story::init(cx); + dropdown_story::init(cx); } pub fn section(title: impl IntoElement, cx: &WindowContext) -> Div { diff --git a/crates/ui/locales/ui.yml b/crates/ui/locales/ui.yml index 733af8f2..4fca3e2d 100644 --- a/crates/ui/locales/ui.yml +++ b/crates/ui/locales/ui.yml @@ -81,3 +81,8 @@ DatePicker: en: Select date zh-CN: 选择日期 zh-HK: 選擇日期 +Dropdown: + placeholder: + en: "Please select" + zh-CN: "请选择" + zh-HK: "請選擇" diff --git a/crates/ui/src/dropdown.rs b/crates/ui/src/dropdown.rs index dbd716e6..a66d0427 100644 --- a/crates/ui/src/dropdown.rs +++ b/crates/ui/src/dropdown.rs @@ -5,6 +5,7 @@ use gpui::{ Render, SharedString, StatefulInteractiveElement, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; +use rust_i18n::t; use crate::{ h_flex, @@ -16,13 +17,14 @@ use crate::{ actions!(dropdown, [Up, Down, Enter, Escape]); +const CONTEXT: &str = "Dropdown"; + pub fn init(cx: &mut AppContext) { - let context = Some("Dropdown"); cx.bind_keys([ - KeyBinding::new("up", Up, context), - KeyBinding::new("down", Down, context), - KeyBinding::new("enter", Enter, context), - KeyBinding::new("escape", Escape, context), + KeyBinding::new("up", Up, Some(CONTEXT)), + KeyBinding::new("down", Down, Some(CONTEXT)), + KeyBinding::new("enter", Enter, Some(CONTEXT)), + KeyBinding::new("escape", Escape, Some(CONTEXT)), ]) } @@ -301,6 +303,7 @@ where selected_index: Option, cx: &mut ViewContext, ) -> Self { + let focus_handle = cx.focus_handle(); let delegate = DropdownListDelegate { delegate, dropdown: cx.view().downgrade(), @@ -316,10 +319,14 @@ where } list }); + + cx.on_blur(&list.focus_handle(cx), Self::on_blur).detach(); + cx.on_blur(&focus_handle, Self::on_blur).detach(); + let mut this = Self { id: id.into(), - focus_handle: cx.focus_handle(), - placeholder: "Select...".into(), + focus_handle, + placeholder: t!("Dropdown.placeholder").into(), list, size: Size::Medium, icon: None, @@ -427,6 +434,16 @@ where self.focus_handle.focus(cx); } + fn on_blur(&mut self, cx: &mut ViewContext) { + // When the dropdown and dropdown menu are both not focused, close the dropdown menu. + if self.list.focus_handle(cx).is_focused(cx) || self.focus_handle.is_focused(cx) { + return; + } + + self.open = false; + cx.notify(); + } + fn up(&mut self, _: &Up, cx: &mut ViewContext) { if !self.open { return; @@ -514,8 +531,12 @@ impl FocusableView for Dropdown where D: DropdownDelegate + 'static, { - fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { - self.focus_handle.clone() + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + if self.open { + self.list.focus_handle(cx) + } else { + self.focus_handle.clone() + } } } @@ -531,7 +552,7 @@ where div() .id(self.id.clone()) - .key_context("Dropdown") + .key_context(CONTEXT) .track_focus(&self.focus_handle) .on_action(cx.listener(Self::up)) .on_action(cx.listener(Self::down))