You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.24 08:36:04