-
Notifications
You must be signed in to change notification settings - Fork 9
Highlight anchors in the page contents navigation, on scroll #61
Changes from all commits
e6eedf6
0adf211
f1a83a8
cd515b0
241dace
367b8b6
56a0d0e
56f7080
cff0bdd
df4b873
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,7 @@ | ||
// from govuk_frontend_toolkit | ||
//= require govuk/stick-at-top-when-scrolling | ||
//= require govuk/stop-scrolling-at-footer | ||
//= require_tree . | ||
|
||
window.GOVUK.stickAtTopWhenScrolling.init(); | ||
window.GOVUK.stopScrollingAtFooter.addEl($('.js-stick-at-top-when-scrolling')); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
;(function (Modules, root) { | ||
'use strict' | ||
|
||
var $ = root.$ | ||
var $window = $(root) | ||
|
||
Modules.HighlightActiveSectionHeading = function () { | ||
var self = this | ||
var _hasResized = true | ||
var _hasScrolled = true | ||
var _interval = 50 | ||
var anchorIDs = [] | ||
|
||
self.getWindowDimensions = function () { | ||
return { | ||
height: $window.height(), | ||
width: $window.width() | ||
} | ||
} | ||
|
||
self.getWindowPositions = function () { | ||
return { | ||
scrollTop: $window.scrollTop() | ||
} | ||
} | ||
|
||
self.getElementOffset = function ($el) { | ||
return $el.offset() | ||
} | ||
|
||
self.start = function ($el) { | ||
$window.resize(self.hasResized) | ||
$window.scroll(self.hasScrolled) | ||
|
||
setInterval(self.checkResize, _interval) | ||
setInterval(self.checkScroll, _interval) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason we're polling at intervals rather than binding directly to the scroll events? If it's to throttle/debounce the scroll event, have we ruled out using e.g. https://github.com/cowboy/jquery-throttle-debounce? Either way, it'd be good to document why we're doing this as it seems a bit odd to me and difficult to follow… There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was following the example over here sticky-element-container.js, as it works in a similar way. If there's a better way to do this I am happy to amend. |
||
|
||
self.$anchors = $el.find('.js-page-contents a') | ||
self.getAnchors() | ||
|
||
self.checkResize() | ||
self.checkScroll() | ||
} | ||
|
||
self.hasResized = function () { | ||
_hasResized = true | ||
return _hasResized | ||
} | ||
|
||
self.hasScrolled = function () { | ||
_hasScrolled = true | ||
return _hasScrolled | ||
} | ||
|
||
self.checkResize = function () { | ||
if (_hasResized) { | ||
_hasResized = false | ||
_hasScrolled = true | ||
} | ||
} | ||
|
||
self.checkScroll = function () { | ||
if (_hasScrolled) { | ||
_hasScrolled = false | ||
var windowDimensions = self.getWindowDimensions() | ||
if ( windowDimensions.width <= 768) { | ||
self.removeActiveItem() | ||
} else { | ||
self.updateActiveNavItem() | ||
} | ||
} | ||
} | ||
|
||
self.getAnchors = function () { | ||
$.each(self.$anchors, function(i) { | ||
var anchorID = $(this).attr('href') | ||
// e.g. anchorIDs['#meeting-the-digital-service-standard', '#understand-your-users', '#research-continually'] | ||
anchorIDs.push(anchorID) | ||
}) | ||
} | ||
|
||
self.getHeadingPosition = function ($theID) { | ||
return $theID.offset() | ||
} | ||
|
||
self.getNextHeadingPosition = function ($theNextID) { | ||
return $theNextID.offset() | ||
} | ||
|
||
self.getFooterPosition = function ($theID) { | ||
return $theID.offset() | ||
} | ||
|
||
self.getDistanceBetweenHeadings = function (headingPosition, nextHeadingPosition) { | ||
var distanceBetweenHeadings = (nextHeadingPosition - headingPosition) | ||
return distanceBetweenHeadings | ||
} | ||
|
||
self.updateActiveNavItem = function () { | ||
var windowVerticalPosition = self.getWindowPositions().scrollTop | ||
var footerPosition = self.getFooterPosition($('#footer')) | ||
|
||
$.each(self.$anchors, function(i) { | ||
|
||
var theID = anchorIDs[i] | ||
var theNextID = anchorIDs[i + 1] | ||
|
||
var $theID = $(theID) | ||
var $theNextID = $(theNextID) | ||
|
||
var headingPosition = self.getHeadingPosition($theID) | ||
|
||
if (!headingPosition) { | ||
return | ||
} | ||
|
||
headingPosition = headingPosition.top | ||
headingPosition = headingPosition - 53 // fix the offset from top of page | ||
|
||
if (theNextID) { | ||
var nextHeadingPosition = self.getNextHeadingPosition($theNextID).top | ||
} | ||
|
||
var distanceBetweenHeadings = self.getDistanceBetweenHeadings(headingPosition, nextHeadingPosition) | ||
if (distanceBetweenHeadings) { | ||
var isPastHeading = (windowVerticalPosition >= headingPosition && windowVerticalPosition < (headingPosition + distanceBetweenHeadings)) | ||
} | ||
// when distanceBetweenHeadings is false (as there isn't a next heading) | ||
else { | ||
var isPastHeading = (windowVerticalPosition >= headingPosition && windowVerticalPosition < footerPosition.top) | ||
} | ||
|
||
if (isPastHeading) { | ||
self.setActiveItem(theID) | ||
} | ||
|
||
}) | ||
|
||
} | ||
|
||
self.setActiveItem = function (theID) { | ||
self.$anchors.removeClass('active') | ||
self.$anchors.filter("[href='" + theID + "']").addClass('active') | ||
} | ||
|
||
self.removeActiveItem = function () { | ||
self.$anchors.removeClass('active') | ||
} | ||
} | ||
})(window.GOVUK.Modules, window) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
/* eslint-env jasmine */ | ||
/* eslint-disable no-multi-str */ | ||
|
||
describe('A highlight active section heading module', function () { | ||
'use strict' | ||
|
||
var module | ||
var $element | ||
|
||
beforeEach(function () { | ||
module = new GOVUK.Modules.HighlightActiveSectionHeading() | ||
|
||
$element = $('<div class="grid-row" data-module="highlight-active-section-heading">\ | ||
<div class="column-third">\ | ||
<div class="page-contents js-page-contents js-stick-at-top-when-scrolling">\ | ||
<h2 class="page-contents__title">Page contents:</h2>\ | ||
<ul class="page-contents__list">\ | ||
<li><a href="#section-1">Section 1</a></li>\ | ||
<li><a href="#section-2">Section 2</a></li>\ | ||
<li><a href="#section-3">Section 3</a></li>\ | ||
</ul>\ | ||
</div>\ | ||
</div>\ | ||
<div class="column-two-thirds">\ | ||
<div class="govspeak-wrapper">\ | ||
<div class="govuk-govspeak">\ | ||
<h2 id="section-1">Section 1</h2>\ | ||
<p>Section 1 text</p>\ | ||
<h2 id="section-2">Section 2</h2>\ | ||
<p>Section 2 text</p>\ | ||
<h2 id="section-3">Section 3</h2>\ | ||
<p>Section 3 text</p>\ | ||
</div>\ | ||
</div>\ | ||
</div>\ | ||
</div>') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering how much of this fixture is useful. We're faking the positions of the window scroll and elements. Maybe a more minimal fixture would: a) make it easier to read what markup is required There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've simplified the fixture to make it easier to read. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice 👍 |
||
|
||
module.getWindowDimensions = function () { | ||
return { | ||
height: 768, | ||
width: 1024 | ||
} | ||
} | ||
module.getFooterPosition = function () { | ||
return { | ||
top: 500 | ||
} | ||
} | ||
module.getHeadingPosition = function () { | ||
return { | ||
top: 100 | ||
} | ||
} | ||
module.getNextHeadingPosition = function () { | ||
return { | ||
top: 200 | ||
} | ||
} | ||
}) | ||
|
||
afterEach(function () { | ||
$(document).off() | ||
}) | ||
|
||
// The anchor link with the href matching testHref should be highlighted | ||
function isLinkHighlighted (testHref) { | ||
var $anchor = $element.find('.js-page-contents a[href="' + testHref + '"]') | ||
expect($anchor.hasClass('active')).toBe(true) | ||
} | ||
|
||
it('When the page loads, it has no highlighted nav items', function () { | ||
module.getWindowPositions = function () { | ||
return { | ||
scrollTop: 0 | ||
} | ||
} | ||
module.start($element) | ||
|
||
var $anchors = $element.find('.js-page-contents a') | ||
expect($anchors.hasClass('active')).toBe(false) | ||
}) | ||
|
||
it('When the page is scrolled, it highlights a nav item', function () { | ||
module.getWindowPositions = function () { | ||
return { | ||
scrollTop: 180 | ||
} | ||
} | ||
module.start($element) | ||
|
||
var $anchors = $element.find('.js-page-contents a') | ||
|
||
isLinkHighlighted('#section-3') | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is totally personal preference, but I've moved to using e.g.
.on('scroll', …)
as I think it makes it a million times clearer that you're setting up a new binding, vs telling the window to resize or scroll itself.