Skip to main content

telegram_webapp_sdk/yew/
bottom_button.rs

1// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4use yew::prelude::*;
5
6use crate::webapp::{BottomButton as TgBottomButton, TelegramWebApp};
7
8/// Yew component that configures the primary Telegram bottom button.
9///
10/// The button is shown when the component is mounted and hidden on drop.
11/// Text, colors and callback can be customized through [`BottomButtonProps`].
12///
13/// # Examples
14///
15/// ```no_run
16/// use telegram_webapp_sdk::yew::BottomButton;
17/// use yew::prelude::*;
18///
19/// #[component]
20/// fn App() -> Html {
21///     let on_click = Callback::from(|_| {});
22///     html! { <BottomButton text="OK" color="#000" text_color="#fff" {on_click} /> }
23/// }
24/// ```
25#[component]
26pub fn BottomButton(props: &BottomButtonProps) -> Html {
27    use_effect_with(props.clone(), |props| {
28        if let Some(app) = TelegramWebApp::instance() {
29            if let Some(color) = props.color.as_ref() {
30                let _ = app.set_bottom_button_color(TgBottomButton::Main, color.as_ref());
31            }
32            if let Some(text_color) = props.text_color.as_ref() {
33                let _ =
34                    app.set_bottom_button_text_color(TgBottomButton::Main, text_color.as_ref());
35            }
36            let _ = app.set_bottom_button_text(TgBottomButton::Main, props.text.as_ref());
37
38            let cb = props.on_click.clone();
39            let handle = app
40                .set_bottom_button_callback(TgBottomButton::Main, move || cb.emit(()))
41                .ok();
42            let _ = app.show_bottom_button(TgBottomButton::Main);
43
44            return Box::new(move || {
45                if let Some(h) = handle
46                    && let Some(app) = TelegramWebApp::instance()
47                {
48                    let _ = app.remove_bottom_button_callback(h);
49                    let _ = app.hide_bottom_button(TgBottomButton::Main);
50                }
51            }) as Box<dyn FnOnce()>;
52        }
53
54        Box::new(|| {}) as Box<dyn FnOnce()>
55    });
56
57    Html::default()
58}
59
60/// Properties for [`BottomButton`].
61#[derive(Properties, PartialEq, Clone)]
62pub struct BottomButtonProps {
63    /// Button text.
64    pub text:       AttrValue,
65    /// Background color in hex format.
66    #[prop_or_default]
67    pub color:      Option<AttrValue>,
68    /// Text color in hex format.
69    #[prop_or_default]
70    pub text_color: Option<AttrValue>,
71    /// Callback triggered on button click.
72    pub on_click:   Callback<()>
73}
74
75#[cfg(all(test, feature = "yew"))]
76mod tests {
77    use std::rc::Rc;
78
79    use js_sys::{Function, Object, Reflect};
80    use wasm_bindgen::{JsCast, JsValue};
81    use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
82    use web_sys::window;
83
84    use super::*;
85
86    wasm_bindgen_test_configure!(run_in_browser);
87
88    #[allow(dead_code)]
89    fn setup_webapp() -> Object {
90        let win = window().expect("window should be available");
91        let telegram = Object::new();
92        let webapp = Object::new();
93        let _ = Reflect::set(&win, &"Telegram".into(), &telegram);
94        let _ = Reflect::set(&telegram, &"WebApp".into(), &webapp);
95        webapp
96    }
97
98    #[wasm_bindgen_test]
99    #[allow(dead_code)]
100    fn renders_and_registers_callback() {
101        let webapp = setup_webapp();
102        let main = Object::new();
103
104        let show = Function::new_with_args("", "this.show_called = true;");
105        let set_text = Function::new_with_args("t", "this.text = t;");
106        let set_color = Function::new_with_args("c", "this.color = c;");
107        let set_text_color = Function::new_with_args("c", "this.text_color = c;");
108        let on_click = Function::new_with_args("cb", "this.cb = cb;");
109        let off_click = Function::new_with_args("", "this.cb = undefined;");
110        let _ = Reflect::set(&main, &"show".into(), &show);
111        let _ = Reflect::set(&main, &"setText".into(), &set_text);
112        let _ = Reflect::set(&main, &"setColor".into(), &set_color);
113        let _ = Reflect::set(&main, &"setTextColor".into(), &set_text_color);
114        let _ = Reflect::set(&main, &"onClick".into(), &on_click);
115        let _ = Reflect::set(&main, &"offClick".into(), &off_click);
116        let _ = Reflect::set(&webapp, &"MainButton".into(), &main);
117
118        let clicked = Rc::new(std::cell::Cell::new(false));
119        let clicked_clone = Rc::clone(&clicked);
120        let props = BottomButtonProps {
121            text:       AttrValue::from("Press"),
122            color:      Some(AttrValue::from("#000000")),
123            text_color: Some(AttrValue::from("#ffffff")),
124            on_click:   Callback::from(move |_| clicked_clone.set(true))
125        };
126        let document = window().unwrap().document().unwrap();
127        yew::Renderer::<BottomButton>::with_root_and_props(document.body().unwrap().into(), props)
128            .render();
129
130        let show_called = Reflect::get(&main, &"show_called".into())
131            .unwrap_or(JsValue::FALSE)
132            .as_bool()
133            .unwrap_or(false);
134        assert!(show_called);
135
136        let text = Reflect::get(&main, &"text".into())
137            .unwrap_or(JsValue::NULL)
138            .as_string()
139            .unwrap();
140        assert_eq!(text, "Press");
141
142        let cb = Reflect::get(&main, &"cb".into())
143            .unwrap()
144            .dyn_into::<Function>()
145            .unwrap();
146        let _ = cb.call0(&JsValue::NULL);
147        assert!(clicked.get());
148    }
149}