fix(common): viewport scroller not finding elements inside the shadow DOM (#41644)
The `ViewportScroller` figures out which element to scroll into view using `document.getElementById`. The problem is that it won't find elements inside the shadow DOM. These changes add some extra logic that goes through all the shadow roots to look for the element. Fixes #41470. PR Close #41644
This commit is contained in:
parent
a99aa29040
commit
1aebf165db
|
@ -123,16 +123,14 @@ export class BrowserViewportScroller implements ViewportScroller {
|
|||
// TODO(atscott): The correct behavior for `getElementsByName` would be to also verify that the
|
||||
// element is an anchor. However, this could be considered a breaking change and should be
|
||||
// done in a major version.
|
||||
const elSelected: HTMLElement|undefined =
|
||||
this.document.getElementById(target) ?? this.document.getElementsByName(target)[0];
|
||||
if (elSelected === undefined) {
|
||||
return;
|
||||
}
|
||||
const elSelected = findAnchorFromDocument(this.document, target);
|
||||
|
||||
this.scrollToElement(elSelected);
|
||||
// After scrolling to the element, the spec dictates that we follow the focus steps for the
|
||||
// target. Rather than following the robust steps, simply attempt focus.
|
||||
this.attemptFocus(elSelected);
|
||||
if (elSelected) {
|
||||
this.scrollToElement(elSelected);
|
||||
// After scrolling to the element, the spec dictates that we follow the focus steps for the
|
||||
// target. Rather than following the robust steps, simply attempt focus.
|
||||
this.attemptFocus(elSelected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -214,6 +212,40 @@ function getScrollRestorationProperty(obj: any): PropertyDescriptor|undefined {
|
|||
return Object.getOwnPropertyDescriptor(obj, 'scrollRestoration');
|
||||
}
|
||||
|
||||
function findAnchorFromDocument(document: Document, target: string): HTMLElement|null {
|
||||
const documentResult = document.getElementById(target) || document.getElementsByName(target)[0];
|
||||
|
||||
if (documentResult) {
|
||||
return documentResult;
|
||||
}
|
||||
|
||||
// `getElementById` and `getElementsByName` won't pierce through the shadow DOM so we
|
||||
// have to traverse the DOM manually and do the lookup through the shadow roots.
|
||||
if (typeof document.createTreeWalker === 'function' && document.body &&
|
||||
((document.body as any).createShadowRoot || document.body.attachShadow)) {
|
||||
const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
|
||||
let currentNode = treeWalker.currentNode as HTMLElement | null;
|
||||
|
||||
while (currentNode) {
|
||||
const shadowRoot = currentNode.shadowRoot;
|
||||
|
||||
if (shadowRoot) {
|
||||
// Note that `ShadowRoot` doesn't support `getElementsByName`
|
||||
// so we have to fall back to `querySelector`.
|
||||
const result =
|
||||
shadowRoot.getElementById(target) || shadowRoot.querySelector(`[name="${target}"]`);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
currentNode = treeWalker.nextNode() as HTMLElement | null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an empty implementation of the viewport scroller.
|
||||
*/
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import {describe, expect, it} from '@angular/core/testing/src/testing_internal';
|
||||
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
|
||||
import {BrowserViewportScroller, ViewportScroller} from '../src/viewport_scroller';
|
||||
|
||||
describe('BrowserViewportScroller', () => {
|
||||
|
@ -44,44 +45,103 @@ describe('BrowserViewportScroller', () => {
|
|||
// Testing scroll behavior does not make sense outside a browser
|
||||
if (isNode) return;
|
||||
const anchor = 'anchor';
|
||||
let tallItem: HTMLDivElement;
|
||||
let el: HTMLAnchorElement;
|
||||
let scroller: BrowserViewportScroller;
|
||||
|
||||
beforeEach(() => {
|
||||
scroller = new BrowserViewportScroller(document, window);
|
||||
scroller.scrollToPosition([0, 0]);
|
||||
|
||||
tallItem = document.createElement('div');
|
||||
tallItem.style.height = '3000px';
|
||||
document.body.appendChild(tallItem);
|
||||
|
||||
el = document.createElement('a');
|
||||
el.innerText = 'some link';
|
||||
el.href = '#';
|
||||
document.body.appendChild(el);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(tallItem);
|
||||
document.body.removeChild(el);
|
||||
});
|
||||
|
||||
it('should scroll when element with matching id is found', () => {
|
||||
el.id = anchor;
|
||||
const {anchorNode, cleanup} = createTallElement();
|
||||
anchorNode.id = anchor;
|
||||
scroller.scrollToAnchor(anchor);
|
||||
expect(scroller.getScrollPosition()[1]).not.toEqual(0);
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('should scroll when anchor with matching name is found', () => {
|
||||
el.name = anchor;
|
||||
const {anchorNode, cleanup} = createTallElement();
|
||||
anchorNode.name = anchor;
|
||||
scroller.scrollToAnchor(anchor);
|
||||
expect(scroller.getScrollPosition()[1]).not.toEqual(0);
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('should not scroll when no matching element is found', () => {
|
||||
const {cleanup} = createTallElement();
|
||||
scroller.scrollToAnchor(anchor);
|
||||
expect(scroller.getScrollPosition()[1]).toEqual(0);
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('should scroll when element with matching id is found inside the shadow DOM', () => {
|
||||
// This test is only relevant for browsers that support shadow DOM.
|
||||
if (!browserDetection.supportsShadowDom) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {anchorNode, cleanup} = createTallElementWithShadowRoot();
|
||||
anchorNode.id = anchor;
|
||||
scroller.scrollToAnchor(anchor);
|
||||
expect(scroller.getScrollPosition()[1]).not.toEqual(0);
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('should scroll when anchor with matching name is found inside the shadow DOM', () => {
|
||||
// This test is only relevant for browsers that support shadow DOM.
|
||||
if (!browserDetection.supportsShadowDom) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {anchorNode, cleanup} = createTallElementWithShadowRoot();
|
||||
anchorNode.name = anchor;
|
||||
scroller.scrollToAnchor(anchor);
|
||||
expect(scroller.getScrollPosition()[1]).not.toEqual(0);
|
||||
cleanup();
|
||||
});
|
||||
|
||||
function createTallElement() {
|
||||
const tallItem = document.createElement('div');
|
||||
tallItem.style.height = '3000px';
|
||||
document.body.appendChild(tallItem);
|
||||
const anchorNode = createAnchorNode();
|
||||
document.body.appendChild(anchorNode);
|
||||
|
||||
return {
|
||||
anchorNode,
|
||||
cleanup: () => {
|
||||
document.body.removeChild(tallItem);
|
||||
document.body.removeChild(anchorNode);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createTallElementWithShadowRoot() {
|
||||
const tallItem = document.createElement('div');
|
||||
tallItem.style.height = '3000px';
|
||||
document.body.appendChild(tallItem);
|
||||
|
||||
const elementWithShadowRoot = document.createElement('div');
|
||||
const shadowRoot = elementWithShadowRoot.attachShadow({mode: 'open'});
|
||||
const anchorNode = createAnchorNode();
|
||||
shadowRoot.appendChild(anchorNode);
|
||||
document.body.appendChild(elementWithShadowRoot);
|
||||
|
||||
return {
|
||||
anchorNode,
|
||||
cleanup: () => {
|
||||
document.body.removeChild(tallItem);
|
||||
document.body.removeChild(elementWithShadowRoot);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createAnchorNode() {
|
||||
const anchorNode = document.createElement('a');
|
||||
anchorNode.innerText = 'some link';
|
||||
anchorNode.href = '#';
|
||||
return anchorNode;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue