diff --git a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
index 08cfd8f86608..23ba7676a643 100644
--- a/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
+++ b/src/Components/Web.JS/src/Rendering/BrowserRenderer.ts
@@ -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');
@@ -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 {
diff --git a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
index 9eb91a80e01d..3c6f015aa7c5 100644
--- a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
+++ b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts
@@ -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;
@@ -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;
}
@@ -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;
diff --git a/src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs
index 5b28c9d299ac..d0b799e3184f 100644
--- a/src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs
+++ b/src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs
@@ -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()
{
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/EnhancedNav/PageWithPreventDefaultLink.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/EnhancedNav/PageWithPreventDefaultLink.razor
new file mode 100644
index 000000000000..07d5ace1f4c1
--- /dev/null
+++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/EnhancedNav/PageWithPreventDefaultLink.razor
@@ -0,0 +1,45 @@
+@page "/nav/prevent-default-link"
+@rendermode RenderMode.InteractiveServer
+
+
+ This page tests whether @@onclick:preventDefault works correctly with enhanced navigation.
+ See issue #52514.
+
Click the link below. The counter should increment and you should NOT navigate away.
++ + Click me (with preventDefault) + +
+ +Click the link below. This uses the known workaround.
++ + Click me (with workaround) + +
+ ++ +
+ +Current URL should stay at /nav/prevent-default-link
+ +@code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Shared/EnhancedNavLayout.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Shared/EnhancedNavLayout.razor index 25544afef64e..6c20de51c949 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Shared/EnhancedNavLayout.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Shared/EnhancedNavLayout.razor @@ -25,6 +25,7 @@