diff --git a/mu-plugins/blocks/local-navigation-bar/index.php b/mu-plugins/blocks/local-navigation-bar/index.php index 0165b456c..7318af8d5 100644 --- a/mu-plugins/blocks/local-navigation-bar/index.php +++ b/mu-plugins/blocks/local-navigation-bar/index.php @@ -10,7 +10,8 @@ add_action( 'init', __NAMESPACE__ . '\init' ); add_filter( 'render_block_data', __NAMESPACE__ . '\update_block_attributes' ); -add_filter( 'render_block', __NAMESPACE__ . '\customize_navigation_block_icon', 10, 2 ); +add_filter( 'render_block_data', __NAMESPACE__ . '\update_child_block_attributes', 10, 3 ); +add_filter( 'render_block_wporg/local-navigation-bar', __NAMESPACE__ . '\customize_navigation_block_icon' ); /** * Registers the block using the metadata loaded from the `block.json` file. @@ -48,7 +49,7 @@ function update_block_attributes( $block ) { // Set layout values if they don't exist. $default_layout = array( 'type' => 'flex', - 'flexWrap' => 'wrap', + 'flexWrap' => 'nowrap', 'justifyContent' => 'space-between', ); if ( ! empty( $block['attrs']['layout'] ) ) { @@ -69,47 +70,107 @@ function update_block_attributes( $block ) { return $block; } +/** + * Ensure the child navigation block uses the expected attributes. + * + * @param array $parsed_block The block being rendered. + * @param array $source_block An un-modified copy of $parsed_block, as it appeared in the source content. + * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block. + * + * @return array The updated block. + */ +function update_child_block_attributes( $parsed_block, $source_block, $parent_block ) { + if ( empty( $parsed_block['blockName'] ) ) { + return $parsed_block; + } + + // If navigation block… + if ( 'core/navigation' === $parsed_block['blockName'] ) { + // with the local navigation bar as a parent… + if ( ! $parent_block || 'wporg/local-navigation-bar' !== $parent_block->name ) { + return $parsed_block; + } + // set the values we need. + $parsed_block['attrs']['icon'] = 'menu'; + $parsed_block['attrs']['fontSize'] = 'small'; + $parsed_block['attrs']['openSubmenusOnClick'] = true; + $parsed_block['attrs']['layout'] = array( + 'type' => 'flex', + 'orientation' => 'horizontal', + ); + + // Add an extra navigation block which is always collapsed, so that it + // can be swapped out when the section title + nav menu collide. + add_filter( 'render_block_core/navigation', __NAMESPACE__ . '\add_extra_navigation', 10, 3 ); + } + + return $parsed_block; +} + +/** + * Inject an extra navigation block into the local nav, which is enabled when the section title is long. + */ +function add_extra_navigation( $block_content, $block ) { + remove_filter( 'render_block_core/navigation', __NAMESPACE__ . '\add_extra_navigation', 10, 3 ); + + // This menu should always be in the collapsed state. + $block['attrs']['overlayMenu'] = 'always'; + + if ( isset( $block['attrs']['className'] ) ) { + $block['attrs']['className'] .= ' wporg-is-collapsed-nav'; + } else { + $block['attrs']['className'] = 'wporg-is-collapsed-nav'; + } + + $menu_block_content = do_blocks( '' ); + $menu_block_content = customize_navigation_block_icon( $menu_block_content ); + return $block_content . $menu_block_content; +} + /** * Replace a nested navigation block mobile button icon with a caret icon. - * Only applies if it has the 3 bar icon set, as this has an svg with to update. + * Only applies if it has the 3 bar icon set, as this has an svg with to update. * * @param string $block_content The block content. - * @param array $block The parsed block data. * * @return string */ -function customize_navigation_block_icon( $block_content, $block ) { - if ( ! empty( $block['blockName'] ) && 'wporg/local-navigation-bar' === $block['blockName'] ) { - $tag_processor = new \WP_HTML_Tag_Processor( $block_content ); +function customize_navigation_block_icon( $block_content ) { + $tag_processor = new \WP_HTML_Tag_Processor( $block_content ); - if ( - $tag_processor->next_tag( array( - 'tag_name' => 'nav', - 'class_name' => 'wp-block-navigation' + if ( + $tag_processor->next_tag( + array( + 'tag_name' => 'nav', + 'class_name' => 'wp-block-navigation', ) - ) ) { - if ( - $tag_processor->next_tag( array( - 'tag_name' => 'button', - 'class_name' => 'wp-block-navigation__responsive-container-open' - ) ) && - $tag_processor->next_tag( 'path' ) - ) { - $tag_processor->set_attribute( 'd', 'M17.5 11.6L12 16l-5.5-4.4.9-1.2L12 14l4.5-3.6 1 1.2z' ); - } - - if ( - $tag_processor->next_tag( array( - 'tag_name' => 'button', - 'class_name' => 'wp-block-navigation__responsive-container-close' - ) ) && - $tag_processor->next_tag( 'path' ) - ) { - $tag_processor->set_attribute( 'd', 'M6.5 12.4L12 8l5.5 4.4-.9 1.2L12 10l-4.5 3.6-1-1.2z' ); - } - - return $tag_processor->get_updated_html(); + ) + ) { + if ( + $tag_processor->next_tag( + array( + 'tag_name' => 'button', + 'class_name' => 'wp-block-navigation__responsive-container-open', + ) + ) && + $tag_processor->next_tag( 'path' ) + ) { + $tag_processor->set_attribute( 'd', 'M17.5 11.6L12 16l-5.5-4.4.9-1.2L12 14l4.5-3.6 1 1.2z' ); } + + if ( + $tag_processor->next_tag( + array( + 'tag_name' => 'button', + 'class_name' => 'wp-block-navigation__responsive-container-close', + ) + ) && + $tag_processor->next_tag( 'path' ) + ) { + $tag_processor->set_attribute( 'd', 'M6.5 12.4L12 8l5.5 4.4-.9 1.2L12 10l-4.5 3.6-1-1.2z' ); + } + + return $tag_processor->get_updated_html(); } return $block_content; diff --git a/mu-plugins/blocks/local-navigation-bar/postcss/style.pcss b/mu-plugins/blocks/local-navigation-bar/postcss/style.pcss index e7ba1a373..d49c96abc 100644 --- a/mu-plugins/blocks/local-navigation-bar/postcss/style.pcss +++ b/mu-plugins/blocks/local-navigation-bar/postcss/style.pcss @@ -1,3 +1,8 @@ +/* Set up the custom properties. These can be overridden by settings in theme.json. */ +:where(body) { + --wp--custom--local-navigation-bar--spacing--height: 60px; +} + :where(.wp-block-wporg-local-navigation-bar) { background-color: var(--wp--preset--color--blueberry-1); color: var(--wp--preset--color--white); @@ -7,14 +12,52 @@ top: var(--wp-admin--admin-bar--height, 0); + /* Set this as a custom property so that it can be changed based on container background. */ + --wp--custom--local-navigation-bar--focus--color: var(--wp--preset--color--white); + --wp--custom--local-navigation-bar--border--color: var(--wp--preset--color--white-opacity-15); + + &:where(.has-background) { + --wp--custom--local-navigation-bar--focus--color: var(--wp--preset--color--charcoal-5); + --wp--custom--local-navigation-bar--border--color: var(--wp--preset--color--black-opacity-15); + } + + &:where(.has-white-background-color) { + --wp--custom--local-navigation-bar--focus--color: var(--wp--preset--color--blueberry-1); + --wp--custom--local-navigation-bar--border--color: var(--wp--preset--color--black-opacity-15); + } + + &:where(.has-charcoal-1-background-color), + &:where(.has-charcoal-2-background-color) { + --wp--custom--local-navigation-bar--focus--color: var(--wp--preset--color--blueberry-2); + } + + &:where(.has-charcoal-2-background-color) { + --wp--custom--local-navigation-bar--border--color: var(--wp--preset--color--charcoal-1); + } + /* If a sticky element is next, it needs to account for the nav bar offset. */ & + :where(.wp-block-group.is-position-sticky) { - top: calc(var(--wp-admin--admin-bar--height, 0px) + 60px); + top: calc(var(--wp-admin--admin-bar--height, 0px) + var(--wp--custom--local-navigation-bar--spacing--height)); } } .wp-block-wporg-local-navigation-bar { height: var(--wp--custom--local-navigation-bar--spacing--height); + margin-inline-start: calc(var(--wp--preset--spacing--10) / -2); + + & a:where(:not(.wp-element-button)) { + padding: calc(var(--wp--preset--spacing--10) / 2); + + &:focus-visible { + outline: none; + border-radius: 2px; + box-shadow: inset 0 0 0 1.5px var(--wp--custom--local-navigation-bar--focus--color); + } + } + + &:not(.is-sticking) { + border-bottom: none !important; + } @media (min-width: 890px) { & .global-header__wporg-logo-mark { @@ -29,6 +72,7 @@ & a { display: block; color: inherit; + padding: 0; } & svg { @@ -44,9 +88,16 @@ visibility: hidden; } + & .wporg-local-navigation-bar__fade-in-scroll { + opacity: 0; + transition: all 0.2s ease-in-out; + visibility: hidden; + } + &.is-sticking { & .global-header__wporg-logo-mark, - & .wporg-local-navigation-bar__show-on-scroll { + & .wporg-local-navigation-bar__show-on-scroll, + & .wporg-local-navigation-bar__fade-in-scroll { opacity: 1; top: 0; visibility: visible; @@ -93,70 +144,174 @@ } & .wp-block-group { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-block: 5px; /* Add space for the focus outline. */ + + > * { + white-space: nowrap; + } + & p { position: relative; - margin-inline-end: var(--wp--preset--spacing--10); - padding-inline-end: var(--wp--preset--spacing--10); + margin-inline-start: var(--wp--preset--spacing--10); + padding-inline-start: var(--wp--preset--spacing--10); - &::after { + &::before { content: "\2022"; position: absolute; - inset-inline-end: -4px; + inset-inline-start: -5px; opacity: 0.4; } - &:last-of-type::after { - display: none; + &:first-of-type { + margin-inline-start: 0; + padding-inline-start: 0; + + &::before { + display: none; + } } } } + @media ( max-width: 550px ) { + & .wp-block-group p:not(.wp-block-site-title) { + display: none; + } + } + /* Navigation. */ + & .wp-block-navigation { + & .wp-block-navigation-submenu__toggle { + padding-block: calc(var(--wp--preset--spacing--10) / 2); + padding-inline-start: calc(var(--wp--preset--spacing--10) / 2); + padding-inline-end: calc(var(--wp--preset--spacing--10) / 2 + 1em); + + &:focus-visible { + outline: none; + border-radius: 2px; + box-shadow: inset 0 0 0 1.5px var(--wp--custom--local-navigation-bar--focus--color); + } + + & + .wp-block-navigation__submenu-icon { + margin-left: -1em; + width: 1em; + } + } + + & .wp-block-navigation__submenu-container { + top: calc(100% + 10px) !important; + left: auto !important; + right: 0 !important; + padding: calc(var(--wp--preset--spacing--10) / 2) !important; - /* Remove padding from menu items with background color, which is used to color the modal background. */ - & .wp-block-navigation ul.has-background, - & .wp-block-navigation:where(.has-background) .wp-block-navigation-item a:not(.wp-element-button), - & .wp-block-navigation:where(.has-background) .wp-block-navigation-submenu a:not(.wp-element-button) { - padding: 0; + a:where(:not(.wp-element-button)):focus-visible { + box-shadow: inset 0 0 0 1.5px var(--wp--custom--local-navigation-bar--focus--color); + } + } } - & .wp-block-navigation__responsive-container { + & .wp-block-navigation.is-collapsed { - /* Adjust the modal container so the close button is not hidden by the global header when open. */ - @media (max-width: 599px) { - top: var(--wp-global-header-height); + /* Remove padding from menu items with background color, which is used to color the modal background. */ + & ul.has-background { + padding: 0; + } - /* Matches the padding of the global header button. */ - padding-right: calc(16px + var(--wp--custom--alignment--scroll-bar-width)) !important; - padding-left: var(--wp--preset--spacing--edge-space) !important; - padding-top: 21px !important; - padding-bottom: 18px !important; + & .wp-block-navigation__responsive-container-open, + & .wp-block-navigation__responsive-container-close { + padding: 17px; - & .wp-block-navigation__submenu-container { - padding: 0 !important; - padding-inline-start: var(--wp--preset--spacing--20, 20px) !important; - margin-top: var(--wp--preset--spacing--20, 20px) !important; - gap: var(--wp--preset--spacing--20, 20px) !important; + &:focus-visible { + outline: none; + border-radius: 2px; + box-shadow: inset 0 0 0 1.5px var(--wp--custom--local-navigation-bar--focus--color); } } - } - & .wp-block-navigation .wp-block-navigation__submenu-container { - top: calc(100% + 10px) !important; - left: auto !important; - right: 0 !important; + & .wp-block-navigation__responsive-container-close { + margin-block-start: calc(var(--wp--custom--local-navigation-bar--spacing--height) * -1) !important; - & .wp-block-navigation-item { - display: block; + @media (max-width: 600px) { + margin-inline-end: calc(16px + var(--wp--custom--alignment--scroll-bar-width)); + } + } + + & .wp-block-navigation__responsive-close, + & .wp-block-navigation__responsive-container-close, + & .wp-block-navigation__responsive-dialog { + background-color: inherit; } - & .wp-block-navigation__submenu-icon { + & .wp-block-navigation__responsive-container .wp-block-navigation__responsive-dialog { + margin-top: 0; + } + + & .wp-block-navigation__container, + & .wp-block-navigation-item, + & .wp-block-navigation-item__content { + width: 100%; + } + + & .wp-block-navigation__responsive-container { + top: calc(var(--wp--custom--local-navigation-bar--spacing--height) + var(--wp-global-header-offset)) !important; + min-width: 14rem; + + /* Make the close button visible, even though it's pulled out of this frame. */ + overflow: visible; + + border-top: 1px solid var(--wp--custom--local-navigation-bar--border--color); + + & .wp-block-navigation__responsive-container-content { + padding-block-start: calc(var(--wp--preset--spacing--10) / 2) !important; + padding-inline: var(--wp--preset--spacing--10) !important; + padding-block-end: calc(var(--wp--preset--spacing--10) / 2) !important; + overflow-y: scroll; + max-height: calc(100vh - var(--wp--custom--local-navigation-bar--spacing--height) - var(--wp-global-header-offset)) !important; + } + + @media (min-width: 601px) { + position: absolute !important; + top: calc(var(--wp--custom--local-navigation-bar--spacing--height) - 1px) !important; + bottom: auto !important; + left: auto !important; + border-top: none; + } + } + + & .wp-block-navigation__responsive-container-content { + gap: 0; + + & .wp-block-navigation-item { + display: block; + + & .wp-block-navigation-item__content { + margin: 0; + padding: var(--wp--preset--spacing--10); + + &:focus-visible { + outline: none; + border-radius: 2px; + box-shadow: inset 0 0 0 1.5px var(--wp--custom--local-navigation-bar--focus--color); + } + } + } + } + } + + & .wp-block-navigation.wporg-is-collapsed-nav { + display: none; + } + + &.wporg-show-collapsed-nav { + & .wp-block-navigation { display: none; } - & .wp-block-navigation__submenu-container { - border: none; - margin-left: 8px; + & .wp-block-navigation.wporg-is-collapsed-nav { + display: block; } } } @@ -167,8 +322,3 @@ 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/local-navigation-bar/src/view.js b/mu-plugins/blocks/local-navigation-bar/src/view.js index 588177553..db49c2db6 100644 --- a/mu-plugins/blocks/local-navigation-bar/src/view.js +++ b/mu-plugins/blocks/local-navigation-bar/src/view.js @@ -36,6 +36,67 @@ function init() { document.addEventListener( 'scroll', debounce( onScroll ), { passive: true } ); onScroll(); + + // Check the size of child elements to determine if the local navigation + // menu should be collapsed in mobile-view by default. If so, toggle a + // CSS class to show the nav block with {"overlayMenu":"always"} + // added by `add_extra_navigation`. + const onResize = () => { + // Bail early on small screens, the visible nav block is already mobile. + if ( window.innerWidth <= 600 ) { + container.classList.remove( 'wporg-show-collapsed-nav' ); + return; + } + + // Fetch the navWidth from a data value which is set on page load, + // so that the uncollapsed visible menu's width is used. + let navWidth = container.dataset.navWidth; + if ( ! navWidth ) { + const navElement = container.querySelector( 'nav:not(.wporg-is-collapsed-nav)' ); + const navGap = parseInt( window.getComputedStyle( navElement ).gap, 10 ) || 20; + // Get the nav width based on items, so that it stays + // consistent even if the menu wraps to a new line. + const menuItems = navElement.querySelectorAll( '.wp-block-navigation__container > li' ); + navWidth = + [ ...menuItems ].reduce( + ( acc, current ) => ( acc += current.getBoundingClientRect().width ), + 0 + ) + + navGap * ( menuItems.length - 1 ); // 20px gap between items. + + // Save the value for future resize callbacks. + container.dataset.navWidth = Math.ceil( navWidth ); + } + + const { + paddingInlineStart = '0px', + paddingInlineEnd = '0px', + gap = '0px', + } = window.getComputedStyle( container ); + + const availableWidth = + window.innerWidth - + parseInt( paddingInlineStart, 10 ) - + parseInt( paddingInlineEnd, 10 ) - + parseInt( gap, 10 ); + + const titleElement = container.querySelector( '.wp-block-site-title, div.wp-block-group' ); + if ( ! titleElement ) { + return; + } + const { width: titleWidth } = titleElement.getBoundingClientRect(); + + const usedWidth = Math.ceil( titleWidth ) + Math.ceil( navWidth ); + + if ( usedWidth > availableWidth ) { + container.classList.add( 'wporg-show-collapsed-nav' ); + } else { + container.classList.remove( 'wporg-show-collapsed-nav' ); + } + }; + + window.addEventListener( 'resize', debounce( onResize ), { passive: true } ); + onResize(); } } window.addEventListener( 'load', init );