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
|
@inherits DComponentBase
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
@* daisyUI Dropdown: https://daisyui.com/components/dropdown/ . The open state lives in C# (@bind-IsOpen):
|
@* 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.
|
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: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))]";
|
+ "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 trigger;
|
||||||
ElementReference content;
|
ElementReference content;
|
||||||
|
IDisposable overlay;
|
||||||
bool focusContent;
|
bool focusContent;
|
||||||
bool focusTrigger;
|
bool focusTrigger;
|
||||||
|
|
||||||
@@ -65,6 +70,25 @@
|
|||||||
_ => null
|
_ => 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)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (focusContent)
|
if (focusContent)
|
||||||
@@ -93,6 +117,7 @@
|
|||||||
focusContent = isOpen && !IsOpen;
|
focusContent = isOpen && !IsOpen;
|
||||||
focusTrigger = !isOpen && IsOpen && giveFocusBack;
|
focusTrigger = !isOpen && IsOpen && giveFocusBack;
|
||||||
IsOpen = isOpen;
|
IsOpen = isOpen;
|
||||||
|
TrackOverlay();
|
||||||
await IsOpenChanged.InvokeAsync(isOpen);
|
await IsOpenChanged.InvokeAsync(isOpen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
@inherits DComponentBase
|
@inherits DComponentBase
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
@* daisyUI Modal: https://daisyui.com/components/modal/ . Always rendered; the open state lives in C# (@bind-IsOpen)
|
@* 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
|
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 bool ShowClose { get; set; } = true;
|
||||||
[Parameter] public string BoxClass { get; set; }
|
[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];
|
readonly string uid = "d" + Guid.NewGuid().ToString("N")[..8];
|
||||||
|
IDisposable overlay;
|
||||||
ElementReference box;
|
ElementReference box;
|
||||||
bool wasOpen;
|
bool wasOpen;
|
||||||
bool focusPending;
|
bool focusPending;
|
||||||
@@ -80,8 +85,26 @@
|
|||||||
if (IsOpen && !wasOpen)
|
if (IsOpen && !wasOpen)
|
||||||
focusPending = true;
|
focusPending = true;
|
||||||
wasOpen = IsOpen;
|
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)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (!focusPending)
|
if (!focusPending)
|
||||||
@@ -98,6 +121,7 @@
|
|||||||
{
|
{
|
||||||
IsOpen = isOpen;
|
IsOpen = isOpen;
|
||||||
wasOpen = isOpen;
|
wasOpen = isOpen;
|
||||||
|
TrackOverlay();
|
||||||
await IsOpenChanged.InvokeAsync(isOpen);
|
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 bool Expanded { get; set; }
|
||||||
[Parameter] public EventCallback<bool> ExpandedChanged { get; set; }
|
[Parameter] public EventCallback<bool> ExpandedChanged { get; set; }
|
||||||
|
|
||||||
|
[Inject] OverlayService Overlays { get; set; }
|
||||||
|
|
||||||
ElementReference closeButton;
|
ElementReference closeButton;
|
||||||
ElementReference expandButton;
|
ElementReference expandButton;
|
||||||
|
IDisposable overlay;
|
||||||
bool focusClose;
|
bool focusClose;
|
||||||
bool focusExpand;
|
bool focusExpand;
|
||||||
|
|
||||||
@@ -115,7 +118,18 @@
|
|||||||
|
|
||||||
protected override void OnInitialized() => IsLoading = false;
|
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)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
@@ -323,6 +337,7 @@
|
|||||||
|
|
||||||
public override async ValueTask DisposeAsync()
|
public override async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
|
overlay?.Dispose();
|
||||||
await cancellation.CancelAsync();
|
await cancellation.CancelAsync();
|
||||||
cancellation.Dispose();
|
cancellation.Dispose();
|
||||||
statsTimer?.Dispose();
|
statsTimer?.Dispose();
|
||||||
|
|||||||
@@ -29,8 +29,30 @@
|
|||||||
[Parameter] public RenderFragment ChildContent { get; set; }
|
[Parameter] public RenderFragment ChildContent { get; set; }
|
||||||
[Parameter] public string Class { get; set; }
|
[Parameter] public string Class { get; set; }
|
||||||
|
|
||||||
|
[Inject] OverlayService Overlays { get; set; }
|
||||||
|
|
||||||
|
IDisposable overlay;
|
||||||
|
|
||||||
protected override void OnInitialized() => IsLoading = false;
|
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 Close() => OnClose.InvokeAsync();
|
||||||
|
|
||||||
Task OnKeyDown(KeyboardEventArgs e) => e.Key == "Escape" ? Close() : Task.CompletedTask;
|
Task OnKeyDown(KeyboardEventArgs e) => e.Key == "Escape" ? Close() : Task.CompletedTask;
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ builder.Services.AddOptions()
|
|||||||
.AddScoped<StreamingService>()
|
.AddScoped<StreamingService>()
|
||||||
.AddScoped<PublicNoticeService>()
|
.AddScoped<PublicNoticeService>()
|
||||||
.AddScoped<ConfirmService>()
|
.AddScoped<ConfirmService>()
|
||||||
|
.AddScoped<OverlayService>()
|
||||||
.AddScoped<MediaService>()
|
.AddScoped<MediaService>()
|
||||||
.AddScoped<GroupService>()
|
.AddScoped<GroupService>()
|
||||||
.AddScoped<InstanceService>()
|
.AddScoped<InstanceService>()
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ public class AppStatusService(IJSRuntime jsRuntime)
|
|||||||
|
|
||||||
public bool IsMobileMedia() => JsSyncRuntime.Invoke<bool>("isMobileMedia");
|
public bool IsMobileMedia() => JsSyncRuntime.Invoke<bool>("isMobileMedia");
|
||||||
|
|
||||||
|
// whether the navigation being decided came from the Back (or Forward) button
|
||||||
|
public bool TookBack() => JsSyncRuntime.Invoke<bool>("tookBack");
|
||||||
|
|
||||||
// whether the pointer can hover (a mouse); a touch screen can't, so what shows on hover there opens on a tap
|
// whether the pointer can hover (a mouse); a touch screen can't, so what shows on hover there opens on a tap
|
||||||
public bool CanHover() => JsSyncRuntime.Invoke<bool>("canHover");
|
public bool CanHover() => JsSyncRuntime.Invoke<bool>("canHover");
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using decePubClient.Components.Daisy;
|
||||||
|
|
||||||
|
namespace decePubClient.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The overlays open now, newest last (IDOverlayHost): MainLayout closes the newest when the Back button is pressed
|
||||||
|
/// while one is open, and all of them when the app goes elsewhere.
|
||||||
|
/// </summary>
|
||||||
|
public class OverlayService : IDOverlayHost
|
||||||
|
{
|
||||||
|
readonly List<Entry> open = [];
|
||||||
|
|
||||||
|
public bool HasOpen
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (open)
|
||||||
|
return open.Count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable Open(Func<Task> close)
|
||||||
|
{
|
||||||
|
var entry = new Entry(this, close);
|
||||||
|
lock (open)
|
||||||
|
open.Add(entry);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CloseTop(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Entry top;
|
||||||
|
lock (open)
|
||||||
|
top = open.LastOrDefault();
|
||||||
|
if (top is null)
|
||||||
|
return;
|
||||||
|
top.Dispose();
|
||||||
|
await top.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CloseAll(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Entry[] all;
|
||||||
|
lock (open)
|
||||||
|
{
|
||||||
|
all = [.. open];
|
||||||
|
open.Clear();
|
||||||
|
}
|
||||||
|
foreach (var entry in all.Reverse())
|
||||||
|
await entry.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Entry(OverlayService owner, Func<Task> close) : IDisposable
|
||||||
|
{
|
||||||
|
public Func<Task> Close { get; } = close;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (owner.open)
|
||||||
|
owner.open.Remove(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
-10
@@ -1,15 +1,47 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
<div class="relative flex flex-col md:flex-row w-full h-full">
|
@* The page and its menu. The overlays it holds (dialogs, menus, sheets: IDOverlayHost, cascaded) close with the phone's
|
||||||
<NavMenu />
|
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 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">
|
<div class="block absolute overflow-y-auto w-full h-full p-3 md:py-4 md:pl-2 md:pr-4">
|
||||||
@Body
|
@Body
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<ToastHost />
|
<ToastHost />
|
||||||
<PublicNoticeHost />
|
<PublicNoticeHost />
|
||||||
<ConfirmHost />
|
<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
@@ -141,7 +141,27 @@
|
|||||||
@code {
|
@code {
|
||||||
[Inject] ConfirmService Confirm { get; set; }
|
[Inject] ConfirmService Confirm { get; set; }
|
||||||
|
|
||||||
|
[Inject] OverlayService Overlays { get; set; }
|
||||||
|
|
||||||
bool isMenuOpen;
|
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;
|
bool isPersonaMenuOpen;
|
||||||
|
|
||||||
string PersonaBackground => CascadingState.Persona?.Header is { Length: > 0 } header && !header.Contains("/missing-")
|
string PersonaBackground => CascadingState.Persona?.Header is { Length: > 0 } header && !header.Contains("/missing-")
|
||||||
@@ -162,37 +182,38 @@
|
|||||||
{
|
{
|
||||||
if (!isMenuOpen)
|
if (!isMenuOpen)
|
||||||
return;
|
return;
|
||||||
isMenuOpen = false;
|
SetMenu(false);
|
||||||
InvokeAsync(StateHasChanged);
|
InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CloseMenu() => isMenuOpen = false;
|
void CloseMenu() => SetMenu(false);
|
||||||
|
|
||||||
void OnKeyDown(KeyboardEventArgs e)
|
void OnKeyDown(KeyboardEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Key == "Escape")
|
if (e.Key == "Escape")
|
||||||
isMenuOpen = false;
|
SetMenu(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async ValueTask DisposeAsync()
|
public override async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
Navigation.LocationChanged -= OnLocationChanged;
|
Navigation.LocationChanged -= OnLocationChanged;
|
||||||
|
overlay?.Dispose();
|
||||||
await base.DisposeAsync();
|
await base.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task SwitchPersona(string personaId)
|
async Task SwitchPersona(string personaId)
|
||||||
{
|
{
|
||||||
isMenuOpen = false;
|
SetMenu(false);
|
||||||
var result = await CascadingState.SwitchPersona(personaId);
|
var result = await CascadingState.SwitchPersona(personaId);
|
||||||
if (result.IsInvalid)
|
if (result.IsInvalid)
|
||||||
Toast.AddResponseError(result);
|
Toast.AddResponseError(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ToggleNavMenu() => isMenuOpen = !isMenuOpen;
|
private void ToggleNavMenu() => SetMenu(!isMenuOpen);
|
||||||
|
|
||||||
async Task Logout()
|
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))
|
if (!await Confirm.Ask(Localizer["Log out?"], Localizer["Every persona of this login is signed out on this device."], Localizer["Logout"], true))
|
||||||
return;
|
return;
|
||||||
Navigation.NavigateTo("logout");
|
Navigation.NavigateTo("logout");
|
||||||
|
|||||||
@@ -310,6 +310,51 @@ namespace decePubClient.E2E.Mobile
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the Back button closes what is open, one at a time, and stays on the page
|
||||||
|
[Fact]
|
||||||
|
public async Task Back_closes_what_is_open()
|
||||||
|
{
|
||||||
|
await using var session = await SignIn.As(browser, Persona(), Phone);
|
||||||
|
await Flow(session, "back", async page =>
|
||||||
|
{
|
||||||
|
var home = page.Url;
|
||||||
|
// a page to go back to, so a Back that isn't held would leave the feed
|
||||||
|
await page.Locator("[data-nav-toggle]").TapAsync();
|
||||||
|
await page.Locator("[data-nav-panel] a[href='explore']").TapAsync();
|
||||||
|
await page.WaitForURLAsync(url => new Uri(url).AbsolutePath == "/explore");
|
||||||
|
await page.Locator("[data-nav-toggle]").TapAsync();
|
||||||
|
await page.Locator("[data-nav-panel] a[href='/']").First.TapAsync();
|
||||||
|
await page.WaitForURLAsync(url => new Uri(url).AbsolutePath == "/");
|
||||||
|
await page.Locator("#feed-scroll article[data-uri]").First.WaitForAsync();
|
||||||
|
|
||||||
|
async Task BackCloses(ILocator open, string what)
|
||||||
|
{
|
||||||
|
await Assertions.Expect(open).ToBeVisibleAsync();
|
||||||
|
await page.GoBackAsync();
|
||||||
|
await Assertions.Expect(open).ToBeHiddenAsync(new() { Timeout = 5000 });
|
||||||
|
Assert.True(new Uri(page.Url).AbsolutePath == "/", $"Back after the {what} left the feed for {page.Url}");
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.Locator("[data-nav-toggle]").TapAsync();
|
||||||
|
await BackCloses(page.Locator("[data-nav-panel]"), "menu");
|
||||||
|
|
||||||
|
var card = Card(page, "footer [title='Other']");
|
||||||
|
await card.Locator("footer [title='Other'] > [role=button]").TapAsync();
|
||||||
|
await BackCloses(card.Locator(".dropdown-open > [data-sheet]"), "More sheet");
|
||||||
|
|
||||||
|
await page.Locator("[data-compose-open]").TapAsync();
|
||||||
|
await BackCloses(page.Locator("[data-sheet]:has(textarea[data-compose])"), "composer");
|
||||||
|
|
||||||
|
await page.Locator("button[title='Globe']").TapAsync();
|
||||||
|
await page.Locator("[data-globe-expand]").TapAsync();
|
||||||
|
await BackCloses(page.Locator("aside[role=dialog]"), "full-screen globe");
|
||||||
|
|
||||||
|
// with nothing open, Back leaves the feed as before
|
||||||
|
await page.GoBackAsync();
|
||||||
|
await page.WaitForURLAsync(url => new Uri(url).AbsolutePath == "/explore", new() { Timeout = 10000 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static async Task<JsonNode> Latest(Pasture.Persona persona) =>
|
static async Task<JsonNode> Latest(Pasture.Persona persona) =>
|
||||||
(await PersonaApi.Call(persona, $"/api/v1/accounts/{persona.LocalId}/statuses?limit=1"))!.AsArray().FirstOrDefault();
|
(await PersonaApi.Call(persona, $"/api/v1/accounts/{persona.LocalId}/statuses?limit=1"))!.AsArray().FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,6 +140,16 @@ document.addEventListener("click", event => {
|
|||||||
window.logFromJs(e.message, "openThread")
|
window.logFromJs(e.message, "openThread")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// The Back button (a popstate) as opposed to the app's own navigation: Blazor's location-changing handlers can't tell
|
||||||
|
// them apart, and MainLayout lets Back close an open overlay but lets the app's navigation through. Registered for the
|
||||||
|
// capture phase, so it runs before Blazor's own listener.
|
||||||
|
let backAt = 0
|
||||||
|
window.addEventListener("popstate", () => backAt = Date.now(), { capture: true })
|
||||||
|
window.tookBack = () => {
|
||||||
|
const back = Date.now() - backAt < 1000
|
||||||
|
backAt = 0
|
||||||
|
return back
|
||||||
|
}
|
||||||
window.canHover = () => {
|
window.canHover = () => {
|
||||||
try {
|
try {
|
||||||
return window.matchMedia("(hover: hover)").matches
|
return window.matchMedia("(hover: hover)").matches
|
||||||
|
|||||||
Reference in new issue
Block a user