Blazor中focusout事件获取relatedTarget的方法及相关疑问
在Blazor中实现多选择下拉组件的焦点管理问题
我想在Blazor中实现类似下面原生JS示例的多选择下拉功能,但遇到两个核心问题:
- Blazor的
FocusEventArgs不支持relatedTarget参数,不知道怎么在focusout事件中获取这个值 - 尝试过用取消令牌+延迟隐藏的方案,但效果不好
另外,官方文档显示Blazor的自定义事件参数里没有relatedTarget,这是不是意味着不建议在自定义参数处理器里依赖这个参数?求解决方案建议。
原生JS参考实现
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div class="multiselect" onfocusin="focusin"> <input class="search" type="search" placeholder="search..."></input> <div class="dropdown" tabindex="0"> <select multiple class="select"> <option>aaaa</option> <option>bbbb</option> <option>cccc</option> </select> <button class="btnOk" type="button">Select</button> <button class="btnCancel" type="button">Cancel</button> </div> </div> <script> const btnOk = document.querySelector('.btnOk'); const btnCancel = document.querySelector('.btnCancel'); const dropdown = document.querySelector('.dropdown'); const select = document.querySelector('.select'); const search = document.querySelector('.search'); const multi = document.querySelector('.multiselect'); multi.addEventListener("focusin", focusin); multi.addEventListener("focusout", focusout); hide(); function focusin(event) { show() } function focusout(event) { console.log(event.relatedTarget); if (needToHide1(event)) { hide(); } } function needToHide(focusoutEvent) { return event.relatedTarget != select && event.relatedTarget != search && event.relatedTarget != dropdown && event.relatedTarget != btnOk && event.relatedTarget != btnCancel } function show() { dropdown.style.display = "block" } function hide() { dropdown.style.display = "none"; } </script> <style> .multiselect { width: 300px; font-size: 20px; } .select { width: 100%; font-size: 20px; } .search { font-size: 20px; width: 100%; } </style> </body> </html>
解决方案建议
1. 通过JS互操作获取relatedTarget
Blazor原生事件参数不包含relatedTarget,但可以通过JS互操作直接处理焦点事件,再把判断结果传给Blazor组件:
Blazor组件代码 (.razor)
<div class="multiselect" @ref="MultiSelectElement"> <input class="search" type="search" placeholder="search..." @onfocus="ShowDropdown" /> <div class="dropdown" tabindex="0" style="display: @(IsDropdownVisible ? "block" : "none")"> <select multiple class="select"> <option>aaaa</option> <option>bbbb</option> <option>cccc</option> </select> <button class="btnOk" type="button" @onclick="HandleOk">Select</button> <button class="btnCancel" type="button" @onclick="HandleCancel">Cancel</button> </div> </div> @code { private ElementReference MultiSelectElement; private bool IsDropdownVisible { get; set; } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await JS.InvokeVoidAsync("registerMultiselectFocusHandlers", MultiSelectElement, DotNetObjectReference.Create(this)); } } [JSInvokable] public void OnMultiselectFocusOut(bool shouldHide) { if (shouldHide) { IsDropdownVisible = false; StateHasChanged(); } } private void ShowDropdown() { IsDropdownVisible = true; } private void HandleOk() { // 处理确认逻辑 IsDropdownVisible = false; } private void HandleCancel() { IsDropdownVisible = false; } }
全局JS脚本
window.registerMultiselectFocusHandlers = (element, dotNetRef) => { const internalElements = Array.from(element.querySelectorAll('.search, .select, .btnOk, .btnCancel, .dropdown')); element.addEventListener('focusout', (event) => { const isFocusingInternal = internalElements.includes(event.relatedTarget); dotNetRef.invokeMethodAsync('OnMultiselectFocusOut', !isFocusingInternal); }); element.addEventListener('focusin', () => { dotNetRef.invokeMethodAsync('OnMultiselectFocusOut', false); }); }
2. 避免依赖relatedTarget的焦点组方案
如果不想依赖relatedTarget,可以通过标记内部焦点状态来实现:
- 给组件内所有可聚焦元素添加
@onfocus事件,标记焦点在组件内部 - 在
focusout事件触发时,延迟短时间再检查焦点状态,决定是否隐藏下拉框
示例代码:
<div class="multiselect"> <input class="search" type="search" placeholder="search..." @onfocus="() => IsFocusedInside = true" @onfocusout="OnContainerFocusOut" /> <div class="dropdown" tabindex="0" style="display: @(IsDropdownVisible ? "block" : "none")"> <select multiple class="select" @onfocus="() => IsFocusedInside = true" @onfocusout="OnContainerFocusOut" /> <button class="btnOk" type="button" @onclick="HandleOk" @onfocus="() => IsFocusedInside = true" /> <button class="btnCancel" type="button" @onclick="HandleCancel" @onfocus="() => IsFocusedInside = true" /> </div> </div> @code { private bool IsDropdownVisible { get; set; } private bool IsFocusedInside { get; set; } private CancellationTokenSource? _cts; private void ShowDropdown() { IsDropdownVisible = true; IsFocusedInside = true; } private async void OnContainerFocusOut() { _cts?.Cancel(); _cts = new CancellationTokenSource(); try { await Task.Delay(100, _cts.Token); if (!IsFocusedInside) { IsDropdownVisible = false; StateHasChanged(); } } catch (OperationCanceledException) { // 焦点移到内部元素,取消隐藏 } finally { IsFocusedInside = false; _cts.Dispose(); } } private void HandleOk() { IsDropdownVisible = false; } private void HandleCancel() { IsDropdownVisible = false; } }
关于relatedTarget的使用建议
Blazor官方未将relatedTarget纳入原生事件参数,核心原因是跨平台兼容性——Server模式下JS事件的部分属性无法高效序列化传递到.NET端。
如果必须使用relatedTarget,优先通过JS互操作在前端完成判断逻辑,再把结果传给Blazor组件,不要尝试自定义事件参数处理器来获取它,这种方式不仅实现复杂,还可能存在跨平台兼容性隐患。
内容的提问来源于stack exchange,提问作者nzim
相关产品推荐
相关产品推荐

