2019-08-07 12:23:07 +01:00
const helpers = require ( './support/helpers' ) ;
2019-07-24 09:54:18 +01:00
const PADDING _BETWEEN _STICKYS = 40 ;
const PADDING _BEFORE _STOPPING _POINT = 10 ;
2019-08-07 12:23:07 +01:00
function getScreenItemBottomPosition ( screenItem ) {
return screenItem . offsetTop + screenItem . offsetHeight ;
} ;
2019-07-29 16:12:38 +01:00
function getCaretPosition ( caretPosition , textarea ) {
return {
top : textarea . offsetTop + caretPosition . top ,
bottom : textarea . offsetTop + caretPosition . top + caretPosition . height ,
height : caretPosition . height
} ;
} ;
2019-07-29 09:18:49 +01:00
function getStickyGroupPosition ( screenMock , opts ) {
const edgePosition = screenMock . window [ opts . edge ] ;
const height = opts . stickyEls
. map ( el => el . offsetHeight )
. reduce ( ( accumulator , height ) => accumulator + height ) ;
if ( opts . edge === 'top' ) {
return {
top : screenMock . window . top ,
bottom : edgePosition + height ,
height : height
} ;
} else {
return {
top : edgePosition - height ,
bottom : screenMock . window . bottom ,
height : height
}
}
} ;
2019-07-29 16:12:38 +01:00
class CaretCoordinates {
constructor ( data ) {
this . top = 5.5 ;
this . left = 2 ;
this . height = 19 ;
}
moveToLine ( lineNumber ) {
const lineHeight = 30 ;
const verticalPadding = 5.5 ;
this . top = ( ( lineNumber - 1 ) * lineHeight ) + verticalPadding ;
}
}
2019-08-07 12:23:07 +01:00
beforeAll ( ( ) => {
require ( '../../app/assets/javascripts/stick-to-window-when-scrolling.js' ) ;
} ) ;
afterAll ( ( ) => {
require ( './support/teardown.js' ) ;
} ) ;
describe ( "Stick to top/bottom of window when scrolling" , ( ) => {
let screenMock ;
describe ( "If intending to stick to the top" , ( ) => {
let inputForm ;
let formFooter ;
let footer ;
let windowHeight ;
2019-07-24 09:54:18 +01:00
let getFurthestTopPoint ;
2019-08-07 12:23:07 +01:00
beforeEach ( ( ) => {
document . body . innerHTML = `
2020-02-19 11:57:15 +00:00
< div class = "govuk-grid-row" >
2020-02-20 16:55:56 +00:00
< main class = "govuk-grid-column-three-quarters column-main" >
2019-08-07 12:23:07 +01:00
< form method = "post" autocomplete = "off" >
2020-02-19 11:57:15 +00:00
< div class = "govuk-grid-row js-stick-at-top-when-scrolling" >
2020-02-19 12:36:02 +00:00
< div class = "govuk-grid-column-two-thirds " >
2019-08-07 12:23:07 +01:00
< div class = "form-group" data - module = "" >
< label class = "form-label" for = "placeholder_value" >
name
< / l a b e l >
< input class = "form-control form-control-1-1 " data - module = "" id = "placeholder_value" name = "placeholder_value" required = "" rows = "8" type = "text" value = "" >
< / d i v >
< / d i v >
< / d i v >
< div class = "page-footer" >
2020-02-04 16:22:54 +00:00
< button type = "submit" class = "govuk-button" > Continue < / b u t t o n >
2019-08-07 12:23:07 +01:00
< / d i v >
< / f o r m >
< / m a i n >
< / d i v >
< footer class = "js-footer" > < / f o o t e r > ` ;
2020-02-19 11:57:15 +00:00
inputForm = document . querySelector ( 'form > .govuk-grid-row' ) ;
2019-08-07 12:23:07 +01:00
formFooter = document . querySelector ( '.page-footer' ) ;
footer = document . querySelector ( '.js-footer' ) ;
windowHeight = 940 ;
// mock the rendering of all components
screenMock = new helpers . ScreenMock ( jest ) ;
screenMock . setWindow ( {
width : 1990 ,
height : windowHeight ,
scrollTop : 0
} ) ;
screenMock . mockPositionAndDimension ( 'inputForm' , inputForm , {
offsetHeight : 168 ,
offsetWidth : 727 ,
offsetTop : 238
} ) ;
screenMock . mockPositionAndDimension ( 'formFooter' , formFooter , {
offsetHeight : 200 ,
offsetWidth : 727 ,
offsetTop : inputForm . offsetTop + 200
} ) ;
screenMock . mockPositionAndDimension ( 'footer' , footer , {
offsetHeight : 535 ,
offsetWidth : 1990 ,
offsetTop : formFooter . offsetTop + formFooter . offsetHeight
} ) ;
getFurthestTopPoint = ( stickysHeight ) => {
return footer . offsetTop - PADDING _BEFORE _STOPPING _POINT - stickysHeight ;
} ;
// the sticky JS polls for changes to position/dimension so we need to fake setTimeout and setInterval
jest . useFakeTimers ( ) ;
} ) ;
afterEach ( ( ) => {
document . body . innerHTML = '' ;
window . GOVUK . stickAtTopWhenScrolling . clearEvents ( ) ;
screenMock . reset ( ) ;
} ) ;
test ( "if top of viewport is above top of element on load, the element should not be marked as sticky" , ( ) => {
// scroll position defaults to 0, element top defaults to 138px
window . GOVUK . stickAtTopWhenScrolling . init ( ) ;
expect ( inputForm . classList . contains ( 'content-fixed-onload' ) ) . toBe ( false ) ;
expect ( inputForm . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ;
} ) ;
2019-07-30 16:21:29 +01:00
test ( "if the window is 768px or less wide and the top of the viewport is below top of element on load, the element should not be marked as sticky" , ( ) => {
screenMock . window . resizeTo ( {
height : windowHeight ,
width : 768
} ) ;
// scroll past top of form
screenMock . scrollTo ( inputForm . offsetTop + 10 ) ;
window . GOVUK . stickAtTopWhenScrolling . init ( ) ;
expect ( inputForm . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ;
expect ( inputForm . classList . contains ( 'content-fixed-onload' ) ) . toBe ( false ) ; // check the class for onload isn't applied
} ) ;
2019-07-25 14:02:33 +01:00
describe ( "if top of viewport is below top of element but still in the scroll area on load" , ( ) => {
2019-08-07 12:23:07 +01:00
2019-07-25 14:02:33 +01:00
beforeEach ( ( ) => {
2019-08-07 12:23:07 +01:00
2019-07-25 14:02:33 +01:00
// scroll past the top of the form
screenMock . scrollTo ( inputForm . offsetTop + 10 ) ;
2019-08-07 12:23:07 +01:00
2019-07-25 14:02:33 +01:00
window . GOVUK . stickAtTopWhenScrolling . init ( ) ;
} ) ;
test ( "the element should be marked as already sticky" , ( ) => {
// `.content-fixed-onload` adds the drop-shadow without fading in to show it did not become sticky from user interaction
expect ( inputForm . classList . contains ( 'content-fixed-onload' ) ) . toBe ( true ) ;
expect ( inputForm . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ;
} ) ;
test ( "a 'shim' element with dimensions matching the sticky element should be added to the document to take up the space it no longer occupies" , ( ) => {
const shim = inputForm . previousElementSibling ;
expect ( shim ) . not . toBeNull ( ) ;
expect ( shim . classList . contains ( 'shim' ) ) . toBe ( true ) ;
expect ( shim . style . height ) . toEqual ( ` ${ inputForm . offsetHeight } px ` ) ;
expect ( shim . style . marginTop ) . toEqual ( '' ) ; // 0px would return an empty string
expect ( shim . style . marginBottom ) . toEqual ( '' ) ; // 0px would return an empty string
} ) ;
2019-08-07 12:23:07 +01:00
} ) ;
test ( "if top of viewport is below the furthest point the top of the element can go in the scroll area on load, the element should be marked as stopped" , ( ) => {
// the element should stop a set distance from the stopping point
2019-07-24 09:54:18 +01:00
const furthestTopPoint = getFurthestTopPoint ( inputForm . offsetHeight ) ;
2019-08-07 12:23:07 +01:00
// scroll past the furthest point
2019-07-24 09:54:18 +01:00
screenMock . scrollTo ( furthestTopPoint + 10 ) ;
2019-08-07 12:23:07 +01:00
window . GOVUK . stickAtTopWhenScrolling . init ( ) ;
// `.content-fixed-onload` adds the drop-shadow without fading in to show it did not become sticky from user interaction
expect ( inputForm . classList . contains ( 'content-fixed-onload' ) ) . toBe ( true ) ;
expect ( inputForm . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ;
// elements are stopped by adding inline styles
expect ( inputForm . style . position ) . toEqual ( 'absolute' ) ;
2019-07-24 09:54:18 +01:00
expect ( inputForm . style . top ) . toEqual ( ` ${ furthestTopPoint } px ` ) ;
2019-08-07 12:23:07 +01:00
} ) ;
describe ( "if viewport top starts above element top" , ( ) => {
beforeEach ( ( ) => {
// default scroll position is above top of form
window . GOVUK . stickAtTopWhenScrolling . init ( ) ;
} ) ;
test ( "if window is scrolled so top of it is below the top of the element, the element should be marked so it becomes sticky to the user" , ( ) => {
// scroll past top of form
screenMock . scrollTo ( inputForm . offsetTop + 10 ) ;
jest . advanceTimersByTime ( 60 ) ; // fake advance of time to something similar to that for a scroll
// `content-fixed` fades the drop-shadow in to show it became sticky from user interaction
expect ( inputForm . classList . contains ( 'content-fixed' ) ) . toBe ( true ) ;
expect ( inputForm . classList . contains ( 'content-fixed-onload' ) ) . toBe ( false ) ; // check the class for onload isn't applied
} ) ;
test ( "if window is scrolled so top of it is below the furthest point the top of the element can go in the scroll area, the element should be stopped" , ( ) => {
2019-07-24 09:54:18 +01:00
const furthestTopPoint = getFurthestTopPoint ( inputForm . offsetHeight ) ;
2019-08-07 12:23:07 +01:00
// scroll past top of form
2019-07-24 09:54:18 +01:00
screenMock . scrollTo ( furthestTopPoint + 10 ) ;
2019-08-07 12:23:07 +01:00
jest . advanceTimersByTime ( 60 ) ; // fake advance of time to something similar to that for a scroll
// `content-fixed` fades the drop-shadow in to show it became sticky from user interaction
expect ( inputForm . classList . contains ( 'content-fixed' ) ) . toBe ( true ) ;
expect ( inputForm . classList . contains ( 'content-fixed-onload' ) ) . toBe ( false ) ; // check the class for onload isn't applied
// elements are stopped by adding inline styles
expect ( inputForm . style . position ) . toEqual ( 'absolute' ) ;
2019-07-24 09:54:18 +01:00
expect ( inputForm . style . top ) . toEqual ( ` ${ furthestTopPoint } px ` ) ;
2019-08-07 12:23:07 +01:00
} ) ;
} ) ;
describe ( "if viewport top starts below element top" , ( ) => {
beforeEach ( ( ) => {
// scroll past top of form
screenMock . scrollTo ( inputForm . offsetTop + 10 ) ;
window . GOVUK . stickAtTopWhenScrolling . init ( ) ;
} ) ;
test ( "if window is scrolled so top of it is above the top of the element, the element should be made not sticky" , ( ) => {
// scroll to top
screenMock . scrollTo ( 0 ) ;
jest . advanceTimersByTime ( 60 ) ; // fake advance of time to something similar to that for a scroll
expect ( inputForm . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ;
expect ( inputForm . classList . contains ( 'content-fixed-onload' ) ) . toBe ( false ) ; // check the class for onload isn't applied
} ) ;
} ) ;
2020-05-28 15:41:59 +01:00
describe ( "if scrollToRevealElement is called with an element" , ( ) => {
let link ;
let linkBottom ;
beforeEach ( ( ) => {
const inputFormBottom = getScreenItemBottomPosition ( inputForm ) ;
inputForm . insertAdjacentHTML ( 'afterEnd' , '<a href="" id="formatting-options">Formatting options</a>' ) ;
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 ] ) ;
} ) ;
} ) ;
2019-07-29 09:18:49 +01:00
describe ( "if element is made sticky and another element underneath it is focused" , ( ) => {
let checkbox ;
let checkboxBottom ;
beforeEach ( ( ) => {
const inputFormBottom = getScreenItemBottomPosition ( inputForm ) ;
inputForm . insertAdjacentHTML ( 'afterEnd' , '<input type="checkbox" name="confirm" value="yes" />' ) ;
checkbox = document . querySelector ( 'input[type=checkbox]' ) ;
screenMock . mockPositionAndDimension ( 'checkbox' , checkbox , {
offsetHeight : 50 , // 118px smaller than the sticky
offsetWidth : 727 ,
offsetTop : inputFormBottom
} ) ;
checkboxBottom = getScreenItemBottomPosition ( checkbox ) ;
// move the sticky over the checkbox. It's 168px high so this position will cause it to overlap.
screenMock . scrollTo ( checkbox . offsetTop - 10 ) ;
window . GOVUK . stickAtTopWhenScrolling . init ( ) ;
} ) ;
afterEach ( ( ) => {
screenMock . window . spies . window . scrollTo . mockClear ( ) ;
} ) ;
test ( "the window should scroll so the focused 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 checkbox position
expect ( stickyPosition . top ) . toBeLessThanOrEqual ( checkbox . offsetTop ) ;
expect ( stickyPosition . bottom ) . toBeGreaterThanOrEqual ( checkboxBottom ) ;
// the sticky element (page footer) is 50 high so should cover the last of the radios if the bottom edge of the viewport is at its bottom
checkbox . focus ( ) ;
stickyPosition = getStickyGroupPosition ( screenMock , { stickyEls : [ inputForm ] , edge : 'top' } ) ;
// the bottom of the sticky element should be at the top of the checkbox
expect ( screenMock . window . spies . window . scrollTo . mock . calls [ 0 ] ) . toEqual ( [ 0 , checkbox . offsetTop - stickyPosition . height ] ) ;
} ) ;
} ) ;
2019-07-29 16:12:38 +01:00
describe ( "if element is made sticky and overlaps a textarea" , ( ) => {
let textarea ;
let textareaBottom ;
let caretCoordinates ;
let caretCoordinatesMock ;
beforeEach ( ( ) => {
const inputFormBottom = getScreenItemBottomPosition ( inputForm ) ;
inputForm . insertAdjacentHTML ( 'afterEnd' , '<textarea name="notes"></textarea>' ) ;
textarea = document . querySelector ( 'textarea' ) ;
// line height: 30px, text height: 19px, lines: 10
screenMock . mockPositionAndDimension ( 'textarea' , textarea , {
offsetHeight : 300 ,
offsetWidth : 727 ,
offsetTop : inputFormBottom
} ) ;
textareaBottom = getScreenItemBottomPosition ( textarea ) ;
// mock calls for caret position, relative to textarea
caretCoordinatesMock = jest . fn ( ( ) => caretCoordinates ) ;
window . getCaretCoordinates = caretCoordinatesMock ;
// start caret on first line
caretCoordinates = new CaretCoordinates ( ) ;
// move the sticky so it overlaps the top 168px of the textarea.
screenMock . scrollTo ( textarea . offsetTop ) ;
// update inputForm position as DOM normally would
inputForm . offsetTop = screenMock . window . top ;
window . GOVUK . stickAtTopWhenScrolling . init ( ) ;
} ) ;
afterEach ( ( ) => {
screenMock . window . spies . window . scrollTo . mockClear ( ) ;
caretCoordinatesMock . mockClear ( ) ;
} ) ;
test ( "if the textarea receives focus while its caret is underneath, the window should scroll to reveal the caret" , ( ) => {
// caret is on first line
const stickyPosition = getStickyGroupPosition ( screenMock , { stickyEls : [ inputForm ] , edge : 'top' } ) ;
const caretPosition = getCaretPosition ( caretCoordinates , textarea ) ;
// sticky position should overlap caret position
expect ( stickyPosition . top ) . toBeLessThanOrEqual ( caretPosition . top ) ;
expect ( stickyPosition . bottom ) . toBeGreaterThanOrEqual ( caretPosition . bottom ) ;
// the sticky element (page footer) is 50 high so should cover the last of the radios if the bottom edge of the viewport is at its bottom
textarea . focus ( ) ;
// the bottom of the sticky element should be at the top of the checkbox
expect ( screenMock . window . spies . window . scrollTo . mock . calls [ 0 ] ) . toEqual ( [ 0 , caretPosition . top - stickyPosition . height ] ) ;
} ) ;
test ( "if the caret is moved so it isn't underneath the sticky element, the window shouldn't scroll" , ( ) => {
// start caret on 7th line which isn't under the sticky element.
caretCoordinates . moveToLine ( 7 ) ;
const stickyPosition = getStickyGroupPosition ( screenMock , { stickyEls : [ inputForm ] , edge : 'top' } ) ;
const caretPosition = getCaretPosition ( caretCoordinates , textarea ) ;
// the sticky element should be above the caret
expect ( stickyPosition . bottom ) . toBeLessThan ( caretPosition . top ) ;
textarea . focus ( ) ;
// no scrolling should have happened
expect ( screenMock . window . spies . window . scrollTo . mock . calls . length ) . toEqual ( 0 ) ;
} ) ;
test ( "if the caret is moved to be underneath the sticky element, the window should scroll to reveal the caret" , ( ) => {
// start caret on 7th line which isn't under the sticky element.
caretCoordinates . moveToLine ( 7 ) ;
// make sure the textarea has focus
textarea . focus ( ) ;
// line 6 is under the sticky element
caretCoordinates . moveToLine ( 6 ) ;
const stickyPosition = getStickyGroupPosition ( screenMock , { stickyEls : [ inputForm ] , edge : 'top' } ) ;
const caretPosition = getCaretPosition ( caretCoordinates , textarea ) ;
// sticky should now overlap the caret
expect ( stickyPosition . bottom ) . toBeGreaterThanOrEqual ( caretPosition . top ) ;
// the sticky element (page footer) is 50 high so should cover the last of the radios if the bottom edge of the viewport is at its bottom
helpers . triggerEvent ( textarea , 'keyup' , { interface : window . KeyboardEvent } ) ;
// the bottom of the sticky element should be at the top of the checkbox
expect ( screenMock . window . spies . window . scrollTo . mock . calls [ 0 ] ) . toEqual ( [ 0 , caretPosition . top - stickyPosition . height ] ) ;
} ) ;
} ) ;
2019-07-24 09:54:18 +01:00
describe ( "if mode is set to 'dialog' and multiple sticky elements share the same scroll area" , ( ) => {
let radios ;
beforeEach ( ( ) => {
const inputFormBottom = getScreenItemBottomPosition ( inputForm ) ;
// set mode to 'dialog' so sticky elements are treated as one item
window . GOVUK . stickAtTopWhenScrolling . setMode ( 'dialog' )
// add another sticky element before the form footer
radios = helpers . getRadioGroup ( {
cssClasses : [ 'js-stick-at-top-when-scrolling' ] ,
2019-08-28 11:06:27 +01:00
name : 'choose-send-time' ,
label : 'Choose send time' ,
2019-07-24 09:54:18 +01:00
fields : [
{
label : 'Now' ,
value : 'now' ,
checked : true
} ,
{
label : 'Tomorrow' ,
value : 'tomorrow' ,
checked : false
} ,
{
label : 'Friday' ,
value : 'friday' ,
checked : false
}
]
} ) ;
formFooter . parentNode . insertBefore ( radios , formFooter ) ;
screenMock . mockPositionAndDimension ( 'radios' , radios , {
offsetHeight : 175 ,
offsetWidth : 727 ,
offsetTop : inputFormBottom
} ) ;
const radiosBottom = getScreenItemBottomPosition ( radios ) ;
// adjust other screen items
// formFooter top should be at the radios bottom
formFooter . offsetTop = radiosBottom ;
// footer top should be at formFooter's bottom
footer . offsetTop = getScreenItemBottomPosition ( formFooter ) ;
} ) ;
2019-08-02 14:03:03 +01:00
afterEach ( ( ) => {
window . GOVUK . stickAtTopWhenScrolling . setMode ( 'default' ) ;
} ) ;
2019-07-24 09:54:18 +01:00
describe ( "if window top is below the top of the highest element on load" , ( ) => {
beforeEach ( ( ) => {
// scroll past top of first sticky element
screenMock . scrollTo ( inputForm . offsetTop + 10 ) ;
window . GOVUK . stickAtTopWhenScrolling . init ( ) ;
} ) ;
test ( "both should be marked as already sticky" , ( ) => {
// `.content-fixed-onload` adds the drop-shadow without fading in to show it did not become sticky from user interaction
expect ( radios . classList . contains ( 'content-fixed-onload' ) ) . toBe ( true ) ;
expect ( radios . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ;
expect ( inputForm . classList . contains ( 'content-fixed-onload' ) ) . toBe ( true ) ;
expect ( inputForm . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ;
} ) ;
test ( "the lowest element should have the drop-shadow" , ( ) => {
expect ( radios . classList . contains ( 'content-fixed__top' ) ) . toBe ( true ) ;
} ) ;
test ( "they should be stacked from the top edge of the viewport in the order they appear in the document" , ( ) => {
expect ( inputForm . style . top ) . toEqual ( '0px' ) ;
// dialog mode removes enough padding to deal with that each sticky element is given to give its content enough space from the surrounding page
expect ( radios . style . top ) . toEqual ( ` ${ inputForm . offsetHeight - PADDING _BETWEEN _STICKYS } px ` ) ;
} ) ;
} ) ;
describe ( "if window top is below the furthest point the highest element can go on load" , ( ) => {
let furthestTopPoint ;
beforeEach ( ( ) => {
// ensure each sticky element is positioned correctly relative to the furthest point
const dialogHeight = ( inputForm . offsetHeight + radios . offsetHeight ) - PADDING _BETWEEN _STICKYS ;
furthestTopPoint = getFurthestTopPoint ( dialogHeight ) ;
// scroll past top of first sticky element
screenMock . scrollTo ( furthestTopPoint + 10 ) ;
window . GOVUK . stickAtTopWhenScrolling . init ( ) ;
} ) ;
test ( "both should be stopped so the bottom of their stack is at the furthest point" , ( ) => {
expect ( inputForm . style . position ) . toEqual ( 'absolute' ) ;
expect ( radios . style . position ) . toEqual ( 'absolute' ) ;
expect ( inputForm . style . top ) . toEqual ( ` ${ furthestTopPoint } px ` ) ;
expect ( radios . style . top ) . toEqual ( ` ${ footer . offsetTop - PADDING _BEFORE _STOPPING _POINT - radios . offsetHeight } px ` ) ;
} ) ;
} ) ;
describe ( "if the group of sticky elements is taller than the window when stuck together" , ( ) => {
// the sticky behaviour should stick as many elements as possible to the top of the viewport
// the rest should be put back into their place in the document
beforeEach ( ( ) => {
const windowHeight = 460 ;
function makeStickysBiggerThanWindow ( ) {
// make the radios too big to fit in the viewport
const fields = [ 'Saturday' , 'Sunday' , 'Monday' , 'Tuesday' , 'Wednesday' , 'Thursday' , 'Friday' ]
. map ( field => {
return {
label : field ,
value : field . toLowerCase ( ) ,
checked : false
}
} ) ;
2019-08-28 11:06:27 +01:00
radios . querySelector ( 'fieldset' ) . insertAdjacentHTML ( 'beforeend' , helpers . getRadios ( fields , 'days' ) ) ;
2019-07-24 09:54:18 +01:00
radios . offsetHeight = 475 ;
} ;
function adjustScreenPositions ( ) {
const radiosBottom = getScreenItemBottomPosition ( radios ) ;
screenMock . window . setHeightTo ( windowHeight ) ;
// formFooter top should be at the radios bottom
formFooter . offsetTop = radiosBottom ;
// footer top should be at formFooter's bottom
footer . offsetTop = getScreenItemBottomPosition ( formFooter ) ;
} ;
makeStickysBiggerThanWindow ( ) ;
adjustScreenPositions ( ) ;
screenMock . scrollTo ( inputForm . offsetTop + 10 ) ;
window . GOVUK . stickAtTopWhenScrolling . init ( )
} ) ;
test ( "the number of elements in the group that are stuck should be reduced until it fits into the viewport" , ( ) => {
// when the group is stuck, pageFooter is closest to the viewport edge so should stay sticky
expect ( inputForm . classList . contains ( 'content-fixed-onload' ) ) . toBe ( true ) ;
expect ( inputForm . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ;
// radios are made not sticky
expect ( radios . classList . contains ( 'content-fixed-onload' ) ) . toBe ( false ) ;
expect ( radios . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ;
} ) ;
test ( "window is scrolled so the sticky elements (stuck and not stuck) appear in sequence" , ( ) => {
// we assume it's useful to start by seeing all the elements in the position they normally sit in the page
expect ( screenMock . window . top ) . toEqual ( inputForm . offsetTop ) ;
} ) ;
} ) ;
} ) ;
2019-08-07 12:23:07 +01:00
} ) ;
describe ( "If intending to stick to the bottom" , ( ) => {
let header ;
let content ;
let pageFooter ;
let windowHeight ;
2019-07-24 09:54:18 +01:00
let getFurthestBottomPoint ;
2019-08-07 12:23:07 +01:00
beforeEach ( ( ) => {
document . body . innerHTML = `
2020-02-20 16:55:56 +00:00
< main role = "main" class = "govuk-grid-column-three-quarters column-main" >
2023-06-08 13:12:00 -04:00
< a class = "usa-back-link" href = "" > Back < / a >
2019-08-07 12:23:07 +01:00
< h1 class = "heading-large js-header" >
Preview of ‘ Content email ’
< / h 1 >
< div class = "wrapper" >
< div class = "email-message-body" >
< h2 > This is the title < / h 2 >
< p > This is a paragraph , defined as a block of content where the contents run horizontally across lines . < / p >
< table role = "presentation" >
< tbody >
< tr >
< td >
< ul >
< li > these < / l i >
< li > are < / l i >
< li > bullet points < / l i >
< / u l >
< / t d >
< / t r >
< / t b o d y >
< / t a b l e >
< blockquote >
< p > This block of text is formatted to stand out from the rest < / p >
< / b l o c k q u o t e >
< p > This is a paragraph with a horizontal line underneath . < / p >
< hr >
< p > This is a paragraph with a horizontal line above . < / p >
2023-08-08 16:19:17 -04:00
< p > This paragraph has a link in it : < a class = "usa-link" href = "https://www.gov.uk" > https : //www.gov.uk</a>.</p>
2019-08-07 12:23:07 +01:00
< / d i v >
< div class = "page-footer js-stick-at-bottom-when-scrolling" >
< form method = "post" action = "" >
2020-02-04 16:22:54 +00:00
< button type = "submit" class = "govuk-button" > Send 1 email < / b u t t o n >
2019-08-07 12:23:07 +01:00
< / f o r m >
< / d i v >
< / d i v >
< / m a i n > ` ;
heading = document . querySelector ( '.js-header' ) ;
content = document . querySelector ( '.email-message-body' ) ;
pageFooter = document . querySelector ( '.page-footer' ) ;
windowHeight = 940 ;
// mock the rendering of all components
screenMock = new helpers . ScreenMock ( jest ) ;
screenMock . setWindow ( {
width : 1990 ,
height : windowHeight ,
scrollTop : 0
} ) ;
screenMock . mockPositionAndDimension ( 'heading' , heading , {
offsetHeight : 75 ,
offsetWidth : 727 ,
offsetTop : 185
} ) ;
screenMock . mockPositionAndDimension ( 'content' , content , {
offsetHeight : 900 ,
offsetWidth : 727 ,
offsetTop : heading . offsetTop + heading . offsetHeight
} ) ;
screenMock . mockPositionAndDimension ( 'pageFooter' , pageFooter , {
offsetHeight : 50 ,
offsetWidth : 727 ,
offsetTop : content . offsetTop + content . offsetHeight
} ) ;
2019-07-24 09:54:18 +01:00
getFurthestBottomPoint = ( stickysTotalHeight ) => {
const headingBottom = getScreenItemBottomPosition ( heading ) ;
2019-08-07 12:23:07 +01:00
2019-07-24 09:54:18 +01:00
return headingBottom + PADDING _BEFORE _STOPPING _POINT + stickysTotalHeight ;
2019-08-07 12:23:07 +01:00
} ;
// the sticky JS polls for changes to position/dimension so we need to fake setTimeout and setInterval
jest . useFakeTimers ( ) ;
} ) ;
afterEach ( ( ) => {
window . GOVUK . stickAtBottomWhenScrolling . clearEvents ( ) ;
} ) ;
test ( "if bottom of viewport is below bottom of element on load, the element should not be marked as sticky" , ( ) => {
const pageFooterBottom = getScreenItemBottomPosition ( pageFooter ) ;
// scroll so the bottom of the window goes past the bottom of the element
screenMock . scrollTo ( ( pageFooterBottom - windowHeight ) + 10 ) ;
window . GOVUK . stickAtBottomWhenScrolling . init ( ) ;
// `.content-fixed-onload` adds the drop-shadow without fading in to show it did not become sticky from user interaction
expect ( pageFooter . classList . contains ( 'content-fixed-onload' ) ) . toBe ( false ) ;
expect ( pageFooter . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ;
} ) ;
2019-07-30 16:21:29 +01:00
test ( "if the window is 768px or less wide and the bottom of the viewport is above bottom of element on load, the element should not be marked as sticky" , ( ) => {
const pageFooterBottom = getScreenItemBottomPosition ( pageFooter ) ;
screenMock . window . resizeTo ( {
height : windowHeight ,
width : 768
} ) ;
// scroll past top of form
screenMock . scrollTo ( pageFooterBottom - 10 ) ;
window . GOVUK . stickAtTopWhenScrolling . init ( ) ;
expect ( pageFooter . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ;
expect ( pageFooter . classList . contains ( 'content-fixed-onload' ) ) . toBe ( false ) ; // check the class for onload isn't applied
} ) ;
2019-07-25 14:02:33 +01:00
describe ( "if bottom of viewport is above bottom of element on load" , ( ) => {
beforeEach ( ( ) => {
2019-08-07 12:23:07 +01:00
2019-07-25 14:02:33 +01:00
// scroll position defaults to 0 so bottom of window starts at 940px. Element bottom defaults to 1160px.
window . GOVUK . stickAtBottomWhenScrolling . init ( ) ;
2019-08-07 12:23:07 +01:00
2019-07-25 14:02:33 +01:00
} ) ;
2019-08-07 12:23:07 +01:00
2019-07-25 14:02:33 +01:00
test ( "the element should be marked as already sticky" , ( ) => {
expect ( pageFooter . classList . contains ( 'content-fixed-onload' ) ) . toBe ( true ) ;
expect ( pageFooter . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ;
} ) ;
test ( "a 'shim' element with dimensions matching the sticky element should be added to the document to take up the space it no longer occupies" , ( ) => {
const shim = pageFooter . nextElementSibling ;
expect ( shim ) . not . toBeNull ( ) ;
expect ( shim . classList . contains ( 'shim' ) ) . toBe ( true ) ;
expect ( shim . style . height ) . toEqual ( ` ${ pageFooter . offsetHeight } px ` ) ;
expect ( shim . style . marginTop ) . toEqual ( '' ) ; // 0px would return an empty string
expect ( shim . style . marginBottom ) . toEqual ( '' ) ; // 0px would return an empty string
} ) ;
2019-08-07 12:23:07 +01:00
} ) ;
test ( "if bottom of viewport is above the furthest point the bottom of the element can go in the scroll area on load, the element should be marked as stopped" , ( ) => {
// change window size so its bottom can go past stopping position
const windowHeight = 200 ;
2019-07-24 09:54:18 +01:00
const furthestBottomPoint = getFurthestBottomPoint ( pageFooter . offsetHeight ) ;
2019-08-07 12:23:07 +01:00
screenMock . window . setHeightTo ( windowHeight ) ;
// scroll the window bottom past the furthest point
2019-07-24 09:54:18 +01:00
screenMock . scrollTo ( ( furthestBottomPoint - windowHeight ) - 10 ) ;
2019-08-07 12:23:07 +01:00
window . GOVUK . stickAtBottomWhenScrolling . init ( ) ;
// `.content-fixed-onload` adds the drop-shadow without fading in to show it did not become sticky from user interaction
expect ( pageFooter . classList . contains ( 'content-fixed-onload' ) ) . toBe ( true ) ;
expect ( pageFooter . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ;
// elements are stopped by adding inline styles
expect ( pageFooter . style . position ) . toEqual ( 'absolute' ) ;
2019-07-24 09:54:18 +01:00
expect ( pageFooter . style . top ) . toEqual ( ` ${ furthestBottomPoint - pageFooter . offsetHeight } px ` ) ;
2019-08-07 12:23:07 +01:00
} ) ;
describe ( "if viewport bottom starts below element bottom" , ( ) => {
let pageFooterBottom ;
beforeEach ( ( ) => {
pageFooterBottom = getScreenItemBottomPosition ( pageFooter ) ;
// change window size so its bottom can go past stopping position
const windowHeight = 600 ;
screenMock . window . setHeightTo ( windowHeight ) ;
// scroll to just below the element
screenMock . scrollTo ( ( pageFooterBottom - windowHeight ) + 10 ) ;
window . GOVUK . stickAtBottomWhenScrolling . init ( ) ;
} ) ;
test ( "if window is scrolled so bottom of it is above the bottom of the element, the element should be marked so it becomes sticky to the user" , ( ) => {
// scroll above bottom of sticky element
screenMock . scrollTo ( ( pageFooterBottom - windowHeight ) - 10 ) ;
jest . advanceTimersByTime ( 60 ) ; // fake advance of time to something similar to that for a scroll
// `content-fixed` fades the drop-shadow in to show it became sticky from user interaction
expect ( pageFooter . classList . contains ( 'content-fixed' ) ) . toBe ( true ) ;
expect ( pageFooter . classList . contains ( 'content-fixed-onload' ) ) . toBe ( false ) ; // check the class for onload isn't applied
} ) ;
test ( "if window is scrolled so bottom of it is above the furthest point the bottom of the element can go in the scroll area, the element should be stopped" , ( ) => {
2019-07-24 09:54:18 +01:00
const furthestBottomPoint = getFurthestBottomPoint ( pageFooter . offsetHeight ) ;
2019-08-02 14:03:03 +01:00
// scroll past furthest point
2019-07-24 09:54:18 +01:00
screenMock . scrollTo ( ( furthestBottomPoint - windowHeight ) - 10 ) ;
2019-08-07 12:23:07 +01:00
jest . advanceTimersByTime ( 60 ) ; // fake advance of time to something similar to that for a scroll
// `content-fixed` fades the drop-shadow in to show it became sticky from user interaction
expect ( pageFooter . classList . contains ( 'content-fixed' ) ) . toBe ( true ) ;
expect ( pageFooter . classList . contains ( 'content-fixed-onload' ) ) . toBe ( false ) ; // check the class for onload isn't applied
// elements are stopped by adding inline styles
expect ( pageFooter . style . position ) . toEqual ( 'absolute' ) ;
2019-07-24 09:54:18 +01:00
expect ( pageFooter . style . top ) . toEqual ( ` ${ furthestBottomPoint - pageFooter . offsetHeight } px ` ) ;
2019-08-07 12:23:07 +01:00
} ) ;
2019-08-02 14:03:03 +01:00
test ( "if window resizes so bottom is above the bottom of the element, the element should be marked so it becomes sticky to the user" , ( ) => {
// resize window so the bottom is 30px above the bottom of the element
screenMock . window . resizeTo ( { height : 560 , width : screenMock . window . width } ) ;
jest . advanceTimersByTime ( 60 ) ; // fake advance of time to something similar to that for a resize
// `content-fixed` fades the drop-shadow in to show it became sticky from user interaction
expect ( pageFooter . classList . contains ( 'content-fixed' ) ) . toBe ( true ) ;
expect ( pageFooter . classList . contains ( 'content-fixed-onload' ) ) . toBe ( false ) ; // check the class for onload isn't applied
} ) ;
2019-08-07 12:23:07 +01:00
} ) ;
2020-05-28 15:41:59 +01:00
describe ( "if scrollToRevealElement is called with an element" , ( ) => {
let link ;
let linkBottom ;
beforeEach ( ( ) => {
const contentBottom = getScreenItemBottomPosition ( content ) ;
content . insertAdjacentHTML ( 'afterEnd' , '<a href="" id="formatting-options">Formatting options</a>' ) ;
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 ] ) ;
} ) ;
} ) ;
2019-08-07 12:23:07 +01:00
describe ( "if viewport bottom starts above element bottom" , ( ) => {
let pageFooterBottom ;
2019-08-02 14:03:03 +01:00
let pageFooterShim ;
2019-08-07 12:23:07 +01:00
beforeEach ( ( ) => {
// scroll position defaults to 0 so bottom of window starts at 940px. Element bottom defaults to 1160px so will be sticky on load.
pageFooterBottom = getScreenItemBottomPosition ( pageFooter ) ;
2019-08-02 14:03:03 +01:00
// shim will be inserted, inheriting space in the page from pageFooter so store this data
const pageFooterData = {
offsetHeight : pageFooter . offsetHeight ,
offsetWidth : pageFooter . offsetWidth ,
offsetTop : pageFooter . offsetTop
} ;
2019-08-07 12:23:07 +01:00
window . GOVUK . stickAtBottomWhenScrolling . init ( ) ;
2019-08-02 14:03:03 +01:00
// add mock for shim
pageFooterShim = document . querySelector ( '.shim' ) ;
screenMock . mockPositionAndDimension ( 'pageFooterShim' , pageFooterShim , pageFooterData ) ;
2019-08-07 12:23:07 +01:00
} ) ;
test ( "if window is scrolled so bottom of it is below the bottom of the element, the element should be made not sticky" , ( ) => {
// scroll so bottom of window is below bottom of element
screenMock . scrollTo ( ( pageFooterBottom - windowHeight ) + 10 ) ;
jest . advanceTimersByTime ( 60 ) ; // fake advance of time to something similar to that for a scroll
expect ( pageFooter . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ;
expect ( pageFooter . classList . contains ( 'content-fixed-onload' ) ) . toBe ( false ) ; // check the class for onload isn't applied
} ) ;
2019-08-02 14:03:03 +01:00
test ( "if window resizes so bottom is below the bottom of the element, the element should be made not sticky" , ( ) => {
// resize window so the bottom is 30px above the bottom of the element
screenMock . window . resizeTo ( { height : pageFooterBottom + 10 , width : screenMock . window . width } ) ;
jest . advanceTimersByTime ( 60 ) ; // fake advance of time to something similar to that for a resize
expect ( pageFooter . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ;
expect ( pageFooter . classList . contains ( 'content-fixed-onload' ) ) . toBe ( false ) ;
} ) ;
test ( "if window resizes so bottom is above the furthest point the bottom of the element can go in the scroll area, the element should be stopped" , ( ) => {
const furthestBottomPoint = getFurthestBottomPoint ( pageFooter . offsetHeight ) ;
// resize window so the bottom is above the furthest point
screenMock . window . resizeTo ( { height : furthestBottomPoint - 10 , width : screenMock . window . width } ) ;
jest . advanceTimersByTime ( 60 ) ; // fake advance of time to something similar to that for a resize
expect ( pageFooter . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ; // applied if made sticky after page load
expect ( pageFooter . classList . contains ( 'content-fixed-onload' ) ) . toBe ( true ) ; // check the class for onload isn't applied
// elements are stopped by adding inline styles
expect ( pageFooter . style . position ) . toEqual ( 'absolute' ) ;
expect ( pageFooter . style . top ) . toEqual ( ` ${ furthestBottomPoint - pageFooter . offsetHeight } px ` ) ;
} ) ;
2019-08-07 12:23:07 +01:00
} ) ;
2019-07-29 09:18:49 +01:00
describe ( "if element is made sticky and another element underneath it is focused" , ( ) => {
let checkbox ;
let checkboxBottom ;
beforeEach ( ( ) => {
const contentBottom = getScreenItemBottomPosition ( content ) ;
content . insertAdjacentHTML ( 'afterEnd' , '<input type="checkbox" name="confirm" value="yes" />' ) ;
checkbox = document . querySelector ( 'input[type=checkbox]' ) ;
screenMock . mockPositionAndDimension ( 'checkbox' , checkbox , {
offsetHeight : 40 , // 10px smaller than the sticky
offsetWidth : 727 ,
offsetTop : contentBottom
} ) ;
checkboxBottom = getScreenItemBottomPosition ( checkbox ) ;
// move the sticky over the checkbox. It's 50px high so this position will cause it to overlap.
screenMock . scrollTo ( ( checkboxBottom - windowHeight ) + 5 ) ;
window . GOVUK . stickAtBottomWhenScrolling . init ( ) ;
} ) ;
afterEach ( ( ) => {
screenMock . window . spies . window . scrollTo . mockClear ( ) ;
} ) ;
test ( "the window should scroll so the focused 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 checkbox position
expect ( stickyPosition . top ) . toBeLessThanOrEqual ( checkbox . offsetTop ) ;
expect ( stickyPosition . bottom ) . toBeGreaterThanOrEqual ( checkboxBottom ) ;
// the sticky element (page footer) is 50 high so should cover the last of the radios if the bottom edge of the viewport is at its bottom
checkbox . focus ( ) ;
stickyPosition = getStickyGroupPosition ( screenMock , { stickyEls : [ pageFooter ] , edge : 'bottom' } ) ;
// the top of the sticky element should be at the bottom of the checkbox
expect ( screenMock . window . spies . window . scrollTo . mock . calls [ 0 ] ) . toEqual ( [ 0 , ( checkboxBottom + pageFooter . offsetHeight ) - windowHeight ] ) ;
} ) ;
} ) ;
2019-07-29 16:12:38 +01:00
describe ( "if element is made sticky and overlaps a textarea" , ( ) => {
let textarea ;
let textareaBottom ;
let caretCoordinates ;
let caretCoordinatesMock ;
beforeEach ( ( ) => {
const contentBottom = getScreenItemBottomPosition ( content ) ;
content . insertAdjacentHTML ( 'afterEnd' , '<textarea name="notes"></textarea>' ) ;
textarea = document . querySelector ( 'textarea' ) ;
// line height: 30px, text height: 19px, 10 lines.
screenMock . mockPositionAndDimension ( 'textarea' , textarea , {
offsetHeight : 300 ,
offsetWidth : 727 ,
offsetTop : contentBottom
} ) ;
textareaBottom = getScreenItemBottomPosition ( textarea ) ;
// start caret on the last line
caretCoordinates = new CaretCoordinates ( ) ;
caretCoordinates . moveToLine ( 10 ) ;
caretCoordinatesMock = jest . fn ( ( ) => caretCoordinates ) ;
window . getCaretCoordinates = caretCoordinatesMock ;
// move the sticky so it overlaps the bottom 50px of the textarea.
screenMock . scrollTo ( textareaBottom - windowHeight ) ;
// update content position as DOM normally would
pageFooter . offsetTop = screenMock . window . bottom - pageFooter . offsetHeight ;
window . GOVUK . stickAtBottomWhenScrolling . init ( ) ;
} ) ;
afterEach ( ( ) => {
screenMock . window . spies . window . scrollTo . mockClear ( ) ;
caretCoordinatesMock . mockClear ( ) ;
} ) ;
test ( "if the textarea receives focus while its caret is underneath, the window should scroll to reveal the caret" , ( ) => {
const stickyPosition = getStickyGroupPosition ( screenMock , { stickyEls : [ pageFooter ] , edge : 'bottom' } ) ;
const caretPosition = getCaretPosition ( caretCoordinates , textarea ) ;
// sticky position should overlap caret position
expect ( stickyPosition . top ) . toBeLessThan ( caretPosition . bottom ) ;
textarea . focus ( ) ;
// the top of the sticky element should be at the bottom of the caret
expect ( screenMock . window . spies . window . scrollTo . mock . calls [ 0 ] ) . toEqual ( [ 0 , caretPosition . bottom - ( windowHeight - stickyPosition . height ) ] ) ;
} ) ;
test ( "if the caret is moved so it isn't underneath the sticky element, the window shouldn't scroll" , ( ) => {
// start caret on 8th line which isn't under the sticky element.
caretCoordinates . moveToLine ( 8 ) ;
const stickyPosition = getStickyGroupPosition ( screenMock , { stickyEls : [ pageFooter ] , edge : 'bottom' } ) ;
const caretPosition = getCaretPosition ( caretCoordinates , textarea ) ;
// the sticky element should be below the caret
expect ( stickyPosition . top ) . toBeGreaterThan ( caretPosition . bottom ) ;
textarea . focus ( ) ;
// no scrolling should have happened
expect ( screenMock . window . spies . window . scrollTo . mock . calls . length ) . toEqual ( 0 ) ;
} ) ;
test ( "if the caret is moved to be underneath the sticky element, the window should scroll to reveal the caret" , ( ) => {
// start caret on 8th line which isn't under the sticky element.
caretCoordinates . moveToLine ( 8 ) ;
// make sure the textarea has focus
textarea . focus ( ) ;
// move the caret underneath the sticky element
caretCoordinates . moveToLine ( 9 ) ;
const stickyPosition = getStickyGroupPosition ( screenMock , { stickyEls : [ pageFooter ] , edge : 'bottom' } ) ;
const caretPosition = getCaretPosition ( caretCoordinates , textarea ) ;
// sticky position should overlap caret position
expect ( stickyPosition . top ) . toBeLessThan ( caretPosition . bottom ) ;
// simulate a press of the down arrow
helpers . triggerEvent ( textarea , 'keyup' , { interface : window . KeyboardEvent } ) ;
// the top of the sticky element should be at the bottom of the caret
expect ( screenMock . window . spies . window . scrollTo . mock . calls [ 0 ] ) . toEqual ( [ 0 , caretPosition . bottom - ( windowHeight - stickyPosition . height ) ] ) ;
} ) ;
} ) ;
2019-07-24 09:54:18 +01:00
describe ( "if mode is set to 'dialog' and multiple sticky elements have the same scroll area" , ( ) => {
let radios ;
beforeEach ( ( ) => {
const contentBottom = getScreenItemBottomPosition ( content ) ;
// set mode to 'dialog' so sticky elements are treated as one item
window . GOVUK . stickAtBottomWhenScrolling . setMode ( 'dialog' )
// add another sticky element before the form footer
radios = helpers . getRadioGroup ( {
cssClasses : [ 'js-stick-at-bottom-when-scrolling' ] ,
2019-08-28 11:06:27 +01:00
name : 'choose-send-time' ,
label : 'Choose send time' ,
2019-07-24 09:54:18 +01:00
fields : [
{
label : 'Now' ,
value : 'now' ,
checked : true
} ,
{
label : 'Tomorrow' ,
value : 'tomorrow' ,
checked : false
} ,
{
label : 'Friday' ,
value : 'friday' ,
checked : false
}
]
} ) ;
pageFooter . parentNode . insertBefore ( radios , pageFooter ) ;
screenMock . mockPositionAndDimension ( 'radios' , radios , {
offsetHeight : 175 ,
offsetWidth : 727 ,
offsetTop : contentBottom
} ) ;
const radiosBottom = getScreenItemBottomPosition ( radios ) ;
// adjust other screen items
// pageFooter top should be at the radios bottom
pageFooter . offsetTop = radiosBottom ;
} ) ;
2019-08-02 14:03:03 +01:00
afterEach ( ( ) => {
window . GOVUK . stickAtBottomWhenScrolling . setMode ( 'default' ) ;
} ) ;
2019-07-24 09:54:18 +01:00
describe ( "if window bottom is above the bottom of the lowest element on load" , ( ) => {
let pageFooterBottom ;
beforeEach ( ( ) => {
pageFooterBottom = getScreenItemBottomPosition ( pageFooter ) ;
// scroll to just above the element
screenMock . scrollTo ( ( pageFooterBottom - windowHeight ) - 10 ) ;
window . GOVUK . stickAtBottomWhenScrolling . init ( ) ;
} ) ;
test ( "both should be marked as already sticky" , ( ) => {
expect ( pageFooter . classList . contains ( 'content-fixed-onload' ) ) . toBe ( true ) ;
expect ( pageFooter . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ;
} ) ;
test ( "the highest element should have the drop-shadow" , ( ) => {
expect ( radios . classList . contains ( 'content-fixed__bottom' ) ) . toBe ( true ) ;
} ) ;
test ( "they should be stacked from the bottom edge of the viewport in the order they appear in the document" , ( ) => {
expect ( radios . style . bottom ) . toEqual ( ` ${ pageFooter . offsetHeight - PADDING _BETWEEN _STICKYS } px ` ) ;
} ) ;
} ) ;
describe ( "if window bottom is above the furthest point the lowest element can go on load" , ( ) => {
let headingBottom ;
beforeEach ( ( ) => {
const dialogHeight = ( pageFooter . offsetHeight + radios . offsetHeight ) - PADDING _BETWEEN _STICKYS ;
const furthestBottomPoint = getFurthestBottomPoint ( dialogHeight ) ;
// change window size so its bottom can go past stopping position
const windowHeight = 200 ;
headingBottom = getScreenItemBottomPosition ( heading ) ;
screenMock . window . setHeightTo ( windowHeight ) ;
screenMock . scrollTo ( ( furthestBottomPoint - windowHeight ) - 10 )
window . GOVUK . stickAtBottomWhenScrolling . init ( ) ;
} ) ;
test ( "both should be stopped so the top of their stack is at the furthest point" , ( ) => {
expect ( radios . style . position ) . toEqual ( 'absolute' ) ;
expect ( pageFooter . style . position ) . toEqual ( 'absolute' ) ;
expect ( radios . style . top ) . toEqual ( ` ${ headingBottom + PADDING _BEFORE _STOPPING _POINT } px ` ) ;
expect ( pageFooter . style . top ) . toEqual ( ` ${ headingBottom + PADDING _BEFORE _STOPPING _POINT + ( radios . offsetHeight - PADDING _BETWEEN _STICKYS ) } px ` ) ;
} ) ;
} ) ;
describe ( "if the group of sticky elements is taller than the window when stuck together" , ( ) => {
let windowHeight ;
let pageFooterBottom ;
// the sticky behaviour should stick as many elements as possible to the bottom of the viewport
// the rest should be put back into their place in the document
beforeEach ( ( ) => {
windowHeight = 460 ;
function makeStickysBiggerThanWindow ( ) {
// make the radios too big to fit in the viewport
const fields = [ 'Saturday' , 'Sunday' , 'Monday' , 'Tuesday' , 'Wednesday' , 'Thursday' , 'Friday' ]
. map ( field => {
return {
label : field ,
value : field . toLowerCase ( ) ,
checked : false
}
} ) ;
2019-08-28 11:06:27 +01:00
radios . querySelector ( 'fieldset' ) . insertAdjacentHTML ( 'beforeend' , helpers . getRadios ( fields , 'days' ) ) ;
2019-07-24 09:54:18 +01:00
radios . offsetHeight = 475 ;
} ;
function adjustScreenPositions ( ) {
const radiosBottom = getScreenItemBottomPosition ( radios ) ;
screenMock . window . setHeightTo ( windowHeight ) ;
// formFooter top should be at the radios bottom
pageFooter . offsetTop = radiosBottom ;
} ;
makeStickysBiggerThanWindow ( ) ;
adjustScreenPositions ( ) ;
pageFooterBottom = getScreenItemBottomPosition ( pageFooter ) ;
screenMock . scrollTo ( ( pageFooterBottom - windowHeight ) - 10 ) ;
window . GOVUK . stickAtBottomWhenScrolling . init ( )
} ) ;
test ( "the number of elements in the group that are stuck should be reduced until it fits into the viewport" , ( ) => {
// when the group is stuck, pageFooter is closest to the viewport edge so should stay sticky
expect ( pageFooter . classList . contains ( 'content-fixed-onload' ) ) . toBe ( true ) ;
expect ( pageFooter . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ;
// radios are made not sticky
expect ( radios . classList . contains ( 'content-fixed-onload' ) ) . toBe ( false ) ;
expect ( radios . classList . contains ( 'content-fixed' ) ) . toBe ( false ) ;
} ) ;
test ( "window is scrolled so the sticky elements (stuck and not stuck) appear in sequence" , ( ) => {
// we assume it's useful to start by seeing all the elements in the position they normally sit in the page
expect ( screenMock . window . top ) . toEqual ( pageFooterBottom - windowHeight ) ;
} ) ;
} ) ;
} ) ;
2019-08-07 12:23:07 +01:00
} ) ;
} ) ;