Skip to content

Commit 4594be6

Browse files
authored
Make event flags work without handler on the page, support preventing wheel and touch events (#62479)
1 parent 5dee190 commit 4594be6

File tree

4 files changed

+314
-6
lines changed

4 files changed

+314
-6
lines changed

src/Components/Web.JS/src/Rendering/Events/EventDelegator.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export class EventDelegator {
121121
for (const handlerInfo of infosForElement.enumerateHandlers()) {
122122
this.eventInfoStore.remove(handlerInfo.eventHandlerId);
123123
}
124+
124125
delete element[this.eventsCollectionKey];
125126
}
126127
}
@@ -135,12 +136,30 @@ export class EventDelegator {
135136

136137
public setStopPropagation(element: Element, eventName: string, value: boolean): void {
137138
const infoForElement = this.getEventHandlerInfosForElement(element, true)!;
139+
const currentValue = infoForElement.stopPropagation(eventName);
138140
infoForElement.stopPropagation(eventName, value);
141+
142+
if (!currentValue && value) {
143+
this.eventInfoStore.addGlobalListener(eventName);
144+
} else if (currentValue && !value) {
145+
this.eventInfoStore.decrementCountByEventName(eventName);
146+
}
139147
}
140148

141149
public setPreventDefault(element: Element, eventName: string, value: boolean): void {
142150
const infoForElement = this.getEventHandlerInfosForElement(element, true)!;
151+
const currentValue = infoForElement.preventDefault(eventName);
143152
infoForElement.preventDefault(eventName, value);
153+
154+
if (!currentValue && value) {
155+
// To ensure that preventDefault works for wheel and touch events,,
156+
// we need to register a listener with the passive mode explicitly disabled.
157+
// Note that this does not change behavior for other events as those
158+
// use active mode by default.
159+
this.eventInfoStore.addActiveGlobalListener(eventName);
160+
} else if (currentValue && !value) {
161+
this.eventInfoStore.decrementCountByEventName(eventName);
162+
}
144163
}
145164

146165
private onGlobalEvent(evt: Event) {
@@ -278,6 +297,25 @@ class EventInfoStore {
278297
}
279298
}
280299

300+
public addActiveGlobalListener(eventName: string) {
301+
// If this event name is an alias, update the global listener for the corresponding browser event
302+
eventName = getBrowserEventName(eventName);
303+
304+
// If the listener for this event is already registered, we recreate it to ensure
305+
// that it is using the active mode.
306+
if (Object.prototype.hasOwnProperty.call(this.countByEventName, eventName)) {
307+
this.countByEventName[eventName]++;
308+
document.removeEventListener(eventName, this.globalListener);
309+
} else {
310+
this.countByEventName[eventName] = 1;
311+
}
312+
313+
// To make delegation work with non-bubbling events, register a 'capture' listener.
314+
// We preserve the non-bubbling behavior by only dispatching such events to the targeted element.
315+
const useCapture = Object.prototype.hasOwnProperty.call(nonBubblingEvents, eventName);
316+
document.addEventListener(eventName, this.globalListener, { capture: useCapture, passive: false });
317+
}
318+
281319
public update(oldEventHandlerId: number, newEventHandlerId: number) {
282320
if (Object.prototype.hasOwnProperty.call(this.infosByEventHandlerId, newEventHandlerId)) {
283321
// Should never happen, but we want to know if it does
@@ -298,16 +336,19 @@ class EventInfoStore {
298336

299337
// If this event name is an alias, update the global listener for the corresponding browser event
300338
const eventName = getBrowserEventName(info.eventName);
301-
302-
if (--this.countByEventName[eventName] === 0) {
303-
delete this.countByEventName[eventName];
304-
document.removeEventListener(eventName, this.globalListener);
305-
}
339+
this.decrementCountByEventName(eventName);
306340
}
307341

308342
return info;
309343
}
310344

345+
public decrementCountByEventName(eventName: string) {
346+
if (--this.countByEventName[eventName] === 0) {
347+
delete this.countByEventName[eventName];
348+
document.removeEventListener(eventName, this.globalListener);
349+
}
350+
}
351+
311352
private handleEventNameAliasAdded(aliasEventName, browserEventName) {
312353
// If an event name alias gets registered later, we need to update the global listener
313354
// registrations to match. This makes it equivalent to the alias having been registered
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Globalization;
5+
using BasicTestApp;
6+
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
7+
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
8+
using Microsoft.AspNetCore.E2ETesting;
9+
using OpenQA.Selenium;
10+
using OpenQA.Selenium.Interactions;
11+
using Xunit.Abstractions;
12+
13+
namespace Microsoft.AspNetCore.Components.E2ETest.Tests;
14+
15+
public class EventFlagsTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>
16+
{
17+
public EventFlagsTest(
18+
BrowserFixture browserFixture,
19+
ToggleExecutionModeServerFixture<Program> serverFixture,
20+
ITestOutputHelper output)
21+
: base(browserFixture, serverFixture, output)
22+
{
23+
}
24+
25+
protected override void InitializeAsyncCore()
26+
{
27+
Navigate(ServerPathBase);
28+
Browser.MountTestComponent<EventFlagsComponent>();
29+
}
30+
31+
[Theory]
32+
[InlineData(true)]
33+
[InlineData(false)]
34+
public void OnMouseDown_WithPreventDefaultEnabled_DoesNotFocusButton(bool handlersEnabled)
35+
{
36+
if (!handlersEnabled)
37+
{
38+
// Disable onmousedown handlers
39+
var toggleHandlers = Browser.Exists(By.Id("toggle-handlers"));
40+
toggleHandlers.Click();
41+
}
42+
43+
var button = Browser.Exists(By.Id("mousedown-test-button"));
44+
button.Click();
45+
46+
// Check that the button has not gained focus (should not be yellow)
47+
var afterClickBackgroundColor = button.GetCssValue("background-color");
48+
Assert.DoesNotContain("255, 255, 0", afterClickBackgroundColor);
49+
}
50+
51+
[Theory]
52+
[InlineData(true)]
53+
[InlineData(false)]
54+
public void OnMouseDown_WithPreventDefaultDisabled_DoesFocusButton(bool handlersEnabled)
55+
{
56+
if (!handlersEnabled)
57+
{
58+
// Disable onmousedown handlers
59+
var toggleHandlers = Browser.Exists(By.Id("toggle-handlers"));
60+
toggleHandlers.Click();
61+
}
62+
63+
// Disable preventDefault
64+
var togglePreventDefault = Browser.Exists(By.Id("toggle-prevent-default"));
65+
togglePreventDefault.Click();
66+
67+
var button = Browser.Exists(By.Id("mousedown-test-button"));
68+
69+
// Get the initial background color and check that it is no yellow
70+
var initialBackgroundColor = button.GetCssValue("background-color");
71+
Assert.DoesNotContain("255, 255, 0", initialBackgroundColor);
72+
73+
button.Click();
74+
75+
// Check that the button has gained focus (yellow background)
76+
var afterClickBackgroundColor = button.GetCssValue("background-color");
77+
Assert.Contains("255, 255, 0", afterClickBackgroundColor);
78+
}
79+
80+
[Fact]
81+
public void OnClick_WithStopPropagationEnabled_DoesNotPropagateToParent()
82+
{
83+
var button = Browser.Exists(By.Id("stop-propagation-test-button"));
84+
button.Click();
85+
86+
var eventLog = Browser.Exists(By.Id("event-log"));
87+
Assert.Contains("mousedown handler called on child", eventLog.Text);
88+
Assert.DoesNotContain("mousedown handler called on parent", eventLog.Text);
89+
}
90+
91+
[Fact]
92+
public void OnClick_WithStopPropagationDisabled_PropagatesToParent()
93+
{
94+
// Disable stopPropagation
95+
var toggleStopPropagation = Browser.Exists(By.Id("toggle-stop-propagation"));
96+
toggleStopPropagation.Click();
97+
98+
var button = Browser.Exists(By.Id("stop-propagation-test-button"));
99+
button.Click();
100+
101+
var eventLog = Browser.Exists(By.Id("event-log"));
102+
Assert.Contains("mousedown handler called on child", eventLog.Text);
103+
Assert.Contains("mousedown handler called on parent", eventLog.Text);
104+
}
105+
106+
[Fact]
107+
public void OnWheel_WithPreventDefaultEnabled_DoesNotScrollDiv()
108+
{
109+
var scrollableDiv = Browser.Exists(By.Id("wheel-test-area"));
110+
111+
// Simulate a wheel scroll action
112+
var scrollOrigin = new WheelInputDevice.ScrollOrigin
113+
{
114+
Element = scrollableDiv,
115+
};
116+
new Actions(Browser)
117+
.ScrollFromOrigin(scrollOrigin, 0, 200)
118+
.Perform();
119+
120+
// The Selenium scrolling action always changes the scrollTop property even when the event is prevented.
121+
// For this reason, we do not check for equality with zero.
122+
var newScrollTop = int.Parse(scrollableDiv.GetDomProperty("scrollTop"), CultureInfo.InvariantCulture);
123+
Assert.True(newScrollTop < 3);
124+
}
125+
126+
[Fact]
127+
public void OnWheel_WithPreventDefaultDisabled_DoesScrollDiv()
128+
{
129+
// Disable preventDefault
130+
var togglePreventDefault = Browser.Exists(By.Id("toggle-prevent-default"));
131+
togglePreventDefault.Click();
132+
133+
var scrollableDiv = Browser.Exists(By.Id("wheel-test-area"));
134+
135+
// Simulate a wheel scroll action
136+
var scrollOrigin = new WheelInputDevice.ScrollOrigin
137+
{
138+
Element = scrollableDiv,
139+
};
140+
new Actions(Browser)
141+
.ScrollFromOrigin(scrollOrigin, 0, 200)
142+
.Perform();
143+
144+
// The Selenium scrolling action is not precise and changes the scrollTop property to e.g. 202 instead of 200.
145+
// For this reason, we do not check for equality with specific value.
146+
var newScrollTop = int.Parse(scrollableDiv.GetDomProperty("scrollTop"), CultureInfo.InvariantCulture);
147+
Assert.True(newScrollTop >= 200);
148+
}
149+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<style>
2+
button:focus {
3+
background-color: yellow !important;
4+
}
5+
</style>
6+
7+
<h1>Event flags</h1>
8+
9+
<div>
10+
<h2>Test Controls</h2>
11+
<button id="toggle-prevent-default" @onclick="TogglePreventDefault">
12+
Toggle preventDefault: @preventDefaultEnabled
13+
</button>
14+
<button id="toggle-stop-propagation" @onclick="ToggleStopPropagation">
15+
Toggle stopPropagation: @stopPropagationEnabled
16+
</button>
17+
<button id="toggle-handlers" @onclick="ToggleEventHandlers">
18+
Toggle Event Handlers: @eventHandlersEnabled
19+
</button>
20+
</div>
21+
22+
@if (eventHandlersEnabled)
23+
{
24+
<h2 @onmousedown="() => { }">OnMouseDown handler present</h2>
25+
<h2 @onwheel="() => { }">OnWheel handler present</h2>
26+
}
27+
else
28+
{
29+
<h2>OnMouseDown handler not present</h2>
30+
<h2>OnWheel handler not present</h2>
31+
}
32+
33+
<div>
34+
<h2>Test Scenarios</h2>
35+
36+
<div id="mousedown-scenario">
37+
<h3>Scenario 1: onmousedown:preventDefault</h3>
38+
<button id="mousedown-test-button" @onmousedown:preventDefault="preventDefaultEnabled">
39+
Should not focus when preventDefault is enabled
40+
</button>
41+
</div>
42+
43+
<div id="stoppropagation-scenario">
44+
<h3>Scenario 2: onclick:stopPropagation</h3>
45+
<div @onclick="OnParentMouseDown">
46+
<p>Parent container</p>
47+
<button id="stop-propagation-test-button" @onclick="OnChildMouseDown"
48+
@onclick:stopPropagation="stopPropagationEnabled">
49+
Should not propagate the Blazor event when stopPropagation is enabled
50+
</button>
51+
</div>
52+
</div>
53+
54+
<div id="wheel-scenario">
55+
<h3>Scenario 3: wheel preventDefault (passive vs active)</h3>
56+
<div id="wheel-test-area" @onwheel:preventDefault="preventDefaultEnabled"
57+
style="width: 400px; height: 200px; border: 2px solid black; overflow-y: scroll;">
58+
<p>Try scrolling with mouse wheel in this area.</p>
59+
<p>If preventDefault is true, scrolling should be blocked.</p>
60+
<p>If preventDefault is false, scrolling should work normally.</p>
61+
<div style="height: 500px;">
62+
<p>This content makes the area scrollable</p>
63+
<p style="margin-top: 200px;">More</p>
64+
<p style="margin-top: 200px;">More</p>
65+
</div>
66+
</div>
67+
</div>
68+
</div>
69+
70+
<pre id="event-log">@eventLog</pre>
71+
72+
73+
@code {
74+
private bool preventDefaultEnabled = true;
75+
private bool stopPropagationEnabled = true;
76+
private bool eventHandlersEnabled = true;
77+
78+
private string eventLog = string.Empty;
79+
80+
private void TogglePreventDefault()
81+
{
82+
preventDefaultEnabled = !preventDefaultEnabled;
83+
StateHasChanged();
84+
}
85+
86+
private void ToggleStopPropagation()
87+
{
88+
stopPropagationEnabled = !stopPropagationEnabled;
89+
StateHasChanged();
90+
}
91+
92+
private void ToggleEventHandlers()
93+
{
94+
eventHandlersEnabled = !eventHandlersEnabled;
95+
StateHasChanged();
96+
}
97+
98+
void LogEvent(string message)
99+
{
100+
if (eventLog != string.Empty)
101+
{
102+
eventLog += Environment.NewLine;
103+
}
104+
105+
eventLog += message;
106+
}
107+
108+
private void OnChildMouseDown()
109+
{
110+
LogEvent("mousedown handler called on child");
111+
}
112+
113+
private void OnParentMouseDown()
114+
{
115+
LogEvent("mousedown handler called on parent");
116+
}
117+
}

src/Components/test/testassets/BasicTestApp/Index.razor

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
<option value="BasicTestApp.EventCustomArgsComponent">Event custom arguments</option>
3737
<option value="BasicTestApp.EventDisablingComponent">Event disabling</option>
3838
<option value="BasicTestApp.EventDuringBatchRendering">Event during batch rendering</option>
39-
<option value="BasicTestApp.EventPreventDefaultComponent">Event preventDefault</option>
39+
<option value="BasicTestApp.EventPreventDefaultComponent">Event preventDefault with submit</option>
40+
<option value="BasicTestApp.EventFlagsComponent">Event flags (preventDefault/stopPropagation)</option>
4041
<option value="BasicTestApp.ExternalContentPackage">External content package</option>
4142
<option value="BasicTestApp.FocusEventComponent">Focus events</option>
4243
<option value="BasicTestApp.FormsTest.InputFocusComponent">Input Focus</option>

0 commit comments

Comments
 (0)