freya_components/
radio.rs

1use dioxus::prelude::*;
2use freya_elements::{
3    self as dioxus_elements,
4    events::KeyboardEvent,
5};
6use freya_hooks::{
7    use_applied_theme,
8    use_focus,
9    RadioTheme,
10    RadioThemeWith,
11};
12
13/// Controlled `Radio` component.
14///
15/// # Styling
16/// Inherits the [`RadioTheme`](freya_hooks::RadioTheme) theme.
17///
18/// # Example
19///
20/// ```rust
21/// # use freya::prelude::*;
22/// #[derive(PartialEq)]
23/// enum Choice {
24///     First,
25///     Second,
26/// }
27///
28/// fn app() -> Element {
29///     let mut selected = use_signal(|| Choice::First);
30///     rsx!(
31///         Tile {
32///             onselect: move |_| selected.set(Choice::First),
33///             leading: rsx!(
34///                 Radio {
35///                     selected: *selected.read() == Choice::First,
36///                 },
37///             ),
38///             label { "First choice" }
39///         }
40///         Tile {
41///             onselect: move |_| selected.set(Choice::Second),
42///             leading: rsx!(
43///                 Radio {
44///                     selected: *selected.read() == Choice::Second,
45///                 },
46///             ),
47///             label { "Second choice" }
48///         }
49///     )
50/// }
51/// # use freya_testing::prelude::*;
52/// # launch_doc(|| {
53/// #   rsx!(
54/// #       Preview {
55/// #           Radio {
56/// #               selected: true
57/// #           }
58/// #       }
59/// #   )
60/// # }, (250., 250.).into(), "./images/gallery_radio.png");
61/// ```
62/// # Preview
63/// ![Radio Preview][radio]
64#[cfg_attr(feature = "docs",
65    doc = embed_doc_image::embed_image!("radio", "images/gallery_radio.png")
66)]
67#[allow(non_snake_case)]
68#[component]
69pub fn Radio(
70    /// Indicate whether this radio is selected or not.
71    selected: bool,
72    /// Theme override.
73    theme: Option<RadioThemeWith>,
74) -> Element {
75    let focus = use_focus();
76    let RadioTheme {
77        unselected_fill,
78        selected_fill,
79        border_fill,
80    } = use_applied_theme!(&theme, radio);
81    let fill = if selected {
82        selected_fill
83    } else {
84        unselected_fill
85    };
86    let border = if focus.is_focused_with_keyboard() {
87        format!("2 inner {fill}, 4 outer {border_fill}")
88    } else {
89        format!("2 inner {fill}")
90    };
91
92    let onkeydown = move |e: KeyboardEvent| {
93        if !focus.validate_keydown(&e) {
94            e.stop_propagation();
95        }
96    };
97
98    rsx!(
99        rect {
100            a11y_id: focus.attribute(),
101            width: "18",
102            height: "18",
103            border,
104            padding: "4",
105            main_align: "center",
106            cross_align: "center",
107            corner_radius: "99",
108            onkeydown,
109            if selected {
110                rect {
111                    width: "10",
112                    height: "10",
113                    background: "{fill}",
114                    corner_radius: "99",
115                }
116            }
117        }
118    )
119}
120
121#[cfg(test)]
122mod test {
123    use dioxus::prelude::use_signal;
124    use freya::prelude::*;
125    use freya_testing::prelude::*;
126
127    #[tokio::test]
128    pub async fn radio() {
129        #[derive(PartialEq)]
130        enum Choice {
131            First,
132            Second,
133            Third,
134        }
135
136        fn radio_app() -> Element {
137            let mut selected = use_signal(|| Choice::First);
138
139            rsx!(
140                Tile {
141                    onselect: move |_| selected.set(Choice::First),
142                    leading: rsx!(
143                        Radio {
144                            selected: *selected.read() == Choice::First,
145                        }
146                    ),
147                    label { "First choice" }
148                }
149                Tile {
150                    onselect: move |_| selected.set(Choice::Second),
151                    leading: rsx!(
152                        Radio {
153                            selected: *selected.read() == Choice::Second,
154                        }
155                    ),
156                    label { "Second choice" }
157                }
158                Tile {
159                    onselect: move |_| selected.set(Choice::Third),
160                    leading: rsx!(
161                        Radio {
162                            selected: *selected.read() == Choice::Third,
163                        }
164                    ),
165                    label { "Third choice" }
166                }
167            )
168        }
169
170        let mut utils = launch_test(radio_app);
171        let root = utils.root();
172        utils.wait_for_update().await;
173
174        // If the inner circle exists it means that the Radio is activated, otherwise it isn't
175        assert!(root.get(0).get(0).get(0).get(0).is_element());
176        assert!(root.get(1).get(0).get(0).get(0).is_placeholder());
177        assert!(root.get(2).get(0).get(0).get(0).is_placeholder());
178
179        utils.click_cursor((20., 50.)).await;
180
181        assert!(root.get(0).get(0).get(0).get(0).is_placeholder());
182        assert!(root.get(1).get(0).get(0).get(0).is_element());
183        assert!(root.get(2).get(0).get(0).get(0).is_placeholder());
184
185        utils.click_cursor((10., 90.)).await;
186
187        assert!(root.get(0).get(0).get(0).get(0).is_placeholder());
188        assert!(root.get(1).get(0).get(0).get(0).is_placeholder());
189        assert!(root.get(2).get(0).get(0).get(0).is_element());
190
191        utils.click_cursor((10., 10.)).await;
192
193        assert!(root.get(0).get(0).get(0).get(0).is_element());
194        assert!(root.get(1).get(0).get(0).get(0).is_placeholder());
195        assert!(root.get(2).get(0).get(0).get(0).is_placeholder());
196    }
197}