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}