Blazor自定义模态对话框无焦点、键盘事件不触发问题求解
问题原因及修复方案
缺陷1:Tab键焦点无法限定在对话框内部
缺失配置
- 对话框根容器未添加
tabindex="-1"属性:普通div默认不可聚焦,也无法作为焦点范围的边界 - 缺少焦点陷阱(Focus Trap)逻辑,未限制Tab/Shift+Tab的焦点移动范围
- 对话框弹出时未主动将焦点定位到对话框内部
缺陷2:Esc/Enter按键事件未触发
缺失配置
- 根容器不可聚焦导致绑定的
@onkeydown事件根本无法接收输入:只有获得焦点的元素才能触发键盘事件 - 未在对话框显示时主动给根容器设置焦点,键盘事件自然无法冒泡到绑定元素
完整修改方案
1. 模板部分修改
给根div添加tabindex="-1"和元素引用,方便后续JS操作:
<!-- 新增tabindex和@ref属性 --> <div @ref="modalOverlayRef" class="custom-modal-overlay fade @showClass" style="@style" aria-modal="@Show" aria-hidden="@(!Show)" aria-labelledby="Dialog" @onclick="HandleCancel" @onkeydown="OverlayKeyDown" tabindex="-1"> <div class="custom-modal-dialog" @onclick:stopPropagation="true"> <div class="custom-modal-header"> <h1>@Title</h1> @if (!ShowCancel) { <div class="cancel-icon" @onclick="HandleCancel"> <span class="material-icons-outlined cancel-icon" role="img" aria-label="Cancel">clear</span> </div> } </div> <div class="custom-modal-content"> @ChildContent </div> <div class="custom-modal-footer"> @if (ShowCancel) { <button type="button" class="btn btn-sm btn-outline-secondary" @onclick="HandleCancel">Cancel</button> } <button type="submit" class="btn btn-sm btn-primary" @onclick="HandleConfirm">OK</button> </div> </div> </div>
2. @code部分修改
注入JS运行时,添加焦点控制逻辑,完善按键判断:
@code { // 新增注入JS运行时 [Inject] private IJSRuntime JSRuntime { get; set; } // 新增根元素引用、打开前焦点存储变量 private ElementReference modalOverlayRef; private ElementReference? previousActiveElement; // 原有参数保持不变 [Parameter] public bool ShowCancel { get; set; } = true; [Parameter] public bool Show { get; set; } [Parameter] public EventCallback<bool> ShowChanged { get; set; } [Parameter] public string Title { get; set; } [Parameter] public RenderFragment ChildContent { get; set; } [Parameter] public EventCallback OnCancel { get; set; } [Parameter] public EventCallback OnConfirm { get; set; } private string showClass => Show ? "show" : string.Empty; private string style => Show ? "display: block;" : "display: none;"; // 新增监听对话框显隐状态,控制焦点切换 protected override async Task OnParametersSetAsync() { if (Show) { // 存储打开对话框前的焦点元素,关闭时归还 previousActiveElement = await JSRuntime.InvokeAsync<ElementReference>("document.activeElement"); // 主动给对话框根元素设置焦点,触发键盘事件监听 await JSRuntime.InvokeVoidAsync("HTMLElement.prototype.focus.call", modalOverlayRef); } else if (previousActiveElement.HasValue) { // 关闭对话框后把焦点归还给之前的元素 await JSRuntime.InvokeVoidAsync("HTMLElement.prototype.focus.call", previousActiveElement.Value); previousActiveElement = null; } } // 完善按键逻辑+焦点陷阱实现 private async Task OverlayKeyDown(KeyboardEventArgs e) { // 处理Esc键关闭 if (e.Key == "Escape") { e.PreventDefault(); await HandleCancel(); return; } // 处理Enter键确认 if (e.Key == "Enter") { e.PreventDefault(); await HandleConfirm(); return; } // 处理Tab键,实现焦点不跑出对话框 if (e.Key == "Tab") { // 获取对话框内所有可聚焦元素 var focusableElements = await JSRuntime.InvokeAsync<ElementReference[]>("eval", $"Array.from(document.querySelector('.custom-modal-overlay.show').querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'))"); if (focusableElements.Length == 0) return; var firstElement = focusableElements[0]; var lastElement = focusableElements[^1]; var currentActive = await JSRuntime.InvokeAsync<ElementReference>("document.activeElement"); // Shift+Tab到第一个元素时自动切到最后一个 if (e.ShiftKey && currentActive.Id == firstElement.Id) { e.PreventDefault(); await JSRuntime.InvokeVoidAsync("HTMLElement.prototype.focus.call", lastElement); } // Tab到最后一个元素时自动切到第一个 else if (!e.ShiftKey && currentActive.Id == lastElement.Id) { e.PreventDefault(); await JSRuntime.InvokeVoidAsync("HTMLElement.prototype.focus.call", firstElement); } } } // 原有方法调整为异步,避免异步调用警告 private async Task HandleCancel() { Show = false; await ShowChanged.InvokeAsync(Show); await OnCancel.InvokeAsync(); } private async Task HandleConfirm() { Show = false; await ShowChanged.InvokeAsync(Show); await OnConfirm.InvokeAsync(); } }
内容的提问来源于stack exchange,提问作者Jakob Lithner
相关产品推荐
相关产品推荐

