chore: audit components
This commit is contained in:
+105
-41
@@ -2,12 +2,13 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
App, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role,
|
||||
SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Window,
|
||||
App, ElementId, InteractiveElement as _, IntoElement, KeyDownEvent, ParentElement as _,
|
||||
RenderOnce, Role, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _,
|
||||
Window, canvas,
|
||||
};
|
||||
use gpui_native_icons::{Icon, IconRole, icon};
|
||||
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
|
||||
use gpui_native_utils::focus_ring;
|
||||
use gpui_native_utils::{focus_handle, focus_ring};
|
||||
|
||||
use crate::common::{Sizable, StyledExt, h_flex};
|
||||
use crate::menu::{Menu, MenuItem};
|
||||
@@ -17,6 +18,7 @@ pub type SelectHandler<T> = Rc<dyn Fn(Option<T>, &mut Window, &mut App)>;
|
||||
|
||||
/// Select option for a [`Select`] control.
|
||||
#[derive(Clone)]
|
||||
#[must_use]
|
||||
pub struct SelectOption<T> {
|
||||
value: T,
|
||||
label: SharedString,
|
||||
@@ -61,6 +63,7 @@ where
|
||||
|
||||
/// A control that shows the chosen value and opens a menu of the choices.
|
||||
#[derive(IntoElement)]
|
||||
#[must_use]
|
||||
pub struct Select<T>
|
||||
where
|
||||
T: Clone + PartialEq + 'static,
|
||||
@@ -107,7 +110,10 @@ where
|
||||
}
|
||||
|
||||
/// Sets the current value. `None` shows the placeholder.
|
||||
pub fn selected(mut self, selected: impl Into<Option<T>>) -> Self {
|
||||
pub fn selected<S>(mut self, selected: S) -> Self
|
||||
where
|
||||
S: Into<Option<T>>,
|
||||
{
|
||||
self.selected = selected.into();
|
||||
self
|
||||
}
|
||||
@@ -187,6 +193,16 @@ where
|
||||
.clone()
|
||||
});
|
||||
|
||||
let focus = focus_handle(&self.id, window, cx);
|
||||
|
||||
// Set when the popup closes without an activation; the trigger claims
|
||||
// focus at paint, leaving keyboard activations to drop focus in place.
|
||||
let refocus_pending = window.with_element_namespace(self.id.clone(), |window| {
|
||||
window
|
||||
.use_keyed_state("select-refocus", cx, |_, _| false)
|
||||
.clone()
|
||||
});
|
||||
|
||||
let is_open = *open.read(cx);
|
||||
let open_for_items = open.clone();
|
||||
|
||||
@@ -220,54 +236,94 @@ where
|
||||
let trigger_label = label.clone();
|
||||
let trigger_style = self.style;
|
||||
|
||||
let trigger = move |_window: &mut Window, _cx: &mut App| {
|
||||
h_flex()
|
||||
.id((trigger_id.clone(), "select-trigger"))
|
||||
.debug_selector(|| "select-trigger".to_string())
|
||||
.role(Role::ComboBox)
|
||||
.aria_label(trigger_label.clone())
|
||||
.cursor_pointer()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.h(metrics.input_height + BORDER_WIDTH * 2.0)
|
||||
.px(metrics.dense_padding_x)
|
||||
.rounded(radius)
|
||||
.border_1()
|
||||
.border_color(rest_border)
|
||||
.bg(input)
|
||||
.text_size(metrics.text_size)
|
||||
.text_color(if trigger_has_value {
|
||||
foreground
|
||||
} else {
|
||||
muted_foreground
|
||||
})
|
||||
.when(is_open && focus_ring_on, |trigger| {
|
||||
trigger.shadow(focus_ring(ring))
|
||||
})
|
||||
.hover(|style| style.bg(muted).border_color(hover_border))
|
||||
.child(trigger_text.clone())
|
||||
.child(
|
||||
icon(IconRole::ChevronDown)
|
||||
.size(icon_size)
|
||||
.color(muted_foreground),
|
||||
)
|
||||
.refine_style(&trigger_style)
|
||||
.into_any_element()
|
||||
let trigger = {
|
||||
let open = open.clone();
|
||||
let focus = focus.clone();
|
||||
let refocus_for_trigger = refocus_pending.clone();
|
||||
move |_window: &mut Window, _cx: &mut App| {
|
||||
h_flex()
|
||||
.id((trigger_id.clone(), "select-trigger"))
|
||||
.debug_selector(|| "select-trigger".to_string())
|
||||
.role(Role::ComboBox)
|
||||
.aria_label(trigger_label.clone())
|
||||
.aria_expanded(is_open)
|
||||
.track_focus(&focus)
|
||||
.tab_stop(true)
|
||||
.focus_visible(move |style| style.shadow(focus_ring(ring)))
|
||||
.on_key_down({
|
||||
let open = open.clone();
|
||||
move |event: &KeyDownEvent, _window, cx| {
|
||||
if !matches!(event.keystroke.key.as_str(), "enter" | "space") {
|
||||
return;
|
||||
}
|
||||
|
||||
open.update(cx, |open, _| *open = !*open);
|
||||
cx.stop_propagation();
|
||||
}
|
||||
})
|
||||
.cursor_pointer()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.h(metrics.input_height + BORDER_WIDTH * 2.0)
|
||||
.px(metrics.dense_padding_x)
|
||||
.rounded(radius)
|
||||
.border_1()
|
||||
.border_color(rest_border)
|
||||
.bg(input)
|
||||
.text_size(metrics.text_size)
|
||||
.text_color(if trigger_has_value {
|
||||
foreground
|
||||
} else {
|
||||
muted_foreground
|
||||
})
|
||||
.when(is_open && focus_ring_on, |trigger| {
|
||||
trigger.shadow(focus_ring(ring))
|
||||
})
|
||||
.hover(|style| style.bg(muted).border_color(hover_border))
|
||||
.child(trigger_text.clone())
|
||||
.child(
|
||||
icon(IconRole::ChevronDown)
|
||||
.size(icon_size)
|
||||
.color(muted_foreground),
|
||||
)
|
||||
.child(
|
||||
canvas(|_, _, _| {}, {
|
||||
let focus = focus.clone();
|
||||
let refocus_pending = refocus_for_trigger.clone();
|
||||
move |_, _, window, cx| {
|
||||
if *refocus_pending.read(cx) {
|
||||
refocus_pending.update(cx, |pending, _| *pending = false);
|
||||
window.focus(&focus, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.absolute()
|
||||
.inset_0(),
|
||||
)
|
||||
.refine_style(&trigger_style)
|
||||
.into_any_element()
|
||||
}
|
||||
};
|
||||
|
||||
let select_id = self.id.clone();
|
||||
let refocus_for_items = refocus_pending.clone();
|
||||
let items = move |_window: &mut Window, _cx: &mut App| {
|
||||
self.options
|
||||
.iter()
|
||||
.map(|option| {
|
||||
.enumerate()
|
||||
.map(|(index, option)| {
|
||||
let is_selected = self.selected.as_ref() == Some(&option.value);
|
||||
|
||||
let handler = {
|
||||
let open = open_for_items.clone();
|
||||
let refocus_pending = refocus_for_items.clone();
|
||||
let value = option.value.clone();
|
||||
let on_change = self.on_change.clone();
|
||||
move |window: &mut Window, cx: &mut App| {
|
||||
open.update(cx, |open, _| *open = false);
|
||||
// Activations own the close, so no focus returns to the trigger.
|
||||
refocus_pending.update(cx, |pending, _| *pending = false);
|
||||
if let Some(on_change) = &on_change {
|
||||
on_change(Some(value.clone()), window, cx);
|
||||
}
|
||||
@@ -275,6 +331,7 @@ where
|
||||
};
|
||||
|
||||
MenuItem::new(option.label.clone())
|
||||
.id((select_id.clone(), format!("option-{index}")))
|
||||
.when_some(option.icon.clone(), |row, icon| row.icon(icon))
|
||||
.checked(is_selected)
|
||||
.handler(handler)
|
||||
@@ -284,8 +341,15 @@ where
|
||||
|
||||
let menu = Menu::new(self.id)
|
||||
.open(is_open)
|
||||
.on_change(move |next, _window, cx| {
|
||||
open.update(cx, |open, _| *open = next);
|
||||
.on_change({
|
||||
let open = open.clone();
|
||||
let refocus_pending = refocus_pending.clone();
|
||||
move |next, _window, cx| {
|
||||
if !next {
|
||||
refocus_pending.update(cx, |pending, _| *pending = true);
|
||||
}
|
||||
open.update(cx, |open, _| *open = next);
|
||||
}
|
||||
})
|
||||
.trigger(trigger)
|
||||
.items(items);
|
||||
|
||||
Reference in New Issue
Block a user