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); 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;