use hikari_palette::classes::{ClassesBuilder, Display, Position, SelectClass};
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use crate::platform;
use crate::{
feedback::{Glow, GlowProps},
portal::{
PortalEntry, PortalMaskMode, PortalPositionStrategy, TriggerPlacement, generate_portal_id,
use_portal,
},
prelude::*,
styled::StyledComponent,
};
pub struct SelectComponent;
#[derive(Clone, Copy, PartialEq, Debug, Default)]
pub enum SelectSize {
#[default]
Md,
Sm,
Lg,
}
#[define_props]
pub struct SelectOption {
#[default]
pub label: String,
#[default]
pub value: String,
}
#[define_props]
pub struct SelectProps {
#[default]
pub options: Vec<SelectOption>,
#[default]
pub placeholder: Option<String>,
#[default]
pub size: SelectSize,
#[default]
pub disabled: bool,
#[default]
pub class: String,
#[default]
pub value: Option<String>,
#[default]
pub on_change: Option<EventHandler<String>>,
}
#[component]
pub fn Select(props: SelectProps) -> Element {
let portal = use_portal();
let internal_value = use_signal(|| props.value.clone().unwrap_or_default());
let dropdown_id = use_signal(String::new);
let open = use_signal(|| false);
let controlled_value = props.value.clone();
let internal_value_for_effect = internal_value.clone();
use_effect(move || {
if let Some(ref v) = controlled_value {
internal_value_for_effect.set(v.clone());
}
});
let entries = portal.entries.clone();
let dropdown_id_sync = dropdown_id.clone();
let open_sync = open.clone();
use_effect(move || {
let current_id = dropdown_id_sync.read();
if !current_id.is_empty() {
let is_in_entries = entries.read().iter().any(|entry| {
if let PortalEntry::Dropdown { id, .. } = entry {
id == &*current_id
} else {
false
}
});
open_sync.set(is_in_entries);
}
});
let current_value = internal_value.get();
let selected_label = props
.options
.iter()
.find(|o| o.value == current_value)
.map(|o| o.label.clone());
let size_class = match props.size {
SelectSize::Sm => SelectClass::Sm,
SelectSize::Md => SelectClass::Md,
SelectSize::Lg => SelectClass::Lg,
};
let open_for_classes = open.clone();
let trigger_classes = ClassesBuilder::new()
.add(SelectClass::SelectTrigger)
.add(size_class)
.add_if(SelectClass::Disabled, || props.disabled)
.add_if(SelectClass::Open, move || open_for_classes.get())
.add_raw(&props.class)
.build();
let options_for_menu = props.options.clone();
let on_change = props.on_change.clone();
let open_for_click = open.clone();
let dropdown_id_for_click = dropdown_id.clone();
let portal_remove = portal.remove_entry.clone();
let portal_add = portal.add_entry.clone();
let internal_value_for_click = internal_value.clone();
let dropdown_id_for_click2 = dropdown_id.clone();
let handle_trigger_click = move |e: MouseEvent| {
e.stop_propagation();
if props.disabled {
return;
}
let current_open = open_for_click.get();
if current_open {
let id = dropdown_id_for_click.get();
if !id.is_empty() {
portal_remove.call(id);
}
} else {
let id = generate_portal_id();
dropdown_id_for_click.set(id.clone());
let trigger_rect_opt = {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
{
if let Some(target_el) =
platform::get_target_element_from_event(e.client_x, e.client_y)
{
platform::get_bounding_rect_by_class_impl("hi-select-trigger", &target_el)
.map(|rect| (rect.x, rect.y, rect.width, rect.height))
} else {
None
}
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
{
None::<(f64, f64, f64, f64)>
}
};
let opts = options_for_menu.clone();
let portal_inner = portal_remove.clone();
let id_inner = id.clone();
let internal_value_clone2 = internal_value_for_click.clone();
let menu_content = rsx! {
div {
class: "hi-select-dropdown",
style: if let Some((_, _, w, _)) = trigger_rect_opt { format!("width: {w}px;") } else { String::new() },
for opt in opts.iter() {
{
let value = opt.value.clone();
let label = opt.label.clone();
let is_selected = value == *internal_value_clone2.read();
let value_for_click = value.clone();
let on_change_item = on_change.clone();
let portal_close = portal_inner.clone();
let id_close = id_inner.clone();
let internal_value_clone3 = internal_value_for_click.clone();
let click_handler = EventHandler::new(move |e: MouseEvent| {
e.stop_propagation();
internal_value_clone3.set(value_for_click.clone());
if let Some(handler) = on_change_item.as_ref() {
handler.call(value_for_click.clone());
}
portal_close.call(id_close.clone());
});
rsx! {
Glow {
block: true,
blur: crate::GlowBlur::Light,
intensity: crate::GlowIntensity::Soft,
div {
class: if is_selected { "hi-select-option hi-select-option-selected" } else { "hi-select-option" },
onclick: click_handler,
"{label}"
}
}
}
}
}
}
};
let entry = PortalEntry::Dropdown {
id,
strategy: PortalPositionStrategy::TriggerBased {
placement: TriggerPlacement::BottomLeft,
},
mask_mode: PortalMaskMode::Transparent,
children: menu_content,
trigger_rect: trigger_rect_opt,
close_on_select: true,
};
portal_add.call(entry);
}
};
let wrapper_classes = ClassesBuilder::new()
.add(Position::Relative)
.add(Display::InlineBlock)
.build();
rsx! {
div { class: wrapper_classes,
div { class: trigger_classes, onclick: handle_trigger_click,
span { class: if selected_label.is_some() { "hi-select-value" } else { "hi-select-placeholder" },
"{if let Some(label) = &selected_label { label.clone() } else { props.placeholder.clone().unwrap_or_else(|| \"请选择\".to_string()) }}"
}
span {
class: "hi-select-arrow",
Icon { icon: MdiIcon::ChevronDown, size: 16, class: String::new(), color: String::new() }
}
}
}
}
}
impl StyledComponent for SelectComponent {
fn styles() -> &'static str {
include_str!(concat!(env!("OUT_DIR"), "/styles/select.css"))
}
fn name() -> &'static str {
"select"
}
}