modal: Only focus modal when it not focused on. (#250)

- Fix dropdown to mouse down out to dismiss.


https://github.com/user-attachments/assets/f5ab4aca-bfdc-460e-ae5a-cd0a31a30906
This commit is contained in:
Jason Lee 2024-09-17 18:48:06 +08:00 committed by GitHub
parent 97e61a507b
commit 8f0ba2e5c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 60 additions and 33 deletions

View file

@ -11,6 +11,7 @@ use ui::{
button::{Button, ButtonStyle, ButtonStyled as _}, button::{Button, ButtonStyle, ButtonStyled as _},
checkbox::Checkbox, checkbox::Checkbox,
date_picker::DatePicker, date_picker::DatePicker,
dropdown::Dropdown,
h_flex, h_flex,
input::TextInput, input::TextInput,
list::{List, ListDelegate, ListItem}, list::{List, ListDelegate, ListItem},
@ -149,6 +150,7 @@ pub struct ModalStory {
input1: View<TextInput>, input1: View<TextInput>,
input2: View<TextInput>, input2: View<TextInput>,
date_picker: View<DatePicker>, date_picker: View<DatePicker>,
dropdown: View<Dropdown<Vec<String>>>,
modal_overlay: bool, modal_overlay: bool,
model_show_close: bool, model_show_close: bool,
model_padding: bool, model_padding: bool,
@ -247,6 +249,18 @@ impl ModalStory {
let input2 = cx.new_view(|cx| TextInput::new(cx).placeholder("Input on the Window")); let input2 = cx.new_view(|cx| TextInput::new(cx).placeholder("Input on the Window"));
let date_picker = let date_picker =
cx.new_view(|cx| DatePicker::new("birthday-picker", cx).placeholder("Date of Birth")); cx.new_view(|cx| DatePicker::new("birthday-picker", cx).placeholder("Date of Birth"));
let dropdown = cx.new_view(|cx| {
Dropdown::new(
"dropdown1",
vec![
"Option 1".to_string(),
"Option 2".to_string(),
"Option 3".to_string(),
],
None,
cx,
)
});
Self { Self {
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
@ -256,6 +270,7 @@ impl ModalStory {
input1, input1,
input2, input2,
date_picker, date_picker,
dropdown,
modal_overlay: true, modal_overlay: true,
model_show_close: true, model_show_close: true,
model_padding: true, model_padding: true,
@ -322,10 +337,10 @@ impl ModalStory {
let modal_padding = self.model_padding; let modal_padding = self.model_padding;
let input1 = self.input1.clone(); let input1 = self.input1.clone();
let date_picker = self.date_picker.clone(); let date_picker = self.date_picker.clone();
let dropdown = self.dropdown.clone();
let view = cx.view().clone(); let view = cx.view().clone();
cx.open_modal(move |modal, cx| { cx.open_modal(move |modal, cx| {
input1.focus_handle(cx).focus(cx);
modal modal
.title("Form Modal") .title("Form Modal")
.overlay(overlay) .overlay(overlay)
@ -337,6 +352,7 @@ impl ModalStory {
.child("This is a modal dialog.") .child("This is a modal dialog.")
.child("You can put anything here.") .child("You can put anything here.")
.child(input1.clone()) .child(input1.clone())
.child(dropdown.clone())
.child(date_picker.clone()), .child(date_picker.clone()),
) )
.footer( .footer(
@ -388,6 +404,8 @@ impl ModalStory {
), ),
) )
}); });
self.input1.focus_handle(cx).focus(cx);
} }
fn on_action_test_action(&mut self, _: &TestAction, cx: &mut ViewContext<Self>) { fn on_action_test_action(&mut self, _: &TestAction, cx: &mut ViewContext<Self>) {

View file

@ -156,6 +156,8 @@ impl ColorPicker {
} }
fn on_escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) { fn on_escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
cx.propagate();
self.open = false; self.open = false;
cx.notify(); cx.notify();
} }
@ -343,7 +345,6 @@ impl Render for ColorPicker {
.position(self.resolved_corner(self.bounds)) .position(self.resolved_corner(self.bounds))
.child( .child(
div() div()
.track_focus(&self.focus_handle)
.occlude() .occlude()
.map(|this| match self.anchor { .map(|this| match self.anchor {
AnchorCorner::TopLeft | AnchorCorner::TopRight => { AnchorCorner::TopLeft | AnchorCorner::TopRight => {

View file

@ -150,29 +150,32 @@ where
} }
fn cancel(&mut self, cx: &mut ViewContext<List<Self>>) { fn cancel(&mut self, cx: &mut ViewContext<List<Self>>) {
if let Some(view) = self.dropdown.upgrade() { let dropdown = self.dropdown.clone();
cx.update_view(&view, |view, cx| { cx.defer(move |_, cx| {
view.focus(cx); _ = dropdown.update(cx, |this, cx| {
view.open = false; this.open = false;
this.focus(cx);
}); });
} });
} }
fn confirm(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>) { fn confirm(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>) {
self.selected_index = ix; self.selected_index = ix;
if let Some(view) = self.dropdown.upgrade() { let selected_value = self
cx.update_view(&view, |view, cx| { .selected_index
let selected_value = self .and_then(|ix| self.delegate.get(ix))
.selected_index .map(|item| item.value().clone());
.and_then(|ix| self.delegate.get(ix)) let dropdown = self.dropdown.clone();
.map(|item| item.value().clone());
cx.defer(move |_, cx| {
_ = dropdown.update(cx, |this, cx| {
cx.emit(DropdownEvent::Confirm(selected_value.clone())); cx.emit(DropdownEvent::Confirm(selected_value.clone()));
view.focus(cx); this.selected_value = selected_value;
view.selected_value = selected_value; this.open = false;
view.open = false; this.focus(cx);
}); });
} });
} }
fn perform_search(&mut self, query: &str, cx: &mut ViewContext<List<Self>>) -> Task<()> { fn perform_search(&mut self, query: &str, cx: &mut ViewContext<List<Self>>) -> Task<()> {
@ -469,6 +472,9 @@ where
} }
fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) { fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
// Propagate the event to the parent view, for example to the Modal to support ENTER to confirm.
cx.propagate();
if !self.open { if !self.open {
self.open = true; self.open = true;
cx.notify(); cx.notify();
@ -489,6 +495,9 @@ where
} }
fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) { fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
// Propagate the event to the parent view, for example to the Modal to support ESC to close.
cx.propagate();
self.open = false; self.open = false;
cx.notify(); cx.notify();
} }
@ -566,7 +575,7 @@ where
let view = cx.view().clone(); let view = cx.view().clone();
let bounds = self.bounds; let bounds = self.bounds;
let allow_open = !(self.open || self.disabled); let allow_open = !(self.open || self.disabled);
let outline_visible = is_focused && !self.disabled; let outline_visible = self.open || is_focused && !self.disabled;
// If the size has change, set size to self.list, to change the QueryInput size. // If the size has change, set size to self.list, to change the QueryInput size.
if self.list.read(cx).size != self.size { if self.list.read(cx).size != self.size {
@ -676,7 +685,6 @@ where
}) })
.child( .child(
v_flex() v_flex()
.track_focus(&self.list.focus_handle(cx))
.occlude() .occlude()
.mt_1p5() .mt_1p5()
.bg(cx.theme().background) .bg(cx.theme().background)
@ -688,7 +696,10 @@ where
cx.dispatch_action(Box::new(Escape)); cx.dispatch_action(Box::new(Escape));
}) })
.child(self.list.clone()), .child(self.list.clone()),
), )
.on_mouse_down_out(cx.listener(|this, _, cx| {
this.escape(&Escape, cx);
})),
), ),
) )
.with_priority(1), .with_priority(1),

