Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make sidebar container scroll internally #554

Merged
merged 23 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c79eae4
Make sidebar container scroll internally
adamwoodnz Jan 10, 2024
1fd41c1
Decouple sidebar container from ToC
adamwoodnz Jan 12, 2024
b05e933
Allow multiple sidebar containers
adamwoodnz Jan 12, 2024
769f18e
Remove debug color
adamwoodnz Jan 12, 2024
49d1fac
Make back to top link optional
adamwoodnz Jan 12, 2024
0eb7dea
Update docs
adamwoodnz Jan 16, 2024
08191e0
Fix postcss selector nesting
adamwoodnz Jan 24, 2024
c914570
Fix postcss selector nesting
adamwoodnz Jan 24, 2024
f1a29d9
Fix postcss selector nesting
adamwoodnz Jan 24, 2024
f291ec1
Remove ToC back to top padding if no content
adamwoodnz Jan 24, 2024
ebb5561
Make sidebar container control padding when fixed
adamwoodnz Jan 29, 2024
7c2c1a7
Move skip link target styles to local nav bar
adamwoodnz Jan 29, 2024
b24b6f2
Make the inline breakpoint configurable for each container
adamwoodnz Jan 29, 2024
740a0aa
Move logic to hide ToC heading if content is empty from JS to render
adamwoodnz Jan 30, 2024
0b9b873
Fix handling of zero css values in getCustomPropValue
adamwoodnz Jan 30, 2024
17d0add
Complete jsdoc
adamwoodnz Jan 30, 2024
abf6ac5
Fix scrolling height calc when logged out
adamwoodnz Jan 30, 2024
91fe052
Revert changing admin bar height detection
adamwoodnz Jan 30, 2024
061aab3
Improve scrollbar styles
adamwoodnz Jan 31, 2024
bcb5688
Only reduce the height when the footer is visible if the sidebar is f…
adamwoodnz Jan 31, 2024
07b512c
Revert "Move logic to hide ToC heading if content is empty from JS to…
adamwoodnz Jan 31, 2024
2aed927
Use snakecase for php vars
adamwoodnz Jan 31, 2024
40f8977
Add bottom padding all the time when floating
adamwoodnz Jan 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions mu-plugins/blocks/local-navigation-bar/postcss/style.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Comment on lines +126 to +132
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved from the sidebar container styles, as it relates directly to the change in positioning of the local nav bar

/* Set up the custom properties. These can be overridden by settings in theme.json. */
:where(body) {
--wp--custom--local-navigation-bar--spacing--height: 60px;
Expand Down
14 changes: 9 additions & 5 deletions mu-plugins/blocks/sidebar-container/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,19 @@ function init() {
* @return string Returns the block markup.
*/
function render( $attributes, $content, $block ) {
$back_to_top = sprintf(
'<p class="has-small-font-size is-link-to-top"><a href="#wp--skip-link--target">%s</a></p>',
esc_html__( '↑ Back to top', 'wporg' )
);
$back_to_top = $attributes['hasBackToTop']
? sprintf(
'<p class="has-small-font-size is-link-to-top"><a href="#wp--skip-link--target">%s</a></p>',
esc_html__( '↑ Back to top', 'wporg' )
)
: '';
$inlineBreakpoint = $attributes['inlineBreakpoint'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$inlineBreakpoint = $attributes['inlineBreakpoint'];
$inline_breakpoint = $attributes['inlineBreakpoint'];

PHP variables should be snake case. It looks like we're not running phpcs in the github actions, but you can run it locally with composer lint mu-plugins/blocks/sidebar-container mu-plugins/blocks/table-of-contents

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2aed927


$wrapper_attributes = get_block_wrapper_attributes();
return sprintf(
'<div %1$s>%2$s%3$s</div>',
'<div %1$s data-breakpoint="%2$s">%3$s%4$s</div>',
$wrapper_attributes,
esc_attr( $inlineBreakpoint ),
$content,
$back_to_top
);
Expand Down
97 changes: 62 additions & 35 deletions mu-plugins/blocks/sidebar-container/postcss/style.pcss
Original file line number Diff line number Diff line change
@@ -1,62 +1,89 @@
.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);

/* 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));
}

& a {
text-decoration-line: none;
& .is-link-to-top {
display: none;

&:hover {
text-decoration-line: underline;
& 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;

position: absolute;
top: calc(var(--wp-global-header-offset, 90px) + var(--wp--custom--local-navigation-bar--spacing--height, 60px));

/* 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;

&:not(.is-fixed-sidebar) {

/* Match width of custom scrollbar when fixed, stops content width changing */
padding-right: 16px;
}

&.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;
overflow-y: scroll;

/* 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);
}
/* Custom scrollbar so that it can be made visible on hover */
&::-webkit-scrollbar,
&::-webkit-scrollbar-thumb {
background-color: transparent;
}

&.is-bottom-sidebar {
position: absolute;
}
&:hover::-webkit-scrollbar-thumb {
background-color: var(--wp--preset--color--charcoal-4);
border: 4.5px solid transparent;
background-clip: content-box;
border-radius: 10px;
}

&.is-fixed-sidebar .is-link-to-top,
&.is-bottom-sidebar .is-link-to-top {
display: block;
margin-top: 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));
}
}

Expand Down
11 changes: 10 additions & 1 deletion mu-plugins/blocks/sidebar-container/src/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
166 changes: 65 additions & 101 deletions mu-plugins/blocks/sidebar-container/src/view.js
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -26,120 +19,91 @@ 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', '' ) );
}
return value;
}

/**
* 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;
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;
}

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;
const { scrollY, innerHeight: windowHeight } = window;
const scrollPosition = scrollY - adminBarHeight;
const localNavOffset = getCustomPropValue( '--local--nav--offset', container );
const paddingTop = getCustomPropValue( '--local--padding', container );

// Is the sidebar bottom crashing into the footer?
if ( footerStart - SPACE_FROM_BOTTOM < sidebarBottom ) {
container.classList.add( 'is-bottom-sidebar' );
// 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 > SPACE_TO_TOP + globalNavHeight + localNavOffset - adminBarHeight - paddingTop
);

// 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` );
const footerStart = mainEl.offsetTop + mainEl.offsetHeight;

return true;
// Is the footer visible in the viewport?
if ( 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 = 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() {
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 ( 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,
adamwoodnz marked this conversation as resolved.
Show resolved Hide resolved
// 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',
} );
}
}
if ( mainEl && containers.length ) {
containers.forEach( ( container ) => {
const scrollHandler = createScrollHandler( container );
scrollHandlers.push( scrollHandler );
window.addEventListener( 'scroll', scrollHandler );
} );

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' );
}
Comment on lines -138 to -142
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to the render

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