diff --git a/mu-plugins/blocks/local-navigation-bar/postcss/style.pcss b/mu-plugins/blocks/local-navigation-bar/postcss/style.pcss
index 0a4b69567..cc94af9ad 100644
--- a/mu-plugins/blocks/local-navigation-bar/postcss/style.pcss
+++ b/mu-plugins/blocks/local-navigation-bar/postcss/style.pcss
@@ -123,6 +123,13 @@
}
}
+@media (min-width: 890px) {
+ /* stylelint-disable selector-id-pattern */
+ #wp--skip-link--target {
+ scroll-margin-top: var(--wp--custom--local-navigation-bar--spacing--height, 0);
+ }
+}
+
/* Set up the custom properties. These can be overridden by settings in theme.json. */
:where(body) {
--wp--custom--local-navigation-bar--spacing--height: 60px;
diff --git a/mu-plugins/blocks/sidebar-container/index.php b/mu-plugins/blocks/sidebar-container/index.php
index 248c386b2..9e126cfd8 100644
--- a/mu-plugins/blocks/sidebar-container/index.php
+++ b/mu-plugins/blocks/sidebar-container/index.php
@@ -39,15 +39,19 @@ function init() {
* @return string Returns the block markup.
*/
function render( $attributes, $content, $block ) {
- $back_to_top = sprintf(
- '
%s
',
- esc_html__( '↑ Back to top', 'wporg' )
- );
-
$wrapper_attributes = get_block_wrapper_attributes();
+ $inline_breakpoint = $attributes['inlineBreakpoint'];
+ $back_to_top = $attributes['hasBackToTop']
+ ? sprintf(
+ '%s
',
+ esc_html__( '↑ Back to top', 'wporg' )
+ )
+ : '';
+
return sprintf(
- '%2$s%3$s
',
+ '%3$s%4$s
',
$wrapper_attributes,
+ esc_attr( $inline_breakpoint ),
$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 20d7aedfb..bf19be0f1 100644
--- a/mu-plugins/blocks/sidebar-container/postcss/style.pcss
+++ b/mu-plugins/blocks/sidebar-container/postcss/style.pcss
@@ -1,62 +1,103 @@
-.wp-block-wporg-sidebar-container .is-link-to-top {
- display: none;
+.wp-block-wporg-sidebar-container {
+ /* stylelint-disable-next-line length-zero-no-unit */
+ --local--offset-top: var(--wp-admin--admin-bar--height, 0px);
- & a {
- text-decoration-line: none;
+ /* 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);
- &:hover {
- text-decoration-line: underline;
- }
+ /* Account for local nav height on larger screens where it becomes fixed. */
+ @media (min-width: 890px) {
+ --local--nav--offset: 0;
+ --local--offset-top: calc(var(--wp-admin--admin-bar--height, 0px) + var(--wp--custom--local-navigation-bar--spacing--height, 60px));
}
-}
-/* Slot the search & table of contents into a floating sidebar on large screens. */
-@media (min-width: 1200px) {
- .wp-block-wporg-sidebar-container {
- --local--block-end-sidebar--width: 340px;
+ & .is-link-to-top {
+ display: none;
- position: absolute;
- top: calc(var(--wp-global-header-offset, 90px) + var(--wp--custom--local-navigation-bar--spacing--height, 60px));
+ & a {
+ text-decoration-line: none;
+
+ &:hover {
+ text-decoration-line: underline;
+ }
+ }
+ }
+
+ /* Slot the search & table of contents into a floating sidebar on large screens. */
+ &.is-floating-sidebar {
+ --local--block-end-sidebar--width: 356px;
- /* Right offset should be "edge spacing" at minimum, otherwise calculate it to be centered. */
- right: max(var(--wp--preset--spacing--edge-space), calc((100% - var(--wp--style--global--wide-size)) / 2));
width: var(--local--block-end-sidebar--width);
- margin-top: var(--wp--custom--wporg-sidebar-container--spacing--margin--top);
- margin-bottom: 0 !important;
+ padding-bottom: var(--local--padding);
+ overflow-y: scroll;
+ overscroll-behavior: contain;
+ scrollbar-color: var(--wp--preset--color--charcoal-5) transparent;
- &.is-fixed-sidebar {
- position: fixed;
- top: 0;
+ /* Custom scrollbar so that it can be made visible on hover */
+ &::-webkit-scrollbar,
+ &::-webkit-scrollbar-track {
+ background-color: transparent;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ &:active,
+ &:hover {
+ background-color: var(--wp--preset--color--charcoal-4) !important;
+ }
+ }
- /* Make the space above the sidebar the same as the height of the local nav. */
- margin-top: calc(var(--wp-admin--admin-bar--height, 0px) + var(--wp--custom--local-navigation-bar--spacing--height, 60px) * 2);
+ &:active,
+ &:focus-within,
+ &:focus,
+ &:hover {
+ &::-webkit-scrollbar-thumb {
+ background-color: var(--wp--preset--color--charcoal-5);
+ border: 4px solid transparent;
+ background-clip: content-box;
+ border-radius: 10px;
+ }
}
- &.is-bottom-sidebar {
- position: absolute;
+ & > * {
+ padding-right: 16px;
}
- &.is-fixed-sidebar .is-link-to-top,
- &.is-bottom-sidebar .is-link-to-top {
- display: block;
- margin-top: 0;
+ &.is-fixed-sidebar {
+ position: fixed;
+ top: 0;
+ height: calc(100vh - var(--local--offset-top));
+ margin-top: var(--local--offset-top) !important;
+ padding: var(--local--padding) 0;
+
+ & .is-link-to-top {
+ display: block;
- & a {
- color: var(--wp--preset--color--charcoal-4);
+ & a {
+ color: var(--wp--preset--color--charcoal-4);
+ }
}
}
- .wp-block-wporg-table-of-contents + .is-link-to-top {
- border-top: 1px solid var(--wp--preset--color--light-grey-1);
+ & * + .is-link-to-top {
padding-top: var(--wp--preset--spacing--20);
+ border-top: 1px solid var(--wp--preset--color--light-grey-1);
}
}
}
-@media (min-width: 890px) {
- /* stylelint-disable selector-id-pattern */
- #wp--skip-link--target {
- scroll-margin-top: var(--wp--custom--local-navigation-bar--spacing--height, 0);
+main .wp-block-wporg-sidebar-container {
+
+ /* Hide the main sidebar until layout classes have been applied, to avoid FOUC */
+ display: none;
+
+ &.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);
+
+ /* Right offset should be "edge spacing" at minimum, otherwise calculate it to be centered. */
+ right: max(var(--wp--preset--spacing--edge-space), calc((100% - var(--wp--style--global--wide-size)) / 2));
}
}
diff --git a/mu-plugins/blocks/sidebar-container/src/block.json b/mu-plugins/blocks/sidebar-container/src/block.json
index 90d9b5ede..557d1652f 100644
--- a/mu-plugins/blocks/sidebar-container/src/block.json
+++ b/mu-plugins/blocks/sidebar-container/src/block.json
@@ -7,7 +7,16 @@
"category": "layout",
"description": "A sticky container to be used in 2-column layouts.",
"textdomain": "wporg",
- "attributes": {},
+ "attributes": {
+ "hasBackToTop": {
+ "type": "boolean",
+ "default": true
+ },
+ "inlineBreakpoint": {
+ "type": "string",
+ "default": "1200px"
+ }
+ },
"supports": {
"inserter": false,
"__experimentalLayout": true,
diff --git a/mu-plugins/blocks/sidebar-container/src/view.js b/mu-plugins/blocks/sidebar-container/src/view.js
index 04f7ff705..f2540f312 100644
--- a/mu-plugins/blocks/sidebar-container/src/view.js
+++ b/mu-plugins/blocks/sidebar-container/src/view.js
@@ -1,20 +1,13 @@
/**
* Fallback values for custom properties match CSS defaults.
*/
-const globalNavHeight = 90;
-
-const LOCAL_NAV_HEIGHT = getCustomPropValue( '--wp--custom--local-navigation-bar--spacing--height' ) || 60;
-const ADMIN_BAR_HEIGHT = parseInt(
- window.getComputedStyle( document.documentElement ).getPropertyValue( 'margin-top' ),
- 10
-);
-const SPACE_FROM_BOTTOM = getCustomPropValue( '--wp--preset--spacing--edge-space' ) || 80;
const SPACE_TO_TOP = getCustomPropValue( '--wp--custom--wporg-sidebar-container--spacing--margin--top' ) || 80;
-const FIXED_HEADER_HEIGHT = globalNavHeight + LOCAL_NAV_HEIGHT + ADMIN_BAR_HEIGHT;
-const SCROLL_POSITION_TO_FIX = globalNavHeight + SPACE_TO_TOP - LOCAL_NAV_HEIGHT - ADMIN_BAR_HEIGHT;
-let container;
+let containers;
let mainEl;
+let adminBarHeight;
+let globalNavHeight;
+const scrollHandlers = [];
/**
* Get the value of a CSS custom property.
@@ -26,6 +19,9 @@ let mainEl;
*/
function getCustomPropValue( name, element = document.body ) {
const value = window.getComputedStyle( element ).getPropertyValue( name );
+ if ( '0' === value ) {
+ return 0;
+ }
if ( 'px' === value.slice( -2 ) ) {
return Number( value.replace( 'px', '' ) );
}
@@ -33,113 +29,82 @@ function getCustomPropValue( name, element = document.body ) {
}
/**
- * Check the position of the sidebar vs the height of the viewport & page
- * container, and toggle the "bottom" class to position the sidebar without
- * overlapping the footer.
+ * Check the position of the sidebar relative to the scroll position,
+ * and toggle the "fixed" class at a certain point.
+ * Reduce the height of the sidebar to stop it overlapping the footer.
*
- * @return {boolean} True if the sidebar is at the bottom of the page.
+ * @param {HTMLElement} container The sidebar container.
+ * @return {Function} onScroll The sidebar scroll handler.
*/
-function onScroll() {
- // Only run the scroll code if the sidebar is floating on a wide screen.
- if ( ! mainEl || ! container || ! window.matchMedia( '(min-width: 1200px)' ).matches ) {
- return false;
- }
-
- const scrollPosition = window.scrollY - ADMIN_BAR_HEIGHT;
-
- if ( ! container.classList.contains( 'is-bottom-sidebar' ) ) {
- const footerStart = mainEl.offsetTop + mainEl.offsetHeight;
- // The pixel location of the bottom of the sidebar, relative to the top of the page.
- const sidebarBottom = scrollPosition + container.offsetHeight + container.offsetTop - ADMIN_BAR_HEIGHT;
+function createScrollHandler( container ) {
+ return function onScroll() {
+ // Only run the scroll code if the sidebar is floating.
+ if ( ! container.classList.contains( 'is-floating-sidebar' ) ) {
+ return false;
+ }
- // Is the sidebar bottom crashing into the footer?
- if ( footerStart - SPACE_FROM_BOTTOM < sidebarBottom ) {
- container.classList.add( 'is-bottom-sidebar' );
+ const { scrollY, innerHeight: windowHeight } = window;
+ const scrollPosition = scrollY - adminBarHeight;
+ const localNavOffset = getCustomPropValue( '--local--nav--offset', container );
+ const paddingTop = getCustomPropValue( '--local--padding', container );
- // Bottom sidebar is absolutely positioned, so we need to set the top relative to the page origin.
- // The pixel location of the top of the sidebar, relative to the footer.
- const sidebarTop =
- footerStart - container.offsetHeight - LOCAL_NAV_HEIGHT * 2 + ADMIN_BAR_HEIGHT - SPACE_FROM_BOTTOM;
- container.style.setProperty( 'top', `${ sidebarTop }px` );
+ // Toggle the fixed position based on whether the scrollPosition is greater than the
+ // initial gap from the top minus the padding applied when fixed.
+ const shouldFix =
+ scrollPosition > SPACE_TO_TOP + globalNavHeight + localNavOffset - adminBarHeight - paddingTop;
+ container.classList.toggle( 'is-fixed-sidebar', shouldFix );
- return true;
+ // If the sidebar is fixed and the footer is visible in the viewport, reduce the height to stop overlap.
+ const footerStart = mainEl.offsetTop + mainEl.offsetHeight;
+ if ( shouldFix && footerStart < scrollPosition + windowHeight ) {
+ container.style.setProperty( 'height', `${ footerStart - scrollPosition - container.offsetTop }px` );
+ } else {
+ container.style.removeProperty( 'height' );
}
- } else if ( container.getBoundingClientRect().top > LOCAL_NAV_HEIGHT * 2 + ADMIN_BAR_HEIGHT ) {
- // If the top of the sidebar is above the top fixing position, switch back to just a fixed sidebar.
- container.classList.remove( 'is-bottom-sidebar' );
- container.style.removeProperty( 'top' );
- }
-
- // Toggle the fixed position based on whether the scrollPosition is greater than the initial gap from the top.
- container.classList.toggle( 'is-fixed-sidebar', scrollPosition > SCROLL_POSITION_TO_FIX );
-
- return false;
+ };
}
-function isSidebarWithinViewport() {
- if ( ! container ) {
- return false;
- }
- // Usable viewport height.
- const viewHeight = window.innerHeight - LOCAL_NAV_HEIGHT + ADMIN_BAR_HEIGHT;
- // Get the height of the sidebar, plus the top offset and 60px for the
- // "Back to top" link, which isn't visible until `is-fixed-sidebar` is
- // added, therefore not included in the offsetHeight value.
- const sidebarHeight = container.offsetHeight + LOCAL_NAV_HEIGHT + 60;
- // If the sidebar is shorter than the view area, apply the class so
- // that it's fixed and scrolls with the page content.
- return sidebarHeight < viewHeight;
+/**
+ * Set the height for the admin bar and global nav vars.
+ * Set the floating sidebar class on each container based on its breakpoint.
+ * Show hidden containers after layout.
+ */
+function onResize() {
+ adminBarHeight = parseInt(
+ window.getComputedStyle( document.documentElement ).getPropertyValue( 'margin-top' ),
+ 10
+ );
+ 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() {
- container = document.querySelector( '.wp-block-wporg-sidebar-container' );
+ containers = document.querySelectorAll( '.wp-block-wporg-sidebar-container' );
mainEl = document.getElementById( 'wp--skip-link--target' );
- const toggleButton = container?.querySelector( '.wporg-table-of-contents__toggle' );
- const list = container?.querySelector( '.wporg-table-of-contents__list' );
- if ( toggleButton && list ) {
- toggleButton.addEventListener( 'click', function () {
- if ( toggleButton.getAttribute( 'aria-expanded' ) === 'true' ) {
- toggleButton.setAttribute( 'aria-expanded', false );
- list.removeAttribute( 'style' );
- } else {
- toggleButton.setAttribute( 'aria-expanded', true );
- list.setAttribute( 'style', 'display:block;' );
- }
+ if ( mainEl && containers.length ) {
+ containers.forEach( ( container ) => {
+ const scrollHandler = createScrollHandler( container );
+ scrollHandlers.push( scrollHandler );
+ window.addEventListener( 'scroll', scrollHandler );
} );
}
- if ( isSidebarWithinViewport() ) {
- onScroll(); // Run once to avoid footer collisions on load (ex, when linked to #reply-title).
- window.addEventListener( 'scroll', onScroll );
-
- const observer = new window.ResizeObserver( () => {
- // If the sidebar is positioned at the bottom and mainEl resizes,
- // it will remain fixed at the previous bottom position, leading to a broken page layout.
- // In this case manually trigger the scroll handler to reposition.
- if ( container.classList.contains( 'is-bottom-sidebar' ) ) {
- container.classList.remove( 'is-bottom-sidebar' );
- container.style.removeProperty( 'top' );
- const isBottom = onScroll();
- // After the sidebar is repositioned, also adjusts the scroll position
- // to a point where the sidebar is visible.
- if ( isBottom ) {
- window.scrollTo( {
- top: container.offsetTop - FIXED_HEADER_HEIGHT,
- behavior: 'instant',
- } );
- }
- }
- } );
-
- observer.observe( mainEl );
- }
-
- // 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' );
- heading?.style.setProperty( 'display', 'none' );
- }
+ // 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 );
}
window.addEventListener( 'load', init );
diff --git a/mu-plugins/blocks/table-of-contents/src/block.json b/mu-plugins/blocks/table-of-contents/src/block.json
index 5cfe99478..77e164600 100644
--- a/mu-plugins/blocks/table-of-contents/src/block.json
+++ b/mu-plugins/blocks/table-of-contents/src/block.json
@@ -26,5 +26,6 @@
}
},
"editorScript": "file:./index.js",
- "style": "file:./style.css"
+ "style": "file:./style.css",
+ "viewScript": "file:./view.js"
}
diff --git a/mu-plugins/blocks/table-of-contents/src/view.js b/mu-plugins/blocks/table-of-contents/src/view.js
new file mode 100644
index 000000000..67ac55992
--- /dev/null
+++ b/mu-plugins/blocks/table-of-contents/src/view.js
@@ -0,0 +1,24 @@
+function init() {
+ const container = document.querySelector( '.wp-block-wporg-table-of-contents' );
+
+ if ( ! container ) {
+ return;
+ }
+
+ const toggleButton = container.querySelector( '.wporg-table-of-contents__toggle' );
+ const list = container.querySelector( '.wporg-table-of-contents__list' );
+
+ if ( toggleButton && list ) {
+ toggleButton.addEventListener( 'click', function () {
+ if ( toggleButton.getAttribute( 'aria-expanded' ) === 'true' ) {
+ toggleButton.setAttribute( 'aria-expanded', false );
+ list.removeAttribute( 'style' );
+ } else {
+ toggleButton.setAttribute( 'aria-expanded', true );
+ list.setAttribute( 'style', 'display:block;' );
+ }
+ } );
+ }
+}
+
+window.addEventListener( 'load', init );