From e2af0b25ea0d55c4081fc7fd7e2bda7bd3e3d9a5 Mon Sep 17 00:00:00 2001 From: Kerry Liu Date: Mon, 28 Mar 2022 06:03:24 -0700 Subject: [PATCH] Navigation: add dedicated sidebar for managing layout of navigation menus (#39290) * Navigation: try adding dedicated sidebar for managing layout of navigation menus * Fix table width to avoid overflow * Iterate on var naming and code comments * Further naming refinement * More var renaming * Rename panel title for clarity * Improve a11y of component * Revert addition of fixed table layout. Introduces too many visual bugs * Wrap items to avoid off screen movers where possible * Move additional padding * Revert id on ListView This is now handled in https://github.com/WordPress/gutenberg/pull/39494 * Use standard getEntityRecords and remove custom entity selector Now moved to https://github.com/WordPress/gutenberg/pull/39498 * Add handling for no data or limited data * Collapse all Nav nodes by default * Only show empty message when there are no menus * Guard against editing unpublished Menus * Select Gallery block in list view correctly * Remove rebase artifact * Add empty message in case of empty menu Co-authored-by: Dave Smith --- .../block-toolbar/block-name-context.js | 8 + .../block-toolbar/block-toolbar-last-item.js | 12 + .../src/components/block-toolbar/index.js | 7 + packages/block-editor/src/components/index.js | 2 + .../src/navigation/edit/index.js | 7 - .../navigation/edit/use-list-view-modal.js | 72 ------ .../site-editor/style-variations.test.js | 2 +- .../src/components/block-editor/index.js | 29 +++ .../edit-site/src/components/sidebar/index.js | 2 + .../sidebar/navigation-menu-sidebar/index.js | 34 +++ .../navigation-inspector.js | 224 ++++++++++++++++++ .../navigation-menu.js | 63 +++++ .../navigation-menu-sidebar/style.scss | 42 ++++ packages/edit-site/src/style.scss | 1 + 14 files changed, 425 insertions(+), 80 deletions(-) create mode 100644 packages/block-editor/src/components/block-toolbar/block-name-context.js create mode 100644 packages/block-editor/src/components/block-toolbar/block-toolbar-last-item.js delete mode 100644 packages/block-library/src/navigation/edit/use-list-view-modal.js create mode 100644 packages/edit-site/src/components/sidebar/navigation-menu-sidebar/index.js create mode 100644 packages/edit-site/src/components/sidebar/navigation-menu-sidebar/navigation-inspector.js create mode 100644 packages/edit-site/src/components/sidebar/navigation-menu-sidebar/navigation-menu.js create mode 100644 packages/edit-site/src/components/sidebar/navigation-menu-sidebar/style.scss diff --git a/packages/block-editor/src/components/block-toolbar/block-name-context.js b/packages/block-editor/src/components/block-toolbar/block-name-context.js new file mode 100644 index 00000000000000..4537f6f861559d --- /dev/null +++ b/packages/block-editor/src/components/block-toolbar/block-name-context.js @@ -0,0 +1,8 @@ +/** + * WordPress dependencies + */ +import { createContext } from '@wordpress/element'; + +const __unstableBlockNameContext = createContext( '' ); + +export default __unstableBlockNameContext; diff --git a/packages/block-editor/src/components/block-toolbar/block-toolbar-last-item.js b/packages/block-editor/src/components/block-toolbar/block-toolbar-last-item.js new file mode 100644 index 00000000000000..e8a81049108a0d --- /dev/null +++ b/packages/block-editor/src/components/block-toolbar/block-toolbar-last-item.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { createSlotFill } from '@wordpress/components'; + +const { Fill: __unstableBlockToolbarLastItem, Slot } = createSlotFill( + '__unstableBlockToolbarLastItem' +); + +__unstableBlockToolbarLastItem.Slot = Slot; + +export default __unstableBlockToolbarLastItem; diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 7a2a078541a0de..58f6b8fd51b954 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -19,11 +19,13 @@ import BlockMover from '../block-mover'; import BlockParentSelector from '../block-parent-selector'; import BlockSwitcher from '../block-switcher'; import BlockControls from '../block-controls'; +import __unstableBlockToolbarLastItem from './block-toolbar-last-item'; import BlockSettingsMenu from '../block-settings-menu'; import { BlockLockToolbar } from '../block-lock'; import { BlockGroupToolbar } from '../convert-to-group-buttons'; import { useShowMoversGestures } from './utils'; import { store as blockEditorStore } from '../../store'; +import __unstableBlockNameContext from './block-name-context'; export default function BlockToolbar( { hideDragHandle } ) { const { @@ -150,6 +152,11 @@ export default function BlockToolbar( { hideDragHandle } ) { group="other" className="block-editor-block-toolbar__slot" /> + <__unstableBlockNameContext.Provider + value={ blockType?.name } + > + <__unstableBlockToolbarLastItem.Slot /> + ) } diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index a3a9b9f061edf2..8e6395f5b5d70a 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -99,6 +99,8 @@ export { default as withColorContext } from './color-palette/with-color-context' */ export { default as __unstableBlockSettingsMenuFirstItem } from './block-settings-menu/block-settings-menu-first-item'; +export { default as __unstableBlockToolbarLastItem } from './block-toolbar/block-toolbar-last-item'; +export { default as __unstableBlockNameContext } from './block-toolbar/block-name-context'; export { default as __unstableInserterMenuExtension } from './inserter-menu-extension'; export { default as __experimentalPreviewOptions } from './preview-options'; export { default as __experimentalUseResizeCanvas } from './use-resize-canvas'; diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index b9c36518e7cc71..b9d3a9b11caf17 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -44,7 +44,6 @@ import { speak } from '@wordpress/a11y'; /** * Internal dependencies */ -import useListViewModal from './use-list-view-modal'; import useNavigationMenu from '../use-navigation-menu'; import useNavigationEntities from '../use-navigation-entities'; import Placeholder from './placeholder'; @@ -287,10 +286,6 @@ function Navigation( { const navRef = useRef(); const isDraftNavigationMenu = navigationMenu?.status === 'draft'; - const { listViewToolbarButton, listViewModal } = useListViewModal( - clientId - ); - const { convert, status: classicMenuConversionStatus, @@ -650,9 +645,7 @@ function Navigation( { /> ) } - { listViewToolbarButton } - { listViewModal } { hasSubmenuIndicatorSetting && ( diff --git a/packages/block-library/src/navigation/edit/use-list-view-modal.js b/packages/block-library/src/navigation/edit/use-list-view-modal.js deleted file mode 100644 index 51b09e52875e77..00000000000000 --- a/packages/block-library/src/navigation/edit/use-list-view-modal.js +++ /dev/null @@ -1,72 +0,0 @@ -/** - * WordPress dependencies - */ -import { - __experimentalListView as ListView, - store as blockEditorStore, -} from '@wordpress/block-editor'; -import { ToolbarButton, Modal } from '@wordpress/components'; -import { useSelect } from '@wordpress/data'; -import { useRef, useEffect, useState } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { listView } from '@wordpress/icons'; - -function NavigationBlockListView( { clientId, __experimentalFeatures } ) { - const blocks = useSelect( - ( select ) => - select( blockEditorStore ).__unstableGetClientIdsTree( clientId ), - [ clientId ] - ); - - const listViewRef = useRef(); - const [ minHeight, setMinHeight ] = useState( 300 ); - useEffect( () => { - setMinHeight( listViewRef?.current?.clientHeight ?? 300 ); - }, [] ); - - return ( -
- -
- ); -} - -export default function useListViewModal( clientId, __experimentalFeatures ) { - const [ isModalOpen, setIsModalOpen ] = useState( false ); - - const listViewToolbarButton = ( - setIsModalOpen( true ) } - icon={ listView } - /> - ); - - const listViewModal = isModalOpen && ( - { - setIsModalOpen( false ); - } } - shouldCloseOnClickOutside={ false } - > - - - ); - - return { - listViewToolbarButton, - listViewModal, - }; -} diff --git a/packages/e2e-tests/specs/site-editor/style-variations.test.js b/packages/e2e-tests/specs/site-editor/style-variations.test.js index c32939d283fdef..16f50fdf9154c5 100644 --- a/packages/e2e-tests/specs/site-editor/style-variations.test.js +++ b/packages/e2e-tests/specs/site-editor/style-variations.test.js @@ -73,7 +73,7 @@ async function getCustomFontSizeValue() { async function getColorValue( colorType ) { return page.evaluate( ( _colorType ) => { return document.evaluate( - `substring-before(substring-after(//div[@aria-label="Settings"]//button[.//*[text()="${ _colorType }"]]//*[contains(@class,"component-color-indicator")]/@style, "background: "), ";")`, + `substring-before(substring-after(//div[contains(@class, "edit-site-global-styles-sidebar__panel")]//button[.//*[text()="${ _colorType }"]]//*[contains(@class,"component-color-indicator")]/@style, "background: "), ";")`, document, null, XPathResult.ANY_TYPE, diff --git a/packages/edit-site/src/components/block-editor/index.js b/packages/edit-site/src/components/block-editor/index.js index d0e390165fd7d5..ffac252f3caaa3 100644 --- a/packages/edit-site/src/components/block-editor/index.js +++ b/packages/edit-site/src/components/block-editor/index.js @@ -15,13 +15,19 @@ import { __experimentalLinkControl, BlockInspector, BlockTools, + __unstableBlockToolbarLastItem, __unstableBlockSettingsMenuFirstItem, __unstableUseTypingObserver as useTypingObserver, BlockEditorKeyboardShortcuts, store as blockEditorStore, + __unstableBlockNameContext, } from '@wordpress/block-editor'; import { useMergeRefs, useViewportMatch } from '@wordpress/compose'; import { ReusableBlocksMenuItems } from '@wordpress/reusable-blocks'; +import { listView } from '@wordpress/icons'; +import { ToolbarButton, ToolbarGroup } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { store as interfaceStore } from '@wordpress/interface'; /** * Internal dependencies @@ -93,6 +99,13 @@ export default function BlockEditor( { setIsInserterOpen } ) { templateType ); const { setPage } = useDispatch( editSiteStore ); + const { enableComplementaryArea } = useDispatch( interfaceStore ); + const openNavigationSidebar = useCallback( () => { + enableComplementaryArea( + 'core/edit-site', + 'edit-site/navigation-menu' + ); + }, [ enableComplementaryArea ] ); const contentRef = useRef(); const mergedRefs = useMergeRefs( [ contentRef, useTypingObserver() ] ); const isMobileViewport = useViewportMatch( 'small', '<' ); @@ -161,6 +174,22 @@ export default function BlockEditor( { setIsInserterOpen } ) { ) } + <__unstableBlockToolbarLastItem> + <__unstableBlockNameContext.Consumer> + { ( blockName ) => + blockName === 'core/navigation' && ( + + + + ) + } + + diff --git a/packages/edit-site/src/components/sidebar/index.js b/packages/edit-site/src/components/sidebar/index.js index aa5eaacc28ab44..3788a3aece74bc 100644 --- a/packages/edit-site/src/components/sidebar/index.js +++ b/packages/edit-site/src/components/sidebar/index.js @@ -14,6 +14,7 @@ import { store as blockEditorStore } from '@wordpress/block-editor'; */ import DefaultSidebar from './default-sidebar'; import GlobalStylesSidebar from './global-styles-sidebar'; +import NavigationMenuSidebar from './navigation-menu-sidebar'; import { STORE_NAME } from '../../store/constants'; import SettingsHeader from './settings-header'; import TemplateCard from './template-card'; @@ -77,6 +78,7 @@ export function SidebarComplementaryAreaFills() { ) } + ); } diff --git a/packages/edit-site/src/components/sidebar/navigation-menu-sidebar/index.js b/packages/edit-site/src/components/sidebar/navigation-menu-sidebar/index.js new file mode 100644 index 00000000000000..1681cf4714852d --- /dev/null +++ b/packages/edit-site/src/components/sidebar/navigation-menu-sidebar/index.js @@ -0,0 +1,34 @@ +/** + * WordPress dependencies + */ +import { FlexBlock, Flex } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { navigation } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import DefaultSidebar from '../default-sidebar'; +import NavigationInspector from './navigation-inspector'; + +export default function NavigationMenuSidebar() { + return ( + + + { __( 'Navigation Menus' ) } + + + } + > + + + ); +} diff --git a/packages/edit-site/src/components/sidebar/navigation-menu-sidebar/navigation-inspector.js b/packages/edit-site/src/components/sidebar/navigation-menu-sidebar/navigation-inspector.js new file mode 100644 index 00000000000000..fa747bb6197421 --- /dev/null +++ b/packages/edit-site/src/components/sidebar/navigation-menu-sidebar/navigation-inspector.js @@ -0,0 +1,224 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { useState, useEffect } from '@wordpress/element'; +import { SelectControl } from '@wordpress/components'; +import { store as coreStore, useEntityBlockEditor } from '@wordpress/core-data'; +import { + store as blockEditorStore, + BlockEditorProvider, +} from '@wordpress/block-editor'; +import { speak } from '@wordpress/a11y'; +import { useInstanceId } from '@wordpress/compose'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import NavigationMenu from './navigation-menu'; + +const NAVIGATION_MENUS_QUERY = [ { per_page: -1, status: 'publish' } ]; + +export default function NavigationInspector() { + const { + selectedNavigationBlockId, + clientIdToRef, + navigationMenus, + isResolvingNavigationMenus, + hasResolvedNavigationMenus, + firstNavigationBlockId, + } = useSelect( ( select ) => { + const { + __experimentalGetActiveBlockIdByBlockNames, + __experimentalGetGlobalBlocksByName, + getBlock, + } = select( blockEditorStore ); + + const { getEntityRecords, hasFinishedResolution, isResolving } = select( + coreStore + ); + + const navigationMenusQuery = [ + 'postType', + 'wp_navigation', + NAVIGATION_MENUS_QUERY[ 0 ], + ]; + + // Get the active Navigation block (if present). + const selectedNavId = __experimentalGetActiveBlockIdByBlockNames( + 'core/navigation' + ); + + // Get all Navigation blocks currently within the editor canvas. + const navBlockIds = __experimentalGetGlobalBlocksByName( + 'core/navigation' + ); + const idToRef = {}; + navBlockIds.forEach( ( id ) => { + idToRef[ id ] = getBlock( id )?.attributes?.ref; + } ); + return { + selectedNavigationBlockId: selectedNavId, + firstNavigationBlockId: navBlockIds?.[ 0 ], + clientIdToRef: idToRef, + navigationMenus: getEntityRecords( ...navigationMenusQuery ), + isResolvingNavigationMenus: isResolving( + 'getEntityRecords', + navigationMenusQuery + ), + hasResolvedNavigationMenus: hasFinishedResolution( + 'getEntityRecords', + navigationMenusQuery + ), + }; + }, [] ); + + const navMenuListId = useInstanceId( + NavigationMenu, + 'edit-site-navigation-inspector-menu' + ); + + const firstNavRefInTemplate = clientIdToRef[ firstNavigationBlockId ]; + const firstNavigationMenuRef = navigationMenus?.[ 0 ]?.id; + + // Default Navigation Menu is either: + // - the Navigation Menu referenced by the first Nav block within the template. + // - the first of the available Navigation Menus (`wp_navigation`) posts. + const defaultNavigationMenuId = + firstNavRefInTemplate || firstNavigationMenuRef; + + // The Navigation Menu manually selected by the user within the Nav inspector. + const [ currentMenuId, setCurrentMenuId ] = useState( + firstNavRefInTemplate + ); + + // If a Nav block is selected within the canvas then set the + // Navigation Menu referenced by it's `ref` attribute to be + // active within the Navigation sidebar. + useEffect( () => { + if ( selectedNavigationBlockId ) { + setCurrentMenuId( clientIdToRef[ selectedNavigationBlockId ] ); + } + }, [ selectedNavigationBlockId ] ); + + let options = []; + if ( navigationMenus ) { + options = navigationMenus.map( ( { id, title } ) => ( { + value: id, + label: title.rendered, + } ) ); + } + + const [ innerBlocks, onInput, onChange ] = useEntityBlockEditor( + 'postType', + 'wp_navigation', + { id: currentMenuId || defaultNavigationMenuId } + ); + + const { isLoadingInnerBlocks, hasLoadedInnerBlocks } = useSelect( + ( select ) => { + const { isResolving, hasFinishedResolution } = select( coreStore ); + return { + isLoadingInnerBlocks: isResolving( 'getEntityRecord', [ + 'postType', + 'wp_navigation', + currentMenuId || defaultNavigationMenuId, + ] ), + hasLoadedInnerBlocks: hasFinishedResolution( + 'getEntityRecord', + [ + 'postType', + 'wp_navigation', + currentMenuId || defaultNavigationMenuId, + ] + ), + }; + }, + [ currentMenuId, defaultNavigationMenuId ] + ); + + const isLoading = ! ( hasResolvedNavigationMenus && hasLoadedInnerBlocks ); + + const hasMoreThanOneNavigationMenu = navigationMenus?.length > 1; + + const hasNavigationMenus = !! navigationMenus?.length; + + // Entity block editor will return entities that are not currently published. + // Guard by only allowing their usage if there are published Nav Menus. + const publishedInnerBlocks = hasNavigationMenus ? innerBlocks : []; + + const hasInnerBlocks = !! publishedInnerBlocks?.length; + + useEffect( () => { + if ( isResolvingNavigationMenus ) { + speak( 'Loading Navigation sidebar menus.' ); + } + + if ( hasResolvedNavigationMenus ) { + speak( 'Navigation sidebar menus have loaded.' ); + } + }, [ isResolvingNavigationMenus, hasResolvedNavigationMenus ] ); + + useEffect( () => { + if ( isLoadingInnerBlocks ) { + speak( 'Loading Navigation sidebar selected menu items.' ); + } + + if ( hasLoadedInnerBlocks ) { + speak( 'Navigation sidebar selected menu items have loaded.' ); + } + }, [ isLoadingInnerBlocks, hasLoadedInnerBlocks ] ); + + return ( +
+ { hasResolvedNavigationMenus && ! hasNavigationMenus && ( +

+ { __( 'There are no Navigation Menus.' ) } +

+ ) } + + { ! hasResolvedNavigationMenus && ( +
+ ) } + { hasResolvedNavigationMenus && hasMoreThanOneNavigationMenu && ( + + setCurrentMenuId( Number( newMenuId ) ) + } + /> + ) } + { isLoading && ( + <> +
+
+
+ + ) } + { hasInnerBlocks && ! isLoading && ( + + + + ) } + + { ! hasInnerBlocks && ! isLoading && ( +

+ { __( 'Navigation Menu is empty.' ) } +

+ ) } +
+ ); +} diff --git a/packages/edit-site/src/components/sidebar/navigation-menu-sidebar/navigation-menu.js b/packages/edit-site/src/components/sidebar/navigation-menu-sidebar/navigation-menu.js new file mode 100644 index 00000000000000..f5601447dd318b --- /dev/null +++ b/packages/edit-site/src/components/sidebar/navigation-menu-sidebar/navigation-menu.js @@ -0,0 +1,63 @@ +/** + * WordPress dependencies + */ +import { + __experimentalListView as ListView, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { useEffect } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; + +const ALLOWED_BLOCKS = { + 'core/navigation': [ + 'core/navigation-link', + 'core/search', + 'core/social-links', + 'core/page-list', + 'core/spacer', + 'core/home-link', + 'core/site-title', + 'core/site-logo', + 'core/navigation-submenu', + ], + 'core/social-links': [ 'core/social-link' ], + 'core/navigation-submenu': [ + 'core/navigation-link', + 'core/navigation-submenu', + ], + 'core/navigation-link': [ + 'core/navigation-link', + 'core/navigation-submenu', + ], +}; + +export default function NavigationMenu( { innerBlocks, id } ) { + const { updateBlockListSettings } = useDispatch( blockEditorStore ); + + //TODO: Block settings are normally updated as a side effect of rendering InnerBlocks in BlockList + //Think through a better way of doing this, possible with adding allowed blocks to block library metadata + useEffect( () => { + updateBlockListSettings( '', { + allowedBlocks: ALLOWED_BLOCKS[ 'core/navigation' ], + } ); + innerBlocks.forEach( ( block ) => { + if ( ALLOWED_BLOCKS[ block.name ] ) { + updateBlockListSettings( block.clientId, { + allowedBlocks: ALLOWED_BLOCKS[ block.name ], + } ); + } + } ); + }, [ updateBlockListSettings, innerBlocks ] ); + return ( + <> + + + ); +} diff --git a/packages/edit-site/src/components/sidebar/navigation-menu-sidebar/style.scss b/packages/edit-site/src/components/sidebar/navigation-menu-sidebar/style.scss new file mode 100644 index 00000000000000..7b5e6ca281dece --- /dev/null +++ b/packages/edit-site/src/components/sidebar/navigation-menu-sidebar/style.scss @@ -0,0 +1,42 @@ +@keyframes loadingpulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0.5; + } + 100% { + opacity: 1; + } +} + +.edit-site-navigation-inspector { + padding: $grid-unit-20; + + .block-editor-list-view-leaf .block-editor-list-view-block-contents { + align-items: flex-start; + white-space: normal; + } + + .block-editor-list-view-block__title { + margin-top: 3px; + } + + .block-editor-list-view-block__menu-cell { + padding-right: 0; + } +} + + +.edit-site-navigation-inspector__placeholder { + padding: $grid-unit-10; + margin: $grid-unit-10; + background-color: $gray-100; + animation: loadingpulse 1s linear infinite; + animation-delay: 0.5s; // avoid animating for fast network responses + + &.is-child { + margin-left: $grid-unit-30; + width: 50%; + } +} diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 883ce1c0642159..b61d06574c7aa0 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -11,6 +11,7 @@ @import "./components/add-new-template/style.scss"; @import "./components/sidebar/style.scss"; @import "./components/sidebar/settings-header/style.scss"; +@import "./components/sidebar/navigation-menu-sidebar/style.scss"; @import "./components/sidebar/template-card/style.scss"; @import "./components/editor/style.scss"; @import "./components/template-details/style.scss";