diff --git a/mu-plugins/blocks/sidebar-container/index.php b/mu-plugins/blocks/sidebar-container/index.php index 08edf085d..4056b6982 100644 --- a/mu-plugins/blocks/sidebar-container/index.php +++ b/mu-plugins/blocks/sidebar-container/index.php @@ -45,11 +45,13 @@ function render( $attributes, $content, $block ) { esc_html__( '↑ Back to top', 'wporg' ) ) : ''; + $inlineBreakpoint = $attributes['inlineBreakpoint']; $wrapper_attributes = get_block_wrapper_attributes(); return sprintf( - '
%2$s%3$s
', + '
%3$s%4$s
', $wrapper_attributes, + esc_attr( $inlineBreakpoint ), $content, $back_to_top ); diff --git a/mu-plugins/blocks/sidebar-container/postcss/style.pcss b/mu-plugins/blocks/sidebar-container/postcss/style.pcss index fe8bcdd4e..deaa07fe6 100644 --- a/mu-plugins/blocks/sidebar-container/postcss/style.pcss +++ b/mu-plugins/blocks/sidebar-container/postcss/style.pcss @@ -1,21 +1,32 @@ -.wp-block-wporg-sidebar-container .is-link-to-top { - display: none; +.wp-block-wporg-sidebar-container { + --local--offset-top: var(--wp-admin--admin-bar--height, 0); + + /* These vars are used in JS calcs */ + --local--nav--offset: var(--wp--custom--local-navigation-bar--spacing--height, 60px); + --local--padding: var(--wp--preset--spacing--20); - & a { - text-decoration-line: none; + /* Account for local nav height on larger screens where it becomes fixed. */ + @media (min-width: 890px) { + /* stylelint-disable-next-line length-zero-no-unit */ + --local--nav--offset: 0px; + --local--offset-top: calc(var(--wp-admin--admin-bar--height, 0px) + var(--wp--custom--local-navigation-bar--spacing--height, 60px)); + } - &:hover { - text-decoration-line: underline; + & .is-link-to-top { + display: none; + + & a { + text-decoration-line: none; + + &:hover { + text-decoration-line: underline; + } } } -} -/* Slot the search & table of contents into a floating sidebar on large screens. */ -@media (min-width: 1200px) { - .wp-block-wporg-sidebar-container { + /* Slot the search & table of contents into a floating sidebar on large screens. */ + &.is-floating-sidebar { --local--block-end-sidebar--width: 340px; - --local--offset-top: calc(var(--wp-admin--admin-bar--height, 0px) + var(--wp--custom--local-navigation-bar--spacing--height, 60px)); - --local--padding-top: var(--wp--preset--spacing--20); width: var(--local--block-end-sidebar--width); @@ -30,7 +41,7 @@ top: 0; height: calc(100vh - var(--local--offset-top)); margin-top: var(--local--offset-top) !important; - padding: var(--local--padding-top) 0; + padding: var(--local--padding) 0; overflow-y: scroll; /* Custom scrollbar so that it can be made visible on hover */ @@ -60,8 +71,14 @@ border-top: 1px solid var(--wp--preset--color--light-grey-1); } } +} + +main .wp-block-wporg-sidebar-container { + + /* Hide the main sidebar until layout classes have been applied, to avoid FOUC */ + display: none; - main .wp-block-wporg-sidebar-container { + &.is-floating-sidebar { position: absolute; top: calc(var(--wp-global-header-offset, 90px) + var(--wp--custom--local-navigation-bar--spacing--height, 60px)); margin-top: var(--wp--custom--wporg-sidebar-container--spacing--margin--top); diff --git a/mu-plugins/blocks/sidebar-container/src/block.json b/mu-plugins/blocks/sidebar-container/src/block.json index c07a7574d..557d1652f 100644 --- a/mu-plugins/blocks/sidebar-container/src/block.json +++ b/mu-plugins/blocks/sidebar-container/src/block.json @@ -11,6 +11,10 @@ "hasBackToTop": { "type": "boolean", "default": true + }, + "inlineBreakpoint": { + "type": "string", + "default": "1200px" } }, "supports": { diff --git a/mu-plugins/blocks/sidebar-container/src/view.js b/mu-plugins/blocks/sidebar-container/src/view.js index 3d5b724a3..d80f0c766 100644 --- a/mu-plugins/blocks/sidebar-container/src/view.js +++ b/mu-plugins/blocks/sidebar-container/src/view.js @@ -1,17 +1,13 @@ /** * Fallback values for custom properties match CSS defaults. */ - -const GLOBAL_NAV_HEIGHT = getCustomPropValue( '--wp-global-header-height' ) || 90; -const ADMIN_BAR_HEIGHT = parseInt( - window.getComputedStyle( document.documentElement ).getPropertyValue( 'margin-top' ), - 10 -); const SPACE_TO_TOP = getCustomPropValue( '--wp--custom--wporg-sidebar-container--spacing--margin--top' ) || 80; -const SCROLL_POSITION_TO_FIX = GLOBAL_NAV_HEIGHT + SPACE_TO_TOP - ADMIN_BAR_HEIGHT; let containers; let mainEl; +let adminBarHeight; +let globalNavHeight; +const scrollHandlers = []; /** * Get the value of a CSS custom property. @@ -30,39 +26,62 @@ function getCustomPropValue( name, element = document.body ) { } /** - * Check the position of each sidebar relative to the scroll position, + * Check the position of the sidebar relative to the scroll position, * and toggle the "fixed" class at a certain point. - * Reduce the height of each sidebar to stop them overlapping the footer. + * Reduce the height of the sidebar to stop it overlapping the footer. + * + * @param {HTMLElement} container */ -function onScroll() { - // Only run the scroll code if the sidebar is floating on a wide screen. - if ( ! window.matchMedia( '(min-width: 1200px)' ).matches ) { - return; - } +function createScrollHandler( container ) { + return function onWindowChange() { + // Only run the scroll code if the sidebar is floating. + if ( ! container.classList.contains( 'is-floating-sidebar' ) ) { + return false; + } - const { scrollY, innerHeight: windowHeight } = window; - const scrollPosition = scrollY - ADMIN_BAR_HEIGHT; + const { scrollY, innerHeight: windowHeight } = window; + const scrollPosition = scrollY - adminBarHeight; + const localNavOffset = getCustomPropValue( '--local--nav--offset', container ); + const paddingTop = getCustomPropValue( '--local--padding', container ); - // Toggle the fixed position based on whether the scrollPosition is greater than the initial gap from the top. - containers.forEach( ( container ) => { + // Toggle the fixed position based on whether the scrollPosition is greater than the + // initial gap from the top minus the padding applied when fixed. container.classList.toggle( 'is-fixed-sidebar', - scrollPosition > SCROLL_POSITION_TO_FIX - getCustomPropValue( '--local--padding-top', container ) + scrollPosition > SPACE_TO_TOP + globalNavHeight + localNavOffset - adminBarHeight - paddingTop ); - } ); - const footerStart = mainEl.offsetTop + mainEl.offsetHeight; + const footerStart = mainEl.offsetTop + mainEl.offsetHeight; - // Is the footer visible in the viewport? - if ( footerStart < scrollPosition + windowHeight ) { - containers.forEach( ( container ) => { + // Is the footer visible in the viewport? + if ( footerStart < scrollPosition + windowHeight ) { container.style.setProperty( 'height', `${ footerStart - scrollPosition - container.offsetTop }px` ); - } ); - } else { - containers.forEach( ( container ) => { + } else { container.style.removeProperty( 'height' ); - } ); - } + } + }; +} + +/** + * Set the height for the admin bar and global nav vars. + * Set the floating sidebar class on each container based on their breakpoint. + * Show hidden containers after layout. + */ +function onResize() { + adminBarHeight = getCustomPropValue( '--wp-admin--admin-bar--height' ) || 32; + globalNavHeight = getCustomPropValue( '--wp-global-header-height' ) || 90; + + containers.forEach( ( container ) => { + // Toggle the floating class based on the configured breakpoint. + const shouldFloat = window.matchMedia( `(min-width: ${ container.dataset.breakpoint })` ).matches; + container.classList.toggle( 'is-floating-sidebar', shouldFloat ); + // Show the sidebar after layout, if it has been hidden to avoid FOUC. + if ( 'none' === window.getComputedStyle( container ).display ) { + container.style.setProperty( 'display', 'revert' ); + } + } ); + + scrollHandlers.forEach( ( handler ) => handler() ); } function init() { @@ -70,10 +89,18 @@ function init() { mainEl = document.getElementById( 'wp--skip-link--target' ); if ( mainEl && containers.length ) { - onScroll(); // Run once to avoid footer collisions on load (ex, when linked to #reply-title). - window.addEventListener( 'scroll', onScroll ); + containers.forEach( ( container ) => { + const scrollHandler = createScrollHandler( container ); + scrollHandlers.push( scrollHandler ); + window.addEventListener( 'scroll', scrollHandler ); + } ); } + // Run once to set height vars and position elements on load. + // Avoids footer collisions (ex, when linked to #reply-title). + onResize(); + window.addEventListener( 'resize', onResize ); + // If there is no table of contents, hide the heading. if ( ! document.querySelector( '.wp-block-wporg-table-of-contents' ) ) { const heading = document.querySelector( '.wp-block-wporg-sidebar-container h2' );