Fixed escape key handling

This commit is contained in:
Thorsten Sommer 2026-06-06 09:51:42 +02:00
parent 6d8b59bb02
commit 075934343e
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
3 changed files with 122 additions and 59 deletions

View File

@ -1,63 +1,59 @@
@inherits MSGComponentBase
<MudDialog>
<DialogContent>
<div @onkeydown="@this.HandleDialogKeyDown">
<MudText Typo="Typo.body1">
@this.Message
</MudText>
<MudList T="Guid" @bind-SelectedValue="@this.selectedWorkspace">
@foreach (var workspace in this.workspaces)
{
<MudListItem Text="@workspace.Name" Icon="@Icons.Material.Filled.Description" Value="@workspace.WorkspaceId" />
}
</MudList>
</div>
<MudText Typo="Typo.body1">
@this.Message
</MudText>
<MudList T="Guid" @bind-SelectedValue="@this.selectedWorkspace">
@foreach (var workspace in this.workspaces)
{
<MudListItem Text="@workspace.Name" Icon="@Icons.Material.Filled.Description" Value="@workspace.WorkspaceId" />
}
</MudList>
</DialogContent>
<DialogActions>
<div style="width: 100%;" @onkeydown="@this.HandleDialogKeyDown">
<MudStack Style="width: 100%;" Spacing="2">
<MudDivider/>
<MudStack Style="width: 100%;" Spacing="2">
<MudDivider/>
@if (this.showCreateWorkspaceForm)
{
<MudForm @ref="this.createWorkspaceForm">
<MudTextField T="string"
@bind-Text="@this.newWorkspaceName"
Variant="Variant.Outlined"
AutoGrow="@false"
Lines="1"
Label="@T("Workspace name")"
Immediate="@true"
Disabled="@this.isCreatingWorkspace"
OnKeyDown="@this.HandleNewWorkspaceNameKeyDown"
Validation="@this.ValidateNewWorkspaceName" />
</MudForm>
}
else
{
<MudButton StartIcon="@Icons.Material.Filled.LibraryAdd" Variant="Variant.Filled" OnClick="@this.ShowCreateWorkspaceForm">
@T("Create new workspace")
</MudButton>
}
<MudStack Row="@true" Justify="Justify.FlexEnd" AlignItems="AlignItems.Center" Wrap="Wrap.NoWrap" Spacing="2">
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
@T("Cancel")
</MudButton>
@if (this.showCreateWorkspaceForm)
{
<MudForm @ref="this.createWorkspaceForm">
<MudTextField T="string"
@bind-Text="@this.newWorkspaceName"
Variant="Variant.Outlined"
AutoGrow="@false"
Lines="1"
Label="@T("Workspace name")"
Immediate="@true"
Disabled="@this.isCreatingWorkspace"
OnKeyDown="@this.HandleNewWorkspaceNameKeyDown"
Validation="@this.ValidateNewWorkspaceName" />
</MudForm>
<MudButton OnClick="@this.CreateWorkspaceAsync" Variant="Variant.Filled" Color="Color.Info" Disabled="@this.isCreatingWorkspace">
@T("Add workspace")
</MudButton>
}
else
{
<MudButton StartIcon="@Icons.Material.Filled.LibraryAdd" Variant="Variant.Filled" OnClick="@this.ShowCreateWorkspaceForm">
@T("Create new workspace")
<MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="Color.Info" Disabled="@(this.selectedWorkspace == Guid.Empty)">
@this.ConfirmText
</MudButton>
}
<MudStack Row="@true" Justify="Justify.FlexEnd" AlignItems="AlignItems.Center" Wrap="Wrap.NoWrap" Spacing="2">
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
@T("Cancel")
</MudButton>
@if (this.showCreateWorkspaceForm)
{
<MudButton OnClick="@this.CreateWorkspaceAsync" Variant="Variant.Filled" Color="Color.Info" Disabled="@this.isCreatingWorkspace">
@T("Add workspace")
</MudButton>
}
else
{
<MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="Color.Info" Disabled="@(this.selectedWorkspace == Guid.Empty)">
@this.ConfirmText
</MudButton>
}
</MudStack>
</MudStack>
</div>
</MudStack>
</DialogActions>
</MudDialog>

