Support custom events and non-bubbling standard events (#722)
* Support non-bubbling events * Support responding to arbitrary events. E2E coverage of this and bubbling. * Rename E2E test files to avoid clash with other PR
This commit is contained in:
parent
f61ed4df4f
commit
be8f2d43ee
|
|
@ -1,5 +1,10 @@
|
|||
import { EventForDotNet, UIEventArgs } from './EventForDotNet';
|
||||
|
||||
const nonBubblingEvents = toLookup([
|
||||
'abort', 'blur', 'change', 'error', 'focus', 'load', 'loadend', 'loadstart', 'mouseenter', 'mouseleave',
|
||||
'progress', 'reset', 'scroll', 'submit', 'unload', 'DOMNodeInsertedIntoDocument', 'DOMNodeRemovedFromDocument'
|
||||
]);
|
||||
|
||||
export interface OnEventCallback {
|
||||
(event: Event, componentId: number, eventHandlerId: number, eventArgs: EventForDotNet<UIEventArgs>): void;
|
||||
}
|
||||
|
|
@ -64,6 +69,7 @@ export class EventDelegator {
|
|||
// Scan up the element hierarchy, looking for any matching registered event handlers
|
||||
let candidateElement = evt.target as Element | null;
|
||||
let eventArgs: EventForDotNet<UIEventArgs> | null = null; // Populate lazily
|
||||
const eventIsNonBubbling = nonBubblingEvents.hasOwnProperty(evt.type);
|
||||
while (candidateElement) {
|
||||
if (candidateElement.hasOwnProperty(this.eventsCollectionKey)) {
|
||||
const handlerInfos = candidateElement[this.eventsCollectionKey];
|
||||
|
|
@ -78,7 +84,7 @@ export class EventDelegator {
|
|||
}
|
||||
}
|
||||
|
||||
candidateElement = candidateElement.parentElement;
|
||||
candidateElement = eventIsNonBubbling ? null : candidateElement.parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -105,7 +111,11 @@ class EventInfoStore {
|
|||
this.countByEventName[eventName]++;
|
||||
} else {
|
||||
this.countByEventName[eventName] = 1;
|
||||
document.addEventListener(eventName, this.globalListener);
|
||||
|
||||
// To make delegation work with non-bubbling events, register a 'capture' listener.
|
||||
// We preserve the non-bubbling behavior by only dispatching such events to the targeted element.
|
||||
const useCapture = nonBubblingEvents.hasOwnProperty(eventName);
|
||||
document.addEventListener(eventName, this.globalListener, useCapture);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -154,3 +164,9 @@ interface EventHandlerInfo {
|
|||
componentId: number;
|
||||
eventHandlerId: number;
|
||||
}
|
||||
|
||||
function toLookup(items: string[]): { [key: string]: boolean } {
|
||||
const result = {};
|
||||
items.forEach(value => { result[value] = true; });
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering
|
|||
return JsonUtil.Deserialize<UIKeyboardEventArgs>(eventArgsJson);
|
||||
case "change":
|
||||
return JsonUtil.Deserialize<UIChangeEventArgs>(eventArgsJson);
|
||||
case "unknown":
|
||||
return JsonUtil.Deserialize<UIEventArgs>(eventArgsJson);
|
||||
default:
|
||||
throw new ArgumentException($"Unsupported value '{eventArgsType}'.", nameof(eventArgsType));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using BasicTestApp;
|
||||
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure;
|
||||
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures;
|
||||
using OpenQA.Selenium;
|
||||
using System;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
|
||||
{
|
||||
public class EventBubblingTest : BasicTestAppTestBase
|
||||
{
|
||||
// Note that currently we only support custom events if they have bubble:true.
|
||||
// That's because the event delegator doesn't know which custom events bubble and which don't,
|
||||
// so it doesn't know whether to register a normal handler or a capturing one. If this becomes
|
||||
// a problem, we could consider registering both types of handler and just bailing out from
|
||||
// the one that doesn't match the 'bubbles' flag on the received event object.
|
||||
|
||||
public EventBubblingTest(
|
||||
BrowserFixture browserFixture,
|
||||
DevHostServerFixture<Program> serverFixture,
|
||||
ITestOutputHelper output)
|
||||
: base(browserFixture, serverFixture, output)
|
||||
{
|
||||
Navigate(ServerPathBase, noReload: true);
|
||||
MountTestComponent<EventBubblingComponent>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BubblingStandardEvent_FiredOnElementWithHandler()
|
||||
{
|
||||
Browser.FindElement(By.Id("button-with-onclick")).Click();
|
||||
|
||||
// Triggers event on target and ancestors with handler in upwards direction
|
||||
Assert.Equal(
|
||||
new[] { "target onclick", "parent onclick" },
|
||||
GetLogLines());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BubblingStandardEvent_FiredOnElementWithoutHandler()
|
||||
{
|
||||
Browser.FindElement(By.Id("button-without-onclick")).Click();
|
||||
|
||||
// Triggers event on ancestors with handler in upwards direction
|
||||
Assert.Equal(
|
||||
new[] { "parent onclick" },
|
||||
GetLogLines());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BubblingCustomEvent_FiredOnElementWithHandler()
|
||||
{
|
||||
TriggerCustomBubblingEvent("element-with-onsneeze", "sneeze");
|
||||
|
||||
// Triggers event on target and ancestors with handler in upwards direction
|
||||
Assert.Equal(
|
||||
new[] { "target onsneeze", "parent onsneeze" },
|
||||
GetLogLines());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BubblingCustomEvent_FiredOnElementWithoutHandler()
|
||||
{
|
||||
TriggerCustomBubblingEvent("element-without-onsneeze", "sneeze");
|
||||
|
||||
// Triggers event on ancestors with handler in upwards direction
|
||||
Assert.Equal(
|
||||
new[] { "parent onsneeze" },
|
||||
GetLogLines());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NonBubblingEvent_FiredOnElementWithHandler()
|
||||
{
|
||||
Browser.FindElement(By.Id("input-with-onfocus")).Click();
|
||||
|
||||
// Triggers event only on target, not other ancestors with event handler
|
||||
Assert.Equal(new[] { "target onfocus" }, GetLogLines());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NonBubblingEvent_FiredOnElementWithoutHandler()
|
||||
{
|
||||
Browser.FindElement(By.Id("input-without-onfocus")).Click();
|
||||
|
||||
// Triggers no event
|
||||
Assert.Empty(GetLogLines());
|
||||
}
|
||||
|
||||
private string[] GetLogLines()
|
||||
=> Browser.FindElement(By.TagName("textarea"))
|
||||
.GetAttribute("value")
|
||||
.Replace("\r\n", "\n")
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
private void TriggerCustomBubblingEvent(string elementId, string eventName)
|
||||
{
|
||||
var jsExecutor = (IJavaScriptExecutor)Browser;
|
||||
jsExecutor.ExecuteScript(
|
||||
$"document.getElementById('{elementId}').dispatchEvent(" +
|
||||
$" new Event('{eventName}', {{ bubbles: true }})" +
|
||||
$")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<h3>Bubbling standard event</h3>
|
||||
|
||||
<div onclick="@(() => LogEvent("parent onclick"))">
|
||||
<button id="button-with-onclick" onclick="@(() => LogEvent("target onclick"))">Button with onclick handler</button>
|
||||
<button id="button-without-onclick" >Button without onclick handler</button>
|
||||
</div>
|
||||
|
||||
<h3>Bubbling custom event</h3>
|
||||
|
||||
<div onsneeze="@(new Action(() => LogEvent("parent onsneeze")))">
|
||||
<div id="element-with-onsneeze" onsneeze="@(new Action(() => LogEvent("target onsneeze")))">Element with onsneeze handler</div>
|
||||
<div id="element-without-onsneeze" >Element without onsneeze handler</div>
|
||||
</div>
|
||||
|
||||
<h3>Non-bubbling standard event</h3>
|
||||
|
||||
<!-- The new Action(...) is needed until we add support for onfocus -->
|
||||
<div onfocus="@(new Action(() => LogEvent("parent onfocus")))">
|
||||
<p>With onfocus: <input id="input-with-onfocus" onfocus="@(new Action(() => LogEvent("target onfocus")))" /></p>
|
||||
<p>Without onfocus: <input id="input-without-onfocus" /></p>
|
||||
</div>
|
||||
|
||||
<h3>Event log</h3>
|
||||
|
||||
<textarea readonly bind="@logValue"></textarea>
|
||||
|
||||
@functions {
|
||||
string logValue = string.Empty;
|
||||
|
||||
void LogEvent(string message)
|
||||
{
|
||||
logValue += message + Environment.NewLine;
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@
|
|||
<option value="BasicTestApp.ElementRefComponent">Element ref component</option>
|
||||
<option value="BasicTestApp.ComponentRefComponent">Component ref component</option>
|
||||
<option value="BasicTestApp.AfterRenderInteropComponent">After-render interop component</option>
|
||||
<option value="BasicTestApp.EventCasesComponent">Event cases</option>
|
||||
<!--<option value="BasicTestApp.RouterTest.Default">Router</option> Excluded because it requires additional setup to work correctly when loaded manually -->
|
||||
</select>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue