use dioxus::prelude::*;
use crate::a11y::ControlSize;
use crate::icon::{Icon, IconKind};
use crate::util::cx;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ChipVariant {
#[default]
Filled,
Outline,
Light,
Subtle,
}
#[derive(Clone, Copy, PartialEq)]
pub struct ChipGroupContext {
pub values: Signal<Vec<String>>,
pub multiple: bool,
pub size: Option<ControlSize>,
pub variant: Option<ChipVariant>,
pub on_change: Option<EventHandler<Vec<String>>>,
}
#[derive(Props, Clone, PartialEq)]
pub struct ChipProps {
#[props(default)]
pub checked: Option<Signal<bool>>,
#[props(default = false)]
pub default_checked: bool,
#[props(default)]
pub value: Option<String>,
#[props(default = ChipVariant::Filled)]
pub variant: ChipVariant,
#[props(default = ControlSize::Md)]
pub size: ControlSize,
#[props(default)]
pub icon: Option<IconKind>,
#[props(default = true)]
pub show_check: bool,
#[props(default = false)]
pub disabled: bool,
#[props(default)]
pub class: Option<String>,
pub children: Element,
#[props(default)]
pub on_click: Option<EventHandler<MouseEvent>>,
#[props(default)]
pub on_toggle: Option<EventHandler<bool>>,
}
#[derive(Props, Clone, PartialEq)]
pub struct ChipGroupProps {
pub value: Signal<Vec<String>>,
#[props(default = false)]
pub multiple: bool,
#[props(default)]
pub size: Option<ControlSize>,
#[props(default)]
pub variant: Option<ChipVariant>,
#[props(default)]
pub class: Option<String>,
pub children: Element,
#[props(default)]
pub on_change: Option<EventHandler<Vec<String>>>,
}
#[component]
pub fn ChipGroup(props: ChipGroupProps) -> Element {
use_context_provider(|| ChipGroupContext {
values: props.value,
multiple: props.multiple,
size: props.size,
variant: props.variant,
on_change: props.on_change,
});
let container_class = cx([
"sd-chip-group",
props.class.as_deref().unwrap_or(""),
]);
rsx! {
div {
class: "{container_class}",
role: "group",
aria_multiselectable: if props.multiple { "true" } else { "false" },
{props.children}
}
}
}
#[component]
pub fn Chip(props: ChipProps) -> Element {
let group_ctx = try_use_context::<ChipGroupContext>();
let mut local_checked = use_signal(|| props.default_checked);
#[cfg(feature = "theme")]
let theme = crate::theme::use_theme();
#[cfg(feature = "theme")]
let radius_class = theme.read().radius_scale.md();
#[cfg(not(feature = "theme"))]
let radius_class = "rounded-full";
let effective_size = group_ctx
.and_then(|c| c.size)
.unwrap_or(props.size);
let effective_variant = group_ctx
.and_then(|c| c.variant)
.unwrap_or(props.variant);
let is_checked = if let (Some(ctx), Some(val)) = (group_ctx, &props.value) {
ctx.values.read().contains(val)
} else if let Some(sig) = props.checked {
*sig.read()
} else {
*local_checked.read()
};
let size_modifier = match effective_size {
ControlSize::Sm => "sd-chip--sm",
ControlSize::Md => "sd-chip--md",
ControlSize::Lg => "sd-chip--lg",
};
let variant_modifier = match effective_variant {
ChipVariant::Filled => "sd-chip--filled",
ChipVariant::Outline => "sd-chip--outline",
ChipVariant::Light => "sd-chip--light",
ChipVariant::Subtle => "sd-chip--subtle",
};
let checked_modifier = if is_checked { "sd-chip--checked" } else { "" };
let disabled_modifier = if props.disabled { "sd-chip--disabled" } else { "" };
let chip_class = cx([
"sd-chip",
size_modifier,
variant_modifier,
checked_modifier,
disabled_modifier,
radius_class,
props.class.as_deref().unwrap_or(""),
]);
let icon_size = match effective_size {
ControlSize::Sm => 12,
ControlSize::Md => 14,
ControlSize::Lg => 16,
};
let val_opt = props.value.clone();
let on_click_prop = props.on_click;
let on_toggle_prop = props.on_toggle;
let checked_sig = props.checked;
let handle_click = move |evt: MouseEvent| {
if props.disabled {
return;
}
if let Some(handler) = on_click_prop.as_ref() {
handler.call(evt);
}
if let (Some(mut ctx), Some(val)) = (group_ctx, &val_opt) {
let mut list = ctx.values.read().clone();
if ctx.multiple {
if let Some(pos) = list.iter().position(|x| x == val) {
list.remove(pos);
} else {
list.push(val.clone());
}
} else {
list = vec![val.clone()];
}
ctx.values.set(list.clone());
if let Some(handler) = ctx.on_change.as_ref() {
handler.call(list);
}
} else if let Some(mut sig) = checked_sig {
let next = !is_checked;
sig.set(next);
if let Some(handler) = on_toggle_prop.as_ref() {
handler.call(next);
}
} else {
let next = !is_checked;
local_checked.set(next);
if let Some(handler) = on_toggle_prop.as_ref() {
handler.call(next);
}
}
};
let role = if let Some(ctx) = group_ctx {
if ctx.multiple { "checkbox" } else { "radio" }
} else {
"checkbox"
};
rsx! {
button {
r#type: "button",
class: "{chip_class}",
role: "{role}",
aria_checked: if is_checked { "true" } else { "false" },
aria_disabled: if props.disabled { "true" } else { "false" },
disabled: props.disabled,
onclick: handle_click,
if is_checked && props.show_check {
span { class: "sd-chip__icon sd-chip__icon--check",
Icon { kind: IconKind::Check, size: icon_size }
}
} else if let Some(ic) = props.icon {
span { class: "sd-chip__icon",
Icon { kind: ic, size: icon_size }
}
}
span { class: "sd-chip__label",
{props.children}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chip_variant_default() {
assert_eq!(ChipVariant::default(), ChipVariant::Filled);
}
#[test]
fn test_chip_control_size_modifiers() {
assert_eq!(ControlSize::Sm.class_modifier(), "--sm");
assert_eq!(ControlSize::Md.class_modifier(), "--md");
assert_eq!(ControlSize::Lg.class_modifier(), "--lg");
}
#[test]
fn test_chip_group_selection_logic() {
let mut selected = vec!["a".to_string(), "b".to_string()];
let item = "a".to_string();
if let Some(pos) = selected.iter().position(|x| x == &item) {
selected.remove(pos);
} else {
selected.push(item);
}
assert_eq!(selected, vec!["b".to_string()]);
let item_a = "a".to_string();
if let Some(pos) = selected.iter().position(|x| x == &item_a) {
selected.remove(pos);
} else {
selected.push(item_a);
}
assert_eq!(selected, vec!["b".to_string(), "a".to_string()]);
let single_item = "c".to_string();
let single_selected = vec![single_item.clone()];
assert_eq!(single_selected, vec!["c".to_string()]);
}
}