Skip to content

Commit 519c564

Browse files
Make link click interception work with elements inside open shadow roots. Fixes #27070
1 parent 80f8669 commit 519c564

File tree

8 files changed

+56
-6
lines changed

8 files changed

+56
-6
lines changed

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

Lines changed: 2 additions & 2 deletions
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.webassembly.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/NavigationManager.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export function attachToEventDelegator(eventDelegator: EventDelegator) {
5252

5353
// Intercept clicks on all <a> elements where the href is within the <base href> URI space
5454
// We must explicitly check if it has an 'href' attribute, because if it doesn't, the result might be null or an empty string depending on the browser
55-
const anchorTarget = findClosestAncestor(event.target as Element | null, 'A') as HTMLAnchorElement | null;
55+
const anchorTarget = findAnchorTarget(event);
5656
const hrefAttributeName = 'href';
5757
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
5858
const targetAttributeValue = anchorTarget.getAttribute('target');
@@ -122,12 +122,34 @@ export function toAbsoluteUri(relativeUri: string) {
122122
return testAnchor.href;
123123
}
124124

125-
function findClosestAncestor(element: Element | null, tagName: string) {
125+
function findAnchorTarget(event: MouseEvent): HTMLAnchorElement | null {
126+
const path = event.composedPath && event.composedPath();
127+
if (path) {
128+
// This logic works with events that target elements within a shadow root,
129+
// as long as the shadow mode is 'open'. For closed shadows, we can't possibly
130+
// know what internal element was clicked.
131+
for (let i = 0; i < path.length; i++) {
132+
const candidate = path[i];
133+
if (candidate instanceof Element && candidate.tagName === 'A') {
134+
return candidate as HTMLAnchorElement;
135+
}
136+
}
137+
return null;
138+
} else {
139+
// Since we're adding use of composedPath in a patch, retain compatibility with any
140+
// legacy browsers that don't support it by falling back on the older logic, even
141+
// though it won't work properly with ShadowDOM. This can be removed in the next
142+
// major release.
143+
return findClosestAnchorAncestorLegacy(event.target as Element | null, 'A');
144+
}
145+
}
146+
147+
function findClosestAnchorAncestorLegacy(element: Element | null, tagName: string) {
126148
return !element
127149
? null
128150
: element.tagName === tagName
129151
? element
130-
: findClosestAncestor(element.parentElement, tagName);
152+
: findClosestAnchorAncestorLegacy(element.parentElement, tagName);
131153
}
132154

133155
function isWithinBaseUriSpace(href: string) {

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,22 @@ public void CanFollowLinkToNotAComponent()
333333
Browser.Equal("Not a component!", () => Browser.Exists(By.Id("test-info")).Text);
334334
}
335335

336+
[Fact]
337+
public void CanFollowLinkDefinedInOpenShadowRoot()
338+
{
339+
SetUrlViaPushState("/");
340+
341+
var app = Browser.MountTestComponent<TestRouter>();
342+
343+
// It's difficult to access elements within a shadow root using Selenium's regular APIs
344+
// Bypass this limitation by clicking the element via JavaScript
345+
var shadowHost = app.FindElement(By.TagName("custom-link-with-shadow-root"));
346+
((IJavaScriptExecutor)Browser).ExecuteScript("arguments[0].shadowRoot.querySelector('a').click()", shadowHost);
347+
348+
Browser.Equal("This is another page.", () => app.FindElement(By.Id("test-info")).Text);
349+
AssertHighlightedLinks("Other", "Other with base-relative URL (matches all)");
350+
}
351+
336352
[Fact]
337353
public void CanGoBackFromNotAComponent()
338354
{

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<li><NavLink href="/subdir/WithLazyLoadedRoutes" id="with-lazy-routes">With lazy loaded routes</NavLink></li>
2525
<li><NavLink href="PreventDefaultCases">preventDefault cases</NavLink></li>
2626
<li><NavLink>Null href never matches</NavLink></li>
27+
<li><custom-link-with-shadow-root target-url="Other"></custom-link-with-shadow-root></li>
2728
</ul>
2829

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

src/Components/test/testassets/BasicTestApp/wwwroot/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<script src="js/jsinteroptests.js"></script>
3131
<script src="js/renderattributestest.js"></script>
3232
<script src="js/webComponentPerformingJsInterop.js"></script>
33+
<script src="js/customLinkElement.js"></script>
3334

3435
<script>
3536
// Used by ElementRefComponent
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// This web component is used from the CanFollowLinkDefinedInOpenShadowRoot test case
2+
3+
window.customElements.define('custom-link-with-shadow-root', class extends HTMLElement {
4+
connectedCallback() {
5+
const shadowRoot = this.attachShadow({ mode: 'open' });
6+
const href = this.getAttribute('target-url');
7+
shadowRoot.innerHTML = `<a href='${href}'>Anchor tag within shadow root</a>`;
8+
}
9+
});

src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<script src="js/jsinteroptests.js"></script>
2121
<script src="js/renderattributestest.js"></script>
2222
<script src="js/webComponentPerformingJsInterop.js"></script>
23+
<script src="js/customLinkElement.js"></script>
2324

2425
<div id="blazor-error-ui">
2526
An unhandled error has occurred.

0 commit comments

Comments
 (0)