use crate::{
core::{
assets,
attrs::{availability_attributes, component_attributes},
platform::DomBridge,
},
prelude::*,
theme::use_stylesheets,
};
const CHECK: Bem = Bem::new("fc-check");
const GROUP: Bem = Bem::new("fc-check-group");
const SWITCH: Bem = Bem::new("fc-switch");
const ROW: Bem = Bem::new("fc-switch-row");
fc_modifier! { enum GroupFlow for GROUP { Stacked => None, Inline => "inline" } }
fc_modifier! {
pub enum SwitchSize for SWITCH {
Md => None,
Sm => "sm",
Lg => "lg",
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CheckState {
#[default]
Off,
On,
Mixed,
}
fn stylesheet() -> Element {
rsx! { if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_CHECK_CSS } } }
}
#[component]
pub fn Checkbox(
label: Element,
#[props(default)] hint: Option<Element>,
#[props(default)] checked: Option<ReadSignal<CheckState>>,
#[props(default)] default_checked: CheckState,
#[props(default)] onchange: Option<EventHandler<bool>>,
#[props(default)] invalid: bool,
#[props(default)] availability: Availability,
#[props(default)] class: Option<String>,
#[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
) -> Element {
let state = use_controllable(
checked,
|| default_checked,
Some(EventHandler::new(move |next: CheckState| {
if let Some(onchange) = onchange {
onchange.call(next == CheckState::On);
}
})),
);
let id = use_id();
let current = state.get();
let mixed = current == CheckState::Mixed;
let effect_id = id.clone();
use_effect(use_reactive!(|mixed| {
let id = effect_id.clone();
spawn(async move {
let _ = DomBridge.set_indeterminate(&id, mixed).await;
});
}));
let blocked = availability != Availability::Enabled;
let attributes = component_attributes(
attributes,
ClassList::new(CHECK.block()).extend(class.as_deref()),
&[],
);
let state_attrs = availability_attributes(&availability);
rsx! {
{stylesheet()}
label { ..attributes,
input { id: "{id}", class: CHECK.element("input"), r#type: "checkbox",
checked: current == CheckState::On,
aria_invalid: invalid.then_some("true"),
onclick: move |event| if blocked { event.prevent_default(); },
onchange: move |event| if !blocked {
state.set(if event.checked() { CheckState::On } else { CheckState::Off });
},
..state_attrs,
}
span { class: CHECK.element("label"), {label} }
if let Some(hint) = hint { span { class: CHECK.element("hint"), {hint} } }
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RadioChoice {
pub name: Id,
pub value: Option<String>,
pub onchange: Option<EventHandler<String>>,
}
#[component]
pub fn CheckGroup(
legend: Element,
#[props(default)] inline: bool,
#[props(default)] radios: Option<RadioChoice>,
#[props(default)] class: Option<String>,
#[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
children: Element,
) -> Element {
let mut context = use_context_provider(|| Signal::new(radios.clone()));
if *context.peek() != radios {
context.set(radios);
}
let attributes = component_attributes(
attributes,
ClassList::new(GROUP.block())
.with_modifier(
GROUP,
if inline {
GroupFlow::Inline
} else {
GroupFlow::Stacked
},
)
.extend(class.as_deref()),
&[],
);
rsx! {
{stylesheet()}
fieldset { ..attributes,
legend { class: GROUP.element("legend"), {legend} }
{children}
}
}
}
#[component]
pub fn RadioScope(radios: RadioChoice, children: Element) -> Element {
let mut context = use_context_provider(|| Signal::new(Some(radios.clone())));
if context.peek().as_ref() != Some(&radios) {
context.set(Some(radios));
}
rsx! { {children} }
}
#[component]
pub fn Radio(
label: Element,
value: String,
#[props(default)] hint: Option<Element>,
#[props(default)] availability: Availability,
#[props(default)] class: Option<String>,
#[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
) -> Element {
let choice = try_consume_context::<Signal<Option<RadioChoice>>>()
.and_then(|context| context.read().clone());
let name = choice.as_ref().map(|c| c.name.to_string());
let checked = choice
.as_ref()
.is_some_and(|c| c.value.as_deref() == Some(value.as_str()));
let onchange = choice.and_then(|c| availability.guard(c.onchange));
let blocked = availability != Availability::Enabled;
let attributes = component_attributes(
attributes,
ClassList::new(CHECK.block()).extend(class.as_deref()),
&[],
);
let state_attrs = availability_attributes(&availability);
rsx! {
{stylesheet()}
label { ..attributes,
input { class: CHECK.element("input"), r#type: "radio", name, value: value.clone(), checked,
onclick: move |event| if blocked { event.prevent_default(); },
onchange: move |_| if let Some(onchange) = onchange { onchange.call(value.clone()); },
..state_attrs,
}
span { class: CHECK.element("label"), {label} }
if let Some(hint) = hint { span { class: CHECK.element("hint"), {hint} } }
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ControlName {
Named(AccessibleName),
Wrapped,
}
impl From<AccessibleName> for ControlName {
fn from(name: AccessibleName) -> Self {
Self::Named(name)
}
}
#[component]
fn SwitchInput(
label: ControlName,
size: SwitchSize,
checked: bool,
onchange: EventHandler<bool>,
invalid: bool,
availability: Availability,
class: Option<String>,
attributes: Vec<Attribute>,
) -> Element {
let blocked = availability != Availability::Enabled;
let mut attributes = component_attributes(
attributes,
ClassList::new(SWITCH.block())
.with_modifier(SWITCH, size)
.extend(class.as_deref()),
&["type", "role", "checked", "aria-label"],
);
attributes.extend(availability_attributes(&availability));
rsx! {
input { r#type: "checkbox", role: "switch", checked,
aria_label: match label { ControlName::Named(name) => Some(name.to_string()), ControlName::Wrapped => None },
aria_invalid: invalid.then_some("true"),
onclick: move |event| if blocked { event.prevent_default(); },
onchange: move |event| if !blocked { onchange.call(event.checked()); },
..attributes,
}
}
}
#[component]
pub fn Switch(
#[props(into)] label: ControlName,
#[props(default)] size: SwitchSize,
#[props(default)] checked: Option<ReadSignal<bool>>,
#[props(default)] default_checked: bool,
#[props(default)] onchange: Option<EventHandler<bool>>,
#[props(default)] invalid: bool,
#[props(default)] availability: Availability,
#[props(default)] class: Option<String>,
#[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
) -> Element {
let state = use_controllable(checked, || default_checked, onchange);
rsx! {
{stylesheet()}
SwitchInput { label, size, checked: state.get(),
onchange: move |value| state.set(value), invalid, availability, class, attributes }
}
}
#[component]
pub fn SwitchRow(
label: Element,
#[props(default)] hint: Option<Element>,
#[props(default)] size: SwitchSize,
#[props(default)] checked: Option<ReadSignal<bool>>,
#[props(default)] default_checked: bool,
#[props(default)] onchange: Option<EventHandler<bool>>,
#[props(default)] availability: Availability,
#[props(default)] class: Option<String>,
#[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
) -> Element {
let state = use_controllable(checked, || default_checked, onchange);
let attributes = component_attributes(
attributes,
ClassList::new(ROW.block()).extend(class.as_deref()),
&[],
);
rsx! {
{stylesheet()}
label { ..attributes,
span { class: ROW.element("text"),
span { class: ROW.element("label"), {label} }
if let Some(hint) = hint { span { class: ROW.element("hint"), {hint} } }
}
SwitchInput { label: ControlName::Wrapped, size, checked: state.get(), onchange: move |value| state.set(value),
invalid: false, availability, class: None, attributes: Vec::new() }
}
}
}