Description HTML <a> elements within shadow roots are invisible to the link click interception code. So when an end user clicks on a link that's inside the shadow root of a custom element, it triggers a full page load instead of a client-side navigation. Customer Impact Reported by a customer at #27070. There's no reasonable workaround (besides not using custom elements and shadow DOM). The impact is that if you're using a library of custom HTML elements, which typically use ShadowDOM to render, then if you use them to produce any links, then those links will seem broken to the end user (as in, they trigger a full-page reload, destroying any app state in WebAssembly memory or discarding the Blazor Server circuit). Regression? No, this has always been the case. However, the use of custom elements and ShadowDOM is a growing area, especially as we try to guide customers to use custom elements like the FAST components. The fact that this use case is growing in importance makes it relevant to consider patching rather than waiting a full year for 6.0. We don't have to ship this in 5.0.1, but we should be prepared to ship it in a patch reasonably soon. Risk This changes the logic for processing clicks on all links, not just the ones in shadow roots. Although I'm not aware of any cases where the new logic might fail, and all our E2E tests pass, maybe I missed some case. It's unfortunate we don't have any opportunity to put this in front of customers any any preview form.
This commit is contained in:
parent
6108c0d03e
commit
fc1ebaaaa6
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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');
|
||||
|
|
@ -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.
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -333,6 +333,22 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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"))>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>`;
|
||||
}
|
||||
});
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue