diff --git a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php index ea94128e1dde29..80104dcd41ba27 100644 --- a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php +++ b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php @@ -59,6 +59,16 @@ private static function is_responsive( $attributes ) { return isset( $attributes['overlayMenu'] ) && 'never' !== $attributes['overlayMenu'] || $has_old_responsive_attribute; } + /** + * Returns whether or not the navigation has a flexible breakpoint. + * + * @param array $attributes The block attributes. + * @return bool Returns whether or not this is responsive navigation. + */ + private static function has_flexible_breakpoint( $attributes ) { + return $attributes['flexibleBreakpoint'] ? 'true' : 'false'; + } + /** * Returns whether or not a navigation has a submenu. * @@ -495,7 +505,9 @@ private static function get_nav_wrapper_attributes( $attributes, $inner_blocks ) $is_responsive_menu = static::is_responsive( $attributes ); $style = static::get_styles( $attributes ); $class = static::get_classes( $attributes ); - $wrapper_attributes = get_block_wrapper_attributes( + $has_flexible_breakpoint = static::has_flexible_breakpoint( $attributes ); + + $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $class, 'style' => $style, @@ -504,7 +516,7 @@ private static function get_nav_wrapper_attributes( $attributes, $inner_blocks ) ); if ( $is_responsive_menu ) { - $nav_element_directives = static::get_nav_element_directives( $should_load_view_script ); + $nav_element_directives = static::get_nav_element_directives( $should_load_view_script, $has_flexible_breakpoint ); $wrapper_attributes .= ' ' . $nav_element_directives; } @@ -515,25 +527,35 @@ private static function get_nav_wrapper_attributes( $attributes, $inner_blocks ) * Get the nav element directives * * @param bool $should_load_view_script Whether or not the view script should be loaded. + * @param bool $has_flexible_breakpoint Whether or not the nav block will have a flexible breakpoint. * @return string the directives for the navigation element. */ - private static function get_nav_element_directives( $should_load_view_script ) { + private static function get_nav_element_directives( $should_load_view_script, $has_flexible_breakpoint ) { if ( ! $should_load_view_script ) { return ''; } // When adding to this array be mindful of security concerns. $nav_element_context = wp_json_encode( array( - 'overlayOpenedBy' => array(), - 'type' => 'overlay', - 'roleAttribute' => '', - 'ariaLabel' => __( 'Menu' ), + 'overlayOpenedBy' => array(), + 'type' => 'overlay', + 'roleAttribute' => '', + 'ariaLabel' => __( 'Menu' ), + 'has_flexible_breakpoint' => $has_flexible_breakpoint, ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP ); + + $flexible_breakpoint_directives = ''; + + if ( $has_flexible_breakpoint ) { + $flexible_breakpoint_directives = 'data-wp-init--overlay="actions.isNavCollapsed"'; + } + return ' data-wp-interactive=\'{"namespace":"core/navigation"}\' - data-wp-context=\'' . $nav_element_context . '\' + data-wp-context=\'' . $nav_element_context . '\' + ' . $flexible_breakpoint_directives . ' '; } diff --git a/packages/block-library/src/navigation/block.json b/packages/block-library/src/navigation/block.json index 9ec919ae38d1fa..fe0a9c76437c00 100644 --- a/packages/block-library/src/navigation/block.json +++ b/packages/block-library/src/navigation/block.json @@ -33,6 +33,10 @@ "type": "boolean", "default": true }, + "flexibleBreakpoint": { + "type": "boolean", + "default": false + }, "openSubmenusOnClick": { "type": "boolean", "default": false diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 2e94cddcc9bc24..cf8218367b9d85 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -98,6 +98,7 @@ function Navigation( { openSubmenusOnClick, overlayMenu, showSubmenuIcon, + flexibleBreakpoint, templateLock, layout: { justifyContent, @@ -522,6 +523,8 @@ function Navigation( { `overlay-menu-preview` ); + const needsBreakpoint = isResponsive && 'mobile' === overlayMenu; + const colorGradientSettings = useMultipleOriginColorsAndGradients(); const stylingInspectorControls = ( <> @@ -593,6 +596,21 @@ function Navigation( { label={ __( 'Always' ) } /> + + { needsBreakpoint && ( + { + setAttributes( { + flexibleBreakpoint: value, + } ); + } } + disabled={ attributes.openSubmenusOnClick } + label={ __( 'Flexible breakpoint' ) } + /> + ) } + { hasSubmenus && ( <>

{ __( 'Submenus' ) }

diff --git a/packages/block-library/src/navigation/style.scss b/packages/block-library/src/navigation/style.scss index 0b70ebb656cfa8..f5f225de3e7d03 100644 --- a/packages/block-library/src/navigation/style.scss +++ b/packages/block-library/src/navigation/style.scss @@ -475,6 +475,17 @@ button.wp-block-navigation-item__content { right: 0; bottom: 0; + &:not(.hidden-by-default):not(.is-menu-open):not(.is-mobile) { + display: block; + position: static; + } + + .is-mobile &:not(.hidden-by-default):not(.is-menu-open) { + display: none; + position: fixed; + } + + // Low specificity so that themes can override. & :where(.wp-block-navigation-item a) { color: inherit; @@ -611,7 +622,7 @@ button.wp-block-navigation-item__content { } } - @include break-small() { + /*@include break-small() { &:not(.hidden-by-default) { &:not(.is-menu-open) { display: block; @@ -632,7 +643,7 @@ button.wp-block-navigation-item__content { left: 0; } } - } + }*/ } // Default menu background and font color. @@ -691,6 +702,10 @@ button.wp-block-navigation-item__content { display: none; } } + + .is-mobile & { + display: block !important; + } } // Button to close the menus. diff --git a/packages/block-library/src/navigation/view.js b/packages/block-library/src/navigation/view.js index fb3919168a2677..82283faf3a55b5 100644 --- a/packages/block-library/src/navigation/view.js +++ b/packages/block-library/src/navigation/view.js @@ -18,7 +18,7 @@ const focusableSelectors = [ // capture the clicks, instead of relying on the focusout event. document.addEventListener( 'click', () => {} ); -const { state, actions } = store( 'core/navigation', { +const { state, actions, callbacks } = store( 'core/navigation', { state: { get roleAttribute() { const ctx = getContext(); @@ -176,7 +176,11 @@ const { state, actions } = store( 'core/navigation', { ctx.lastFocusableElement = focusableElements[ focusableElements.length - 1 ]; } + window.addEventListener( 'resize', () => + callbacks.isNavCollapsed( ref ) + ); }, + focusFirstElement() { const { ref } = getElement(); if ( state.isMenuOpen ) { @@ -185,5 +189,101 @@ const { state, actions } = store( 'core/navigation', { focusableElements?.[ 0 ]?.focus(); } }, + + isNavCollapsed( ref ) { + // remove the is-mobile class to avoid false positives. + ref.closest( 'nav' ).classList.remove( 'is-mobile' ); + + // test if the nav items are wrapping before testing if the actual nav is wrapping inside its parent to avoid the recursive function if possible + if ( + areItemsWrapping( ref ) === true || + isNavWrapping( ref ) === true + ) { + ref.closest( 'nav' ).classList.add( 'is-mobile' ); + } else { + ref.closest( 'nav' ).classList.remove( 'is-mobile' ); + } + }, }, } ); + +function areItemsWrapping( + wrapper, + children = wrapper.querySelectorAll( 'li' ) +) { + const wrapperDimensions = wrapper.getBoundingClientRect(); + //we store an array with the width of each item + const itemsWidths = getItemWidths( children ); + let totalWidth = 0; + let isWrapping = false; + + //the nav block may have row-gap applied, which is not calculated in getItemWidths + const computedStyle = window.getComputedStyle( wrapper ); + const rowGap = parseFloat( computedStyle.rowGap ) || 0; + + for ( let i = 0, len = itemsWidths.length; i < len; i++ ) { + totalWidth += itemsWidths[ i ]; + if ( rowGap > 0 && i > 0 ) { + totalWidth += rowGap; + } + if ( parseInt( totalWidth ) > parseInt( wrapperDimensions.width ) ) { + isWrapping = true; + } + } + return isWrapping; +} + +function isNavWrapping( ref ) { + let isWrapping = false; + //how can we check if the nav element is wrapped inside its parent if we don't know anything about it (the parent)? + //for debugging purposes + const container = ref.closest( 'nav' ); //getFlexParent( ref ); + if ( container !== null ) { + const childrenWrapper = container.querySelector( + 'ul.wp-block-navigation' + ); + isWrapping = + childrenWrapper && + childrenWrapper.children && + areItemsWrapping( + container, + Array.from( + container.querySelector( 'ul.wp-block-navigation' ).children + ) + ); + } + + return isWrapping; +} + +/* I'm not sure we still need this - can we just get the nearest nav element? +function getFlexParent( elem ) { + if ( elem === document.body ) { + // Base case: Stop recursion once we go all the way to the body to avoid infinite recursion + return null; + } + const parent = elem.parentNode; + const containerStyles = window.getComputedStyle( parent ); + const isFlexWrap = + containerStyles.getPropertyValue( 'flex-wrap' ) === 'wrap'; + if ( isFlexWrap ) { + return parent; + } + return getFlexParent( parent ); +}*/ + +function getItemWidths( items ) { + const itemsWidths = []; + + items.forEach( function ( item ) { + const style = item.currentStyle || window.getComputedStyle( item ); + const itemDimensions = item.getBoundingClientRect(); + const width = parseFloat( itemDimensions.width ); + const marginLeft = parseFloat( style.marginLeft ); + const marginRight = parseFloat( style.marginRight ); + const totalWidth = width + marginLeft + marginRight; + + itemsWidths.push( totalWidth ); + } ); + return itemsWidths; +}