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

Navigation: Add an option to allow navigation to switch to overlay mode when wrapping #57587

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
266055a
Navigation: Add an option to allow navigation to switch to overlay mo…
scruffian Jan 5, 2024
94eb09a
reinstate the correct nav wrapping check
scruffian Jan 5, 2024
4f49b5f
remove the recursion
scruffian Jan 5, 2024
73e4a0f
fix flicker
scruffian Jan 5, 2024
6ca6911
update navigation block on resize
scruffian Jan 5, 2024
927f0fd
Just get the width, don't bother getting the whole rect
scruffian Jan 5, 2024
a90a0ce
Just return when we have a truthful result
scruffian Jan 5, 2024
8685579
replace foreach with map
scruffian Jan 5, 2024
fba9042
remove the listener on unmount
scruffian Jan 5, 2024
cfccc80
fix the array map
scruffian Jan 5, 2024
ae6948d
move to a hook
scruffian Jan 5, 2024
1ed6e61
remove event listener on unmount
scruffian Jan 5, 2024
ca3f86c
don't parseInt, we want an exact value
scruffian Jan 5, 2024
02ce2c9
check flex parent when wrapping
scruffian Jan 9, 2024
5de3831
target only direct descendants of the parent
MaggieCabrera Jan 9, 2024
f8d132e
fix child selector
MaggieCabrera Jan 11, 2024
55e7728
use parseInt to compare widths
MaggieCabrera Jan 11, 2024
aa60154
use a select control instead
scruffian Jan 11, 2024
e41111f
update the state when layout changes not just on resize
scruffian Jan 11, 2024
f94192c
open the collapsed nav so we can measure to see if we need to hide it
scruffian Jan 12, 2024
1685e57
prevent infinite loop
scruffian Jan 12, 2024
df3f335
update to use customselectcontrol
scruffian Jan 12, 2024
3e3acc0
update copy
scruffian Jan 12, 2024
e7173c6
reverted UI changes to original state
MaggieCabrera Jan 19, 2024
a16e10b
added experiment setting
MaggieCabrera Jan 19, 2024
9e5b861
made auto the default when flag is present
MaggieCabrera Jan 19, 2024
67e700f
rebased and moved changes to WP_Navigation_Block_Renderer new location
MaggieCabrera Jan 29, 2024
b3783d0
encapsulate experiment changes
MaggieCabrera Feb 2, 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
3 changes: 3 additions & 0 deletions lib/experimental/editor-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ function gutenberg_enable_experiments() {
if ( gutenberg_is_experiment_enabled( 'gutenberg-no-tinymce' ) ) {
wp_add_inline_script( 'wp-block-library', 'window.__experimentalDisableTinymce = true', 'before' );
}
if ( $gutenberg_experiments && array_key_exists( 'gutenberg-navigation-overlay-auto', $gutenberg_experiments ) ) {
wp_add_inline_script( 'wp-block-editor', 'window.__experimentalNavOverlayAuto = true', 'before' );
}
}

add_action( 'admin_init', 'gutenberg_enable_experiments' );
Expand Down
12 changes: 12 additions & 0 deletions lib/experiments-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,18 @@ function gutenberg_initialize_experiments_settings() {
)
);

add_settings_field(
'gutenberg-navigation-overlay-auto',
__( 'Navigation overlay', 'gutenberg' ),
'gutenberg_display_experiment_field',
'gutenberg-experiments',
'gutenberg_experiments_section',
array(
'label' => __( 'Allow navigation to switch to overlay mode when wrapping automatically', 'gutenberg' ),
'id' => 'gutenberg-navigation-overlay-auto',
)
);

