From b81240b3d93fe6980f50b3a4f8c07de4ba32d4fa Mon Sep 17 00:00:00 2001 From: Graham Lewis <44037625+gclssvglx@users.noreply.github.com> Date: Fri, 12 Nov 2021 17:08:40 +0000 Subject: [PATCH] Convert sticky-element.js to use the Module pattern What Currently, the sticky-element in Government Frontend does not follow the JavaScript module pattern. Convert the existing code to be consistent with this pattern. Why? The module pattern is the standard approach and this JavaScript component does not follow the convention. A note on the tests The tests for sticky-element do not actually test physical scrolling on the page. Instead they test for presence of specific CSS classes. Because of this it was found necessary to run the checkResize() and checkScroll() methods prior to expecting these classes to have changed. In effect this simulates physically scrolling or resizing the page. --- .../modules/sticky-element-container.js | 197 +++++++++--------- .../modules/sticky-element-container.spec.js | 100 +++++---- 2 files changed, 154 insertions(+), 143 deletions(-) diff --git a/app/assets/javascripts/modules/sticky-element-container.js b/app/assets/javascripts/modules/sticky-element-container.js index 0357b70ef..114011c2c 100644 --- a/app/assets/javascripts/modules/sticky-element-container.js +++ b/app/assets/javascripts/modules/sticky-element-container.js @@ -7,111 +7,112 @@ Use 'data-module="sticky-element-container"' to instantiate, and add `[data-sticky-element]` to the child you want to position. */ + +window.GOVUK = window.GOVUK || {} +window.GOVUK.Modules = window.GOVUK.Modules || {}; + (function (Modules) { - 'use strict' + function StickyElementContainer (element) { + this.wrapper = element + this.stickyElement = this.wrapper.querySelector('[data-sticky-element]') + this.hasResized = true + this.hasScrolled = true + this.interval = 50 + this.windowVerticalPosition = 1 + this.startPosition = 0 + this.stopPosition = 0 + } + + StickyElementContainer.prototype.init = function () { + if (!this.stickyElement) return + + window.onresize = this.onResize + window.onscroll = this.onScroll + setInterval(this.checkResize.bind(this), this.interval) + setInterval(this.checkScroll.bind(this), this.interval) + this.checkResize() + this.checkScroll() + this.stickyElement.classList.add('sticky-element--enabled') + } + + StickyElementContainer.prototype.getWindowDimensions = function () { + return { + height: window.innerHeight, + width: window.innerWidth + } + } + + StickyElementContainer.prototype.getWindowPositions = function () { + return { + scrollTop: window.scrollY + } + } + + StickyElementContainer.prototype.onResize = function () { + this.hasResized = true + } + + StickyElementContainer.prototype.onScroll = function () { + this.hasScrolled = true + } - Modules.StickyElementContainer = function () { - var self = this + StickyElementContainer.prototype.checkResize = function () { + if (this.hasResized) { + this.hasResized = false + this.hasScrolled = true - self.getWindowDimensions = function () { - return { - height: window.innerHeight, - width: window.innerWidth - } + var windowDimensions = this.getWindowDimensions() + var elementHeight = this.wrapper.offsetHeight || parseFloat(this.wrapper.style.height.replace('px', '')) + this.startPosition = this.wrapper.offsetTop + this.stopPosition = this.wrapper.offsetTop + elementHeight - windowDimensions.height } + } + + StickyElementContainer.prototype.checkScroll = function () { + if (this.hasScrolled) { + this.hasScrolled = false + this.hasResized = true + + this.windowVerticalPosition = this.getWindowPositions().scrollTop + + this.updateVisibility() + this.updatePosition() + } + } - self.getWindowPositions = function () { - return { - scrollTop: window.scrollY - } + StickyElementContainer.prototype.updateVisibility = function () { + var isPastStart = this.startPosition < this.windowVerticalPosition + if (isPastStart) { + this.show() + } else { + this.hide() } + } - self.start = function ($el) { - var wrapper = $el[0] - var stickyElement = wrapper.querySelector('[data-sticky-element]') - - var hasResized = true - var hasScrolled = true - var interval = 50 - var windowVerticalPosition = 1 - var startPosition, stopPosition - - initialise() - - function initialise () { - window.onresize = onResize - window.onscroll = onScroll - setInterval(checkResize, interval) - setInterval(checkScroll, interval) - checkResize() - checkScroll() - stickyElement.classList.add('sticky-element--enabled') - } - - function onResize () { - hasResized = true - } - - function onScroll () { - hasScrolled = true - } - - function checkResize () { - if (hasResized) { - hasResized = false - hasScrolled = true - - var windowDimensions = self.getWindowDimensions() - var elementHeight = wrapper.offsetHeight || parseFloat(wrapper.style.height.replace('px', '')) - startPosition = wrapper.offsetTop - stopPosition = wrapper.offsetTop + elementHeight - windowDimensions.height - } - } - - function checkScroll () { - if (hasScrolled) { - hasScrolled = false - - windowVerticalPosition = self.getWindowPositions().scrollTop - - updateVisibility() - updatePosition() - } - } - - function updateVisibility () { - var isPastStart = startPosition < windowVerticalPosition - if (isPastStart) { - show() - } else { - hide() - } - } - - function updatePosition () { - var isPastEnd = stopPosition < windowVerticalPosition - if (isPastEnd) { - stickToParent() - } else { - stickToWindow() - } - } - - function stickToWindow () { - stickyElement.classList.add('sticky-element--stuck-to-window') - } - - function stickToParent () { - stickyElement.classList.remove('sticky-element--stuck-to-window') - } - - function show () { - stickyElement.classList.remove('sticky-element--hidden') - } - - function hide () { - stickyElement.classList.add('sticky-element--hidden') - } + StickyElementContainer.prototype.updatePosition = function () { + var isPastEnd = this.stopPosition < this.windowVerticalPosition + if (isPastEnd) { + this.stickToParent() + } else { + this.stickToWindow() } } + + StickyElementContainer.prototype.stickToWindow = function () { + this.stickyElement.classList.add('sticky-element--stuck-to-window') + } + + StickyElementContainer.prototype.stickToParent = function () { + this.stickyElement.classList.remove('sticky-element--stuck-to-window') + } + + StickyElementContainer.prototype.show = function () { + this.stickyElement.classList.remove('sticky-element--hidden') + } + + StickyElementContainer.prototype.hide = function () { + this.stickyElement.classList.add('sticky-element--hidden') + } + + Modules.StickyElementContainer = StickyElementContainer })(window.GOVUK.Modules) diff --git a/spec/javascripts/modules/sticky-element-container.spec.js b/spec/javascripts/modules/sticky-element-container.spec.js index 35356892a..8c22ec286 100644 --- a/spec/javascripts/modules/sticky-element-container.spec.js +++ b/spec/javascripts/modules/sticky-element-container.spec.js @@ -2,62 +2,72 @@ describe('A sticky-element-container module', function () { 'use strict' var GOVUK = window.GOVUK - var $ = window.$ - var instance - beforeEach(function () { - instance = new GOVUK.Modules.StickyElementContainer() - }) + describe('on desktop', function () { + var $element + var $footer + var instance + + beforeEach(function () { + $element = $( + '
' + + '
' + + 'Content' + + '
' + + '
' + ) + + instance = new GOVUK.Modules.StickyElementContainer($element[0]) + $footer = $element.find('[data-sticky-element]') + + instance.getWindowDimensions = function () { + return { + height: 768, + width: 1024 + } + } + }) - describe('in a large parent element', function () { - var $element = $( - '
' + - '
' + - 'Content' + - '
' + - '
' - ) - var $footer = $element.find('[data-sticky-element]') - - describe('on desktop', function () { - beforeEach(function () { - instance.getWindowDimensions = function () { - return { - height: 768, - width: 1024 - } + it('hides the element, when scrolled at the top', function () { + instance.getWindowPositions = function () { + return { + scrollTop: 0 } - }) + } - it('hides the element, when scrolled at the top', function () { - instance.start($element) + instance.checkResize() + instance.checkScroll() - expect($footer.hasClass('sticky-element--hidden')).toBe(true) - }) + expect($footer.hasClass('sticky-element--hidden')).toBe(true) + expect($footer.hasClass('sticky-element--stuck-to-window')).toBe(true) + }) - it('shows the element, stuck to the window, when scrolled in the middle', function () { - instance.getWindowPositions = function () { - return { - scrollTop: 5000 - } + it('shows the element, stuck to the window, when scrolled in the middle', function () { + instance.getWindowPositions = function () { + return { + scrollTop: 5000 } - instance.start($element) + } - expect($footer.hasClass('sticky-element--hidden')).toBe(false) - expect($footer.hasClass('sticky-element--stuck-to-window')).toBe(true) - }) + instance.checkResize() + instance.checkScroll() - it('shows the element, stuck to the parent, when scrolled at the bottom', function () { - instance.getWindowPositions = function () { - return { - scrollTop: 9800 - } + expect($footer.hasClass('sticky-element--hidden')).toBe(false) + expect($footer.hasClass('sticky-element--stuck-to-window')).toBe(true) + }) + + it('shows the element, stuck to the parent, when scrolled at the bottom', function () { + instance.getWindowPositions = function () { + return { + scrollTop: 9800 } - instance.start($element) + } + + instance.checkResize() + instance.checkScroll() - expect($footer.hasClass('sticky-element--hidden')).toBe(false) - expect($footer.hasClass('sticky-element--stuck-to-window')).toBe(false) - }) + expect($footer.hasClass('sticky-element--hidden')).toBe(false) + expect($footer.hasClass('sticky-element--stuck-to-window')).toBe(false) }) }) })