diff --git a/aio/src/app/shared/scroll.service.spec.ts b/aio/src/app/shared/scroll.service.spec.ts index 7aeaedb78af..9e76f286553 100644 --- a/aio/src/app/shared/scroll.service.spec.ts +++ b/aio/src/app/shared/scroll.service.spec.ts @@ -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); diff --git a/aio/src/app/shared/scroll.service.ts b/aio/src/app/shared/scroll.service.ts index 394e52aa45f..0855ae091f9 100644 --- a/aio/src/app/shared/scroll.service.ts +++ b/aio/src/app/shared/scroll.service.ts @@ -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(); // 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`.