Skip to content

Commit 53276fd

Browse files
authored
#216 feat: add BackButton and SettingsButton components for Leptos and Yew (#217)
1 parent 775d0bf commit 53276fd

7 files changed

Lines changed: 419 additions & 0 deletions

File tree

‎src/leptos.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
// SPDX-FileCopyrightText: 2025 RAprogramm <andrey.rozanov.vl@gmail.com>
22
// SPDX-License-Identifier: MIT
33

4+
pub mod back_button;
45
pub mod bottom_button;
56
pub mod safe_area;
7+
pub mod settings_button;
68
pub mod theme;
79
pub mod viewport;
810

11+
pub use back_button::BackButton;
912
pub use bottom_button::BottomButton;
1013
use leptos::prelude::provide_context;
1114
pub use safe_area::{SafeAreaState, use_safe_area};
15+
pub use settings_button::SettingsButton;
1216
pub use theme::{ThemeState, use_theme};
1317
pub use viewport::{ViewportState, use_viewport};
1418
use wasm_bindgen::JsValue;

‎src/leptos/back_button.rs‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// SPDX-FileCopyrightText: 2025-2026 RAprogramm <andrey.rozanov.vl@gmail.com>
2+
// SPDX-License-Identifier: MIT
3+
4+
use std::cell::RefCell;
5+
6+
use leptos::prelude::*;
7+
8+
use crate::{
9+
logger,
10+
webapp::{EventHandle, TelegramWebApp}
11+
};
12+
13+
thread_local! {
14+
static BACK_BUTTON_HANDLE: RefCell<Option<EventHandle<dyn FnMut()>>> =
15+
const { RefCell::new(None) };
16+
}
17+
18+
/// Leptos component for the native back button.
19+
///
20+
/// Mirrors the React SDK's `BackButton` ergonomics. Drives `WebApp.BackButton`:
21+
/// shows/hides it based on the `visible` signal, registers the optional click
22+
/// callback, and cleans both up on unmount.
23+
///
24+
/// # Examples
25+
/// ```no_run
26+
/// use leptos::prelude::*;
27+
/// use telegram_webapp_sdk::leptos::BackButton;
28+
///
29+
/// #[component]
30+
/// fn App() -> impl IntoView {
31+
/// let visible = RwSignal::new(true);
32+
/// view! {
33+
/// <BackButton
34+
/// visible=visible
35+
/// on_click=move || { web_sys::console::log_1(&"back".into()); }
36+
/// />
37+
/// }
38+
/// }
39+
/// ```
40+
#[component]
41+
pub fn BackButton<F>(
42+
#[prop(into)] visible: Signal<bool>,
43+
#[prop(optional)] on_click: Option<F>
44+
) -> impl IntoView
45+
where
46+
F: Fn() + Clone + 'static
47+
{
48+
Effect::new(move |_| {
49+
if let Some(app) = TelegramWebApp::instance() {
50+
let result = if visible.get() {
51+
app.show_back_button()
52+
} else {
53+
app.hide_back_button()
54+
};
55+
if let Err(err) = result {
56+
logger::error(&format!("BackButton visibility toggle failed: {err:?}"));
57+
}
58+
}
59+
});
60+
61+
if let Some(cb) = on_click
62+
&& let Some(app) = TelegramWebApp::instance()
63+
{
64+
match app.set_back_button_callback(cb) {
65+
Ok(handle) => BACK_BUTTON_HANDLE.with(|c| {
66+
*c.borrow_mut() = Some(handle);
67+
}),
68+
Err(err) => logger::error(&format!("set_back_button_callback failed: {err:?}"))
69+
}
70+
}
71+
72+
on_cleanup(move || {
73+
if let Some(app) = TelegramWebApp::instance() {
74+
BACK_BUTTON_HANDLE.with(|c| {
75+
if let Some(handle) = c.borrow_mut().take()
76+
&& let Err(err) = app.remove_back_button_callback(handle)
77+
{
78+
logger::error(&format!("remove_back_button_callback failed: {err:?}"));
79+
}
80+
});
81+
if let Err(err) = app.hide_back_button() {
82+
logger::error(&format!("hide_back_button failed: {err:?}"));
83+
}
84+
}
85+
});
86+
87+
View::new(())
88+
}

