Skip to content

Commit 12b5fa0

Browse files
authored
Fix Navigation for anchor inside <svg> element (#51706)
* fix enhanced nav for svg element * small fix * simplify findClosestAnchorAncestorLegacy * added test for svg element inside anchor * removed _blazorDisableComposedPath logic and findClosestAnchorAncestorLegacy function * added test for anchor inside svg element dor client-side navigation * added SVGAElement to findAnchorTarget and
1 parent bcb735a commit 12b5fa0

File tree

10 files changed

+120
-31
lines changed

10 files changed

+120
-31
lines changed

src/Components/Web.JS/dist/Release/blazor.server.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.web.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.webview.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Services/NavigationEnhancement.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ function onDocumentClick(event: MouseEvent) {
8282
return;
8383
}
8484

85-
if (event.target instanceof HTMLElement && !enhancedNavigationIsEnabledForElement(event.target)) {
85+
if (event.target instanceof Element && !enhancedNavigationIsEnabledForElement(event.target)) {
8686
return;
8787
}
8888

@@ -391,7 +391,7 @@ function splitStream(frameBoundaryMarker: string) {
391391
});
392392
}
393393

394-
function enhancedNavigationIsEnabledForElement(element: HTMLElement): boolean {
394+
function enhancedNavigationIsEnabledForElement(element: Element): boolean {
395395
// For links, they default to being enhanced, but you can override at any ancestor level (both positively and negatively)
396396
const closestOverride = element.closest('[data-enhance-nav]');
397397
if (closestOverride) {

src/Components/Web.JS/src/Services/NavigationUtils.ts

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -86,42 +86,26 @@ function eventHasSpecialKey(event: MouseEvent) {
8686
return event.ctrlKey || event.shiftKey || event.altKey || event.metaKey;
8787
}
8888

89-
function canProcessAnchor(anchorTarget: HTMLAnchorElement) {
89+
function canProcessAnchor(anchorTarget: HTMLAnchorElement | SVGAElement) {
9090
const targetAttributeValue = anchorTarget.getAttribute('target');
9191
const opensInSameFrame = !targetAttributeValue || targetAttributeValue === '_self';
9292
return opensInSameFrame && anchorTarget.hasAttribute('href') && !anchorTarget.hasAttribute('download');
9393
}
9494

95-
function findAnchorTarget(event: MouseEvent): HTMLAnchorElement | null {
96-
// _blazorDisableComposedPath is a temporary escape hatch in case any problems are discovered
97-
// in this logic. It can be removed in a later release, and should not be considered supported API.
98-
const path = !window['_blazorDisableComposedPath'] && event.composedPath && event.composedPath();
95+
function findAnchorTarget(event: MouseEvent): HTMLAnchorElement | SVGAElement | null {
96+
const path = event.composedPath && event.composedPath();
9997
if (path) {
10098
// This logic works with events that target elements within a shadow root,
10199
// as long as the shadow mode is 'open'. For closed shadows, we can't possibly
102100
// know what internal element was clicked.
103101
for (let i = 0; i < path.length; i++) {
104102
const candidate = path[i];
105-
if (candidate instanceof Element && candidate.tagName === 'A') {
106-
return candidate as HTMLAnchorElement;
103+
if (candidate instanceof HTMLAnchorElement || candidate instanceof SVGAElement) {
104+
return candidate;
107105
}
108-
}
109-
return null;
110-
} else {
111-
// Since we're adding use of composedPath in a patch, retain compatibility with any
112-
// legacy browsers that don't support it by falling back on the older logic, even
113-
// though it won't work properly with ShadowDOM. This can be removed in the next
114-
// major release.
115-
return findClosestAnchorAncestorLegacy(event.target as Element | null, 'A');
116-
}
117-
}
118-
119-
function findClosestAnchorAncestorLegacy(element: Element | null, tagName: string) {
120-
return !element
121-
? null
122-
: element.tagName === tagName
123-
? element
124-
: findClosestAnchorAncestorLegacy(element.parentElement, tagName);
106+
}
107+
}
108+
return null;
125109
}
126110

127111
export function hasInteractiveRouter(): boolean {

src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ public void EnhancedNavCanBeDisabledHierarchically()
9898
// Check we got there, but we did *not* retain the <h1> element
9999
Browser.Equal("Other", () => Browser.Exists(By.TagName("h1")).Text);
100100
Assert.Throws<StaleElementReferenceException>(() => originalH1Elem.Text);
101-
102101
}
103102

104103
[Fact]
@@ -114,7 +113,51 @@ public void EnhancedNavCanBeReenabledHierarchically()
114113
// Check we got there, and it did retain the <h1> element
115114
Browser.Equal("Other", () => Browser.Exists(By.TagName("h1")).Text);
116115
Assert.Equal("Other", originalH1Elem.Text);
116+
}
117+
118+
[Fact]
119+
public void EnhancedNavWorksInsideSVGElement()
120+
{
121+
Navigate($"{ServerPathBase}/nav");
122+
123+
var originalH1Elem = Browser.Exists(By.TagName("h1"));
124+
Browser.Equal("Hello", () => originalH1Elem.Text);
125+
126+
Browser.Exists(By.TagName("nav")).FindElement(By.Id("svg-nav-link")).Click();
127+
128+
// Check we got there, and it did retain the <h1> element
129+
Browser.Equal("Other", () => Browser.Exists(By.TagName("h1")).Text);
130+
Assert.Equal("Other", originalH1Elem.Text);
131+
}
117132

133+
[Fact]
134+
public void EnhancedNavCanBeDisabledInSVGElementContainingAnchor()
135+
{
136+
Navigate($"{ServerPathBase}/nav");
137+
138+
var originalH1Elem = Browser.Exists(By.TagName("h1"));
139+
Browser.Equal("Hello", () => originalH1Elem.Text);
140+
141+
Browser.Exists(By.TagName("nav")).FindElement(By.Id("svg-not-enhanced-nav-link")).Click();
142+
143+
// Check we got there, but we did *not* retain the <h1> element
144+
Browser.Equal("Other", () => Browser.Exists(By.TagName("h1")).Text);
145+
Assert.Throws<StaleElementReferenceException>(() => originalH1Elem.Text);
146+
}
147+
148+
[Fact]
149+
public void EnhancedNavCanBeDisabledInSVGElementInsideAnchor()
150+
{
151+
Navigate($"{ServerPathBase}/nav");
152+
153+
var originalH1Elem = Browser.Exists(By.TagName("h1"));
154+
Browser.Equal("Hello", () => originalH1Elem.Text);
155+
156+
Browser.Exists(By.TagName("nav")).FindElement(By.Id("svg-in-anchor-not-enhanced-nav-link")).Click();
157+
158+
// Check we got there, but we did *not* retain the <h1> element
159+
Browser.Equal("Other", () => Browser.Exists(By.TagName("h1")).Text);
160+
Assert.Throws<StaleElementReferenceException>(() => originalH1Elem.Text);
118161
}
119162

120163
[Fact]

src/Components/test/E2ETest/Tests/RoutingTest.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1784,4 +1784,19 @@ private void AssertHighlightedLinks(params string[] linkTexts)
17841784
.FindElements(By.CssSelector("a.active"))
17851785
.Select(x => x.Text));
17861786
}
1787+
1788+
[Fact]
1789+
public void ClickOnAnchorInsideSVGElementGetsIntercepted()
1790+
{
1791+
SetUrlViaPushState("/");
1792+
var app = Browser.MountTestComponent<TestRouter>();
1793+
app.FindElement(By.LinkText("Anchor inside SVG Element")).Click();
1794+
1795+
Browser.Equal("0", () => Browser.Exists(By.Id("location-changed-count")).Text);
1796+
1797+
Browser.FindElement(By.Id("svg-link")).Click();
1798+
1799+
// If the click was intercepted then LocationChanged works
1800+
Browser.Equal("1", () => Browser.Exists(By.Id("location-changed-count")).Text);
1801+
}
17871802
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
@page "/SVGNavigation"
2+
@inject NavigationManager NavigationManager
3+
@using Microsoft.AspNetCore.Components.Routing
4+
5+
<svg width="400px" height="30px" data-enhance-nav="true">
6+
<a href="SVGNavigation" id="svg-link">
7+
<text x="15" y="15" font-size="15">SVG data-enhance-nav</text>
8+
</a>
9+
</svg>
10+
11+
<br />
12+
13+
<p>LocationChanged: <span id="location-changed-count">@_locationChangedCount</span></p>
14+
15+
@code {
16+
private int _locationChangedCount = 0;
17+
18+
protected override void OnInitialized()
19+
{
20+
NavigationManager.LocationChanged += OnLocationChanged;
21+
}
22+
23+
private void OnLocationChanged(object sender, LocationChangedEventArgs args)
24+
{
25+
_locationChangedCount++;
26+
StateHasChanged();
27+
}
28+
}

src/Components/test/testassets/BasicTestApp/RouterTest/Links.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<li><NavLink>Null href never matches</NavLink></li>
3939
<li><custom-link-with-shadow-root target-url="Other"></custom-link-with-shadow-root></li>
4040
<li><NavLink href="/subdir/LongPageWithHash">Long page with hash</NavLink></li>
41+
<li><NavLink href="/subdir/SVGNavigation">Anchor inside SVG Element</NavLink></li>
4142
</ul>
4243

4344
<button id="do-navigation" @onclick=@(x => NavigationManager.NavigateTo("Other"))>

src/Components/test/testassets/Components.TestServer/RazorComponents/Shared/EnhancedNavLayout.razor

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,24 @@
2323
<NavLink href="nav/location-changed/server">LocationChanged/LocationChanging event (server)</NavLink>
2424
<NavLink href="nav/location-changed/wasm">LocationChanged/LocationChanging event (wasm)</NavLink>
2525
<NavLink href="nav/location-changed/server-and-wasm">LocationChanged/LocationChanging event (server-and-wasm)</NavLink>
26+
<br />
27+
<a href="nav/other" data-enhance-nav="false">
28+
<svg width="100" height="100" id="svg-in-anchor-not-enhanced-nav-link">
29+
<rect width="50" height="50" style="fill:rgb(0,0,255);" />
30+
</svg>
31+
</a>
32+
<svg width="100px" height="30px" id="svg-nav-link">
33+
<NavLink href="nav/other">
34+
<text x="15" y="15" font-size="15">Other SVG</text>
35+
</NavLink>
36+
</svg>
37+
<br />
38+
<svg width="200px" height="30px" id="svg-not-enhanced-nav-link" data-enhance-nav="false">
39+
<NavLink href="nav/other">
40+
<text x="15" y="15" font-size="15">Other SVG (no enhanced nav)</text>
41+
</NavLink>
42+
</svg>
43+
<br />
2644
</nav>
27-
<hr/>
45+
<hr />
2846
@Body

0 commit comments

Comments
 (0)