From d40d3b2433e18ce839642d4dfd8750de164b2a87 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Mon, 12 Jul 2021 14:19:49 +1000 Subject: [PATCH 1/4] Add a Navigation Heading block. --- lib/blocks.php | 1 + packages/block-library/src/index.js | 2 + .../src/navigation-heading/block.json | 32 +++ .../src/navigation-heading/edit.js | 233 ++++++++++++++++++ .../src/navigation-heading/index.js | 25 ++ .../src/navigation-heading/index.php | 205 +++++++++++++++ .../src/navigation-heading/save.js | 8 + packages/block-library/src/navigation/edit.js | 1 + .../block-library/src/navigation/index.php | 4 +- .../block-library/src/navigation/style.scss | 3 +- .../blocks/core__navigation-heading.html | 2 + .../blocks/core__navigation-heading.json | 12 + .../core__navigation-heading.parsed.json | 13 + .../core__navigation-heading.serialized.html | 1 + 14 files changed, 539 insertions(+), 3 deletions(-) create mode 100644 packages/block-library/src/navigation-heading/block.json create mode 100644 packages/block-library/src/navigation-heading/edit.js create mode 100644 packages/block-library/src/navigation-heading/index.js create mode 100644 packages/block-library/src/navigation-heading/index.php create mode 100644 packages/block-library/src/navigation-heading/save.js create mode 100644 packages/e2e-tests/fixtures/blocks/core__navigation-heading.html create mode 100644 packages/e2e-tests/fixtures/blocks/core__navigation-heading.json create mode 100644 packages/e2e-tests/fixtures/blocks/core__navigation-heading.parsed.json create mode 100644 packages/e2e-tests/fixtures/blocks/core__navigation-heading.serialized.html diff --git a/lib/blocks.php b/lib/blocks.php index cc3f4c8662254..a3ed5a393dea3 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -59,6 +59,7 @@ function gutenberg_reregister_core_block_types() { 'loginout.php' => 'core/loginout', 'navigation.php' => 'core/navigation', 'navigation-link.php' => 'core/navigation-link', + 'navigation-heading.php' => 'core/navigation-heading', 'home-link.php' => 'core/home-link', 'rss.php' => 'core/rss', 'search.php' => 'core/search', diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index da7fcacc8cadf..14e6f471b1101 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -33,6 +33,7 @@ import * as html from './html'; import * as mediaText from './media-text'; import * as navigation from './navigation'; import * as navigationLink from './navigation-link'; +import * as navigationHeading from './navigation-heading'; import * as homeLink from './home-link'; import * as latestComments from './latest-comments'; import * as latestPosts from './latest-posts'; @@ -230,6 +231,7 @@ export const __experimentalRegisterExperimentalCoreBlocks = [ navigation, navigationLink, + navigationHeading, homeLink, // Register Full Site Editing Blocks. diff --git a/packages/block-library/src/navigation-heading/block.json b/packages/block-library/src/navigation-heading/block.json new file mode 100644 index 0000000000000..2503d3f196f69 --- /dev/null +++ b/packages/block-library/src/navigation-heading/block.json @@ -0,0 +1,32 @@ +{ + "apiVersion": 2, + "name": "core/navigation-heading", + "title": "Navigation Heading", + "category": "design", + "parent": [ + "core/navigation" + ], + "description": "Add a heading to your navigation.", + "textdomain": "default", + "attributes": { + "label": { + "type": "string" + } + }, + "usesContext": [ + "textColor", + "customTextColor", + "backgroundColor", + "customBackgroundColor", + "fontSize", + "customFontSize", + "showSubmenuIcon", + "style" + ], + "supports": { + "reusable": false, + "html": false + }, + "editorStyle": "wp-block-navigation-link-editor", + "style": "wp-block-navigation-link" +} diff --git a/packages/block-library/src/navigation-heading/edit.js b/packages/block-library/src/navigation-heading/edit.js new file mode 100644 index 0000000000000..c3556931bc3a4 --- /dev/null +++ b/packages/block-library/src/navigation-heading/edit.js @@ -0,0 +1,233 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { createBlock } from '@wordpress/blocks'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { ToolbarButton, ToolbarGroup } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { + BlockControls, + InnerBlocks, + __experimentalUseInnerBlocksProps as useInnerBlocksProps, + RichText, + useBlockProps, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { Fragment, useState, useEffect, useRef } from '@wordpress/element'; + +import { addSubmenu } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { ItemSubmenuIcon } from '../navigation-link/icons'; + +const ALLOWED_BLOCKS = [ 'core/navigation-link' ]; + +/** + * A React hook to determine if it's dragging within the target element. + * + * @typedef {import('@wordpress/element').RefObject} RefObject + * + * @param {RefObject} elementRef The target elementRef object. + * + * @return {boolean} Is dragging within the target element. + */ +const useIsDraggingWithin = ( elementRef ) => { + const [ isDraggingWithin, setIsDraggingWithin ] = useState( false ); + + useEffect( () => { + const { ownerDocument } = elementRef.current; + + function handleDragStart( event ) { + // Check the first time when the dragging starts. + handleDragEnter( event ); + } + + // Set to false whenever the user cancel the drag event by either releasing the mouse or press Escape. + function handleDragEnd() { + setIsDraggingWithin( false ); + } + + function handleDragEnter( event ) { + // Check if the current target is inside the item element. + if ( elementRef.current.contains( event.target ) ) { + setIsDraggingWithin( true ); + } else { + setIsDraggingWithin( false ); + } + } + + // Bind these events to the document to catch all drag events. + // Ideally, we can also use `event.relatedTarget`, but sadly that + // doesn't work in Safari. + ownerDocument.addEventListener( 'dragstart', handleDragStart ); + ownerDocument.addEventListener( 'dragend', handleDragEnd ); + ownerDocument.addEventListener( 'dragenter', handleDragEnter ); + + return () => { + ownerDocument.removeEventListener( 'dragstart', handleDragStart ); + ownerDocument.removeEventListener( 'dragend', handleDragEnd ); + ownerDocument.removeEventListener( 'dragenter', handleDragEnter ); + }; + }, [] ); + + return isDraggingWithin; +}; + +export default function NavigationHeadingEdit( { + attributes, + isSelected, + setAttributes, + insertBlocksAfter, + mergeBlocks, + onReplace, + context, + clientId, +} ) { + const { label } = attributes; + + const { textColor, backgroundColor, style, showSubmenuIcon } = context; + const { insertBlock } = useDispatch( blockEditorStore ); + const listItemRef = useRef( null ); + const isDraggingWithin = useIsDraggingWithin( listItemRef ); + const itemLabelPlaceholder = __( 'Add linkā€¦' ); + const ref = useRef(); + + const { + isParentOfSelectedBlock, + isImmediateParentOfSelectedBlock, + hasDescendants, + selectedBlockHasDescendants, + numberOfDescendants, + } = useSelect( + ( select ) => { + const { + getClientIdsOfDescendants, + hasSelectedInnerBlock, + getSelectedBlockClientId, + } = select( blockEditorStore ); + + const selectedBlockId = getSelectedBlockClientId(); + + const descendants = getClientIdsOfDescendants( [ clientId ] ) + .length; + + return { + isImmediateParentOfSelectedBlock: hasSelectedInnerBlock( + clientId, + false + ), + hasDescendants: !! descendants, + selectedBlockHasDescendants: !! getClientIdsOfDescendants( [ + selectedBlockId, + ] )?.length, + numberOfDescendants: descendants, + }; + }, + [ clientId ] + ); + + /** + * Insert a link block when submenu is added. + */ + function insertLinkBlock() { + const insertionPoint = numberOfDescendants; + const blockToInsert = createBlock( 'core/navigation-link' ); + insertBlock( blockToInsert, insertionPoint, clientId ); + } + + const blockProps = useBlockProps( { + ref: listItemRef, + className: classnames( { + 'is-editing': isSelected || isParentOfSelectedBlock, + 'is-dragging-within': isDraggingWithin, + 'has-child': hasDescendants, + 'has-text-color': !! textColor || !! style?.color?.text, + [ `has-${ textColor }-color` ]: !! textColor, + 'has-background': !! backgroundColor || !! style?.color?.background, + [ `has-${ backgroundColor }-background-color` ]: !! backgroundColor, + } ), + style: { + color: style?.color?.text, + backgroundColor: style?.color?.background, + }, + } ); + + const innerBlocksProps = useInnerBlocksProps( + { + className: classnames( 'wp-block-navigation-link__container', { + 'is-parent-of-selected-block': isParentOfSelectedBlock, + } ), + }, + { + allowedBlocks: ALLOWED_BLOCKS, + renderAppender: + ( isSelected && hasDescendants ) || + ( isImmediateParentOfSelectedBlock && + ! selectedBlockHasDescendants ) || + // Show the appender while dragging to allow inserting element between item and the appender. + hasDescendants + ? InnerBlocks.DefaultAppender + : false, + } + ); + + return ( + + + + + + +
+ { /* eslint-disable jsx-a11y/anchor-is-valid */ } + + { /* eslint-enable */ } + + setAttributes( { label: labelValue } ) + } + onMerge={ mergeBlocks } + onReplace={ onReplace } + __unstableOnSplitAtEnd={ () => + insertBlocksAfter( + createBlock( 'core/navigation-link' ) + ) + } + aria-label={ __( 'Navigation link text' ) } + placeholder={ itemLabelPlaceholder } + withoutInteractiveFormatting + allowedFormats={ [ + 'core/bold', + 'core/italic', + 'core/image', + 'core/strikethrough', + ] } + /> + + { hasDescendants && showSubmenuIcon && ( + + + + ) } + +
+
+ + ); +} diff --git a/packages/block-library/src/navigation-heading/index.js b/packages/block-library/src/navigation-heading/index.js new file mode 100644 index 0000000000000..2e132bfcff95c --- /dev/null +++ b/packages/block-library/src/navigation-heading/index.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import edit from './edit'; +import save from './save'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + edit, + save, + example: { + attributes: { + label: __( 'Projects' ), + }, + }, +}; diff --git a/packages/block-library/src/navigation-heading/index.php b/packages/block-library/src/navigation-heading/index.php new file mode 100644 index 0000000000000..4101e2a9642e6 --- /dev/null +++ b/packages/block-library/src/navigation-heading/index.php @@ -0,0 +1,205 @@ + array(), + 'inline_styles' => '', + ); + + // Text color. + $has_named_text_color = array_key_exists( 'textColor', $context ); + $has_custom_text_color = isset( $context['style']['color']['text'] ); + + // If has text color. + if ( $has_custom_text_color || $has_named_text_color ) { + // Add has-text-color class. + $colors['css_classes'][] = 'has-text-color'; + } + + if ( $has_named_text_color ) { + // Add the color class. + $colors['css_classes'][] = sprintf( 'has-%s-color', $context['textColor'] ); + } elseif ( $has_custom_text_color ) { + // Add the custom color inline style. + $colors['inline_styles'] .= sprintf( 'color: %s;', $context['style']['color']['text'] ); + } + + // Background color. + $has_named_background_color = array_key_exists( 'backgroundColor', $context ); + $has_custom_background_color = isset( $context['style']['color']['background'] ); + + // If has background color. + if ( $has_custom_background_color || $has_named_background_color ) { + // Add has-background class. + $colors['css_classes'][] = 'has-background'; + } + + if ( $has_named_background_color ) { + // Add the background-color class. + $colors['css_classes'][] = sprintf( 'has-%s-background-color', $context['backgroundColor'] ); + } elseif ( $has_custom_background_color ) { + // Add the custom background-color inline style. + $colors['inline_styles'] .= sprintf( 'background-color: %s;', $context['style']['color']['background'] ); + } + + return $colors; +} + +/** + * Build an array with CSS classes and inline styles defining the font sizes + * which will be applied to the navigation markup in the front-end. + * + * @param array $context Navigation block context. + * @return array Font size CSS classes and inline styles. + */ +function block_core_navigation_heading_build_css_font_sizes( $context ) { + // CSS classes. + $font_sizes = array( + 'css_classes' => array(), + 'inline_styles' => '', + ); + + $has_named_font_size = array_key_exists( 'fontSize', $context ); + $has_custom_font_size = isset( $context['style']['typography']['fontSize'] ); + + if ( $has_named_font_size ) { + // Add the font size class. + $font_sizes['css_classes'][] = sprintf( 'has-%s-font-size', $context['fontSize'] ); + } elseif ( $has_custom_font_size ) { + // Add the custom font size inline style. + $font_sizes['inline_styles'] = sprintf( 'font-size: %spx;', $context['style']['typography']['fontSize'] ); + } + + return $font_sizes; +} + +/** + * Returns the top-level submenu SVG chevron icon. + * + * @return string + */ +function block_core_navigation_heading_render_submenu_icon() { + return ''; +} + +/** + * Renders the `core/navigation-heading` block. + * + * @param array $attributes The block attributes. + * @param array $content The saved content. + * @param array $block The parsed block. + * + * @return string Returns the post content with the legacy widget added. + */ +function render_block_core_navigation_heading( $attributes, $content, $block ) { + // Don't render the block's subtree if it has no label. + if ( empty( $attributes['label'] ) ) { + return ''; + } + + $colors = block_core_navigation_heading_build_css_colors( $block->context ); + $font_sizes = block_core_navigation_heading_build_css_font_sizes( $block->context ); + $classes = array_merge( + $colors['css_classes'], + $font_sizes['css_classes'] + ); + $style_attribute = ( $colors['inline_styles'] . $font_sizes['inline_styles'] ); + + $css_classes = trim( implode( ' ', $classes ) ); + $has_submenu = count( $block->inner_blocks ) > 0; + + $class_name = ! empty( $attributes['className'] ) ? implode( ' ', (array) $attributes['className'] ) : false; + + if ( false !== $class_name ) { + $css_classes .= ' ' . $class_name; + } + + $wrapper_attributes = get_block_wrapper_attributes( + array( + 'class' => $css_classes . ( $has_submenu ? ' has-child' : '' ), + 'style' => $style_attribute, + ) + ); + $html = '
  • ' . + ''; + + // Start heading tag content. + // Wrap title with span to isolate it from submenu icon. + $html .= ''; + + if ( isset( $attributes['label'] ) ) { + $html .= wp_kses( + $attributes['label'], + array( + 'code' => array(), + 'em' => array(), + 'img' => array( + 'scale' => array(), + 'class' => array(), + 'style' => array(), + 'src' => array(), + 'alt' => array(), + ), + 's' => array(), + 'span' => array( + 'style' => array(), + ), + 'strong' => array(), + ) + ); + } + + $html .= ''; + + if ( isset( $block->context['showSubmenuIcon'] ) && $block->context['showSubmenuIcon'] && $has_submenu ) { + // The submenu icon can be hidden by a CSS rule on the Navigation Block. + $html .= '' . block_core_navigation_heading_render_submenu_icon() . ''; + } + + $html .= ''; + // End heading tag content. + + if ( $has_submenu ) { + $inner_blocks_html = ''; + foreach ( $block->inner_blocks as $inner_block ) { + $inner_blocks_html .= $inner_block->render(); + } + + $html .= sprintf( + '', + $inner_blocks_html + ); + } + + $html .= '
  • '; + + return $html; +} + +/** + * Register the navigation heading block. + * + * @uses render_block_core_navigation() + * @throws WP_Error An WP_Error exception parsing the block definition. + */ +function register_block_core_navigation_heading() { + register_block_type_from_metadata( + __DIR__ . '/navigation-heading', + array( + 'render_callback' => 'render_block_core_navigation_heading', + ) + ); +} +add_action( 'init', 'register_block_core_navigation_heading' ); diff --git a/packages/block-library/src/navigation-heading/save.js b/packages/block-library/src/navigation-heading/save.js new file mode 100644 index 0000000000000..17571d8f30d2d --- /dev/null +++ b/packages/block-library/src/navigation-heading/save.js @@ -0,0 +1,8 @@ +/** + * WordPress dependencies + */ +import { InnerBlocks } from '@wordpress/block-editor'; + +export default function save() { + return ; +} diff --git a/packages/block-library/src/navigation/edit.js b/packages/block-library/src/navigation/edit.js index 0e0e0ab021ce5..a792c1511c99a 100644 --- a/packages/block-library/src/navigation/edit.js +++ b/packages/block-library/src/navigation/edit.js @@ -31,6 +31,7 @@ import ResponsiveWrapper from './responsive-wrapper'; const ALLOWED_BLOCKS = [ 'core/navigation-link', + 'core/navigation-heading', 'core/search', 'core/social-links', 'core/page-list', diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index efc18558ca3f7..2a643103c9cd0 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -256,11 +256,11 @@ function render_block_core_navigation( $attributes, $content, $block ) { $inner_blocks_html = ''; $is_list_open = false; foreach ( $inner_blocks as $inner_block ) { - if ( ( 'core/navigation-link' === $inner_block->name || 'core/home-link' === $inner_block->name ) && false === $is_list_open ) { + if ( ( 'core/navigation-link' === $inner_block->name || 'core/home-link' === $inner_block->name || 'core/navigation-heading' === $inner_block->name ) && false === $is_list_open ) { $is_list_open = true; $inner_blocks_html .= '
      '; } - if ( 'core/navigation-link' !== $inner_block->name && 'core/home-link' !== $inner_block->name && true === $is_list_open ) { + if ( 'core/navigation-link' !== $inner_block->name && 'core/home-link' !== $inner_block->name && 'core/navigation-heading' !== $inner_block->name && true === $is_list_open ) { $is_list_open = false; $inner_blocks_html .= '
    '; } diff --git a/packages/block-library/src/navigation/style.scss b/packages/block-library/src/navigation/style.scss index 60b939799bb82..799553e0b5bc0 100644 --- a/packages/block-library/src/navigation/style.scss +++ b/packages/block-library/src/navigation/style.scss @@ -28,7 +28,8 @@ // Menu item container. .wp-block-pages-list__item, - .wp-block-navigation-link { + .wp-block-navigation-link, + .wp-block-navigation-heading { display: flex; align-items: center; position: relative; diff --git a/packages/e2e-tests/fixtures/blocks/core__navigation-heading.html b/packages/e2e-tests/fixtures/blocks/core__navigation-heading.html new file mode 100644 index 0000000000000..c618d32b322b1 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__navigation-heading.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/blocks/core__navigation-heading.json b/packages/e2e-tests/fixtures/blocks/core__navigation-heading.json new file mode 100644 index 0000000000000..4737111f89255 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__navigation-heading.json @@ -0,0 +1,12 @@ +[ + { + "clientId": "_clientId_0", + "name": "core/navigation-heading", + "isValid": true, + "attributes": { + "label": "WordPress" + }, + "innerBlocks": [], + "originalContent": "" + } +] diff --git a/packages/e2e-tests/fixtures/blocks/core__navigation-heading.parsed.json b/packages/e2e-tests/fixtures/blocks/core__navigation-heading.parsed.json new file mode 100644 index 0000000000000..1eb490b6a6b38 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__navigation-heading.parsed.json @@ -0,0 +1,13 @@ +[ + { + "blockName": "core/navigation-heading", + "attrs": { + "label": "WordPress" + }, + "innerBlocks": [], + "innerHTML": "\n", + "innerContent": [ + "\n" + ] + } +] diff --git a/packages/e2e-tests/fixtures/blocks/core__navigation-heading.serialized.html b/packages/e2e-tests/fixtures/blocks/core__navigation-heading.serialized.html new file mode 100644 index 0000000000000..1339a46239e3e --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__navigation-heading.serialized.html @@ -0,0 +1 @@ + From 020a91a6dae7780db7eaa768e35189eb9da520dc Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Tue, 13 Jul 2021 10:40:30 +1000 Subject: [PATCH 2/4] Add tabindex to open submenu with keyboard. --- packages/block-library/src/navigation-heading/edit.js | 5 ++++- packages/block-library/src/navigation-heading/index.php | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/navigation-heading/edit.js b/packages/block-library/src/navigation-heading/edit.js index c3556931bc3a4..2677bcc594255 100644 --- a/packages/block-library/src/navigation-heading/edit.js +++ b/packages/block-library/src/navigation-heading/edit.js @@ -192,7 +192,10 @@ export default function NavigationHeadingEdit( {
    { /* eslint-disable jsx-a11y/anchor-is-valid */ } - + { /* eslint-enable */ } ' . - ''; + ''; // Start heading tag content. // Wrap title with span to isolate it from submenu icon. From 8fc2a281990afc07783577e2a7d3d8e12b62f884 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Tue, 13 Jul 2021 16:23:19 +1000 Subject: [PATCH 3/4] Fix php linting error. --- packages/block-library/src/navigation-heading/index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/navigation-heading/index.php b/packages/block-library/src/navigation-heading/index.php index f2aef7b637b73..eb3e0de683f42 100644 --- a/packages/block-library/src/navigation-heading/index.php +++ b/packages/block-library/src/navigation-heading/index.php @@ -138,7 +138,7 @@ function render_block_core_navigation_heading( $attributes, $content, $block ) { // Start heading tag content. // Wrap title with span to isolate it from submenu icon. $html .= ''; - + if ( isset( $attributes['label'] ) ) { $html .= wp_kses( $attributes['label'], From f5ba10e05582acd07310eae79cecff6fea04405c Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Wed, 14 Jul 2021 11:15:13 +1000 Subject: [PATCH 4/4] Fix php linting error --- packages/block-library/src/navigation-heading/index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/navigation-heading/index.php b/packages/block-library/src/navigation-heading/index.php index eb3e0de683f42..89f4ab5cdc296 100644 --- a/packages/block-library/src/navigation-heading/index.php +++ b/packages/block-library/src/navigation-heading/index.php @@ -138,7 +138,7 @@ function render_block_core_navigation_heading( $attributes, $content, $block ) { // Start heading tag content. // Wrap title with span to isolate it from submenu icon. $html .= ''; - + if ( isset( $attributes['label'] ) ) { $html .= wp_kses( $attributes['label'],