Skip to content

Commit

Permalink
Convert sticky-element.js to use the Module pattern
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gclssvglx committed Nov 19, 2021
1 parent 5bd0809 commit b81240b
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 143 deletions.
197 changes: 99 additions & 98 deletions app/assets/javascripts/modules/sticky-element-container.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
100 changes: 55 additions & 45 deletions spec/javascripts/modules/sticky-element-container.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = $(
'<div data-module="sticky-element-container" style="height: 9001px; margin-bottom: 1000px">' +
'<div data-sticky-element>' +
'<span>Content</span>' +
'</div>' +
'</div>'
)

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 = $(
'<div data-module="sticky-element-container" style="height: 9001px; margin-bottom: 1000px">' +
'<div data-sticky-element>' +
'<span>Content</span>' +
'</div>' +
'</div>'
)
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)
})
})
})

0 comments on commit b81240b

Please sign in to comment.