diff --git a/resources/skins.citizen.scripts/scrollObserver.js b/resources/skins.citizen.scripts/scrollObserver.js index 79833655..28f5761f 100644 --- a/resources/skins.citizen.scripts/scrollObserver.js +++ b/resources/skins.citizen.scripts/scrollObserver.js @@ -1,24 +1,56 @@ /** - * Create an observer for showing/hiding feature and for firing scroll event hooks. + * Create an observer based vertical scroll direction + * + * @param {Function} onScrollDown functionality for when viewport is scrolled down + * @param {Function} onScrollUp functionality for when viewport is scrolled up + * @param {number} threshold minimum scrolled px to trigger the function + * @return {void} + */ +function initDirectionObserver( onScrollDown, onScrollUp, threshold ) { + const throttle = require( 'mediawiki.util' ).throttle; + + let lastScrollTop = window.scrollY; + + const onScroll = () => { + const scrollTop = window.scrollY; + + if ( Math.abs( scrollTop - lastScrollTop ) < threshold ) { + return; + } + + if ( scrollTop > lastScrollTop ) { + onScrollDown(); + } else { + onScrollUp(); + } + lastScrollTop = scrollTop; + }; + + window.addEventListener( 'scroll', throttle( onScroll, 250 ) ); +} + +/** + * Create an observer based on element visiblity. * Based on Vector * - * @param {Function} show functionality for when feature is visible - * @param {Function} hide functionality for when feature is hidden + * @param {Function} onHidden functionality for when the element is visible + * @param {Function} onVisible functionality for when the element is hidden * @return {IntersectionObserver} */ -function initScrollObserver( show, hide ) { +function initIntersectionObserver( onHidden, onVisible ) { /* eslint-disable-next-line compat/compat */ return new IntersectionObserver( ( entries ) => { if ( !entries[ 0 ].isIntersecting && entries[ 0 ].boundingClientRect.top < 0 ) { // Viewport has crossed the bottom edge of the target element. - show(); + onHidden(); } else { // Viewport is above the bottom edge of the target element. - hide(); + onVisible(); } } ); } module.exports = { - initScrollObserver + initDirectionObserver, + initIntersectionObserver }; diff --git a/resources/skins.citizen.scripts/skin.js b/resources/skins.citizen.scripts/skin.js index 15ef03af..b453a97c 100644 --- a/resources/skins.citizen.scripts/skin.js +++ b/resources/skins.citizen.scripts/skin.js @@ -62,14 +62,27 @@ function uncheckCheckboxHacks() { * @return {void} */ function initStickyHeader( document ) { + const scrollObserver = require( './scrollObserver.js' ); + + // Detect scroll direction and add the right class + scrollObserver.initDirectionObserver( + () => { + document.body.classList.remove( 'citizen-scroll--up' ); + document.body.classList.add( 'citizen-scroll--down' ); + }, + () => { + document.body.classList.remove( 'citizen-scroll--down' ); + document.body.classList.add( 'citizen-scroll--up' ); + }, + 100 + ); + const sentinel = document.getElementById( 'citizen-body-header-sticky-sentinel' ); // In some pages we use display:none to disable the sticky header // Do not start observer if it is set to display:none if ( sentinel && getComputedStyle( sentinel ).getPropertyValue( 'display' ) !== 'none' ) { - const scrollObserver = require( './scrollObserver.js' ); - - const observer = scrollObserver.initScrollObserver( + const observer = scrollObserver.initIntersectionObserver( () => { document.body.classList.add( 'citizen-body-header--sticky' ); }, diff --git a/resources/skins.citizen.styles/Header.less b/resources/skins.citizen.styles/Header.less index 2b3a52db..701c3891 100644 --- a/resources/skins.citizen.styles/Header.less +++ b/resources/skins.citizen.styles/Header.less @@ -145,3 +145,14 @@ border-right: 1px solid var( --border-color-base ); } } + +/* Hide header when scroll down on smaller screen sizes */ +@media ( max-width: @width-breakpoint-tablet ) { + .citizen-header { + transition: @transition-transform; + } + + .citizen-scroll--down .citizen-header { + transform: translateY( 100% ); + } +} diff --git a/resources/skins.citizen.styles/Stickyheader.less b/resources/skins.citizen.styles/Stickyheader.less index 0bcd1fd7..7615f9ae 100644 --- a/resources/skins.citizen.styles/Stickyheader.less +++ b/resources/skins.citizen.styles/Stickyheader.less @@ -65,6 +65,22 @@ } } +// Hide sticky header on scroll down on smaller screens +@media ( max-width: @width-breakpoint-tablet ) { + .citizen-body-header--sticky { + .mw-body-header { + transition: @transition-transform; + } + + &.citizen-scroll--down { + .mw-body-header { + transform: translateY( -100% ); + } + } + } +} + +// Make sticky header more compact if there are less screen estate @media ( max-width: @width-breakpoint-tablet ), ( max-height: 800px ) { .citizen-body-header--sticky { #siteSub { diff --git a/skin.json b/skin.json index 5354cff1..9c8a5a5f 100644 --- a/skin.json +++ b/skin.json @@ -191,6 +191,9 @@ "resources/skins.citizen.scripts/search.js", "resources/skins.citizen.scripts/tableOfContents.js" ], + "dependencies": [ + "mediawiki.util" + ], "targets": [ "desktop", "mobile"