use crate::utils;
use dioxus::prelude::*;
const CHECKBOX_CSS: &str = include_str!("./checkbox.css");
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum CheckedState {
Checked,
Unchecked,
Indeterminate,
}
impl CheckedState {
pub fn to_bool(&self) -> bool {
matches!(self, CheckedState::Checked)
}
pub fn toggle(&self) -> Self {
match self {
CheckedState::Indeterminate => CheckedState::Checked,
CheckedState::Checked => CheckedState::Unchecked,
CheckedState::Unchecked => CheckedState::Checked,
}
}
pub fn data_state(&self) -> &'static str {
match self {
CheckedState::Checked => "checked",
CheckedState::Unchecked => "unchecked",
CheckedState::Indeterminate => "indeterminate",
}
}
pub fn is_indeterminate(&self) -> bool {
matches!(self, CheckedState::Indeterminate)
}
}
#[derive(Clone)]
pub struct CheckboxContext {
pub checked: Signal<CheckedState>,
pub disabled: bool,
pub required: bool,
pub name: Option<String>,
pub form: Option<String>,
pub value: String,
pub has_consumer_stopped_propagation: Signal<bool>,
pub is_form_control: bool,
}
#[derive(Props, Clone, PartialEq)]
pub struct CheckboxProviderProps {
#[props(optional)]
pub checked: Option<CheckedState>,
#[props(default = CheckedState::Unchecked)]
pub default_checked: CheckedState,
#[props(optional)]
pub onchange: Option<EventHandler<CheckedState>>,
#[props(default = false)]
pub disabled: bool,
#[props(default = false)]
pub required: bool,
#[props(optional)]
pub name: Option<String>,
#[props(optional)]
pub form: Option<String>,
#[props(default = "on".to_string())]
pub value: String,
pub children: Element,
}
#[component]
pub fn CheckboxProvider(props: CheckboxProviderProps) -> Element {
let mut checked = use_signal(|| props.default_checked);
let has_consumer_stopped_propagation = use_signal(|| false);
if let Some(controlled) = props.checked {
checked.set(controlled);
}
let is_form_control = props.form.is_some() || true;
use_context_provider(|| CheckboxContext {
checked,
disabled: props.disabled,
required: props.required,
name: props.name.clone(),
form: props.form.clone(),
value: props.value.clone(),
has_consumer_stopped_propagation,
is_form_control,
});
rsx! {
style { {CHECKBOX_CSS} }
{props.children}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct CheckboxTriggerProps {
#[props(optional)]
pub class: Option<String>,
#[props(optional)]
pub id: Option<String>,
#[props(optional)]
pub onclick: Option<EventHandler<MouseEvent>>,
#[props(optional)]
pub onkeydown: Option<EventHandler<KeyboardEvent>>,
pub children: Element,
}
#[component]
pub fn CheckboxTrigger(props: CheckboxTriggerProps) -> Element {
let context = use_context::<CheckboxContext>();
let mut checked = context.checked;
let _initial_checked = use_signal(|| checked());
let form_id = context.form.clone();
use_effect(move || {
if let Some(_form_id) = &form_id {
}
});
let handle_click = move |evt: MouseEvent| {
let new_state = checked().toggle();
checked.set(new_state);
if let Some(handler) = &props.onclick {
handler.call(evt);
}
if context.is_form_control {
}
};
let handle_keydown = move |evt: KeyboardEvent| {
if evt.key() == Key::Enter {
evt.prevent_default();
}
if let Some(handler) = &props.onkeydown {
handler.call(evt);
}
};
let class_name = utils::cn(vec![
Some("peer inline-flex h-4 w-4 shrink-0 items-center justify-center border border-primary rounded bg-background ring-offset-background cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"),
props.class.as_deref(),
]);
let checked_state = checked();
rsx! {
button {
r#type: "button",
role: "checkbox",
id: props.id.as_deref(),
class: "{class_name}",
disabled: context.disabled,
"aria-checked": if checked_state.is_indeterminate() {
"mixed"
} else if checked_state.to_bool() {
"true"
} else {
"false"
},
"aria-required": if context.required { "true" } else { "false" },
"data-state": "{checked_state.data_state()}",
"data-disabled": if context.disabled { "" } else { "" },
onclick: handle_click,
onkeydown: handle_keydown,
{props.children}
}
if context.is_form_control {
CheckboxBubbleInput {}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct CheckboxIndicatorProps {
#[props(optional)]
pub class: Option<String>,
#[props(default = false)]
pub force_mount: bool,
#[props(default)]
pub children: Element,
}
#[component]
pub fn CheckboxIndicator(props: CheckboxIndicatorProps) -> Element {
let context = use_context::<CheckboxContext>();
let class_name = utils::cn(vec![
Some("flex items-center justify-center text-current pointer-events-none"),
props.class.as_deref(),
]);
let checked_state = *context.checked.read();
let should_render = props.force_mount
|| checked_state == CheckedState::Checked
|| checked_state == CheckedState::Indeterminate;
if should_render {
rsx! {
span {
class: "{class_name}",
"data-state": "{checked_state.data_state()}",
"data-disabled": if context.disabled { "" } else { "" },
style: "pointer-events: none;",
if checked_state == CheckedState::Checked {
svg {
class: "size-3.5",
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
view_box: "0 0 24 24",
fill: "none",
stroke: "currentColor",
stroke_width: "2",
stroke_linecap: "round",
stroke_linejoin: "round",
path { d: "M20 6 9 17l-5-5" }
}
} else if checked_state == CheckedState::Indeterminate {
svg {
class: "size-3.5",
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
view_box: "0 0 24 24",
fill: "none",
stroke: "currentColor",
stroke_width: "2",
stroke_linecap: "round",
stroke_linejoin: "round",
line { x1: "5", y1: "12", x2: "19", y2: "12" }
}
}
{props.children}
}
}
} else {
rsx! {}
}
}
#[component]
pub fn CheckboxBubbleInput() -> Element {
let context = use_context::<CheckboxContext>();
let checked_state = *context.checked.read();
rsx! {
input {
r#type: "checkbox",
"aria-hidden": "true",
checked: checked_state.to_bool(),
required: context.required,
disabled: context.disabled,
name: context.name.as_deref(),
value: "{context.value}",
form: context.form.as_deref(),
tabindex: -1,
style: "position: absolute; pointer-events: none; opacity: 0; margin: 0; transform: translateX(-100%);",
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct CheckboxProps {
#[props(optional)]
pub checked: Option<CheckedState>,
#[props(default = CheckedState::Unchecked)]
pub default_checked: CheckedState,
#[props(optional)]
pub onchange: Option<EventHandler<CheckedState>>,
#[props(default = false)]
pub disabled: bool,
#[props(default = false)]
pub required: bool,
#[props(optional)]
pub name: Option<String>,
#[props(optional)]
pub form: Option<String>,
#[props(optional)]
pub id: Option<String>,
#[props(default = "on".to_string())]
pub value: String,
#[props(optional)]
pub class: Option<String>,
#[props(default)]
pub children: Element,
}
#[component]
pub fn Checkbox(props: CheckboxProps) -> Element {
rsx! {
CheckboxProvider {
checked: props.checked,
default_checked: props.default_checked,
onchange: props.onchange,
disabled: props.disabled,
required: props.required,
name: props.name.clone(),
form: props.form.clone(),
value: props.value.clone(),
CheckboxTrigger {
id: props.id.clone(),
class: props.class.clone(),
{props.children}
}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct CheckboxLabelProps {
#[props(default)]
pub children: Element,
#[props(optional)]
pub for_id: Option<String>,
#[props(optional)]
pub class: Option<String>,
}
#[component]
pub fn CheckboxLabel(props: CheckboxLabelProps) -> Element {
let class_name = utils::cn(vec![
Some("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"),
props.class.as_deref(),
]);
rsx! {
label {
class: "{class_name}",
r#for: props.for_id.as_deref(),
{props.children}
}
}
}