Skip to content

Commit

Permalink
Fix shortcuts changing multiple browser windows (dotnet#2470)
Browse files Browse the repository at this point in the history
* Fix shortcuts changing multiple browser windows

* Clean up

(cherry picked from commit fcadd63)
  • Loading branch information
JamesNK authored and adamint committed Feb 27, 2024
1 parent 3a86e2e commit 13a46cb
Show file tree
Hide file tree
Showing 8 changed files with 50 additions and 25 deletions.
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

0 comments on commit 13a46cb

Please sign in to comment.