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

View File

@ -12,6 +12,9 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
[CascadingParameter] [CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = null!; private IMudDialogInstance MudDialog { get; set; } = null!;
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
[Parameter] [Parameter]
public string Message { get; set; } = string.Empty; public string Message { get; set; } = string.Empty;
@ -22,7 +25,9 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
public string ConfirmText { get; set; } = "OK"; public string ConfirmText { get; set; } = "OK";
private readonly List<WorkspaceSelectionItem> workspaces = []; 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 Guid selectedWorkspace;
private string newWorkspaceName = string.Empty; private string newWorkspaceName = string.Empty;
private bool isCreatingWorkspace; private bool isCreatingWorkspace;
@ -44,6 +49,17 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
await base.OnInitializedAsync(); 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 #endregion
private string? ValidateNewWorkspaceName(string? workspaceName) private string? ValidateNewWorkspaceName(string? workspaceName)
@ -80,16 +96,6 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
await this.CreateWorkspaceAsync(); 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() private void ShowCreateWorkspaceForm()
{ {
this.createWorkspaceError = null; this.createWorkspaceError = null;
@ -100,6 +106,9 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
private async Task CreateWorkspaceAsync() private async Task CreateWorkspaceAsync()
{ {
if (this.createWorkspaceForm is null)
return;
this.createWorkspaceError = null; this.createWorkspaceError = null;
this.createWorkspaceErrorName = null; this.createWorkspaceErrorName = null;
await this.createWorkspaceForm.Validate(); await this.createWorkspaceForm.Validate();
@ -141,9 +150,40 @@ public partial class WorkspaceSelectionDialog : MSGComponentBase
this.createWorkspaceError = null; this.createWorkspaceError = null;
this.createWorkspaceErrorName = null; this.createWorkspaceErrorName = null;
this.newWorkspaceName = string.Empty; this.newWorkspaceName = string.Empty;
this.createWorkspaceForm.ResetValidation(); this.createWorkspaceForm?.ResetValidation();
this.showCreateWorkspaceForm = false; 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)); 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 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)
}