From 413e3c4e81c54147a57125a2b18aaa48c422afbb Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Tue, 26 May 2020 09:58:41 +0100 Subject: [PATCH 1/2] Add scrollToRevealElement method to stickys API When an element is obscured by the sticky nav, this method allows you to scroll the page until it is revealled. The bulk of this code was added in: https://github.com/alphagov/notifications-admin/pull/2843 to ensure elements with focus were in view. This moves that into a public method so, as well as being called by the focus event handler, it can be called directly by other code. These changes include code that adds a 'sticky-scroll-area' class to scroll areas not explicitly marked as such in the base HTML but made to be a scroll area by the sticky JS. --- .../stick-to-window-when-scrolling.js | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/stick-to-window-when-scrolling.js b/app/assets/javascripts/stick-to-window-when-scrolling.js index c75779109..da1129742 100644 --- a/app/assets/javascripts/stick-to-window-when-scrolling.js +++ b/app/assets/javascripts/stick-to-window-when-scrolling.js @@ -10,7 +10,10 @@ var $el = el.$fixedEl; var $scrollArea = $el.closest('.sticky-scroll-area'); - $scrollArea = $scrollArea.length ? $scrollArea : $el.parent(); + if($scrollArea.length === 0) { + $scrollArea = $el.parent(); + $scrollArea.addClass('sticky-scroll-area'); + } this._els = [el]; this.edge = edge; @@ -46,11 +49,11 @@ return focused; }, - forCaret: function (evt) { - var textarea = evt.target; + forCaret: function ($textarea) { + var textarea = $textarea.get(0); var caretCoordinates = window.getCaretCoordinates(textarea, textarea.selectionEnd); var focused = { - 'top': $(textarea).offset().top + caretCoordinates.top, + 'top': $textarea.offset().top + caretCoordinates.top, 'height': caretCoordinates.height, 'type': 'caret' }; @@ -61,21 +64,23 @@ } }; ScrollArea.prototype.focusHandler = function (e) { - var $focusedElement = $(document.activeElement); - var nodeName = $focusedElement.get(0).nodeName.toLowerCase(); + this.scrollToRevealElement($(document.activeElement)); + }; + ScrollArea.prototype.scrollToRevealElement = function ($el) { + var nodeName = $el.get(0).nodeName.toLowerCase(); var endOfFurthestEl = focusOverlap.endOfFurthestEl(this._els, this.edge); var isInSticky = function () { - return $focusedElement.closest(this.selector).length > 0; + return $el.closest(this.selector).length > 0; }.bind(this); var focused; var overlap; // if textarea is focused, we care about checking the caret, not the whole element if (nodeName === 'textarea') { - focused = this.getFocusedDetails.forCaret(e); + focused = this.getFocusedDetails.forCaret($el); } else { if (isInSticky()) { return; } - focused = this.getFocusedDetails.forElement($focusedElement); + focused = this.getFocusedDetails.forElement($el); } overlap = focusOverlap.getOverlap(focused, this.edge, endOfFurthestEl); @@ -586,6 +591,18 @@ this.syncWithDOM(onSyncComplete); }; + // Public method to scroll so an element isn't covered by the sticky nav + Sticky.prototype.scrollToRevealElement = function (el) { + var $el = $(el); + var scrollAreaNode = $el.closest('.sticky-scroll-area').get(0); + var matches = $.grep(scrollAreas._scrollAreas, function (scrollArea) { + return scrollArea.node === scrollAreaNode; + }); + + if (matches.length) { + matches[0].scrollToRevealElement($el); + } + }; Sticky.prototype.setElWidth = function (el) { var $el = el.$fixedEl; var scrollArea = scrollAreas.getAreaByEl(el); From e62fc846c7b55460bbea866a5f044cc02bf2fb2b Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Thu, 28 May 2020 15:41:59 +0100 Subject: [PATCH 2/2] Add test for scrollToRevealElement method --- .../stick-to-window-when-scrolling.test.js | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/tests/javascripts/stick-to-window-when-scrolling.test.js b/tests/javascripts/stick-to-window-when-scrolling.test.js index 5b6314a58..f94a220f9 100644 --- a/tests/javascripts/stick-to-window-when-scrolling.test.js +++ b/tests/javascripts/stick-to-window-when-scrolling.test.js @@ -292,6 +292,61 @@ describe("Stick to top/bottom of window when scrolling", () => { }); + describe("if scrollToRevealElement is called with an element", () => { + + let link; + let linkBottom; + + beforeEach(() => { + + const inputFormBottom = getScreenItemBottomPosition(inputForm); + + inputForm.insertAdjacentHTML('afterEnd', 'Formatting options'); + link = document.querySelector('#formatting-options'); + + screenMock.mockPositionAndDimension('link', link, { + offsetHeight: 25, // 143px smaller than the sticky + offsetWidth: 727, + offsetTop: inputFormBottom + }); + + linkBottom = getScreenItemBottomPosition(link); + + // move the sticky over the link. It's 168px high so this position will cause it to overlap. + screenMock.scrollTo(link.offsetTop - 140); + + window.GOVUK.stickAtTopWhenScrolling.init(); + + }); + + afterEach(() => { + + screenMock.window.spies.window.scrollTo.mockClear(); + + }); + + test("the window should scroll so the element is revealed", () => { + + // update inputForm position as DOM normally would + inputForm.offsetTop = screenMock.window.top; + + let stickyPosition = getStickyGroupPosition(screenMock, { stickyEls: [inputForm], edge: 'top' }); + + // sticky position should overlap link position + expect(stickyPosition.top).toBeLessThanOrEqual(link.offsetTop); + expect(stickyPosition.bottom).toBeGreaterThanOrEqual(linkBottom); + + window.GOVUK.stickAtTopWhenScrolling.scrollToRevealElement(link); + + stickyPosition = getStickyGroupPosition(screenMock, { stickyEls: [inputForm], edge: 'top' }); + + // the bottom of the sticky element should be at the top of the link + expect(screenMock.window.spies.window.scrollTo.mock.calls[0]).toEqual([0, link.offsetTop - stickyPosition.height]); + + }); + + }); + describe("if element is made sticky and another element underneath it is focused", () => { let checkbox; @@ -904,6 +959,61 @@ describe("Stick to top/bottom of window when scrolling", () => { }); + describe("if scrollToRevealElement is called with an element", () => { + + let link; + let linkBottom; + + beforeEach(() => { + + const contentBottom = getScreenItemBottomPosition(content); + + content.insertAdjacentHTML('afterEnd', 'Formatting options'); + link = document.querySelector('#formatting-options'); + + screenMock.mockPositionAndDimension('link', link, { + offsetHeight: 25, // 25px smaller than the sticky + offsetWidth: 727, + offsetTop: contentBottom + }); + + linkBottom = getScreenItemBottomPosition(link); + + // move the sticky over the link. It's 50px high so this position will cause it to overlap. + screenMock.scrollTo((linkBottom - windowHeight) + 5); + + window.GOVUK.stickAtBottomWhenScrolling.init(); + + }); + + afterEach(() => { + + screenMock.window.spies.window.scrollTo.mockClear(); + + }); + + test("the window should scroll so the element is revealed", () => { + + // update inputForm position as DOM normally would + pageFooter.offsetTop = screenMock.window.bottom - pageFooter.offsetHeight; + + let stickyPosition = getStickyGroupPosition(screenMock, { stickyEls: [pageFooter], edge: 'bottom' }); + + // sticky position should overlap link position + expect(stickyPosition.top).toBeLessThanOrEqual(link.offsetTop); + expect(stickyPosition.bottom).toBeGreaterThanOrEqual(linkBottom); + + window.GOVUK.stickAtBottomWhenScrolling.scrollToRevealElement(link) + + stickyPosition = getStickyGroupPosition(screenMock, { stickyEls: [pageFooter], edge: 'bottom' }); + + // the top of the sticky element should be at the bottom of the link + expect(screenMock.window.spies.window.scrollTo.mock.calls[0]).toEqual([0, (linkBottom + pageFooter.offsetHeight) - windowHeight]); + + }); + + }); + describe("if viewport bottom starts above element bottom", () => { let pageFooterBottom;