diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 38ad3e2e11bd13..a737007d4f729e 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -461,7 +461,7 @@ A collection of blocks that allow visitors to get around your site. ([Source](ht - **Name:** core/navigation - **Category:** theme - **Supports:** align (full, wide), ariaLabel, inserter, interactivity, layout (allowSizingOnChildren, default, ~~allowInheriting~~, ~~allowSwitching~~, ~~allowVerticalAlignment~~), spacing (blockGap, units), typography (fontSize, lineHeight), ~~html~~, ~~renaming~~ -- **Attributes:** __unstableLocation, backgroundColor, customBackgroundColor, customOverlayBackgroundColor, customOverlayTextColor, customTextColor, hasIcon, icon, maxNestingLevel, openSubmenusOnClick, overlayBackgroundColor, overlayMenu, overlayTextColor, ref, rgbBackgroundColor, rgbTextColor, showSubmenuIcon, templateLock, textColor +- **Attributes:** __unstableLocation, backgroundColor, customBackgroundColor, customOverlayBackgroundColor, customOverlayTextColor, customTextColor, hasIcon, icon, maxNestingLevel, openSubmenusOnClick, overlayBackgroundColor, overlayId, overlayMenu, overlayTextColor, ref, rgbBackgroundColor, rgbTextColor, showSubmenuIcon, templateLock, textColor ## Custom Link @@ -473,6 +473,15 @@ Add a page, link, or another item to your navigation. ([Source](https://github.c - **Supports:** interactivity (clientNavigation), typography (fontSize, lineHeight), ~~html~~, ~~renaming~~, ~~reusable~~ - **Attributes:** description, id, isTopLevelLink, kind, label, opensInNewTab, rel, title, type, url +## Navigation Overlay Close + +Add a Close button to your Navigation Overlay. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/navigation-overlay-close)) + +- **Name:** core/navigation-overlay-close +- **Category:** design +- **Supports:** anchor, color (background, text, ~~link~~), dimensions (height, width), interactivity, spacing (margin, padding, units), ~~html~~, ~~multiple~~, ~~reusable~~ +- **Attributes:** hasIcon + ## Submenu Add a submenu to your navigation. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/navigation-submenu)) diff --git a/lib/blocks.php b/lib/blocks.php index e1d4622a0f23da..53e86882da85ae 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -81,6 +81,7 @@ function gutenberg_reregister_core_block_types() { 'navigation.php' => 'core/navigation', 'navigation-link.php' => 'core/navigation-link', 'navigation-submenu.php' => 'core/navigation-submenu', + 'navigation-overlay-close.php' => 'core/navigation-overlay-close', 'page-list.php' => 'core/page-list', 'page-list-item.php' => 'core/page-list-item', 'pattern.php' => 'core/pattern', diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index fc67f2c9d43770..4ccd98256b98ec 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -77,3 +77,68 @@ function wp_enqueue_block_view_script( $block_name, $args ) { add_filter( 'render_block', $callback, 10, 2 ); } } + +function add_navigation_overlay_area( $areas ) { + $areas[] = array( + 'area' => 'navigation-overlay', + 'label' => _x( 'Navigation Overlay', 'template part area' ), + 'description' => __( + 'An area for navigation overlay content.' + ), + 'area_tag' => 'section', + 'icon' => 'handle', + ); + return $areas; +} +add_filter( 'default_wp_template_part_areas', 'add_navigation_overlay_area', 10, 1 ); + +function add_default_navigation_overlay_template_part( $block_template, $id, $template_type ) { + + // if the template type is not template part, return the block template + if ( 'wp_template_part' !== $template_type ) { + return $block_template; + } + + // If its not the "Core" Navigation Overlay, return the block template. + if ( $id !== 'core//navigation-overlay' ) { + return $block_template; + } + + // If the block template is not empty, return the "found" block template. + // Failure to do this will override any "found" overlay template part from the Theme. + if ( ! empty( $block_template ) ) { + return $block_template; + } + + // Return a default template part for the Navigation Overlay. + // This is essentially a "Core" fallback in case the Theme does not provide one. + $template = new WP_Block_Template(); + + // TODO: should we provide "$theme" here at all as this is a "Core" template. + $template->id = 'core' . '//' . 'navigation-overlay'; + $template->theme = 'core'; + $template->slug = 'navigation-overlay'; + $template->source = 'custom'; + $template->type = 'wp_template_part'; + $template->title = 'Navigation Overlay'; + $template->status = 'publish'; + $template->has_theme_file = null; + $template->is_custom = false; + $template->modified = null; + $template->origin = null; + $template->author = null; + + // Set the area to match the Navigation Overlay area. + $template->area = 'navigation-overlay'; + + // The content is the default Navigation Overlay template part. This will only be used + // if the Theme does not provide a template part for the Navigation Overlay. + // PHP is used here to allow for translation of the default template part. + ob_start(); + include __DIR__ . '/navigation-overlay.php'; + $template->content = ob_get_clean(); + + return $template; +} + +add_filter( 'get_block_file_template', 'add_default_navigation_overlay_template_part', 10, 3 ); diff --git a/lib/experimental/navigation-overlay.php b/lib/experimental/navigation-overlay.php new file mode 100644 index 00000000000000..c41a7541789100 --- /dev/null +++ b/lib/experimental/navigation-overlay.php @@ -0,0 +1,9 @@ + +
+
+ + + +
+
+ diff --git a/package-lock.json b/package-lock.json index 9ca357997a7f5a..46982666d7d4a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54473,6 +54473,7 @@ "@wordpress/private-apis": "file:../private-apis", "@wordpress/reusable-blocks": "file:../reusable-blocks", "@wordpress/rich-text": "file:../rich-text", + "@wordpress/router": "file:../router", "@wordpress/server-side-render": "file:../server-side-render", "@wordpress/url": "file:../url", "@wordpress/viewport": "file:../viewport", @@ -69818,6 +69819,7 @@ "@wordpress/private-apis": "file:../private-apis", "@wordpress/reusable-blocks": "file:../reusable-blocks", "@wordpress/rich-text": "file:../rich-text", + "@wordpress/router": "file:../router", "@wordpress/server-side-render": "file:../server-side-render", "@wordpress/url": "file:../url", "@wordpress/viewport": "file:../viewport", diff --git a/packages/block-library/package.json b/packages/block-library/package.json index b27704fcd52fb7..ff0d4443b9cece 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -59,6 +59,7 @@ "@wordpress/private-apis": "file:../private-apis", "@wordpress/reusable-blocks": "file:../reusable-blocks", "@wordpress/rich-text": "file:../rich-text", + "@wordpress/router": "file:../router", "@wordpress/server-side-render": "file:../server-side-render", "@wordpress/url": "file:../url", "@wordpress/viewport": "file:../viewport", diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index e2e0fd9e414ef3..365a9f7805640f 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -69,6 +69,7 @@ import * as more from './more'; import * as navigation from './navigation'; import * as navigationLink from './navigation-link'; import * as navigationSubmenu from './navigation-submenu'; +import * as navigationOverlayClose from './navigation-overlay-close'; import * as nextpage from './nextpage'; import * as pattern from './pattern'; import * as pageList from './page-list'; @@ -186,6 +187,7 @@ const getAllBlocks = () => { navigation, navigationLink, navigationSubmenu, + navigationOverlayClose, siteLogo, siteTitle, siteTagline, diff --git a/packages/block-library/src/navigation-overlay-close/block.json b/packages/block-library/src/navigation-overlay-close/block.json new file mode 100644 index 00000000000000..8df3fcc3aa74b8 --- /dev/null +++ b/packages/block-library/src/navigation-overlay-close/block.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "core/navigation-overlay-close", + "title": "Navigation Overlay Close", + "category": "design", + "description": "Add a Close button to your Navigation Overlay.", + "textdomain": "default", + "icon": "dismiss", + "attributes": { + "hasIcon": { + "type": "boolean", + "default": true + } + }, + "usesContext": [ + "textColor", + "customTextColor", + "backgroundColor", + "customBackgroundColor" + ], + "supports": { + "multiple": false, + "reusable": false, + "html": false, + "dimensions": { + "width": true, + "height": true + }, + "color": { + "link": false, + "text": true, + "background": true + }, + "interactivity": true, + "anchor": true, + "spacing": { + "margin": true, + "padding": true, + "units": [ "px", "em", "rem", "vh", "vw" ], + "__experimentalDefaultControls": { + "margin": true, + "padding": true + } + } + }, + "editorStyle": "wp-block-navigation-overlay-close-editor", + "style": "wp-block-navigation-overlay-close" +} diff --git a/packages/block-library/src/navigation-overlay-close/edit.js b/packages/block-library/src/navigation-overlay-close/edit.js new file mode 100644 index 00000000000000..72e09f77f17366 --- /dev/null +++ b/packages/block-library/src/navigation-overlay-close/edit.js @@ -0,0 +1,55 @@ +/** + * WordPress dependencies + */ +import { Button, Icon, ToggleControl, PanelBody } from '@wordpress/components'; +import { close } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; +import { useBlockProps, InspectorControls } from '@wordpress/block-editor'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; + +/** + * Internal dependencies + */ +import { unlock } from '../lock-unlock'; + +const { useHistory } = unlock( routerPrivateApis ); + +export default function Edit( { attributes, isSelected, setAttributes } ) { + const blockProps = useBlockProps(); + const history = useHistory(); + const { hasIcon } = attributes; + + const closeText = __( 'Close' ); + + const onClick = () => { + if ( isSelected ) { + // Exit navigation overlay edit mode. + history.back(); + } + }; + + blockProps.onClick = onClick; + + return ( + <> + + + + setAttributes( { hasIcon: value } ) + } + checked={ hasIcon } + /> + + + + + ); +} diff --git a/packages/block-library/src/navigation-overlay-close/index.js b/packages/block-library/src/navigation-overlay-close/index.js new file mode 100644 index 00000000000000..0a048e1e6409da --- /dev/null +++ b/packages/block-library/src/navigation-overlay-close/index.js @@ -0,0 +1,16 @@ +/** + * Internal dependencies + */ +import initBlock from '../utils/init-block'; +import metadata from './block.json'; +import edit from './edit'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + edit, +}; + +export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/navigation-overlay-close/index.php b/packages/block-library/src/navigation-overlay-close/index.php new file mode 100644 index 00000000000000..8ebf367ba14f5b --- /dev/null +++ b/packages/block-library/src/navigation-overlay-close/index.php @@ -0,0 +1,48 @@ +'; + + $hasIcon = ! empty( $attributes['hasIcon'] ); + + $wrapper_attributes = get_block_wrapper_attributes( + array_filter( // Removes any empty attributes. + // Attributes + array( + // This directive is duplicated in the Navigation Block itself. + // See WP_Navigation_Block_Renderer::get_responsive_container_markup(). + // Changes to this directive should be reflected there as well. + 'data-wp-on--click' => 'actions.closeMenuOnClick', + 'aria-label' => $hasIcon ? __( 'Close menu' ) : false, + ) + ) + ); + + $content = $hasIcon ? $close_icon : __( 'Close menu' ); + + return sprintf( + '', + $wrapper_attributes, + $content, + ); + +} + + +/** + * Registers the `core/navigation-overlay-close` block on server. + */ +function register_block_core_navigation_overlay_close() { + register_block_type_from_metadata( + __DIR__ . '/navigation-overlay-close', + array( + 'render_callback' => 'render_block_core_navigation_overlay_close', + ) + ); +} +add_action( 'init', 'register_block_core_navigation_overlay_close' ); diff --git a/packages/block-library/src/navigation-overlay-close/init.js b/packages/block-library/src/navigation-overlay-close/init.js new file mode 100644 index 00000000000000..79f0492c2cb2f8 --- /dev/null +++ b/packages/block-library/src/navigation-overlay-close/init.js @@ -0,0 +1,6 @@ +/** + * Internal dependencies + */ +import { init } from './'; + +export default init(); diff --git a/packages/block-library/src/navigation-overlay-close/style.scss b/packages/block-library/src/navigation-overlay-close/style.scss new file mode 100644 index 00000000000000..acfd3bc9f2b9fc --- /dev/null +++ b/packages/block-library/src/navigation-overlay-close/style.scss @@ -0,0 +1,34 @@ + +// Size of burger and close icons. +$navigation-icon-size: 24px; + +// Menu and close buttons. +.wp-block-navigation-overlay-close { + height: auto; // remove default height applied to button component + vertical-align: middle; + cursor: pointer; + border: none; + margin: 0; + padding: 0; + text-transform: inherit; + z-index: 2; // Needs to be above the modal z index itself. + background-color: inherit; // remove user agent stylesheet default. + color: inherit; // remove user agent stylesheet default. + + // When set to collapse into a text button, it should inherit the parent font. + // This needs specificity to override inherited properties by the button element and component. + &.wp-block-navigation-overlay-close { + font-family: inherit; + font-weight: inherit; + font-size: inherit; + } + + svg { + fill: currentColor; + pointer-events: none; + display: block; + width: $navigation-icon-size; + height: $navigation-icon-size; + } +} + diff --git a/packages/block-library/src/navigation/block.json b/packages/block-library/src/navigation/block.json index eef6af390de78a..81086dfd417639 100644 --- a/packages/block-library/src/navigation/block.json +++ b/packages/block-library/src/navigation/block.json @@ -22,7 +22,7 @@ "textdomain": "default", "attributes": { "ref": { - "type": "number" + "type": [ "number", "string" ] }, "textColor": { "type": "string" @@ -84,6 +84,9 @@ "templateLock": { "type": [ "string", "boolean" ], "enum": [ "all", "insert", "contentOnly", false ] + }, + "overlayId": { + "type": "string" } }, "providesContext": { diff --git a/packages/block-library/src/navigation/constants.js b/packages/block-library/src/navigation/constants.js index 154c490e83839b..9a3baf9fbb67c6 100644 --- a/packages/block-library/src/navigation/constants.js +++ b/packages/block-library/src/navigation/constants.js @@ -7,6 +7,8 @@ export const PRIORITIZED_INSERTER_BLOCKS = [ 'core/navigation-link', ]; +export const NAVIGATION_OVERLAY_TEMPLATE_PART_AREA = 'navigation-overlay'; + // These parameters must be kept aligned with those in // lib/compat/wordpress-6.3/navigation-block-preloading.php // and diff --git a/packages/block-library/src/navigation/edit/edit-overlay-button.js b/packages/block-library/src/navigation/edit/edit-overlay-button.js new file mode 100644 index 00000000000000..2043e6246527ca --- /dev/null +++ b/packages/block-library/src/navigation/edit/edit-overlay-button.js @@ -0,0 +1,172 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + Button, + MenuGroup, + MenuItem, + MenuItemsChoice, + DropdownMenu, +} from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { store as coreStore } from '@wordpress/core-data'; +import { parse, serialize } from '@wordpress/blocks'; +import { moreVertical } from '@wordpress/icons'; +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; +import useGoToOverlayEditor from './use-go-to-overlay-editor'; +import useOverlay from './use-overlay'; + +const { useHistory } = unlock( routerPrivateApis ); + +export default function EditOverlayButton( { + navRef, + attributes, + setAttributes, +} ) { + const currentOverlayId = attributes?.overlayId; + + // Get any custom overlay attached to this block, + // falling back to the one provided by the Theme. + const overlay = useOverlay( currentOverlayId ); + + const { coreOverlay, allOverlays } = useSelect( ( select ) => { + return { + // Get the default template part that core provides. + coreOverlay: select( coreStore ).getEntityRecord( + 'postType', + 'wp_template_part', + `core//navigation-overlay` + ), + // Get all the overlays. + allOverlays: select( coreStore ).getEntityRecords( + 'postType', + 'wp_template_part', + { + area: 'navigation-overlay', + } + ), + }; + }, [] ); + + const { saveEntityRecord } = useDispatch( coreStore ); + + const history = useHistory(); + + const goToOverlayEditor = useGoToOverlayEditor(); + + async function handleEditOverlay( event ) { + event.preventDefault(); + + // There may already be an overlay with the slug `navigation-overlay`. + // This might be a user created one, or one provided by the theme. + // If so, then go directly to the editor for that overlay template part. + if ( overlay ) { + goToOverlayEditor( overlay.id, navRef ); + return; + } + + // If there is not overlay then create one using the base template part + // provided by Core. + // TODO: catch and handle errors. + const overlayBlocks = buildOverlayBlocks( coreOverlay.content.raw ); + + // The new overlay should use the current Theme's slug. + const newOverlay = await createOverlay( overlayBlocks ); + + goToOverlayEditor( newOverlay?.id, navRef ); + } + + async function handleCreateNewOverlay() { + const overlayBlocks = buildOverlayBlocks( overlay.content.raw ); + + const newOverlay = await createOverlay( overlayBlocks ); + + setAttributes( { + overlayId: newOverlay?.id, + } ); + + goToOverlayEditor( newOverlay?.id, navRef ); + } + + function buildOverlayBlocks( content ) { + const parsedBlocks = parse( content ); + return parsedBlocks; + } + + async function createOverlay( overlayBlocks ) { + return await saveEntityRecord( + 'postType', + 'wp_template_part', + { + slug: `navigation-overlay`, // `theme//` prefix is appended automatically. + title: `Navigation Overlay`, + content: serialize( overlayBlocks ), + area: 'navigation-overlay', + }, + { throwOnError: true } + ); + } + + // Map the overlay records to format + const overlayChoices = allOverlays?.map( ( overlayRecord ) => { + return { + label: overlayRecord.title.rendered, // decodeEntities required + value: overlayRecord.id, + }; + } ); + + if ( ! history || ( ! coreOverlay && ! overlay ) ) { + return null; + } + + return ( + <> + + + { () => ( + <> + + { + setAttributes( { + overlayId: newOverlayId, + } ); + } } + choices={ overlayChoices } + disabled={ overlayChoices?.length === 0 } + /> + + + + { + event.preventDefault(); + handleCreateNewOverlay(); + } } + > + { __( 'Create new overlay' ) } + + + + ) } + + + ); +} diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 3cacd814119e6f..0bdf01b27f891b 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -35,6 +35,7 @@ import { ToggleControl, __experimentalToggleGroupControl as ToggleGroupControl, __experimentalToggleGroupControlOption as ToggleGroupControlOption, + __experimentalHStack as HStack, Button, Spinner, Notice, @@ -43,6 +44,7 @@ import { __, sprintf } from '@wordpress/i18n'; import { speak } from '@wordpress/a11y'; import { close, Icon } from '@wordpress/icons'; import { useInstanceId } from '@wordpress/compose'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; /** * Internal dependencies @@ -72,6 +74,20 @@ import DeletedNavigationWarning from './deleted-navigation-warning'; import AccessibleDescription from './accessible-description'; import AccessibleMenuDescription from './accessible-menu-description'; import { unlock } from '../../lock-unlock'; +import EditOverlayButton from './edit-overlay-button'; +import useIsWithinOverlay from './use-is-within-overlay'; +import useGoToOverlayEditor from './use-go-to-overlay-editor'; +import useOverlay from './use-overlay'; + +const { useLocation } = unlock( routerPrivateApis ); + +function useInheritedRef() { + const { + params: { myNavRef }, + } = useLocation(); + + return myNavRef; +} function Navigation( { attributes, @@ -108,7 +124,13 @@ function Navigation( { icon = 'handle', } = attributes; - const ref = attributes.ref; + const [ tempRef, setTempRef ] = useState( null ); + + const ref = attributes.ref || tempRef; + + const inheritedRef = useInheritedRef(); + + const isInheritRefMode = !! inheritedRef; const setRef = useCallback( ( postId ) => { @@ -122,6 +144,15 @@ function Navigation( { const blockEditingMode = useBlockEditingMode(); + const isInsideOverlay = useIsWithinOverlay(); + + const showOverlayControls = ! isInsideOverlay; + + const customOverlay = useOverlay( attributes?.overlayId ); + const goToOverlayEditor = useGoToOverlayEditor(); + + const hasCustomOverlay = !! customOverlay; + // Preload classic menus, so that they don't suddenly pop-in when viewing // the Select Menu dropdown. const { menus: classicMenus } = useNavigationEntities(); @@ -233,11 +264,21 @@ function Navigation( { : null; useEffect( () => { + // Todo: set the ref based on context. + if ( isInheritRefMode ) { + setTempRef( inheritedRef ); + return; + } + // If: // - there is an existing menu, OR // - there are existing (uncontrolled) inner blocks // ...then don't request a fallback menu. - if ( ref || hasUnsavedBlocks || ! navigationFallbackId ) { + if ( + ( ref && ! isInheritRefMode ) || + hasUnsavedBlocks || + ! navigationFallbackId + ) { return; } @@ -255,6 +296,8 @@ function Navigation( { hasUnsavedBlocks, navigationFallbackId, __unstableMarkNextChangeAsNotPersistent, + isInheritRefMode, + inheritedRef, ] ); const navRef = useRef(); @@ -358,6 +401,17 @@ function Navigation( { handleUpdateMenu( menuId ); }; + const onToggleOverlayMenu = ( _toggleVal ) => { + if ( hasCustomOverlay && _toggleVal ) { + // If there is a Custom Overlay and the user is trying to open the menu + // then edit the overlay template part. + goToOverlayEditor( customOverlay?.id, ref ); + } else { + // Otherwise just toggle the default overlay witin the editor. + setResponsiveMenuVisibility( _toggleVal ); + } + }; + useEffect( () => { hideNavigationMenuStatusNotice(); @@ -528,7 +582,7 @@ function Navigation( { { hasSubmenuIndicatorSetting && ( - { isResponsive && ( + { isResponsive && showOverlayControls && ( <>