diff --git a/SteUp.Maui/Platforms/Android/MainActivity.cs b/SteUp.Maui/Platforms/Android/MainActivity.cs
index 6b64ac5..f406f0d 100644
--- a/SteUp.Maui/Platforms/Android/MainActivity.cs
+++ b/SteUp.Maui/Platforms/Android/MainActivity.cs
@@ -16,11 +16,15 @@ namespace SteUp.Maui
ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
+ private BackButtonService? _backButton;
+ private BlazorBackCallback? _backCallback;
+
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
ApplySystemBarsAppearance();
+ HookBackButton();
// Da Android 15 (API 35) il edge-to-edge e' forzato e adjustResize non ridimensiona
// piu' la finestra: la tastiera si limita a coprire la WebView. Riduciamo noi il
@@ -44,14 +48,78 @@ namespace SteUp.Maui
controller.AppearanceLightNavigationBars = true;
}
- public override void OnBackPressed()
+ ///
+ /// Il tasto indietro va intercettato sul dispatcher di AndroidX, non
+ /// sovrascrivendo OnBackPressed: da AndroidX Activity 1.6 il gesto passa
+ /// tutto di li' e il metodo dell'Activity non viene piu' invocato — la
+ /// UI Blazor non riceveva mai il back e il lavoro non salvato del form
+ /// se ne andava con la navigazione.
+ ///
+ /// Il callback resta acceso solo finche' c'e' davvero qualcuno in
+ /// ascolto: quando la UI non ha nulla da dire, il back torna a
+ /// comportarsi come sempre invece di essere inghiottito.
+ ///
+ private void HookBackButton()
{
- // Se la UI Blazor ha un form aperto se ne occupa lei (propone il salvataggio
- // come bozza); altrimenti comportamento standard.
- var backButton = IPlatformApplication.Current?.Services.GetService();
- if (backButton?.TryHandleBackPressed() == true) return;
+ _backButton = IPlatformApplication.Current?.Services.GetService();
+ if (_backButton is null) return;
- base.OnBackPressed();
+ _backCallback = new BlazorBackCallback(_backButton);
+ OnBackPressedDispatcher.AddCallback(this, _backCallback);
+
+ _backCallback.Enabled = _backButton.HasHandlers;
+ _backButton.HandlersChanged += OnBackHandlersChanged;
+ }
+
+ private void OnBackHandlersChanged(bool hasHandlers)
+ {
+ if (_backCallback is null) return;
+
+ _backCallback.Enabled = hasHandlers;
+ if (!hasHandlers) return;
+
+ // Il dispatcher consulta i callback dall'ultimo registrato al primo,
+ // e BlazorWebView registra il suo (che fa tornare indietro la
+ // cronologia della WebView) quando l'handler viene creato, cioe' dopo
+ // OnCreate. Ri-registrandoci nel momento in cui la UI ha davvero
+ // qualcosa da dire torniamo in cima e il back arriva prima a noi.
+ _backCallback.Remove();
+ OnBackPressedDispatcher.AddCallback(this, _backCallback);
+ }
+
+ ///
+ /// Con la navigazione a tre tasti il back arriva come evento di tastiera
+ /// e la WebView lo intercetta per conto suo, tornando indietro nella
+ /// propria cronologia: ne' l'Activity ne' il dispatcher lo vedono mai.
+ /// Qui la chiave passa prima della gerarchia di view, quindi e' l'unico
+ /// punto in cui possiamo fermarla mentre un form ha lavoro non salvato.
+ /// Il callback sul dispatcher resta per la navigazione a gesti, dove
+ /// evento di tastiera non ce n'e'.
+ ///
+ public override bool DispatchKeyEvent(KeyEvent? e)
+ {
+ if (e?.KeyCode == Keycode.Back && _backButton?.HasHandlers == true)
+ {
+ // Si consuma anche il DOWN: lasciandolo passare la WebView
+ // reagirebbe lo stesso.
+ if (e.Action == KeyEventActions.Up) _backButton.TryHandleBackPressed();
+ return true;
+ }
+
+ return base.DispatchKeyEvent(e);
+ }
+
+ protected override void OnDestroy()
+ {
+ if (_backButton is not null) _backButton.HandlersChanged -= OnBackHandlersChanged;
+
+ base.OnDestroy();
+ }
+
+ private sealed class BlazorBackCallback(BackButtonService service)
+ : AndroidX.Activity.OnBackPressedCallback(false)
+ {
+ public override void HandleOnBackPressed() => service.TryHandleBackPressed();
}
private sealed class ImeInsetsListener : Java.Lang.Object, IOnApplyWindowInsetsListener
diff --git a/SteUp.Shared/Components/Layout/Sheet/ActionSheet.razor b/SteUp.Shared/Components/Layout/Sheet/ActionSheet.razor
index 8f4e0b3..a95e1e4 100644
--- a/SteUp.Shared/Components/Layout/Sheet/ActionSheet.razor
+++ b/SteUp.Shared/Components/Layout/Sheet/ActionSheet.razor
@@ -1,4 +1,8 @@
-@*
+@using SteUp.Shared.Core.Messages.System
+@inject BackButtonService BackButton
+@implements IDisposable
+
+@*
Bottom sheet (STILE_UI §6.14): sale dal basso, maniglia di trascinamento,
testata con titolo e chiusura, un'unica linea di struttura, corpo scorrevole.
Sostituisce i menu flottanti: stessa funzione, bersagli molto piu' grandi.
@@ -43,9 +47,35 @@
[Parameter] public RenderFragment? ChildContent { get; set; }
+ // Il tasto indietro del dispositivo chiude lo sheet invece di uscire dalla
+ // schermata sotto: e' il gesto che l'utente si aspetta, ed e' anche la
+ // ragione per cui i gestori sono impilati (vedi BackButtonService).
+ private IDisposable? _backRegistration;
+
+ protected override void OnParametersSet()
+ {
+ if (Open && _backRegistration is null)
+ {
+ _backRegistration = BackButton.Register(() => InvokeAsync(Close));
+ }
+ else if (!Open)
+ {
+ Unregister();
+ }
+ }
+
private async Task Close()
{
+ Unregister();
Open = false;
await OpenChanged.InvokeAsync(false);
}
+
+ private void Unregister()
+ {
+ _backRegistration?.Dispose();
+ _backRegistration = null;
+ }
+
+ public void Dispose() => Unregister();
}
diff --git a/SteUp.Shared/Core/Messages/System/BackButtonService.cs b/SteUp.Shared/Core/Messages/System/BackButtonService.cs
index 711a02c..ab95b55 100644
--- a/SteUp.Shared/Core/Messages/System/BackButtonService.cs
+++ b/SteUp.Shared/Core/Messages/System/BackButtonService.cs
@@ -5,15 +5,32 @@ namespace SteUp.Shared.Core.Messages.System;
/// Il form scheda si registra mentre è aperto così il back non manda l'app in
/// background buttando via il lavoro non salvato: passa invece dallo stesso
/// percorso del pulsante "Annulla", che propone il salvataggio come bozza.
+///
+/// I gestori sono impilati: sopra il form può aprirsi un bottom sheet, e il
+/// tasto indietro deve chiudere prima quello. Chiudendolo si torna al gestore
+/// sottostante, che resta valido — senza la pila il sheet, uscendo di scena,
+/// porterebbe via con sé anche la registrazione del form.
///
public class BackButtonService
{
- private Func? _handler;
+ private readonly List> _handlers = [];
- /// Registra il gestore corrente. Ritorna l'azione di deregistrazione.
+ ///
+ /// Segnala all'host se in questo momento c'è qualcuno da avvisare. L'host
+ /// se ne serve per accendere e spegnere la propria intercettazione, così
+ /// quando la UI non ha nulla da dire il tasto indietro torna a comportarsi
+ /// esattamente come sempre.
+ ///
+ public event Action? HandlersChanged;
+
+ public bool HasHandlers => _handlers.Count > 0;
+
+ /// Registra il gestore corrente in cima alla pila. Ritorna la deregistrazione.
public IDisposable Register(Func handler)
{
- _handler = handler;
+ _handlers.Add(handler);
+ HandlersChanged?.Invoke(true);
+
return new Registration(this, handler);
}
@@ -23,11 +40,10 @@ public class BackButtonService
///
public bool TryHandleBackPressed()
{
- var handler = _handler;
- if (handler is null) return false;
+ if (_handlers.Count == 0) return false;
// Fire-and-forget: il gestore rientra nel dispatcher Blazor da solo.
- _ = handler();
+ _ = _handlers[^1]();
return true;
}
@@ -35,8 +51,8 @@ public class BackButtonService
{
public void Dispose()
{
- // Non azzerare se nel frattempo si è registrato qualcun altro.
- if (ReferenceEquals(owner._handler, handler)) owner._handler = null;
+ if (!owner._handlers.Remove(handler)) return;
+ owner.HandlersChanged?.Invoke(owner.HasHandlers);
}
}
}