Skip to content

Fix link click interception in shadow roots #27587

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
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
4 changes: 2 additions & 2 deletions src/Components/Web.JS/dist/Release/blazor.server.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.webassembly.js

Large diffs are not rendered by default.

30 changes: 27 additions & 3 deletions src/Components/Web.JS/src/Services/NavigationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function attachToEventDelegator(eventDelegator: EventDelegator) {

// Intercept clicks on all <a> elements where the href is within the <base href> URI space
// 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
const anchorTarget = findClosestAncestor(event.target as Element | null, 'A') as HTMLAnchorElement | null;
const anchorTarget = findAnchorTarget(event);
const hrefAttributeName = 'href';
if (anchorTarget && anchorTarget.hasAttribute(hrefAttributeName)) {
const targetAttributeValue = anchorTarget.getAttribute('target');
Expand Down Expand Up @@ -122,12 +122,36 @@ export function toAbsoluteUri(relativeUri: string) {
return testAnchor.href;
}

function findClosestAncestor(element: Element | null, tagName: string) {
function findAnchorTarget(event: MouseEvent): HTMLAnchorElement | null {
// _blazorDisableComposedPath is a temporary escape hatch in case any problems are discovered
// in this logic. It can be removed in a later release, and should not be considered supported API.
const path = !window['_blazorDisableComposedPath'] && event.composedPath && event.composedPath();
if (path) {
// This logic works with events that target elements within a shadow root,
// as long as the shadow mode is 'open'. For closed shadows, we can't possibly
// know what internal element was clicked.
for (let i = 0; i < path.length; i++) {
const candidate = path[i];
if (candidate instanceof Element && candidate.tagName === 'A') {
return candidate as HTMLAnchorElement;
}
}
return null;
} else {
// Since we're adding use of composedPath in a patch, retain compatibility with any
// legacy browsers that don't support it by falling back on the older logic, even
// though it won't work properly with ShadowDOM. This can be removed in the next
// major release.
Copy link
Member Author

@SteveSandersonMS SteveSandersonMS Nov 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK there aren't any supported browsers that don't support Event.composedPath.

The only reason I'm keeping the older logic as fallback is in case someone has somehow polyfilled enough to make Blazor Server 5.0 run on IE11/legacy-Edge, in which case we don't want this patch to create the need to add yet another polyfill. I doubt anyone is in that situation, but this is being super-cautious because patch.

return findClosestAnchorAncestorLegacy(event.target as Element | null, 'A');
}
}

function findClosestAnchorAncestorLegacy(element: Element | null, tagName: string) {
return !element
? null
: element.tagName === tagName
? element
: findClosestAncestor(element.parentElement, tagName);
: findClosestAnchorAncestorLegacy(element.parentElement, tagName);
}

function isWithinBaseUriSpace(href: string) {
Expand Down
16 changes: 16 additions & 0 deletions src/Components/test/E2ETest/Tests/RoutingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,22 @@ public void CanFollowLinkToNotAComponent()
Browser.Equal("Not a component!", () => Browser.Exists(By.Id("test-info")).Text);
}

[Fact]
public void CanFollowLinkDefinedInOpenShadowRoot()
{
SetUrlViaPushState("/");

var app = Browser.MountTestComponent<TestRouter>();

// It's difficult to access elements within a shadow root using Selenium's regular APIs
// Bypass this limitation by clicking the element via JavaScript
var shadowHost = app.FindElement(By.TagName("custom-link-with-shadow-root"));
((IJavaScriptExecutor)Browser).ExecuteScript("arguments[0].shadowRoot.querySelector('a').click()", shadowHost);

Browser.Equal("This is another page.", () => app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("Other", "Other with base-relative URL (matches all)");
}

[Fact]
public void CanGoBackFromNotAComponent()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<li><NavLink href="/subdir/WithLazyLoadedRoutes" id="with-lazy-routes">With lazy loaded routes</NavLink></li>
<li><NavLink href="PreventDefaultCases">preventDefault cases</NavLink></li>
<li><NavLink>Null href never matches</NavLink></li>
<li><custom-link-with-shadow-root target-url="Other"></custom-link-with-shadow-root></li>
</ul>

<button id="do-navigation" @onclick=@(x => NavigationManager.NavigateTo("Other"))>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<script src="js/jsinteroptests.js"></script>
<script src="js/renderattributestest.js"></script>
<script src="js/webComponentPerformingJsInterop.js"></script>
<script src="js/customLinkElement.js"></script>

<script>
// Used by ElementRefComponent
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// This web component is used from the CanFollowLinkDefinedInOpenShadowRoot test case

window.customElements.define('custom-link-with-shadow-root', class extends HTMLElement {
connectedCallback() {
const shadowRoot = this.attachShadow({ mode: 'open' });
const href = this.getAttribute('target-url');
shadowRoot.innerHTML = `<a href='${href}'>Anchor tag within shadow root</a>`;
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<script src="js/jsinteroptests.js"></script>
<script src="js/renderattributestest.js"></script>
<script src="js/webComponentPerformingJsInterop.js"></script>
<script src="js/customLinkElement.js"></script>

<div id="blazor-error-ui">
An unhandled error has occurred.
Expand Down