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

Add a Navigation Heading block. #33351

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions lib/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions packages/block-library/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -230,6 +231,7 @@ export const __experimentalRegisterExperimentalCoreBlocks =
[
navigation,
navigationLink,
navigationHeading,
homeLink,

// Register Full Site Editing Blocks.
Expand Down
32 changes: 32 additions & 0 deletions packages/block-library/src/navigation-heading/block.json
Original file line number Diff line number Diff line change
@@ -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"
}
236 changes: 236 additions & 0 deletions packages/block-library/src/navigation-heading/edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
/**
* 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<HTMLElement>} 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 (
<Fragment>
<BlockControls>
<ToolbarGroup>
<ToolbarButton
name="submenu"
icon={ addSubmenu }
title={ __( 'Add submenu' ) }
onClick={ insertLinkBlock }
/>
</ToolbarGroup>
</BlockControls>
<div { ...blockProps }>
{ /* eslint-disable jsx-a11y/anchor-is-valid */ }
<span
className="wp-block-navigation-link__content"
tabIndex="0"
>
{ /* eslint-enable */ }
<RichText
ref={ ref }
identifier="label"
className="wp-block-navigation-link__label"
value={ label }
onChange={ ( labelValue ) =>
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 && (
<span className="wp-block-navigation-link__submenu-icon">
<ItemSubmenuIcon />
</span>
) }
</span>
<div { ...innerBlocksProps } />
</div>
</Fragment>
);
}
25 changes: 25 additions & 0 deletions packages/block-library/src/navigation-heading/index.js
Original file line number Diff line number Diff line change
@@ -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' ),
},
},
};
Loading