From 8524187869363ff01588a18837184e698ab5b8ee Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Thu, 8 Jun 2017 12:51:43 +0300 Subject: [PATCH] fix(aio): restrain scrolling inside ToC (when cursor over ToC) Previously, when scrolling the ToC and reaching the top/bottom, further mousewheel events would result in scrolling the window (and thus the main content). This is standard browser behavior. In the case of the ToC though, the `ScrollSpy` would detect scrolling in the main content and scroll the active ToC to entry into view, thus resetting the scroll position of the ToC. Reproduction: 1. Open `~/guide/template-syntax`. 2. Start scrolling through the long ToC. 3. Try to go to the bottom of the ToC. 4. Once you reach the bottom, the main content starts scrolling down. 5. The first section ("HTML in templates") becomes "active", so the ToC is scrolled back up to make its corresponding entry visible. 6. Go back to step 2. This commit improves the UX, by not allowing the main content to scroll when the cursor is ovr the ToC and the user has scrolled all the way to the top/bottom of it. --- aio/src/app/app.component.html | 2 +- aio/src/app/app.component.spec.ts | 57 +++++++++++++++++++++++++++++++ aio/src/app/app.component.ts | 19 +++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/aio/src/app/app.component.html b/aio/src/app/app.component.html index e929dfac86..58ebe94341 100644 --- a/aio/src/app/app.component.html +++ b/aio/src/app/app.component.html @@ -33,7 +33,7 @@ -
+
diff --git a/aio/src/app/app.component.spec.ts b/aio/src/app/app.component.spec.ts index 836b180285..6f9f995a75 100644 --- a/aio/src/app/app.component.spec.ts +++ b/aio/src/app/app.component.spec.ts @@ -472,6 +472,53 @@ describe('AppComponent', () => { })); }); + describe('restrainScrolling()', () => { + const preventedScrolling = (currentTarget: object, deltaY: number) => { + const evt = { + deltaY, + currentTarget, + defaultPrevented: false, + preventDefault() { this.defaultPrevented = true; } + } as any as WheelEvent; + + component.restrainScrolling(evt); + + return evt.defaultPrevented; + }; + + it('should prevent scrolling up if already at the top', () => { + const elem = {scrollTop: 0}; + + expect(preventedScrolling(elem, -100)).toBe(true); + expect(preventedScrolling(elem, +100)).toBe(false); + expect(preventedScrolling(elem, -10)).toBe(true); + }); + + it('should prevent scrolling down if already at the bottom', () => { + const elem = {scrollTop: 100, scrollHeight: 150, clientHeight: 50}; + + expect(preventedScrolling(elem, +10)).toBe(true); + expect(preventedScrolling(elem, -10)).toBe(false); + expect(preventedScrolling(elem, +5)).toBe(true); + + elem.clientHeight -= 10; + expect(preventedScrolling(elem, +5)).toBe(false); + + elem.scrollHeight -= 20; + expect(preventedScrolling(elem, +5)).toBe(true); + + elem.scrollTop -= 30; + expect(preventedScrolling(elem, +5)).toBe(false); + }); + + it('should not prevent scrolling if neither at the top nor at the bottom', () => { + const elem = {scrollTop: 50, scrollHeight: 150, clientHeight: 50}; + + expect(preventedScrolling(elem, +100)).toBe(false); + expect(preventedScrolling(elem, -100)).toBe(false); + }); + }); + describe('aio-toc', () => { let tocDebugElement: DebugElement; let tocContainer: DebugElement; @@ -495,6 +542,16 @@ describe('AppComponent', () => { expect(tocContainer.styles['max-height']).toBe('100px'); }); + + it('should restrain scrolling inside the ToC container', () => { + const restrainScrolling = spyOn(component, 'restrainScrolling'); + const evt = {}; + + expect(restrainScrolling).not.toHaveBeenCalled(); + + tocContainer.triggerEventHandler('mousewheel', evt); + expect(restrainScrolling).toHaveBeenCalledWith(evt); + }); }); describe('footer', () => { diff --git a/aio/src/app/app.component.ts b/aio/src/app/app.component.ts index 26afa78a82..7f20212c82 100644 --- a/aio/src/app/app.component.ts +++ b/aio/src/app/app.component.ts @@ -275,6 +275,25 @@ export class AppComponent implements OnInit { this.tocMaxHeight = (document.body.scrollHeight - window.pageYOffset - this.tocMaxHeightOffset).toFixed(2); } + // Restrain scrolling inside an element, when the cursor is over it + restrainScrolling(evt: WheelEvent) { + const elem = evt.currentTarget as Element; + const scrollTop = elem.scrollTop; + + if (evt.deltaY < 0) { + // Trying to scroll up: Prevent scrolling if already at the top. + if (scrollTop < 1) { + evt.preventDefault(); + } + } else { + // Trying to scroll down: Prevent scrolling if already at the bottom. + const maxScrollTop = elem.scrollHeight - elem.clientHeight; + if (maxScrollTop - scrollTop < 1) { + evt.preventDefault(); + } + } + } + // Search related methods and handlers