1use crate::{
3 core::{
4 assets,
5 attrs::{availability_attributes, component_attributes},
6 platform::DomBridge,
7 },
8 prelude::*,
9 theme::use_stylesheets,
10};
11const CHECK: Bem = Bem::new("fc-check");
12const GROUP: Bem = Bem::new("fc-check-group");
13const SWITCH: Bem = Bem::new("fc-switch");
14const ROW: Bem = Bem::new("fc-switch-row");
15fc_modifier! { enum GroupFlow for GROUP { Stacked => None, Inline => "inline" } }
16fc_modifier! {
17 pub enum SwitchSize for SWITCH {
19 Md => None,
21 Sm => "sm",
23 Lg => "lg",
25 }
26}
27
28#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum CheckState {
31 #[default]
33 Off,
34 On,
36 Mixed,
38}
39
40fn stylesheet() -> Element {
41 rsx! { if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_CHECK_CSS } } }
42}
43
44#[component]
50pub fn Checkbox(
51 label: Element,
52 #[props(default)] hint: Option<Element>,
53 #[props(default)] checked: Option<ReadSignal<CheckState>>,
54 #[props(default)] default_checked: CheckState,
55 #[props(default)] onchange: Option<EventHandler<bool>>,
56 #[props(default)] invalid: bool,
57 #[props(default)] availability: Availability,
58 #[props(default)] class: Option<String>,
59 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
60) -> Element {
61 let state = use_controllable(
62 checked,
63 || default_checked,
64 Some(EventHandler::new(move |next: CheckState| {
65 if let Some(onchange) = onchange {
66 onchange.call(next == CheckState::On);
67 }
68 })),
69 );
70 let id = use_id();
71 let current = state.get();
72 let mixed = current == CheckState::Mixed;
73 let effect_id = id.clone();
74 use_effect(use_reactive!(|mixed| {
75 let id = effect_id.clone();
76 spawn(async move {
77 let _ = DomBridge.set_indeterminate(&id, mixed).await;
80 });
81 }));
82 let blocked = availability != Availability::Enabled;
83 let attributes = component_attributes(
84 attributes,
85 ClassList::new(CHECK.block()).extend(class.as_deref()),
86 &[],
87 );
88 let state_attrs = availability_attributes(&availability);
89 rsx! {
90 {stylesheet()}
91 label { ..attributes,
92 input { id: "{id}", class: CHECK.element("input"), r#type: "checkbox",
93 checked: current == CheckState::On,
94 aria_invalid: invalid.then_some("true"),
95 onclick: move |event| if blocked { event.prevent_default(); },
96 onchange: move |event| if !blocked {
97 state.set(if event.checked() { CheckState::On } else { CheckState::Off });
98 },
99 ..state_attrs,
100 }
101 span { class: CHECK.element("label"), {label} }
102 if let Some(hint) = hint { span { class: CHECK.element("hint"), {hint} } }
103 }
104 }
105}
106
107#[derive(Debug, Clone, PartialEq)]
109pub struct RadioChoice {
110 pub name: Id,
112 pub value: Option<String>,
114 pub onchange: Option<EventHandler<String>>,
116}
117
118#[component]
120pub fn CheckGroup(
121 legend: Element,
122 #[props(default)] inline: bool,
123 #[props(default)] radios: Option<RadioChoice>,
124 #[props(default)] class: Option<String>,
125 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
126 children: Element,
127) -> Element {
128 let mut context = use_context_provider(|| Signal::new(radios.clone()));
129 if *context.peek() != radios {
130 context.set(radios);
131 }
132 let attributes = component_attributes(
133 attributes,
134 ClassList::new(GROUP.block())
135 .with_modifier(
136 GROUP,
137 if inline {
138 GroupFlow::Inline
139 } else {
140 GroupFlow::Stacked
141 },
142 )
143 .extend(class.as_deref()),
144 &[],
145 );
146 rsx! {
147 {stylesheet()}
148 fieldset { ..attributes,
149 legend { class: GROUP.element("legend"), {legend} }
150 {children}
151 }
152 }
153}
154
155#[component]
160pub fn RadioScope(radios: RadioChoice, children: Element) -> Element {
161 let mut context = use_context_provider(|| Signal::new(Some(radios.clone())));
162 if context.peek().as_ref() != Some(&radios) {
163 context.set(Some(radios));
164 }
165 rsx! { {children} }
166}
167
168#[component]
170pub fn Radio(
171 label: Element,
172 value: String,
173 #[props(default)] hint: Option<Element>,
174 #[props(default)] availability: Availability,
175 #[props(default)] class: Option<String>,
176 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
177) -> Element {
178 let choice = try_consume_context::<Signal<Option<RadioChoice>>>()
179 .and_then(|context| context.read().clone());
180 let name = choice.as_ref().map(|c| c.name.to_string());
181 let checked = choice
182 .as_ref()
183 .is_some_and(|c| c.value.as_deref() == Some(value.as_str()));
184 let onchange = choice.and_then(|c| availability.guard(c.onchange));
185 let blocked = availability != Availability::Enabled;
186 let attributes = component_attributes(
187 attributes,
188 ClassList::new(CHECK.block()).extend(class.as_deref()),
189 &[],
190 );
191 let state_attrs = availability_attributes(&availability);
192 rsx! {
193 {stylesheet()}
194 label { ..attributes,
195 input { class: CHECK.element("input"), r#type: "radio", name, value: value.clone(), checked,
196 onclick: move |event| if blocked { event.prevent_default(); },
197 onchange: move |_| if let Some(onchange) = onchange { onchange.call(value.clone()); },
198 ..state_attrs,
199 }
200 span { class: CHECK.element("label"), {label} }
201 if let Some(hint) = hint { span { class: CHECK.element("hint"), {hint} } }
202 }
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq)]
208pub enum ControlName {
209 Named(AccessibleName),
211 Wrapped,
215}
216impl From<AccessibleName> for ControlName {
217 fn from(name: AccessibleName) -> Self {
218 Self::Named(name)
219 }
220}
221
222#[component]
223fn SwitchInput(
224 label: ControlName,
225 size: SwitchSize,
226 checked: bool,
227 onchange: EventHandler<bool>,
228 invalid: bool,
229 availability: Availability,
230 class: Option<String>,
231 attributes: Vec<Attribute>,
232) -> Element {
233 let blocked = availability != Availability::Enabled;
234 let mut attributes = component_attributes(
235 attributes,
236 ClassList::new(SWITCH.block())
237 .with_modifier(SWITCH, size)
238 .extend(class.as_deref()),
239 &["type", "role", "checked", "aria-label"],
240 );
241 attributes.extend(availability_attributes(&availability));
242 rsx! {
243 input { r#type: "checkbox", role: "switch", checked,
244 aria_label: match label { ControlName::Named(name) => Some(name.to_string()), ControlName::Wrapped => None },
245 aria_invalid: invalid.then_some("true"),
246 onclick: move |event| if blocked { event.prevent_default(); },
247 onchange: move |event| if !blocked { onchange.call(event.checked()); },
248 ..attributes,
249 }
250 }
251}
252
253#[component]
255pub fn Switch(
256 #[props(into)] label: ControlName,
257 #[props(default)] size: SwitchSize,
258 #[props(default)] checked: Option<ReadSignal<bool>>,
259 #[props(default)] default_checked: bool,
260 #[props(default)] onchange: Option<EventHandler<bool>>,
261 #[props(default)] invalid: bool,
262 #[props(default)] availability: Availability,
263 #[props(default)] class: Option<String>,
264 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
265) -> Element {
266 let state = use_controllable(checked, || default_checked, onchange);
267 rsx! {
268 {stylesheet()}
269 SwitchInput { label, size, checked: state.get(),
270 onchange: move |value| state.set(value), invalid, availability, class, attributes }
271 }
272}
273
274#[component]
276pub fn SwitchRow(
277 label: Element,
278 #[props(default)] hint: Option<Element>,
279 #[props(default)] size: SwitchSize,
280 #[props(default)] checked: Option<ReadSignal<bool>>,
281 #[props(default)] default_checked: bool,
282 #[props(default)] onchange: Option<EventHandler<bool>>,
283 #[props(default)] availability: Availability,
284 #[props(default)] class: Option<String>,
285 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
286) -> Element {
287 let state = use_controllable(checked, || default_checked, onchange);
288 let attributes = component_attributes(
289 attributes,
290 ClassList::new(ROW.block()).extend(class.as_deref()),
291 &[],
292 );
293 rsx! {
294 {stylesheet()}
295 label { ..attributes,
296 span { class: ROW.element("text"),
297 span { class: ROW.element("label"), {label} }
298 if let Some(hint) = hint { span { class: ROW.element("hint"), {hint} } }
299 }
300 SwitchInput { label: ControlName::Wrapped, size, checked: state.get(), onchange: move |value| state.set(value),
301 invalid: false, availability, class: None, attributes: Vec::new() }
302 }
303 }
304}