register_setting(
'gutenberg-experiments',
'gutenberg-experiments'
Expand Down
18 changes: 7 additions & 11 deletions packages/block-library/src/navigation/edit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {
import { __, sprintf } from '@wordpress/i18n';
import { speak } from '@wordpress/a11y';
import { close, Icon } from '@wordpress/icons';
import { useInstanceId, useMediaQuery } from '@wordpress/compose';
import { useInstanceId } from '@wordpress/compose';

/**
* Internal dependencies
Expand Down Expand Up @@ -71,7 +71,7 @@ import MenuInspectorControls from './menu-inspector-controls';
import DeletedNavigationWarning from './deleted-navigation-warning';
import AccessibleDescription from './accessible-description';
import AccessibleMenuDescription from './accessible-menu-description';
import { NAVIGATION_MOBILE_COLLAPSE } from '../constants';
import useIsCollapsed from '../use-is-collapsed';
import { unlock } from '../../lock-unlock';

function Navigation( {
Expand Down Expand Up @@ -297,15 +297,10 @@ function Navigation( {
),
[ clientId ]
);
const overlayType = window?.__experimentalNavOverlayAuto
? 'auto'
: overlayMenu;
const isResponsive = 'never' !== overlayMenu;
const isMobileBreakPoint = useMediaQuery(
`(max-width: ${ NAVIGATION_MOBILE_COLLAPSE })`
);

const isCollapsed =
( 'mobile' === overlayMenu && isMobileBreakPoint ) ||
'always' === overlayMenu;

const blockProps = useBlockProps( {
ref: navRef,
className: classnames(
Expand All @@ -319,7 +314,7 @@ function Navigation( {
'is-vertical': orientation === 'vertical',
'no-wrap': flexWrap === 'nowrap',
'is-responsive': isResponsive,
'is-collapsed': isCollapsed,
'is-collapsed': useIsCollapsed( overlayType, navRef ),
'has-text-color': !! textColor.color || !! textColor?.class,
[ getColorClassName( 'color', textColor?.slug ) ]:
!! textColor?.slug,
Expand Down Expand Up @@ -576,6 +571,7 @@ function Navigation( {
</div>
</>
) }

<h3>{ __( 'Overlay Menu' ) }</h3>
<ToggleGroupControl
__nextHasNoMarginBottom
Expand Down
111 changes: 88 additions & 23 deletions packages/block-library/src/navigation/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -379,10 +379,32 @@
return implode( ' ', $classes );
}

/**
* Returns whether or not the navigation is always overlay.
*
* @param array $attributes The block attributes.
* @return bool Returns whether or not the navigation is always overlay.
*/
private static function is_always_overlay( $attributes ) {
return isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu'];
}

/**
* Returns whether or not the navigation is collapsable.
*
* @param array $attributes The block attributes.
* @return bool Returns whether or not the navigation is collapsable.
*/
private static function is_collapsable( $attributes ) {

$gutenberg_experiments = get_option( 'gutenberg-experiments' );
if ( empty( $gutenberg_experiments ) || ! array_key_exists( 'gutenberg-navigation-overlay-auto', $gutenberg_experiments ) ) {
return;
}

return isset( $attributes['overlayMenu'] ) && in_array( $attributes['overlayMenu'], array( 'mobile', 'auto' ), true );
}

/**
* Get styles for the navigation block.
*
Expand Down Expand Up @@ -530,29 +552,72 @@
if ( ! $is_interactive ) {
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' ),
),
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP
);
$nav_element_directives = '
data-wp-interactive=\'{"namespace":"core/navigation"}\'
data-wp-context=\'' . $nav_element_context . '\'
';

/*
* When the navigation's 'overlayMenu' attribute is set to 'always', JavaScript
* is not needed for collapsing the menu because the class is set manually.
*/
if ( ! static::is_always_overlay( $attributes ) ) {
$nav_element_directives .= 'data-wp-init="callbacks.initNav"';
$nav_element_directives .= ' '; // space separator
$nav_element_directives .= 'data-wp-class--is-collapsed="context.isCollapsed"';

$gutenberg_experiments = get_option( 'gutenberg-experiments' );
$is_experiment = ( $gutenberg_experiments && array_key_exists( 'gutenberg-navigation-overlay-auto', $gutenberg_experiments ) ) ? true : false;

if( $is_experiment ) {

Check failure on line 559 in packages/block-library/src/navigation/index.php

View workflow job for this annotation

GitHub Actions / PHP coding standards

Space after opening control structure is required

Check failure on line 559 in packages/block-library/src/navigation/index.php

View workflow job for this annotation

GitHub Actions / PHP coding standards

No space before opening parenthesis is prohibited

Check failure on line 559 in packages/block-library/src/navigation/index.php

View workflow job for this annotation

GitHub Actions / PHP coding standards

Expected 1 space after IF keyword; 0 found
$overlay_menu = $is_experiment ? 'auto' : $attributes['overlayMenu'];

// When adding to this array be mindful of security concerns.
$nav_element_context = wp_json_encode(

Check warning on line 563 in packages/block-library/src/navigation/index.php

View workflow job for this annotation

GitHub Actions / PHP coding standards

Equals sign not aligned correctly; expected 1 space but found 4 spaces
array(
'overlayOpenedBy' => array(),
'type' => 'overlay',
'roleAttribute' => '',
'ariaLabel' => __( 'Menu' ),
'overlayMenu' => $overlay_menu,
),
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP
);

$nav_element_directives = '
data-wp-interactive=\'{"namespace":"core/navigation"}\'
data-wp-context=\'' . $nav_element_context . '\'
';

/*
* When the navigation's 'overlayMenu' attribute is set to 'always', JavaScript
* is not needed for collapsing the menu because the class is set manually.
*/
if ( static::is_collapsable( $attributes ) ) {
$nav_element_directives .= ' '; // space separator
$nav_element_directives .= 'data-wp-class--is-collapsed="context.isCollapsed"';
}

if ( isset( $overlay_menu ) && 'mobile' === $overlay_menu ) {
$nav_element_directives .= ' '; // space separator
$nav_element_directives .= 'data-wp-init="callbacks.initMobileNav"';
}
if ( isset( $overlay_menu ) && 'auto' === $overlay_menu ) {
$nav_element_directives .= ' '; // space separator
$nav_element_directives .= 'data-wp-init="callbacks.initAutoNav"';
}
} else {
// When adding to this array be mindful of security concerns.
$nav_element_context = wp_json_encode(

Check warning on line 598 in packages/block-library/src/navigation/index.php

View workflow job for this annotation

GitHub Actions / PHP coding standards

Equals sign not aligned correctly; expected 1 space but found 4 spaces
array(
'overlayOpenedBy' => array(),
'type' => 'overlay',
'roleAttribute' => '',
'ariaLabel' => __( 'Menu' ),
),
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP
);

$nav_element_directives = '
data-wp-interactive=\'{"namespace":"core/navigation"}\'
data-wp-context=\'' . $nav_element_context . '\'
';

/*
* When the navigation's 'overlayMenu' attribute is set to 'always', JavaScript
* is not needed for collapsing the menu because the class is set manually.
*/
if ( ! static::is_always_overlay( $attributes ) ) {
$nav_element_directives .= ' '; // space separator
$nav_element_directives .= 'data-wp-class--is-collapsed="context.isCollapsed"';
}
}

return $nav_element_directives;
Expand Down
108 changes: 108 additions & 0 deletions packages/block-library/src/navigation/is-wrapping.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* Determines if the children of a wrapper element are wrapping.
*
* @param {Element} wrapper The element to determine if its children are wrapping
* @param {Array} children The children of the wrapper element
*
* @return {boolean} Whether the children of the wrapper element are wrapping.
*/
function areItemsWrapping(
wrapper,
children = wrapper.querySelectorAll(
'ul > .wp-block-navigation-item:not(ul ul .wp-block-navigation-item)'
)
) {
const wrapperDimensions = wrapper.getBoundingClientRect();
const wrapperWidth = wrapperDimensions.width;
//we store an array with the width of each item
const itemsWidths = getItemWidths( children );
let totalWidth = 0;

//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( wrapperWidth ) ) {
return true;
}
}
return false;
}

/**
* Determines if the navigation element itself is wrapping.
*
* @param {Element} navElement Wrapper element of the navigation block.
* @return {boolean} Whether the nav element itself is wrapping.
*/
function isNavElementWrapping( navElement ) {
//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 = getFlexParent( navElement );
if ( container !== null ) {
return areItemsWrapping( container, Array.from( container.children ) );
}

return false;
}

/**
* Returns an array with the width of each item.
*
* @param {Array} items The items to get the width of.
* @return {Array} An array with the width of each item.
*/
function getItemWidths( items ) {
return Array.from( items ).map( ( 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;

return totalWidth;
} );
}

function getFlexParent( element ) {
// We need to do this check rather than document.body to account for iframes
if ( element.tagName === 'BODY' ) {
// Base case: Stop recursion once we go all the way to the body to avoid infinite recursion
return null;
}
const parent = element.parentNode;
const containerStyles = window.getComputedStyle( parent );
const isFlexWrap =
containerStyles.getPropertyValue( 'flex-wrap' ) === 'wrap';
if ( isFlexWrap ) {
return parent;
}
// This recursion checks whether the nav element is wrapped inside a flex container.
return getFlexParent( parent );
}

/**
* Determines if the navigation block is wrapping.
*
* @param {Element} navElement Wrapper element of the navigation block.
* @return {boolean} Whether the navigation block is wrapping.
*/
function navigationIsWrapping( navElement ) {
if ( ! navElement ) {
return false;
}

return (
areItemsWrapping( navElement ) === true ||
isNavElementWrapping( navElement ) === true
);
}

export default navigationIsWrapping;
Loading
Loading