‎src/leptos/settings_button.rs‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// SPDX-FileCopyrightText: 2025-2026 RAprogramm <andrey.rozanov.vl@gmail.com>
2+
// SPDX-License-Identifier: MIT
3+
4+
use std::cell::RefCell;
5+
6+
use leptos::prelude::*;
7+
8+
use crate::{
9+
logger,
10+
webapp::{EventHandle, TelegramWebApp}
11+
};
12+
13+
thread_local! {
14+
static SETTINGS_BUTTON_HANDLE: RefCell<Option<EventHandle<dyn FnMut()>>> =
15+
const { RefCell::new(None) };
16+
}
17+
18+
/// Leptos component for the native settings button.
19+
///
20+
/// Drives `WebApp.SettingsButton`: shows/hides it based on the `visible`
21+
/// signal, registers the optional click callback, and cleans both up on
22+
/// unmount.
23+
///
24+
/// # Examples
25+
/// ```no_run
26+
/// use leptos::prelude::*;
27+
/// use telegram_webapp_sdk::leptos::SettingsButton;
28+
///
29+
/// #[component]
30+
/// fn App() -> impl IntoView {
31+
/// let visible = RwSignal::new(true);
32+
/// view! {
33+
/// <SettingsButton
34+
/// visible=visible
35+
/// on_click=move || { web_sys::console::log_1(&"settings".into()); }
36+
/// />
37+
/// }
38+
/// }
39+
/// ```
40+
#[component]
41+
pub fn SettingsButton<F>(
42+
#[prop(into)] visible: Signal<bool>,
43+
#[prop(optional)] on_click: Option<F>
44+
) -> impl IntoView
45+
where
46+
F: Fn() + Clone + 'static
47+
{
48+
Effect::new(move |_| {
49+
if let Some(app) = TelegramWebApp::instance() {
50+
let result = if visible.get() {
51+
app.show_settings_button()
52+
} else {
53+
app.hide_settings_button()
54+
};
55+
if let Err(err) = result {
56+
logger::error(&format!("SettingsButton visibility toggle failed: {err:?}"));
57+
}
58+
}
59+
});
60+
61+
if let Some(cb) = on_click
62+
&& let Some(app) = TelegramWebApp::instance()
63+
{
64+
match app.set_settings_button_callback(cb) {
65+
Ok(handle) => SETTINGS_BUTTON_HANDLE.with(|c| {
66+
*c.borrow_mut() = Some(handle);
67+
}),
68+
Err(err) => logger::error(&format!("set_settings_button_callback failed: {err:?}"))
69+
}
70+
}
71+
72+
on_cleanup(move || {
73+
if let Some(app) = TelegramWebApp::instance() {
74+
SETTINGS_BUTTON_HANDLE.with(|c| {
75+
if let Some(handle) = c.borrow_mut().take()
76+
&& let Err(err) = app.remove_settings_button_callback(handle)
77+
{
78+
logger::error(&format!("remove_settings_button_callback failed: {err:?}"));
79+
}
80+
});
81+
if let Err(err) = app.hide_settings_button() {
82+
logger::error(&format!("hide_settings_button failed: {err:?}"));
83+
}
84+
}
85+
});
86+
87+
View::new(())
88+
}

‎src/webapp/buttons.rs‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,69 @@ impl TelegramWebApp {
515515
.unwrap_or(false)
516516
}
517517

518+
// === Settings button operations ===
519+
520+
/// Show the native settings button.
521+
///
522+
/// # Errors
523+
/// Returns [`JsValue`] if the underlying JS call fails.
524+
pub fn show_settings_button(&self) -> Result<(), JsValue> {
525+
self.call_nested0("SettingsButton", "show")
526+
}
527+
528+
/// Hide the native settings button.
529+
///
530+
/// # Errors
531+
/// Returns [`JsValue`] if the underlying JS call fails.
532+
pub fn hide_settings_button(&self) -> Result<(), JsValue> {
533+
self.call_nested0("SettingsButton", "hide")
534+
}
535+
536+
/// Registers a callback for the native settings button.
537+
///
538+
/// Returns an [`EventHandle`] that can be passed to
539+
/// [`remove_settings_button_callback`](Self::remove_settings_button_callback).
540+
///
541+
/// # Errors
542+
/// Returns [`JsValue`] if the underlying JS call fails.
543+
pub fn set_settings_button_callback<F>(
544+
&self,
545+
callback: F
546+
) -> Result<EventHandle<dyn FnMut()>, JsValue>
547+
where
548+
F: 'static + Fn()
549+
{
550+
let button_val = Reflect::get(&self.inner, &"SettingsButton".into())?;
551+
let button = button_val.dyn_into::<Object>()?;
552+
let cb = Closure::<dyn FnMut()>::new(callback);
553+
let f = Reflect::get(&button, &"onClick".into())?;
554+
let func = f
555+
.dyn_ref::<Function>()
556+
.ok_or_else(|| JsValue::from_str("onClick is not a function"))?;
557+
func.call1(&button, cb.as_ref().unchecked_ref())?;
558+
Ok(EventHandle::new(button, "offClick", None, cb))
559+
}
560+
561+
/// Remove previously set settings button callback.
562+
///
563+
/// # Errors
564+
/// Returns [`JsValue`] if the underlying JS call fails.
565+
pub fn remove_settings_button_callback(
566+
&self,
567+
handle: EventHandle<dyn FnMut()>
568+
) -> Result<(), JsValue> {
569+
handle.unregister()
570+
}
571+
572+
/// Returns whether the native settings button is visible.
573+
pub fn is_settings_button_visible(&self) -> bool {
574+
Reflect::get(&self.inner, &"SettingsButton".into())
575+
.ok()
576+
.and_then(|bb| Reflect::get(&bb, &"isVisible".into()).ok())
577+
.and_then(|v| v.as_bool())
578+
.unwrap_or(false)
579+
}
580+
518581
// === Legacy aliases for main button ===
519582

