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 27 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
35 changes: 33 additions & 2 deletions packages/block-library/src/navigation/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -379,10 +379,26 @@ private static function get_classes( $attributes ) {
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 ) {
return isset( $attributes['overlayMenu'] ) && in_array( $attributes['overlayMenu'], array( 'mobile', 'auto' ), true );
}

/**
* Get styles for the navigation block.
*
Expand Down Expand Up @@ -530,13 +546,20 @@ private static function get_nav_element_directives( $is_interactive, $attributes
if ( ! $is_interactive ) {
return '';
}

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

$overlay_menu = $is_experiment ? 'auto' : $attributes['overlayMenu'];

// When adding to this array be mindful of security concerns.
$nav_element_context = wp_json_encode(
array(
'overlayOpenedBy' => array(),
'type' => 'overlay',
'roleAttribute' => '',
'ariaLabel' => __( 'Menu' ),
'overlayMenu' => $overlay_menu,
),
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP
);
Expand All @@ -549,12 +572,20 @@ private static function get_nav_element_directives( $is_interactive, $attributes
* 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"';
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"';
}

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;
97 changes: 97 additions & 0 deletions packages/block-library/src/navigation/use-is-collapsed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* WordPress dependencies
*/
import { debounce, useMediaQuery } from '@wordpress/compose';
import { useState, useLayoutEffect } from '@wordpress/element';

/**
* Internal dependencies
*/
import { NAVIGATION_MOBILE_COLLAPSE } from './constants';
import navigationIsWrapping from './is-wrapping';

function useIsCollapsed( overlayMenu, navRef ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Something about this hook is buggy. Its possible to see a wrapped navigation block in the editor.

const isMobileBreakPoint = useMediaQuery(
`(max-width: ${ NAVIGATION_MOBILE_COLLAPSE })`
);

// Determines the conditions under which the navigation should be collapsed.
const shouldBeCollapsed = () => {
// If the overlay menu is set to always, then it should always be collapsed.
if ( 'always' === overlayMenu ) {
return true;
}

// If the overlay menu is set to mobile and the screen is at the mobile breakpoint, then it should be collapsed.
if ( 'mobile' === overlayMenu && isMobileBreakPoint ) {
return true;
}

// If the overlay menu is set to auto, then we need to check if the navigation is wrapping.
if ( 'auto' === overlayMenu ) {
if ( ! navRef.current ) {
return false;
}

// If the navigation is already collapsed, then it should stay collapsed.
// We uncollapse it when the screen is resized so that we can measure the full width of the nav.
// It's not ideal to use the actual class name here.
if ( navRef.current.classList.contains( 'is-collapsed' ) ) {
return true;
}

return navigationIsWrapping( navRef.current );
}
};

const [ isCollapsed, setIsCollapsed ] = useState( shouldBeCollapsed() );

// We need a layout effect to respond to changed in isMobileBreakPoint.
useLayoutEffect( () => {
function updateIsCollapsed() {
setIsCollapsed( shouldBeCollapsed() );
}

function setIsCollapsedFalse() {
setIsCollapsed( false );
}

// This is wrapped in a function so that we can unbind it later.
function debouncedSetIsCollapsedFalse() {
return debounce( setIsCollapsedFalse, 50 ); // Has to be less than debouncedUpdateIsCollapsed.
}
Comment on lines +60 to +62
Copy link
Contributor

Choose a reason for hiding this comment

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

At this point it's probably worth moving to a useCallback to avoid re-creating a debounced function each time effect is called?


// This is wrapped in a function so that we can unbind it later.
function debouncedUpdateIsCollapsed() {
return debounce( updateIsCollapsed, 100 );
}

// Set the value of isCollapsed when the effect runs.
updateIsCollapsed();

// We only need to add listeners if the overlayMenu is set to auto.
if ( 'auto' === overlayMenu ) {
// Adds a listener to set isCollapsed be false so we can measure the full width of the nav.
window.addEventListener( 'resize', debouncedSetIsCollapsedFalse() );

// Then add a debounced listener to update isCollapsed.
window.addEventListener( 'resize', debouncedUpdateIsCollapsed() );

// Remove the listener when the component is unmounted.
return () => {
window.removeEventListener(
'resize',
debouncedUpdateIsCollapsed()
);
window.removeEventListener(
'resize',
debouncedSetIsCollapsedFalse()
);
};
}
} );

return isCollapsed;
}

export default useIsCollapsed;
Loading
Loading