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:
1 parent
5522fe4fbb
commit
f1ac5eaad2
12 files changed
+290
-17
No files matched your search
@@ -1,4 +1,5 @@
|
||||
@inherits DComponentBase
|
||||
@implements IDisposable
|
||||
|
||||
@* daisyUI Dropdown: https://daisyui.com/components/dropdown/ . The open state lives in C# (@bind-IsOpen):
|
||||
dropdown-open shows the content and dropdown-close keeps the focus-within rule from showing it while closed.
|
||||
@@ -42,8 +43,12 @@
|
||||
+ "max-md:overflow-y-auto max-md:overscroll-contain max-md:rounded-b-none max-md:translate-none max-md:scale-100 "
|
||||
+ "max-md:p-3 max-md:pb-[max(0.75rem,env(safe-area-inset-bottom))]";
|
||||
|
||||
/// <summary>Where it says it is open, so the Back button closes it (cascaded by the app; none, none said).</summary>
|
||||
[CascadingParameter] IDOverlayHost Overlays { get; set; }
|
||||
|
||||
ElementReference trigger;
|
||||
ElementReference content;
|
||||
IDisposable overlay;
|
||||
bool focusContent;
|
||||
bool focusTrigger;
|
||||
|
||||
@@ -65,6 +70,25 @@
|
||||
_ => null
|
||||
};
|
||||
|
||||
protected override void OnParametersSet() => TrackOverlay();
|
||||
|
||||
void TrackOverlay()
|
||||
{
|
||||
if (IsOpen && overlay is null)
|
||||
overlay = Overlays?.Open(() => InvokeAsync(async () =>
|
||||
{
|
||||
await SetOpen(false, true);
|
||||
StateHasChanged();
|
||||
}));
|
||||
else if (!IsOpen && overlay is not null)
|
||||
{
|
||||
overlay.Dispose();
|
||||
overlay = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => overlay?.Dispose();
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (focusContent)
|
||||
@@ -93,6 +117,7 @@
|
||||
focusContent = isOpen && !IsOpen;
|
||||
focusTrigger = !isOpen && IsOpen && giveFocusBack;
|
||||
IsOpen = isOpen;
|
||||
TrackOverlay();
|
||||
await IsOpenChanged.InvokeAsync(isOpen);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
@inherits DComponentBase
|
||||
@implements IDisposable
|
||||
|
||||
@* daisyUI Modal: https://daisyui.com/components/modal/ . Always rendered; the open state lives in C# (@bind-IsOpen)
|
||||
and shows as modal-open. On opening, the box takes the focus (Blazor's FocusAsync) so that Escape, handled on the
|
||||
@@ -58,7 +59,11 @@
|
||||
[Parameter] public bool ShowClose { get; set; } = true;
|
||||
[Parameter] public string BoxClass { get; set; }
|
||||
|
||||
/// <summary>Where it says it is open, so the Back button closes it (cascaded by the app; none, none said).</summary>
|
||||
[CascadingParameter] IDOverlayHost Overlays { get; set; }
|
||||
|
||||
readonly string uid = "d" + Guid.NewGuid().ToString("N")[..8];
|
||||
IDisposable overlay;
|
||||
ElementReference box;
|
||||
bool wasOpen;
|
||||
bool focusPending;
|
||||
@@ -80,8 +85,26 @@
|
||||
if (IsOpen && !wasOpen)
|
||||
focusPending = true;
|
||||
wasOpen = IsOpen;
|
||||
TrackOverlay();
|
||||
}
|
||||
|
||||
void TrackOverlay()
|
||||
{
|
||||
if (IsOpen && overlay is null)
|
||||
overlay = Overlays?.Open(() => InvokeAsync(async () =>
|
||||
{
|
||||
await SetOpen(false);
|
||||
StateHasChanged();
|
||||
}));
|
||||
else if (!IsOpen && overlay is not null)
|
||||
{
|
||||
overlay.Dispose();
|
||||
overlay = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => overlay?.Dispose();
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!focusPending)
|
||||
@@ -98,6 +121,7 @@
|
||||
{
|
||||
IsOpen = isOpen;
|
||||
wasOpen = isOpen;
|
||||
TrackOverlay();
|
||||
await IsOpenChanged.InvokeAsync(isOpen);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace decePubClient.Components.Daisy;
|
||||
|
||||
/// <summary>
|
||||
/// Where an open overlay (a dialog, a menu, a sheet) says it is open, so that the phone's Back button closes it instead
|
||||
/// of leaving the page: Open registers how to close it and returns what ends the registration once it is closed. The
|
||||
/// app cascades one (OverlayService, from MainLayout); a component without one simply doesn't register.
|
||||
/// </summary>
|
||||
public interface IDOverlayHost
|
||||
{
|
||||
IDisposable Open(Func<Task> close);
|
||||
}
|
||||
@@ -82,8 +82,11 @@
|
||||
[Parameter] public bool Expanded { get; set; }
|
||||
[Parameter] public EventCallback<bool> ExpandedChanged { get; set; }
|
||||
|
||||
[Inject] OverlayService Overlays { get; set; }
|
||||
|
||||
ElementReference closeButton;
|
||||
ElementReference expandButton;
|
||||
IDisposable overlay;
|
||||
bool focusClose;
|
||||
bool focusExpand;
|
||||
|
||||
@@ -115,7 +118,18 @@
|
||||
|
||||
protected override void OnInitialized() => IsLoading = false;
|
||||
|
||||
protected override void OnParametersSet() => postsHandedOver = true;
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
postsHandedOver = true;
|
||||
// full screen, the Back button closes it
|
||||
if (Expanded && overlay is null)
|
||||
overlay = Overlays.Open(() => InvokeAsync(Close));
|
||||
else if (!Expanded && overlay is not null)
|
||||
{
|
||||
overlay.Dispose();
|
||||
overlay = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
@@ -323,6 +337,7 @@
|
||||
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
overlay?.Dispose();
|
||||
await cancellation.CancelAsync();
|
||||
cancellation.Dispose();
|
||||
statsTimer?.Dispose();
|
||||
|
||||
@@ -29,8 +29,30 @@
|
||||
[Parameter] public RenderFragment ChildContent { get; set; }
|
||||
[Parameter] public string Class { get; set; }
|
||||
|
||||
[Inject] OverlayService Overlays { get; set; }
|
||||
|
||||
IDisposable overlay;
|
||||
|
||||
protected override void OnInitialized() => IsLoading = false;
|
||||
|
||||
// open, the Back button closes it
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (IsOpen && overlay is null)
|
||||
overlay = Overlays.Open(() => InvokeAsync(Close));
|
||||
else if (!IsOpen && overlay is not null)
|
||||
{
|
||||
overlay.Dispose();
|
||||
overlay = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
overlay?.Dispose();
|
||||
await base.DisposeAsync();
|
||||
}
|
||||
|
||||
Task Close() => OnClose.InvokeAsync();
|
||||
|
||||
Task OnKeyDown(KeyboardEventArgs e) => e.Key == "Escape" ? Close() : Task.CompletedTask;
|
||||
|
||||
Reference in new issue
Block a user