Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: #502 Scrollspy 3 event listener leaks #503

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions spec/helpers/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,23 @@ const KEYMAP = {
'105': '9'
};

function XloadHtml(html) {
function XloadHtml(html, options) {
options = options ? options : {};
const defaultOptions = { insertionType: 'append' };
options = {
...defaultOptions,
...options
};

const div = document.createElement('div');
div.classList.add('please-delete-me');
div.innerHTML = html;
document.body.appendChild(div);

if (options.insertionType === 'append') {
document.body.appendChild(div);
} else if (options.insertionType === 'prepend') {
document.body.prepend(div);
}
}

function XunloadFixtures() {
Expand Down
90 changes: 90 additions & 0 deletions spec/tests/scrollspy/scrollspySpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
describe('Scrollspy Plugin', () => {
const fixture = `
<div class="row">
<div class="col m7">
<div id="introduction" class="section scrollspy"
style="height: 100vh; margin: 0; padding: 0; background-color: red;">
introduction
</div>
<div id="initialization" class="section scrollspy"
style="height: 100vh; margin: 0; padding: 0; background-color: green;">
initialization
</div>
<div id="options" class="section scrollspy"
style="height: 100vh; margin: 0; padding: 0; background-color: yellow;">
options
</div>
</div>

<div class="col hide-on-small-only m5">
<div class="toc-wrapper pinned" style="top: 0px;">
<div style="height: 1px">
<ul class="section table-of-contents">
<li>
<a href="#introduction" class="">Introduction</a>
</li>
<li>
<a href="#initialization" class="">Initialization</a>
</li>
<li>
<a href="#options" class="">Options</a>
</li>
</ul>
</div>
</div>
</div>
</div>
`;
let scrollspyInstances = [];

beforeEach(() => {
XloadHtml(fixture, { insertionType: 'prepend' });
window.scrollTo(0, 0);
const elements = document.querySelectorAll('.scrollspy');
scrollspyInstances = M.ScrollSpy.init(elements, {});
});

afterEach(() => {
scrollspyInstances.forEach((value) => value.destroy());
XunloadFixtures();
});

function resetScrollspy(options) {
options = options ? options : {};
scrollspyInstances.forEach((value) => value.destroy());
const elements = document.querySelectorAll('.scrollspy');
scrollspyInstances = M.ScrollSpy.init(elements, options);
}

function clickLink(value) {
document.querySelector(`a[href="#${value}"]`).click();
}

describe('Scrollspy basic test cases', () => {
it('Test scrollspy smooth behavior positive case', (done) => {
resetScrollspy({ behavior: 'smooth' });
const viewportHeightPx = window.innerHeight;

clickLink('options');
setTimeout(() => {
const scrollTop = window.scrollY;
expect(scrollTop).toBe(viewportHeightPx * 2);
done();
}, 900);
});

it('Test scrollspy smooth behavior negative case', (done) => {
resetScrollspy({ behavior: 'smooth' });
const viewportHeightPx = window.innerHeight;

clickLink('options');
setTimeout(() => {
const scrollTop = window.scrollY;
expect(scrollTop)
.withContext("Scroll animation shouldn't reach the element in the given time")
.toBeLessThan(viewportHeightPx * 2);
done();
}, 5);
});
});
});
88 changes: 44 additions & 44 deletions src/scrollspy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class ScrollSpy extends Component<ScrollSpyOptions> {
this.tickId = -1;
this.id = ScrollSpy._increment;
this._setupEventHandlers();
this._handleWindowScroll();
ScrollSpy._handleWindowScroll();
}

static get defaults(): ScrollSpyOptions {
Expand Down Expand Up @@ -88,49 +88,7 @@ export class ScrollSpy extends Component<ScrollSpyOptions> {
return (el as any).M_ScrollSpy;
}

destroy() {
ScrollSpy._elements.splice(ScrollSpy._elements.indexOf(this), 1);
ScrollSpy._elementsInView.splice(ScrollSpy._elementsInView.indexOf(this), 1);
ScrollSpy._visibleElements.splice(ScrollSpy._visibleElements.indexOf(this.el), 1);
ScrollSpy._count--;
this._removeEventHandlers();
const actElem = document.querySelector(this.options.getActiveElement(this.el.id));
actElem.classList.remove(this.options.activeClass);
(this.el as any).M_ScrollSpy = undefined;
}

_setupEventHandlers() {
if (ScrollSpy._count === 1) {
window.addEventListener('scroll', this._handleWindowScroll);
window.addEventListener('resize', this._handleThrottledResize);
document.body.addEventListener('click', this._handleTriggerClick);
}
}

_removeEventHandlers() {
if (ScrollSpy._count === 0) {
window.removeEventListener('scroll', this._handleWindowScroll);
window.removeEventListener('resize', this._handleThrottledResize);
document.body.removeEventListener('click', this._handleTriggerClick);
}
}

_handleThrottledResize: () => void = Utils.throttle(function(){ this._handleWindowScroll(); }, 200).bind(this);

_handleTriggerClick = (e: MouseEvent) => {
const trigger = e.target;
for (let i = ScrollSpy._elements.length - 1; i >= 0; i--) {
const scrollspy = ScrollSpy._elements[i];
const x = document.querySelector('a[href="#'+scrollspy.el.id+'"]');
if (trigger === x) {
e.preventDefault();
scrollspy.el.scrollIntoView({behavior: 'smooth'});
break;
}
}
}

_handleWindowScroll = () => {
private static _handleWindowScroll() {
// unique tick id
ScrollSpy._ticks++;

Expand Down Expand Up @@ -167,6 +125,48 @@ export class ScrollSpy extends Component<ScrollSpyOptions> {
ScrollSpy._elementsInView = intersections;
}

private static _handleThrottledResize = Utils.throttle(function () { ScrollSpy._handleWindowScroll(); }, 200);

private static _handleTriggerClick(e: MouseEvent) {
const trigger = e.target;
for (let i = ScrollSpy._elements.length - 1; i >= 0; i--) {
const scrollspy = ScrollSpy._elements[i];
const x = document.querySelector('a[href="#' + scrollspy.el.id + '"]');
if (trigger === x) {
e.preventDefault();
scrollspy.el.scrollIntoView({ behavior: 'smooth' });
break;
}
}
}

destroy() {
ScrollSpy._elements.splice(ScrollSpy._elements.indexOf(this), 1);
ScrollSpy._elementsInView.splice(ScrollSpy._elementsInView.indexOf(this), 1);
ScrollSpy._visibleElements.splice(ScrollSpy._visibleElements.indexOf(this.el), 1);
ScrollSpy._count--;
this._removeEventHandlers();
const actElem = document.querySelector(this.options.getActiveElement(this.el.id));
actElem.classList.remove(this.options.activeClass);
(this.el as any).M_ScrollSpy = undefined;
}

_setupEventHandlers() {
if (ScrollSpy._count === 1) {
window.addEventListener('scroll', ScrollSpy._handleWindowScroll);
window.addEventListener('resize', ScrollSpy._handleThrottledResize);
document.body.addEventListener('click', ScrollSpy._handleTriggerClick);
}
}

_removeEventHandlers() {
if (ScrollSpy._count === 0) {
window.removeEventListener('scroll', ScrollSpy._handleWindowScroll);
window.removeEventListener('resize', ScrollSpy._handleThrottledResize);
document.body.removeEventListener('click', ScrollSpy._handleTriggerClick);
}
}

static _offset(el) {
const box = el.getBoundingClientRect();
const docElem = document.documentElement;
Expand Down
Loading