Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { EventDelegator } from './Events/EventDelegator';
import { LogicalElement, PermutationListEntry, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement, isMathMLElement, permuteLogicalChildren, getClosestDomElement, emptyLogicalElement, getLogicalChildrenArray, depthFirstNodeTreeTraversal } from './LogicalElements';
import { applyCaptureIdToElement } from './ElementReferenceCapture';
import { attachToEventDelegator as attachNavigationManagerToEventDelegator } from '../Services/NavigationManager';
import { attachEnhancedNavigationClickHandlerToEventDelegator } from '../Services/NavigationEnhancement';
import { applyAnyDeferredValue, tryApplySpecialProperty } from './DomSpecialPropertyUtil';
const sharedTemplateElemForParsing = document.createElement('template');
const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g');
Expand All @@ -31,6 +32,11 @@ export class BrowserRenderer {
// we wire up the navigation manager to the event delegator so it has the option to participate
// in the synthetic event bubbling process later
attachNavigationManagerToEventDelegator(this.eventDelegator);

// Similarly, wire up enhanced navigation so that @onclick:preventDefault can be respected.
// This ensures enhanced navigation click handling runs after EventDelegator processes Blazor event handlers.
// See https://github.com/dotnet/aspnetcore/issues/52514
attachEnhancedNavigationClickHandlerToEventDelegator(this.eventDelegator.notifyAfterClick.bind(this.eventDelegator));
}

public getRootComponentCount(): number {
Expand Down
58 changes: 43 additions & 15 deletions src/Components/Web.JS/src/Services/NavigationEnhancement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ let currentEnhancedNavigationAbortController: AbortController | null;
let navigationEnhancementCallbacks: NavigationEnhancementCallbacks;
let performingEnhancedPageLoad: boolean;

// When an EventDelegator is available, we defer click handling to it so that
// @onclick:preventDefault can be respected. See https://github.com/dotnet/aspnetcore/issues/52514
let hasEventDelegatorClickHandler: boolean = false;

// This gets initialized to the current URL when we load.
// After that, it gets updated every time we successfully complete a navigation.
let currentContentUrl = location.href;
Expand Down Expand Up @@ -71,23 +75,18 @@ export function detachProgressivelyEnhancedNavigationListener() {
window.removeEventListener('popstate', onPopState);
}

function performProgrammaticEnhancedNavigation(absoluteInternalHref: string, replace: boolean) : void {
const originalLocation = location.href;

if (replace) {
history.replaceState(null, /* ignored title */ '', absoluteInternalHref);
} else {
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
}

if (!isForSamePath(absoluteInternalHref, originalLocation)) {
scheduleScrollReset(ScrollResetSchedule.AfterDocumentUpdate);
}

performEnhancedPageLoad(absoluteInternalHref, /* interceptedLink */ false);
// Called from BrowserRenderer to wire up the enhanced navigation click handler through
// the EventDelegator, ensuring it runs after @onclick:preventDefault has been processed.
// See https://github.com/dotnet/aspnetcore/issues/52514
export function attachEnhancedNavigationClickHandlerToEventDelegator(notifyAfterClick: (callback: (event: MouseEvent) => void) => void) {
hasEventDelegatorClickHandler = true;
notifyAfterClick(handleEnhancedNavigationClick);
}

function onDocumentClick(event: MouseEvent) {
// Core logic for handling a click for enhanced navigation purposes.
// This is called either from the native click handler (pure SSR) or from EventDelegator's
// afterClick callback (interactive scenarios).
function handleEnhancedNavigationClick(event: MouseEvent) {
if (hasInteractiveRouter()) {
return;
}
Expand All @@ -114,6 +113,35 @@ function onDocumentClick(event: MouseEvent) {
});
}

function performProgrammaticEnhancedNavigation(absoluteInternalHref: string, replace: boolean) : void {
const originalLocation = location.href;

if (replace) {
history.replaceState(null, /* ignored title */ '', absoluteInternalHref);
} else {
history.pushState(null, /* ignored title */ '', absoluteInternalHref);
}

if (!isForSamePath(absoluteInternalHref, originalLocation)) {
scheduleScrollReset(ScrollResetSchedule.AfterDocumentUpdate);
}

performEnhancedPageLoad(absoluteInternalHref, /* interceptedLink */ false);
}

function onDocumentClick(event: MouseEvent) {
// If we have an EventDelegator handling clicks, skip this native handler.
// The EventDelegator will call handleEnhancedNavigationClick after processing
// @onclick:preventDefault and other Blazor event handlers.
// See https://github.com/dotnet/aspnetcore/issues/52514
if (hasEventDelegatorClickHandler) {
return;
}

// No EventDelegator available (pure SSR), handle the click directly.
handleEnhancedNavigationClick(event);
}

function onPopState(state: PopStateEvent) {
if (hasInteractiveRouter()) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,41 @@ public void CanReceiveNullParameterValueOnEnhancedNavigation(string renderMode)
Assert.DoesNotContain(logs, log => log.Message.Contains("Error"));
}

[Fact]
public void PreventDefaultOnClickIsRespectedWithEnhancedNavigation()
{
// See: https://github.com/dotnet/aspnetcore/issues/52514
// Verifies that @onclick:preventDefault on a link prevents enhanced navigation from intercepting the click
Navigate($"{ServerPathBase}/nav");
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);

Browser.Exists(By.TagName("nav")).FindElement(By.LinkText("PreventDefault link test")).Click();
Browser.Equal("PreventDefault Link Test", () => Browser.Exists(By.TagName("h1")).Text);
Browser.Equal("0", () => Browser.Exists(By.Id("current-count")).Text);

// Store the current URL to verify it doesn't change
var originalUrl = Browser.Url;
Assert.EndsWith("/nav/prevent-default-link", originalUrl);

// Click the link with @onclick:preventDefault - should increment count but NOT navigate
Browser.Exists(By.Id("prevent-default-link")).Click();
Browser.Equal("1", () => Browser.Exists(By.Id("current-count")).Text);
Assert.Equal(originalUrl, Browser.Url); // URL should not have changed

// Click again to verify it continues working
Browser.Exists(By.Id("prevent-default-link")).Click();
Browser.Equal("2", () => Browser.Exists(By.Id("current-count")).Text);
Assert.Equal(originalUrl, Browser.Url);

// Also verify the button works (reference case)
Browser.Exists(By.Id("increment-button")).Click();
Browser.Equal("3", () => Browser.Exists(By.Id("current-count")).Text);

// No errors should be logged
var logs = Browser.GetBrowserLogs(LogLevel.Warning);
Assert.DoesNotContain(logs, log => log.Message.Contains("Error"));
}

[Fact]
public void CanUpdateHrefOnLinkTagWithIntegrity()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
@page "/nav/prevent-default-link"
@rendermode RenderMode.InteractiveServer

<PageTitle>PreventDefault Link Test</PageTitle>

<h1>PreventDefault Link Test</h1>

<p>
This page tests whether <code>@@onclick:preventDefault</code> works correctly with enhanced navigation.
See <a href="https://github.com/dotnet/aspnetcore/issues/52514">issue #52514</a>.
</p>

<h3>Current Count: <span id="current-count">@currentCount</span></h3>

<h4>Test Case 1: Link with preventDefault</h4>
<p>Click the link below. The counter should increment and you should NOT navigate away.</p>
<p>
<a id="prevent-default-link" href="#" @onclick="IncrementCount" @onclick:preventDefault>
Click me (with preventDefault)
</a>
</p>

<h4>Test Case 2: Link with preventDefault AND data-enhance-nav="false" (Workaround)</h4>
<p>Click the link below. This uses the known workaround.</p>
<p>
<a id="prevent-default-link-with-workaround" href="#" @onclick="IncrementCount" @onclick:preventDefault data-enhance-nav="false">
Click me (with workaround)
</a>
</p>

<h4>Test Case 3: Button (Reference)</h4>
<p>
<button id="increment-button" @onclick="IncrementCount">Click me (button)</button>
</p>

<p id="current-url">Current URL should stay at /nav/prevent-default-link</p>

@code {
private int currentCount = 0;

private void IncrementCount()
{
currentCount++;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<NavLink href="nav/location-changed/server-and-wasm">LocationChanged/LocationChanging event (server-and-wasm)</NavLink>
<NavLink href="nav/null-parameter/server">Null component parameter (server)</NavLink>
<NavLink href="nav/null-parameter/wasm">Null component parameter (wasm)</NavLink>
<NavLink href="nav/prevent-default-link">PreventDefault link test</NavLink>
<br />
<a href="nav/other" data-enhance-nav="false">
<svg width="100" height="100" id="svg-in-anchor-not-enhanced-nav-link">
Expand Down
Loading