adui_dioxus/components/
switch.rs

1use 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/// Visual size of the switch.
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
10pub enum SwitchSize {
11    #[default]
12    Default,
13    Small,
14}
15
16/// Props for the Switch component.
17#[derive(Props, Clone, PartialEq)]
18pub struct SwitchProps {
19    /// Controlled checked state.
20    #[props(optional)]
21    pub checked: Option<bool>,
22    /// Initial value in uncontrolled mode.
23    #[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    /// Change event with the next checked state.
40    #[props(optional)]
41    pub on_change: Option<EventHandler<bool>>,
42}
43
44/// Ant Design flavored switch component.
45#[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    // 同步内部状态与 Form 字段:
66    // - 若 Form 中有布尔值,则以 Form 为准;
67    // - 若 Form 中无值,则回退到 default_checked。
68    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    // 始终尝试写回 FormStore(如果存在),确保提交时能拿到 newsletter 字段
173    if let Some(ctx) = form_control {
174        ctx.set_value(Value::Bool(next));
175    }
176
177    // 在非受控模式下更新内部状态,驱动 UI 立即切换
178    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}