View File

@ -12,6 +12,9 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = null!;
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
[Parameter]
public string Message { get; set; } = string.Empty;
@ -22,7 +25,9 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
public string ConfirmText { get; set; } = "OK";
private readonly List<WorkspaceSelectionItem> workspaces = [];
private MudForm createWorkspaceForm = null!;
private readonly string escapeHandlerId = $"workspace-selection-dialog-{Guid.NewGuid():N}";
private MudForm? createWorkspaceForm;
private DotNetObjectReference<WorkspaceSelectionDialog>? dotNetReference;
private Guid selectedWorkspace;
private string newWorkspaceName = string.Empty;
private bool isCreatingWorkspace;
@ -44,6 +49,17 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
await base.OnInitializedAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
this.dotNetReference = DotNetObjectReference.Create(this);
await this.JsRuntime.InvokeVoidAsync("registerEscapeHandler", this.escapeHandlerId, this.dotNetReference);
}
await base.OnAfterRenderAsync(firstRender);
}
#endregion
private string? ValidateNewWorkspaceName(string? workspaceName)
@ -80,16 +96,6 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
await this.CreateWorkspaceAsync();
}
private void HandleDialogKeyDown(KeyboardEventArgs keyEvent)
{
var key = keyEvent.Key.ToLowerInvariant();
var code = keyEvent.Code.ToLowerInvariant();
if (key is not "escape" && code is not "escape")
return;
this.Cancel();
}
private void ShowCreateWorkspaceForm()
{
this.createWorkspaceError = null;
@ -100,6 +106,9 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
private async Task CreateWorkspaceAsync()
{
if (this.createWorkspaceForm is null)
return;
this.createWorkspaceError = null;
this.createWorkspaceErrorName = null;
await this.createWorkspaceForm.Validate();
@ -141,9 +150,40 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
this.createWorkspaceError = null;
this.createWorkspaceErrorName = null;
this.newWorkspaceName = string.Empty;
this.createWorkspaceForm.ResetValidation();
this.createWorkspaceForm?.ResetValidation();
this.showCreateWorkspaceForm = false;
}
[JSInvokable]
public async Task HandleEscapeKeyAsync()
{
await this.InvokeAsync(() =>
{
this.Cancel();
this.StateHasChanged();
});
}
private void Confirm() => this.MudDialog.Close(DialogResult.Ok(this.selectedWorkspace));
#region Overrides of MSGComponentBase
protected override void DisposeResources()
{
try
{
_ = this.JsRuntime.InvokeVoidAsync("unregisterEscapeHandler", this.escapeHandlerId).AsTask();
}
catch
{
// Ignore JS cleanup errors while the dialog is being disposed.
}
this.dotNetReference?.Dispose();
this.dotNetReference = null;
base.DisposeResources();
}
#endregion
}

View File

@ -131,3 +131,30 @@ window.formatChatInputMarkdown = function (inputId, formatType) {
return nextValue
}
const escapeHandlers = new Map()
window.registerEscapeHandler = function (id, dotNetReference) {
window.unregisterEscapeHandler(id)
const handler = function (event) {
if (event.key !== 'Escape')
return
event.preventDefault()
event.stopPropagation()
dotNetReference.invokeMethodAsync('HandleEscapeKeyAsync').catch(() => {})
}
document.addEventListener('keydown', handler, true)
escapeHandlers.set(id, handler)
}
window.unregisterEscapeHandler = function (id) {
const handler = escapeHandlers.get(id)
if (!handler)
return
document.removeEventListener('keydown', handler, true)
escapeHandlers.delete(id)
}