adui_dioxus/components/
switch.rs1use crate::components::control::{ControlStatus, push_status_class};
2use crate::components::form::use_form_item_control;
3use dioxus::events::KeyboardEvent;
4use dioxus::prelude::Key;
5use dioxus::prelude::*;
6use serde_json::Value;
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
10pub enum SwitchSize {
11 #[default]
12 Default,
13 Small,
14}
15
16#[derive(Props, Clone, PartialEq)]
18pub struct SwitchProps {
19 #[props(optional)]
21 pub checked: Option<bool>,
22 #[props(default)]
24 pub default_checked: bool,
25 #[props(default)]
26 pub disabled: bool,
27 #[props(default)]
28 pub size: SwitchSize,
29 #[props(optional)]
30 pub checked_children: Option<Element>,
31 #[props(optional)]
32 pub un_checked_children: Option<Element>,
33 #[props(optional)]
34 pub status: Option<ControlStatus>,
35 #[props(optional)]
36 pub class: Option<String>,
37 #[props(optional)]
38 pub style: Option<String>,
39 #[props(optional)]
41 pub on_change: Option<EventHandler<bool>>,
42}
43
44#[component]
46pub fn Switch(props: SwitchProps) -> Element {
47 let SwitchProps {
48 checked,
49 default_checked,
50 disabled,
51 size,
52 checked_children,
53 un_checked_children,
54 status,
55 class,
56 style,
57 on_change,
58 } = props;
59
60 let form_control = use_form_item_control();
61 let controlled_by_prop = checked.is_some();
62
63 let inner_checked = use_signal(|| default_checked);
64
65 if let Some(ctx) = &form_control {
69 let form_value = ctx.value();
70 let mut inner_signal = inner_checked;
71 if let Some(Value::Bool(b)) = form_value {
72 if *inner_signal.read() != b {
73 inner_signal.set(b);
74 }
75 } else if form_value.is_none() && *inner_signal.read() != default_checked {
76 inner_signal.set(default_checked);
77 }
78 }
79
80 let is_disabled = disabled || form_control.as_ref().is_some_and(|ctx| ctx.is_disabled());
81
82 let is_checked = *inner_checked.read();
83
84 let mut class_list = vec!["adui-switch".to_string()];
85 if size == SwitchSize::Small {
86 class_list.push("adui-switch-small".into());
87 }
88 if is_checked {
89 class_list.push("adui-switch-checked".into());
90 }
91 if is_disabled {
92 class_list.push("adui-switch-disabled".into());
93 }
94 push_status_class(&mut class_list, status);
95 if let Some(extra) = class {
96 class_list.push(extra);
97 }
98 let class_attr = class_list.join(" ");
99 let style_attr = style.unwrap_or_default();
100
101 let inner_for_toggle = inner_checked;
102 let form_for_toggle = form_control.clone();
103 let on_change_cb = on_change;
104 let disabled_flag = disabled;
105
106 rsx! {
107 button {
108 class: "{class_attr}",
109 style: "{style_attr}",
110 role: "switch",
111 "aria-checked": is_checked,
112 "aria-disabled": is_disabled,
113 r#type: "button",
114 disabled: is_disabled,
115 onclick: {
116 let mut inner_for_toggle = inner_for_toggle;
117 let form_for_toggle = form_for_toggle.clone();
118 move |_| {
119 let is_disabled_now = disabled_flag || form_for_toggle.as_ref().is_some_and(|ctx| ctx.is_disabled());
120 if is_disabled_now {
121 return;
122 }
123 handle_switch_toggle(
124 &form_for_toggle,
125 controlled_by_prop,
126 &mut inner_for_toggle,
127 on_change_cb,
128 );
129 }
130 },
131 onkeydown: {
132 let mut inner_for_toggle = inner_for_toggle;
133 let form_for_toggle = form_for_toggle.clone();
134 move |evt: KeyboardEvent| {
135 let is_disabled_now = disabled_flag || form_for_toggle.as_ref().is_some_and(|ctx| ctx.is_disabled());
136 if !is_disabled_now && key_triggers_toggle(&evt.key()) {
137 handle_switch_toggle(
138 &form_for_toggle,
139 controlled_by_prop,
140 &mut inner_for_toggle,
141 on_change_cb,
142 );
143 }
144 }
145 },
146 span { class: "adui-switch-handle" }
147 span {
148 class: "adui-switch-inner",
149 if is_checked {
150 if let Some(content) = checked_children {
151 {content}
152 }
153 } else {
154 if let Some(content) = un_checked_children {
155 {content}
156 }
157 }
158 }
159 }
160 }
161}
162
163fn handle_switch_toggle(
164 form_control: &Option<crate::components::form::FormItemControlContext>,
165 controlled_by_prop: bool,
166 inner: &mut Signal<bool>,
167 on_change: Option<EventHandler<bool>>,
168) {
169 let current = *inner.read();
170 let next = !current;
171
172 if let Some(ctx) = form_control {
174 ctx.set_value(Value::Bool(next));
175 }
176
177 if !controlled_by_prop {
179 let mut state = *inner;
180 state.set(next);
181 }
182
183 if let Some(cb) = on_change {
184 cb.call(next);
185 }
186}
187
188fn key_triggers_toggle(key: &Key) -> bool {
189 match key {
190 Key::Enter => true,
191 Key::Character(text) if text == " " => true,
192 _ => false,
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn switch_size_default_value() {
202 assert_eq!(SwitchSize::Default, SwitchSize::default());
203 }
204
205 #[test]
206 fn switch_size_all_variants() {
207 assert_eq!(SwitchSize::Default, SwitchSize::Default);
208 assert_eq!(SwitchSize::Small, SwitchSize::Small);
209 assert_ne!(SwitchSize::Default, SwitchSize::Small);
210 }
211
212 #[test]
213 fn switch_size_equality() {
214 let size1 = SwitchSize::Default;
215 let size2 = SwitchSize::Default;
216 let size3 = SwitchSize::Small;
217 assert_eq!(size1, size2);
218 assert_ne!(size1, size3);
219 }
220
221 #[test]
222 fn switch_size_clone() {
223 let original = SwitchSize::Small;
224 let cloned = original;
225 assert_eq!(original, cloned);
226 }
227
228 #[test]
229 fn switch_size_debug() {
230 let size = SwitchSize::Default;
231 let debug_str = format!("{:?}", size);
232 assert!(debug_str.contains("Default") || debug_str.contains("Small"));
233 }
234
235 #[test]
236 fn switch_props_defaults() {
237 }
242
243 #[test]
244 fn key_triggers_toggle_enter() {
245 assert!(key_triggers_toggle(&Key::Enter));
246 }
247
248 #[test]
249 fn key_triggers_toggle_space() {
250 assert!(key_triggers_toggle(&Key::Character(" ".into())));
251 }
252
253 #[test]
254 fn key_triggers_toggle_other_keys() {
255 assert!(!key_triggers_toggle(&Key::ArrowLeft));
256 assert!(!key_triggers_toggle(&Key::ArrowRight));
257 assert!(!key_triggers_toggle(&Key::Character("a".into())));
258 assert!(!key_triggers_toggle(&Key::Escape));
259 }
260
261 #[test]
262 fn key_triggers_toggle_space_variations() {
263 assert!(key_triggers_toggle(&Key::Character(" ".into())));
265 assert!(!key_triggers_toggle(&Key::Character("\t".into())));
267 assert!(!key_triggers_toggle(&Key::Character("\n".into())));
268 }
269
270 #[test]
271 fn key_triggers_toggle_arrow_keys() {
272 assert!(!key_triggers_toggle(&Key::ArrowUp));
273 assert!(!key_triggers_toggle(&Key::ArrowDown));
274 }
275
276 #[test]
277 fn key_triggers_toggle_modifier_keys() {
278 assert!(!key_triggers_toggle(&Key::Control));
279 assert!(!key_triggers_toggle(&Key::Shift));
280 assert!(!key_triggers_toggle(&Key::Alt));
281 assert!(!key_triggers_toggle(&Key::Meta));
282 }
283
284 #[test]
285 fn key_triggers_toggle_function_keys() {
286 assert!(!key_triggers_toggle(&Key::F1));
287 assert!(!key_triggers_toggle(&Key::F12));
288 }
289
290 #[test]
291 fn key_triggers_toggle_navigation_keys() {
292 assert!(!key_triggers_toggle(&Key::Home));
293 assert!(!key_triggers_toggle(&Key::End));
294 assert!(!key_triggers_toggle(&Key::PageUp));
295 assert!(!key_triggers_toggle(&Key::PageDown));
296 }
297
298 #[test]
299 fn key_triggers_toggle_other_special_keys() {
300 assert!(!key_triggers_toggle(&Key::Backspace));
301 assert!(!key_triggers_toggle(&Key::Delete));
302 assert!(!key_triggers_toggle(&Key::Tab));
303 assert!(!key_triggers_toggle(&Key::CapsLock));
304 }
305
306 #[test]
307 fn key_triggers_toggle_character_keys() {
308 assert!(!key_triggers_toggle(&Key::Character("0".into())));
310 assert!(!key_triggers_toggle(&Key::Character("9".into())));
311 assert!(!key_triggers_toggle(&Key::Character("A".into())));
312 assert!(!key_triggers_toggle(&Key::Character("z".into())));
313 assert!(!key_triggers_toggle(&Key::Character("!".into())));
314 assert!(!key_triggers_toggle(&Key::Character("@".into())));
315 }
316
317 #[test]
318 fn key_triggers_toggle_enter_vs_space() {
319 assert!(key_triggers_toggle(&Key::Enter));
321 assert!(key_triggers_toggle(&Key::Character(" ".into())));
322 }
323
324 #[test]
325 fn handle_switch_toggle_logic() {
326 }
332
333 #[test]
334 fn switch_size_all_variants_equality() {
335 let sizes = [SwitchSize::Default, SwitchSize::Small];
336 for (i, size1) in sizes.iter().enumerate() {
337 for (j, size2) in sizes.iter().enumerate() {
338 if i == j {
339 assert_eq!(size1, size2);
340 } else {
341 assert_ne!(size1, size2);
342 }
343 }
344 }
345 }
346
347 #[test]
348 fn switch_size_copy_semantics() {
349 let size = SwitchSize::Small;
351 let size2 = size;
352 assert_eq!(size, size2);
353 }
354
355 #[test]
356 fn key_triggers_toggle_unicode_space() {
357 assert!(key_triggers_toggle(&Key::Character(" ".into())));
359 assert!(!key_triggers_toggle(&Key::Character("\u{00A0}".into())));
361 }
362
363 #[test]
364 fn key_triggers_toggle_empty_string() {
365 assert!(!key_triggers_toggle(&Key::Character("".into())));
367 }
368
369 #[test]
370 fn key_triggers_toggle_multiple_spaces() {
371 assert!(key_triggers_toggle(&Key::Character(" ".into())));
373 assert!(!key_triggers_toggle(&Key::Character(" ".into())));
375 }
376}