View file

@ -103,7 +103,12 @@ impl<'a> ContextModal for WindowContext<'a> {
fn close_modal(&mut self) { fn close_modal(&mut self) {
Root::update(self, move |root, cx| { Root::update(self, move |root, cx| {
root.active_modals.pop(); root.active_modals.pop();
if root.active_modals.len() == 0 {
if let Some(top_modal) = root.active_modals.last() {
// Focus the next modal.
top_modal.focus_handle.focus(cx);
} else {
// Restore focus if there are no more modals.
root.focus_back(cx); root.focus_back(cx);
} }
cx.notify(); cx.notify();
@ -288,8 +293,6 @@ impl Root {
return None; return None;
} }
let modals_len = active_modals.len();
Some( Some(
div().children(active_modals.iter().enumerate().map(|(i, active_modal)| { div().children(active_modals.iter().enumerate().map(|(i, active_modal)| {
let mut modal = Modal::new(cx); let mut modal = Modal::new(cx);
@ -302,14 +305,6 @@ impl Root {
// So we keep the focus handle in the `active_modal`, this is owned by the `Root`. // So we keep the focus handle in the `active_modal`, this is owned by the `Root`.
modal.focus_handle = active_modal.focus_handle.clone(); modal.focus_handle = active_modal.focus_handle.clone();
// Focus to the top modal.
if i == modals_len - 1 {
// Check to avoid focus, when the modal is already focused.
if !modal.focus_handle.contains_focused(cx) {
cx.focus(&modal.focus_handle);
}
}
// Keep only have one overlay, we only render the first modal with overlay. // Keep only have one overlay, we only render the first modal with overlay.
if has_overlay { if has_overlay {
modal.overlay_visible = false; modal.overlay_visible = false;

View file

@ -85,7 +85,7 @@ pub trait StyledExt: Styled + Sized {
/// Render a 1px blue border, when if the element is focused /// Render a 1px blue border, when if the element is focused
fn debug_focused(self, focus_handle: &FocusHandle, cx: &WindowContext) -> Self { fn debug_focused(self, focus_handle: &FocusHandle, cx: &WindowContext) -> Self {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
if focus_handle.is_focused(cx) { if focus_handle.contains_focused(cx) {
self.debug_blue() self.debug_blue()
} else { } else {
self self

View file

@ -65,6 +65,7 @@ impl DatePicker {
cx.subscribe(&calendar, |this, _, ev: &CalendarEvent, cx| match ev { cx.subscribe(&calendar, |this, _, ev: &CalendarEvent, cx| match ev {
CalendarEvent::Selected(date) => { CalendarEvent::Selected(date) => {
this.update_date(*date, true, cx); this.update_date(*date, true, cx);
this.focus_handle.focus(cx);
} }
}) })
.detach(); .detach();
@ -138,6 +139,7 @@ impl DatePicker {
fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) { fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
self.open = false; self.open = false;
self.focus_handle.focus(cx);
cx.notify(); cx.notify();
} }
@ -195,7 +197,7 @@ impl Render for DatePicker {
.id(self.id.clone()) .id(self.id.clone())
.key_context("DatePicker") .key_context("DatePicker")
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.on_action(cx.listener(Self::escape)) .when(self.open, |this| this.on_action(cx.listener(Self::escape)))
.w_full() .w_full()
.relative() .relative()
.map(|this| match self.width { .map(|this| match self.width {