fix(docs-infra): focus elements when scrolling to them (if focusable) (#43460)

When using `ScrollService#scrollToElement()` to scroll to a specific
element, also focus the element if it is focusable (i.e. if it has a
`focus()` method).

This will be useful for implementing a "skip to content" link as
described in #42936.

PR Close #43460
This commit is contained in:
George Kalpakas
2021-09-19 11:46:48 +03:00
committed by Jessica Janiuk
parent 0e2ff28627
commit 235e4aaa25
2 changed files with 19 additions and 10 deletions
+14 -6
View File
@@ -15,9 +15,9 @@ describe('ScrollService', () => {
return instance;
};
const topOfPageElem = {} as Element;
const topOfPageElem = {} as HTMLElement;
let injector: Injector;
let document: MockDocument;
let document: Document & MockDocument;
let platformLocation: MockPlatformLocation;
let scrollService: ScrollService;
let location: SpyLocation;
@@ -60,7 +60,7 @@ describe('ScrollService', () => {
});
platformLocation = injector.get(PlatformLocation);
document = injector.get(DOCUMENT) as unknown as MockDocument;
document = injector.get(DOCUMENT) as unknown as Document & MockDocument;
scrollService = injector.get(ScrollService);
location = injector.get(Location) as unknown as SpyLocation;
sessionStorage = injector.get(SessionStorage);
@@ -241,14 +241,22 @@ describe('ScrollService', () => {
describe('#scrollToElement', () => {
it('should scroll to element', () => {
const element: Element = new MockElement() as any;
const element: HTMLElement = new MockElement() as any;
scrollService.scrollToElement(element);
expect(element.scrollIntoView).toHaveBeenCalled();
expect(window.scrollBy).toHaveBeenCalledWith(0, -scrollService.topOffset);
});
it('should focus the element if focusable', () => {
const element: HTMLElement = Object.assign(new MockElement() as any, {
focus: jasmine.createSpy('Element focus'),
});
scrollService.scrollToElement(element);
expect(element.focus).toHaveBeenCalled();
});
it('should not scroll more than necessary (e.g. for elements close to the bottom)', () => {
const element: Element = new MockElement() as any;
const element: HTMLElement = new MockElement() as any;
const getBoundingClientRect = element.getBoundingClientRect as jasmine.Spy;
const topOffset = scrollService.topOffset;
@@ -264,7 +272,7 @@ describe('ScrollService', () => {
});
it('should scroll all the way to the top if close enough', () => {
const element: Element = new MockElement() as any;
const element: HTMLElement = new MockElement() as any;
(window as any).pageYOffset = 25;
scrollService.scrollToElement(element);
+5 -4
View File
@@ -17,7 +17,7 @@ export const topMargin = 16;
@Injectable()
export class ScrollService implements OnDestroy {
private _topOffset: number|null;
private _topOfPageElement: Element;
private _topOfPageElement: HTMLElement;
private onDestroy = new Subject<void>();
// The scroll position which has to be restored, after a `popstate` event.
@@ -44,7 +44,7 @@ export class ScrollService implements OnDestroy {
}
constructor(
@Inject(DOCUMENT) private document: any, private platformLocation: PlatformLocation,
@Inject(DOCUMENT) private document: Document, private platformLocation: PlatformLocation,
private viewportScroller: ViewportScroller, private location: Location,
@Inject(SessionStorage) private storage: Storage) {
// On resize, the toolbar might change height, so "invalidate" the top offset.
@@ -100,7 +100,7 @@ export class ScrollService implements OnDestroy {
*/
scroll() {
const hash = this.getCurrentHash();
const element: HTMLElement = hash ? this.document.getElementById(hash) : this.topOfPageElement;
const element = hash ? this.document.getElementById(hash) ?? null : this.topOfPageElement;
this.scrollToElement(element);
}
@@ -147,9 +147,10 @@ export class ScrollService implements OnDestroy {
* Scroll to the element.
* Don't scroll if no element.
*/
scrollToElement(element: Element|null) {
scrollToElement(element: HTMLElement|null) {
if (element) {
element.scrollIntoView();
element.focus?.();
if (window && window.scrollBy) {
// Scroll as much as necessary to align the top of `element` at `topOffset`.