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.
This commit is contained in:
parent
f7422a9607
commit
8524187869
|
@ -33,7 +33,7 @@
|
||||||
|
|
||||||
</md-sidenav-container>
|
</md-sidenav-container>
|
||||||
|
|
||||||
<div *ngIf="showFloatingToc" class="toc-container" [style.max-height.px]="tocMaxHeight">
|
<div *ngIf="showFloatingToc" class="toc-container" [style.max-height.px]="tocMaxHeight" (mousewheel)="restrainScrolling($event)">
|
||||||
<aio-toc></aio-toc>
|
<aio-toc></aio-toc>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -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', () => {
|
describe('aio-toc', () => {
|
||||||
let tocDebugElement: DebugElement;
|
let tocDebugElement: DebugElement;
|
||||||
let tocContainer: DebugElement;
|
let tocContainer: DebugElement;
|
||||||
|
@ -495,6 +542,16 @@ describe('AppComponent', () => {
|
||||||
|
|
||||||
expect(tocContainer.styles['max-height']).toBe('100px');
|
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', () => {
|
describe('footer', () => {
|
||||||
|
|
|
@ -275,6 +275,25 @@ export class AppComponent implements OnInit {
|
||||||
this.tocMaxHeight = (document.body.scrollHeight - window.pageYOffset - this.tocMaxHeightOffset).toFixed(2);
|
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
|
// Search related methods and handlers
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue