Back closes what is open

On a phone the Back button left the page under an open menu, sheet or dialog. An open overlay now says so (new
IDOverlayHost, cascaded by MainLayout; OverlayService keeps them newest last): DModal and DDropdown through the
cascade, so the D components stay free of app services, and the nav menu, PhoneSheet and the full-screen globe through
the service. MainLayout's location-changing handler lets Back close the newest overlay instead of leaving the page; a
link followed or the app's own navigation (signing out after its question, say) closes them all and goes on. Blazor
can't tell Back from the app's navigation, so a capture-phase popstate listener in main.js marks Back
(AppStatusService.TookBack). No history entries are pushed. Phone flow: Back closes the menu, a card's More sheet, the
composer and the full-screen globe one after another without leaving the feed, and with nothing open goes back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-06 23:49:49 +02:00
1 parent 5522fe4fbb
commit f1ac5eaad2
12 files changed
+290 -17

No files matched your search

+42 -10
View File
@@ -1,15 +1,47 @@
@inherits LayoutComponentBase
@inherits LayoutComponentBase
@implements IDisposable
<div class="relative flex flex-col md:flex-row w-full h-full">
<NavMenu />
@* The page and its menu. The overlays it holds (dialogs, menus, sheets: IDOverlayHost, cascaded) close with the phone's
Back button: while one is open, Back closes the newest instead of leaving the page; a link followed or the app's own
navigation closes them all and goes on. *@
<CascadingValue Value="(IDOverlayHost)Overlays" IsFixed="true">
<div class="relative flex flex-col md:flex-row w-full h-full">
<NavMenu />
<div class="block flex-1 relative w-full h-full">
<div class="block absolute overflow-y-auto w-full h-full p-3 md:py-4 md:pl-2 md:pr-4">
@Body
<div class="block flex-1 relative w-full h-full">
<div class="block absolute overflow-y-auto w-full h-full p-3 md:py-4 md:pl-2 md:pr-4">
@Body
</div>
</div>
</div>
</div>
<ToastHost />
<PublicNoticeHost />
<ConfirmHost />
<ToastHost />
<PublicNoticeHost />
<ConfirmHost />
</CascadingValue>
@code {
[Inject] OverlayService Overlays { get; set; }
[Inject] NavigationManager Navigation { get; set; }
[Inject] AppStatusService Status { get; set; }
IDisposable changing;
protected override void OnInitialized() => changing = Navigation.RegisterLocationChangingHandler(OnLocationChanging);
async ValueTask OnLocationChanging(LocationChangingContext context)
{
var back = !context.IsNavigationIntercepted && Status.TookBack();
if (!Overlays.HasOpen)
return;
if (back)
{
context.PreventNavigation();
await Overlays.CloseTop(context.CancellationToken);
return;
}
await Overlays.CloseAll(context.CancellationToken);
}
public void Dispose() => changing?.Dispose();
}
+27 -6
View File
@@ -141,7 +141,27 @@
@code {
[Inject] ConfirmService Confirm { get; set; }
[Inject] OverlayService Overlays { get; set; }
bool isMenuOpen;
IDisposable overlay;
// open, the Back button closes it
void SetMenu(bool open)
{
isMenuOpen = open;
if (open && overlay is null)
overlay = Overlays.Open(() => InvokeAsync(() =>
{
SetMenu(false);
StateHasChanged();
}));
else if (!open && overlay is not null)
{
overlay.Dispose();
overlay = null;
}
}
bool isPersonaMenuOpen;
string PersonaBackground => CascadingState.Persona?.Header is { Length: > 0 } header && !header.Contains("/missing-")
@@ -162,37 +182,38 @@
{
if (!isMenuOpen)
return;
isMenuOpen = false;
SetMenu(false);
InvokeAsync(StateHasChanged);
}
void CloseMenu() => isMenuOpen = false;
void CloseMenu() => SetMenu(false);
void OnKeyDown(KeyboardEventArgs e)
{
if (e.Key == "Escape")
isMenuOpen = false;
SetMenu(false);
}
public override async ValueTask DisposeAsync()
{
Navigation.LocationChanged -= OnLocationChanged;
overlay?.Dispose();
await base.DisposeAsync();
}
async Task SwitchPersona(string personaId)
{
isMenuOpen = false;
SetMenu(false);
var result = await CascadingState.SwitchPersona(personaId);
if (result.IsInvalid)
Toast.AddResponseError(result);
}
private void ToggleNavMenu() => isMenuOpen = !isMenuOpen;
private void ToggleNavMenu() => SetMenu(!isMenuOpen);
async Task Logout()
{
isMenuOpen = false;
SetMenu(false);
if (!await Confirm.Ask(Localizer["Log out?"], Localizer["Every persona of this login is signed out on this device."], Localizer["Logout"], true))
return;
Navigation.NavigateTo("logout");