diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index eda2a2b7967dfd..351a3fdf62afe4 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -78,9 +78,6 @@ function Navigation( { setOverlayBackgroundColor, overlayTextColor, setOverlayTextColor, - - // These props are used by the navigation editor to override specific - // navigation block settings. hasSubmenuIndicatorSetting = true, hasColorSettings = true, customPlaceholder: CustomPlaceholder = null, @@ -95,40 +92,30 @@ function Navigation( { flexWrap = 'wrap', } = {}, hasIcon, + ref, } = attributes; - const ref = attributes.ref; + const textDecoration = attributes.style?.typography?.textDecoration; const registry = useRegistry(); - const setRef = ( postId ) => { - setAttributes( { ref: postId } ); - }; - const [ hasAlreadyRendered, RecursionProvider ] = useNoRecursiveRenders( - `navigationMenu/${ ref }` - ); + const { + replaceInnerBlocks, + selectBlock, + __unstableMarkNextChangeAsNotPersistent, + } = useDispatch( blockEditorStore ); + + const { + hasUncontrolledInnerBlocks, + uncontrolledInnerBlocks, + isInnerBlockSelected, + innerBlocks, + } = useInnerBlocks( clientId ); // Preload classic menus, so that they don't suddenly pop-in when viewing // the Select Menu dropdown. useNavigationEntities(); - const [ showNavigationMenuStatusNotice, hideNavigationMenuStatusNotice ] = - useNavigationNotice( { - name: 'block-library/core/navigation/status', - } ); - - const [ showClassicMenuConversionNotice, hideClassicMenuConversionNotice ] = - useNavigationNotice( { - name: 'block-library/core/navigation/classic-menu-conversion', - } ); - - const [ - showNavigationMenuPermissionsNotice, - hideNavigationMenuPermissionsNotice, - ] = useNavigationNotice( { - name: 'block-library/core/navigation/permissions/update', - } ); - const { create: createNavigationMenu, status: createNavigationMenuStatus, @@ -139,59 +126,6 @@ function Navigation( { isError: createNavigationMenuIsError, } = useCreateNavigationMenu( clientId ); - useEffect( () => { - hideNavigationMenuStatusNotice(); - - if ( isCreatingNavigationMenu ) { - speak( __( `Creating Navigation Menu.` ) ); - } - - if ( createNavigationMenuIsSuccess ) { - setRef( createNavigationMenuPost.id ); - selectBlock( clientId ); - - showNavigationMenuStatusNotice( - __( `Navigation Menu successfully created.` ) - ); - } - - if ( createNavigationMenuIsError ) { - showNavigationMenuStatusNotice( - __( 'Failed to create Navigation Menu.' ) - ); - } - }, [ - createNavigationMenu, - createNavigationMenuStatus, - createNavigationMenuError, - createNavigationMenuPost, - ] ); - - const { - hasUncontrolledInnerBlocks, - uncontrolledInnerBlocks, - isInnerBlockSelected, - innerBlocks, - } = useInnerBlocks( clientId ); - - const hasSubmenus = !! innerBlocks.find( - ( block ) => block.name === 'core/navigation-submenu' - ); - - const { - replaceInnerBlocks, - selectBlock, - __unstableMarkNextChangeAsNotPersistent, - } = useDispatch( blockEditorStore ); - - const [ hasSavedUnsavedInnerBlocks, setHasSavedUnsavedInnerBlocks ] = - useState( false ); - - const [ isResponsiveMenuOpen, setResponsiveMenuVisibility ] = - useState( false ); - - const [ overlayMenuPreview, setOverlayMenuPreview ] = useState( false ); - const { hasResolvedNavigationMenus, isNavigationMenuResolved, @@ -207,26 +141,6 @@ function Navigation( { hasResolvedCanUserCreateNavigationMenu, } = useNavigationMenu( ref ); - // Attempt to retrieve and prioritize any existing navigation menu unless - // a specific ref is allocated or the user is explicitly creating a new menu. The aim is - // for the block to "just work" from a user perspective using existing data. - useEffect( () => { - if ( - isCreatingNavigationMenu || - ref || - ! navigationMenus?.length || - navigationMenus?.length > 1 - ) { - return; - } - - setRef( navigationMenus[ 0 ].id ); - }, [ navigationMenus ] ); - - const navRef = useRef(); - - const isDraftNavigationMenu = navigationMenu?.status === 'draft'; - const { convert, status: classicMenuConversionStatus, @@ -234,42 +148,47 @@ function Navigation( { value: classicMenuConversionResult, } = useConvertClassicToBlockMenu( clientId ); - const isConvertingClassicMenu = - classicMenuConversionStatus === CLASSIC_MENU_CONVERSION_PENDING; + const [ hasAlreadyRendered, RecursionProvider ] = useNoRecursiveRenders( + `navigationMenu/${ ref }` + ); - // The standard HTML5 tag for the block wrapper. - const TagName = 'nav'; + const hasBlockOverlay = useBlockOverlayActive( clientId ); - // "placeholder" shown if: - // - there is no ref attribute pointing to a Navigation Post. - // - there is no classic menu conversion process in progress. - // - there is no menu creation process in progress. - // - there are no uncontrolled blocks. - const isPlaceholder = - ! ref && - ! isCreatingNavigationMenu && - ! isConvertingClassicMenu && - hasResolvedNavigationMenus && - ! hasUncontrolledInnerBlocks; + const [ showNavigationMenuStatusNotice, hideNavigationMenuStatusNotice ] = + useNavigationNotice( { + name: 'block-library/core/navigation/status', + } ); - const isEntityAvailable = - ! isNavigationMenuMissing && isNavigationMenuResolved; + const [ showClassicMenuConversionNotice, hideClassicMenuConversionNotice ] = + useNavigationNotice( { + name: 'block-library/core/navigation/classic-menu-conversion', + } ); - // "loading" state: - // - there is a menu creation process in progress. - // - there is a classic menu conversion process in progress. - // OR - // - there is a ref attribute pointing to a Navigation Post - // - the Navigation Post isn't available (hasn't resolved) yet. - const isLoading = - ! hasResolvedNavigationMenus || - isCreatingNavigationMenu || - isConvertingClassicMenu || - !! ( ref && ! isEntityAvailable && ! isConvertingClassicMenu ); + const [ + showNavigationMenuPermissionsNotice, + hideNavigationMenuPermissionsNotice, + ] = useNavigationNotice( { + name: 'block-library/core/navigation/permissions/update', + } ); - const textDecoration = attributes.style?.typography?.textDecoration; + const [ hasSavedUnsavedInnerBlocks, setHasSavedUnsavedInnerBlocks ] = + useState( false ); + const [ isResponsiveMenuOpen, setResponsiveMenuVisibility ] = + useState( false ); + const [ overlayMenuPreview, setOverlayMenuPreview ] = useState( false ); + const [ detectedBackgroundColor, setDetectedBackgroundColor ] = useState(); + const [ detectedColor, setDetectedColor ] = useState(); + const [ + detectedOverlayBackgroundColor, + setDetectedOverlayBackgroundColor, + ] = useState(); + const [ detectedOverlayColor, setDetectedOverlayColor ] = useState(); + const [ shouldFocusNavigationSelector, setShouldFocusNavigationSelector ] = + useState( false ); + + const navRef = useRef(); + const navigationSelectorRef = useRef(); - const hasBlockOverlay = useBlockOverlayActive( clientId ); const blockProps = useBlockProps( { ref: navRef, @@ -382,99 +301,87 @@ function Navigation( { if ( ! isSelected && ! isInnerBlockSelected ) { hideNavigationMenuPermissionsNotice(); } + const isResponsive = 'never' !== overlayMenu; - if ( isSelected || isInnerBlockSelected ) { - if ( - ref && - hasResolvedCanUserUpdateNavigationMenu && - ! canUserUpdateNavigationMenu - ) { - showNavigationMenuPermissionsNotice( - __( - 'You do not have permission to edit this Menu. Any changes made will not be saved.' - ) - ); - } + const isDraftNavigationMenu = navigationMenu?.status === 'draft'; + const isConvertingClassicMenu = + classicMenuConversionStatus === CLASSIC_MENU_CONVERSION_PENDING; - if ( - ! ref && - hasResolvedCanUserCreateNavigationMenu && - ! canUserCreateNavigationMenu - ) { - showNavigationMenuPermissionsNotice( - __( - 'You do not have permission to create Navigation Menus.' - ) - ); - } - } - }, [ - isSelected, - isInnerBlockSelected, - canUserUpdateNavigationMenu, - hasResolvedCanUserUpdateNavigationMenu, - canUserCreateNavigationMenu, - hasResolvedCanUserCreateNavigationMenu, - ref, - ] ); + // "placeholder" shown if: + // - there is no ref attribute pointing to a Navigation Post. + // - there is no classic menu conversion process in progress. + // - there is no menu creation process in progress. + // - there are no uncontrolled blocks. + const isPlaceholder = + ! ref && + ! isCreatingNavigationMenu && + ! isConvertingClassicMenu && + hasResolvedNavigationMenus && + ! hasUncontrolledInnerBlocks; - const navigationSelectorRef = useRef(); - const [ shouldFocusNavigationSelector, setShouldFocusNavigationSelector ] = - useState( false ); - const handleSelectNavigation = useCallback( - ( navPostOrClassicMenu ) => { - if ( ! navPostOrClassicMenu ) { - return; - } + const isEntityAvailable = + ! isNavigationMenuMissing && isNavigationMenuResolved; - const isClassicMenu = - navPostOrClassicMenu.hasOwnProperty( 'auto_add' ); + // "loading" state: + // - there is a menu creation process in progress. + // - there is a classic menu conversion process in progress. + // OR + // - there is a ref attribute pointing to a Navigation Post + // - the Navigation Post isn't available (hasn't resolved) yet. + const isLoading = + ! hasResolvedNavigationMenus || + isCreatingNavigationMenu || + isConvertingClassicMenu || + !! ( ref && ! isEntityAvailable && ! isConvertingClassicMenu ); - if ( isClassicMenu ) { - convert( navPostOrClassicMenu.id, navPostOrClassicMenu.name ); - } else { - handleUpdateMenu( navPostOrClassicMenu.id ); - } - setShouldFocusNavigationSelector( true ); - }, - [ convert, handleUpdateMenu ] + const hasSubmenus = !! innerBlocks.find( + ( block ) => block.name === 'core/navigation-submenu' ); - // Focus support after menu selection. - useEffect( () => { - if ( - isDraftNavigationMenu || - ! isEntityAvailable || - ! shouldFocusNavigationSelector - ) { - return; - } - navigationSelectorRef?.current?.focus(); - setShouldFocusNavigationSelector( false ); - }, [ - isDraftNavigationMenu, - isEntityAvailable, - shouldFocusNavigationSelector, - ] ); + // If the block has inner blocks, but no menu id, then these blocks are either: + // - inserted via a pattern. + // - inserted directly via Code View (or otherwise). + // - from an older version of navigation block added before the block used a wp_navigation entity. + // Consider this state as 'unsaved' and offer an uncontrolled version of inner blocks, + // that automatically saves the menu as an entity when changes are made to the inner blocks. + const hasUnsavedBlocks = hasUncontrolledInnerBlocks && ! isEntityAvailable; - const resetToEmptyBlock = useCallback( () => { - registry.batch( () => { - setAttributes( { - ref: undefined, - } ); - if ( ! ref ) { - replaceInnerBlocks( clientId, [] ); - } - } ); - }, [ clientId, ref ] ); + // The standard HTML5 tag for the block wrapper. + const TagName = 'nav'; - const isResponsive = 'never' !== overlayMenu; + const overlayClassnames = classnames( { + 'has-text-color': + !! overlayTextColor.color || !! overlayTextColor?.class, + [ getColorClassName( 'color', overlayTextColor?.slug ) ]: + !! overlayTextColor?.slug, + 'has-background': + !! overlayBackgroundColor.color || overlayBackgroundColor?.class, + [ getColorClassName( + 'background-color', + overlayBackgroundColor?.slug + ) ]: !! overlayBackgroundColor?.slug, + } ); + + const overlayStyles = { + color: ! overlayTextColor?.slug && overlayTextColor?.color, + backgroundColor: + ! overlayBackgroundColor?.slug && + overlayBackgroundColor?.color && + overlayBackgroundColor.color, + }; + + // Turn on contrast checker for web only since it's not supported on mobile yet. + const enableContrastChecking = Platform.OS === 'web'; const overlayMenuPreviewClasses = classnames( 'wp-block-navigation__overlay-menu-preview', { open: overlayMenuPreview } ); + const PlaceholderComponent = CustomPlaceholder + ? CustomPlaceholder + : Placeholder; + const stylingInspectorControls = ( { hasSubmenuIndicatorSetting && ( @@ -606,13 +513,205 @@ function Navigation( { ); - // If the block has inner blocks, but no menu id, then these blocks are either: - // - inserted via a pattern. - // - inserted directly via Code View (or otherwise). - // - from an older version of navigation block added before the block used a wp_navigation entity. - // Consider this state as 'unsaved' and offer an uncontrolled version of inner blocks, - // that automatically saves the menu as an entity when changes are made to the inner blocks. - const hasUnsavedBlocks = hasUncontrolledInnerBlocks && ! isEntityAvailable; + const setRef = ( postId ) => { + setAttributes( { ref: postId } ); + }; + + const handleUpdateMenu = ( menuId ) => { + setRef( menuId ); + selectBlock( clientId ); + }; + + const handleSelectNavigation = useCallback( + ( navPostOrClassicMenu ) => { + if ( ! navPostOrClassicMenu ) { + return; + } + + const isClassicMenu = + navPostOrClassicMenu.hasOwnProperty( 'auto_add' ); + + if ( isClassicMenu ) { + convert( navPostOrClassicMenu.id, navPostOrClassicMenu.name ); + } else { + handleUpdateMenu( navPostOrClassicMenu.id ); + } + setShouldFocusNavigationSelector( true ); + }, + [ convert, handleUpdateMenu ] + ); + + const resetToEmptyBlock = useCallback( () => { + registry.batch( () => { + setAttributes( { + ref: undefined, + } ); + if ( ! ref ) { + replaceInnerBlocks( clientId, [] ); + } + } ); + }, [ clientId, ref ] ); + + useEffect( () => { + hideNavigationMenuStatusNotice(); + + if ( isCreatingNavigationMenu ) { + speak( __( `Creating Navigation Menu.` ) ); + } + + if ( createNavigationMenuIsSuccess ) { + setRef( createNavigationMenuPost.id ); + selectBlock( clientId ); + + showNavigationMenuStatusNotice( + __( `Navigation Menu successfully created.` ) + ); + } + + if ( createNavigationMenuIsError ) { + showNavigationMenuStatusNotice( + __( 'Failed to create Navigation Menu.' ) + ); + } + }, [ + createNavigationMenu, + createNavigationMenuStatus, + createNavigationMenuError, + createNavigationMenuPost, + ] ); + + // Attempt to retrieve and prioritize any existing navigation menu unless + // a specific ref is allocated or the user is explicitly creating a new menu. The aim is + // for the block to "just work" from a user perspective using existing data. + useEffect( () => { + if ( + isCreatingNavigationMenu || + ref || + ! navigationMenus?.length || + navigationMenus?.length > 1 + ) { + return; + } + + setRef( navigationMenus[ 0 ].id ); + }, [ navigationMenus ] ); + + useEffect( () => { + hideClassicMenuConversionNotice(); + if ( classicMenuConversionStatus === CLASSIC_MENU_CONVERSION_PENDING ) { + speak( __( 'Classic menu importing.' ) ); + } + + if ( + classicMenuConversionStatus === CLASSIC_MENU_CONVERSION_SUCCESS && + classicMenuConversionResult + ) { + handleUpdateMenu( classicMenuConversionResult?.id ); + showClassicMenuConversionNotice( + __( 'Classic menu imported successfully.' ) + ); + speak( __( 'Classic menu imported successfully.' ) ); + } + + if ( classicMenuConversionStatus === CLASSIC_MENU_CONVERSION_ERROR ) { + showClassicMenuConversionNotice( + __( 'Classic menu import failed.' ) + ); + speak( __( 'Classic menu import failed.' ) ); + } + }, [ + classicMenuConversionStatus, + classicMenuConversionResult, + classicMenuConversionError, + ] ); + + // Spacer block needs orientation from context. This is a patch until + // https://github.com/WordPress/gutenberg/issues/36197 is addressed. + useEffect( () => { + if ( orientation ) { + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( { orientation } ); + } + }, [ orientation ] ); + + useEffect( () => { + if ( ! enableContrastChecking ) { + return; + } + detectColors( + navRef.current, + setDetectedColor, + setDetectedBackgroundColor + ); + const subMenuElement = navRef.current?.querySelector( + '[data-type="core/navigation-link"] [data-type="core/navigation-link"]' + ); + if ( subMenuElement ) { + detectColors( + subMenuElement, + setDetectedOverlayColor, + setDetectedOverlayBackgroundColor + ); + } + } ); + + useEffect( () => { + if ( ! isSelected && ! isInnerBlockSelected ) { + hideNavigationMenuPermissionsNotice(); + } + + if ( isSelected || isInnerBlockSelected ) { + if ( + ref && + hasResolvedCanUserUpdateNavigationMenu && + ! canUserUpdateNavigationMenu + ) { + showNavigationMenuPermissionsNotice( + __( + 'You do not have permission to edit this Menu. Any changes made will not be saved.' + ) + ); + } + + if ( + ! ref && + hasResolvedCanUserCreateNavigationMenu && + ! canUserCreateNavigationMenu + ) { + showNavigationMenuPermissionsNotice( + __( + 'You do not have permission to create Navigation Menus.' + ) + ); + } + } + }, [ + isSelected, + isInnerBlockSelected, + canUserUpdateNavigationMenu, + hasResolvedCanUserUpdateNavigationMenu, + canUserCreateNavigationMenu, + hasResolvedCanUserCreateNavigationMenu, + ref, + ] ); + + // Focus support after menu selection. + useEffect( () => { + if ( + isDraftNavigationMenu || + ! isEntityAvailable || + ! shouldFocusNavigationSelector + ) { + return; + } + navigationSelectorRef?.current?.focus(); + setShouldFocusNavigationSelector( false ); + }, [ + isDraftNavigationMenu, + isEntityAvailable, + shouldFocusNavigationSelector, + ] ); + if ( hasUnsavedBlocks ) { return ( @@ -678,10 +777,6 @@ function Navigation( { ); } - const PlaceholderComponent = CustomPlaceholder - ? CustomPlaceholder - : Placeholder; - if ( isPlaceholder ) { return ( diff --git a/packages/block-library/src/navigation/edit/use-notices.js b/packages/block-library/src/navigation/edit/use-notices.js new file mode 100644 index 00000000000000..e69de29bb2d1d6