520583
/// Legacy alias for [`Self::show_bottom_button`] with

‎src/yew.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@ use yew::prelude::{hook, use_effect, use_state};
88

99
use crate::core::{context::TelegramContext, safe_context::get_context};
1010

11+
pub mod back_button;
1112
pub mod bottom_button;
1213
pub mod safe_area;
14+
pub mod settings_button;
1315
pub mod theme;
1416
pub mod viewport;
1517

18+
pub use back_button::BackButton;
1619
pub use bottom_button::BottomButton;
1720
pub use safe_area::{SafeAreaState, use_safe_area};
21+
pub use settings_button::SettingsButton;
1822
pub use theme::{ThemeState, use_theme};
1923
pub use viewport::{ViewportState, use_viewport};
2024

‎src/yew/back_button.rs‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// SPDX-FileCopyrightText: 2025-2026 RAprogramm <andrey.rozanov.vl@gmail.com>
2+
// SPDX-License-Identifier: MIT
3+
4+
use std::cell::RefCell;
5+
6+
use yew::prelude::{Callback, Html, Properties, function_component, html, use_effect_with};
7+
8+
use crate::{
9+
logger,
10+
webapp::{EventHandle, TelegramWebApp}
11+
};
12+
13+
thread_local! {
14+
static BACK_BUTTON_HANDLE: RefCell<Option<EventHandle<dyn FnMut()>>> =
15+
const { RefCell::new(None) };
16+
}
17+
18+
/// Props for [`BackButton`].
19+
#[derive(Properties, PartialEq)]
20+
pub struct BackButtonProps {
21+
/// Whether the native back button is shown.
22+
#[prop_or(true)]
23+
pub visible: bool,
24+
/// Click handler.
25+
#[prop_or_default]
26+
pub on_click: Callback<()>
27+
}
28+
29+
/// Yew component for the native back button.
30+
///
31+
/// Shows or hides `WebApp.BackButton` based on the `visible` prop and routes
32+
/// clicks through the `on_click` callback. The subscription is removed and the
33+
/// button hidden on unmount.
34+
///
35+
/// # Examples
36+
/// ```no_run
37+
/// use telegram_webapp_sdk::yew::BackButton;
38+
/// use yew::prelude::*;
39+
///
40+
/// #[function_component(App)]
41+
/// fn app() -> Html {
42+
/// let cb = Callback::from(|_| {
43+
/// web_sys::console::log_1(&"back".into());
44+
/// });
45+
/// html! { <BackButton visible={true} on_click={cb} /> }
46+
/// }
47+
/// ```
48+
#[function_component(BackButton)]
49+
pub fn back_button(props: &BackButtonProps) -> Html {
50+
let visible = props.visible;
51+
let on_click = props.on_click.clone();
52+
53+
use_effect_with(visible, move |&v| {
54+
if let Some(app) = TelegramWebApp::instance() {
55+
let result = if v {
56+
app.show_back_button()
57+
} else {
58+
app.hide_back_button()
59+
};
60+
if let Err(err) = result {
61+
logger::error(&format!("BackButton visibility toggle failed: {err:?}"));
62+
}
63+
}
64+
|| ()
65+
});
66+
67+
use_effect_with((), move |()| {
68+
if let Some(app) = TelegramWebApp::instance() {
69+
match app.set_back_button_callback(move || on_click.emit(())) {
70+
Ok(handle) => BACK_BUTTON_HANDLE.with(|c| {
71+
*c.borrow_mut() = Some(handle);
72+
}),
73+
Err(err) => logger::error(&format!("set_back_button_callback failed: {err:?}"))
74+
}
75+
}
76+
|| {
77+
if let Some(app) = TelegramWebApp::instance() {
78+
BACK_BUTTON_HANDLE.with(|c| {
79+
if let Some(handle) = c.borrow_mut().take()
80+
&& let Err(err) = app.remove_back_button_callback(handle)
81+
{
82+
logger::error(&format!("remove_back_button_callback failed: {err:?}"));
83+
}
84+
});
85+
if let Err(err) = app.hide_back_button() {
86+
logger::error(&format!("hide_back_button failed: {err:?}"));
87+
}
88+
}
89+
}
90+
});
91+
92+
html! {}
93+
}

0 commit comments

Comments
 (0)