Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix shortcuts changing multiple browser windows #2470

Merged
merged 2 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
@using Aspire.Dashboard.Resources
@inject IStringLocalizer<ControlsStrings> Loc
@implements Aspire.Dashboard.Model.IGlobalKeydownListener
@typeparam T

<div class="summary-details-container">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Utils;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
Expand All @@ -11,7 +12,7 @@

namespace Aspire.Dashboard.Components.Controls;

public partial class SummaryDetailsView<T>
public partial class SummaryDetailsView<T> : IGlobalKeydownListener, IDisposable
{
[Parameter]
public RenderFragment? Summary { get; set; }
Expand Down Expand Up @@ -60,6 +61,9 @@ public partial class SummaryDetailsView<T>
[Inject]
public required IJSRuntime JS { get; set; }

[Inject]
public required ShortcutManager ShortcutManager { get; set; }

private readonly Icon _splitHorizontalIcon = new Icons.Regular.Size16.SplitHorizontal();
private readonly Icon _splitVerticalIcon = new Icons.Regular.Size16.SplitVertical();

Expand Down
2 changes: 0 additions & 2 deletions src/Aspire.Dashboard/Components/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
@using Aspire.Dashboard.Components.CustomIcons;
@using Aspire.Dashboard.Resources
@inherits LayoutComponentBase
@implements Aspire.Dashboard.Model.IGlobalKeydownListener
@implements IAsyncDisposable

<div class="layout">
<div class="aspire-icon">
Expand Down
25 changes: 18 additions & 7 deletions src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@

namespace Aspire.Dashboard.Components.Layout;

public partial class MainLayout
public partial class MainLayout : IGlobalKeydownListener, IAsyncDisposable
{
private IDisposable? _themeChangedSubscription;
private IDisposable? _locationChangingRegistration;
private IJSObjectReference? _jsModule;
private IJSObjectReference? _keyboardHandlers;
private DotNetObjectReference<ShortcutManager>? _shortcutManagerReference;
private IDialogReference? _openPageDialog;

private const string SettingsDialogId = "SettingsDialog";
Expand All @@ -40,6 +41,9 @@ public partial class MainLayout
[Inject]
public required IDashboardClient DashboardClient { get; init; }

[Inject]
public required ShortcutManager ShortcutManager { get; init; }

protected override void OnInitialized()
{
// Theme change can be triggered from the settings dialog. This logic applies the new theme to the browser window.
Expand Down Expand Up @@ -75,7 +79,8 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
if (firstRender)
{
_jsModule = await JS.InvokeAsync<IJSObjectReference>("import", "/js/theme.js");
_keyboardHandlers = await JS.InvokeAsync<IJSObjectReference>("window.registerGlobalKeydownListener", typeof(App).Assembly.GetName().Name);
_shortcutManagerReference = DotNetObjectReference.Create(ShortcutManager);
_keyboardHandlers = await JS.InvokeAsync<IJSObjectReference>("window.registerGlobalKeydownListener", _shortcutManagerReference);
ShortcutManager.AddGlobalKeydownListener(this);

DialogService.OnDialogCloseRequested += (reference, _) =>
Expand Down Expand Up @@ -178,15 +183,21 @@ public async Task OnPageKeyDownAsync(KeyboardEventArgs args)
}
}

public void Dispose()
public async ValueTask DisposeAsync()
{
_shortcutManagerReference?.Dispose();
_themeChangedSubscription?.Dispose();
_locationChangingRegistration?.Dispose();
ShortcutManager.RemoveGlobalKeydownListener(this);
}

public async ValueTask DisposeAsync()
{
await JS.InvokeVoidAsync("window.unregisterGlobalKeydownListener", _keyboardHandlers);
try
{
await JS.InvokeVoidAsync("window.unregisterGlobalKeydownListener", _keyboardHandlers);
}
catch (JSDisconnectedException)
{
// Per https://learn.microsoft.com/aspnet/core/blazor/javascript-interoperability/?view=aspnetcore-7.0#javascript-interop-calls-without-a-circuit
// this is one of the calls that will fail if the circuit is disconnected, and we just need to catch the exception so it doesn't pollute the logs
}
}
}
2 changes: 2 additions & 0 deletions src/Aspire.Dashboard/DashboardWebApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ public DashboardWebApplication(Action<WebApplicationBuilder>? configureBuilder =
builder.Services.AddFluentUIComponents();

builder.Services.AddSingleton<ThemeManager>();
// ShortcutManager is scoped because we want shortcuts to apply one browser window.
builder.Services.AddScoped<ShortcutManager>();

builder.Services.AddLocalization();

Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Model/IGlobalKeydownListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace Aspire.Dashboard.Model;

public interface IGlobalKeydownListener : IDisposable
public interface IGlobalKeydownListener
{
Task OnPageKeyDownAsync(KeyboardEventArgs args);
}
21 changes: 13 additions & 8 deletions src/Aspire.Dashboard/ShortcutManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,28 @@

namespace Aspire.Dashboard;

public static class ShortcutManager
public sealed class ShortcutManager : IDisposable
{
private static readonly ConcurrentDictionary<IGlobalKeydownListener, IGlobalKeydownListener> s_globalKeydownListenerComponents = [];
private readonly ConcurrentDictionary<IGlobalKeydownListener, IGlobalKeydownListener> _keydownListenerComponents = [];

public static void AddGlobalKeydownListener(IGlobalKeydownListener listener)
public void AddGlobalKeydownListener(IGlobalKeydownListener listener)
{
s_globalKeydownListenerComponents[listener] = listener;
_keydownListenerComponents[listener] = listener;
}

public static void RemoveGlobalKeydownListener(IGlobalKeydownListener listener)
public void RemoveGlobalKeydownListener(IGlobalKeydownListener listener)
{
s_globalKeydownListenerComponents.Remove(listener, out _);
_keydownListenerComponents.Remove(listener, out _);
}

[JSInvokable]
public static Task OnGlobalKeyDown(KeyboardEventArgs args)
public Task OnGlobalKeyDown(KeyboardEventArgs args)
{
return Task.WhenAll(s_globalKeydownListenerComponents.Values.Select(component => component.OnPageKeyDownAsync(args)));
return Task.WhenAll(_keydownListenerComponents.Values.Select(component => component.OnPageKeyDownAsync(args)));
}

public void Dispose()
{
_keydownListenerComponents.Clear();
}
}
16 changes: 11 additions & 5 deletions src/Aspire.Dashboard/wwwroot/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,27 +275,33 @@ function isInputElement(element, isRoot, isShadowRoot) {
const tag = element.tagName.toLowerCase();
// comes from https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event
// fluent-select does not use <select /> element
if (tag === "input" || tag === "textarea" || tag === "select" || tag === "fluent-select") return true;
if (tag === "input" || tag === "textarea" || tag === "select" || tag === "fluent-select") {
return true;
}

if (isShadowRoot || isRoot) {
const elementChildren = element.children;
for (let i = 0; i < elementChildren.length; i++) {
if (isInputElement(elementChildren[i], false, isShadowRoot)) return true;
if (isInputElement(elementChildren[i], false, isShadowRoot)) {
return true;
}
}
}

const shadowRoot = element.shadowRoot;
if (shadowRoot) {
const shadowRootChildren = shadowRoot.children;
for (let i = 0; i < shadowRootChildren.length; i++) {
if (isInputElement(shadowRootChildren[i], false, true)) return true;
if (isInputElement(shadowRootChildren[i], false, true)) {
return true;
}
}
}

return false;
}

window.registerGlobalKeydownListener = function(assemblyName) {
window.registerGlobalKeydownListener = function(shortcutManager) {
const serializeEvent = function (e) {
if (e) {
return {
Expand All @@ -314,7 +320,7 @@ window.registerGlobalKeydownListener = function(assemblyName) {

const keydownListener = function (e) {
if (!isActiveElementInput()) {
DotNet.invokeMethodAsync(assemblyName, 'OnGlobalKeyDown', serializeEvent(e))
shortcutManager.invokeMethodAsync('OnGlobalKeyDown', serializeEvent(e));
}
}

Expand Down