diff --git a/packages/block-library/src/heading/edit.js b/packages/block-library/src/heading/edit.js index cf723896a55a03..90f8ddc8a071ad 100644 --- a/packages/block-library/src/heading/edit.js +++ b/packages/block-library/src/heading/edit.js @@ -2,6 +2,7 @@ * Internal dependencies */ import HeadingToolbar from './heading-toolbar'; +import HeadingLevelChecker from './level-checker'; /** * WordPress dependencies @@ -24,6 +25,7 @@ export default function HeadingEdit( { insertBlocksAfter, onReplace, className, + clientId, } ) { const { align, content, level, placeholder } = attributes; const tagName = 'h' + level; @@ -37,6 +39,7 @@ export default function HeadingEdit( {

{ __( 'Level' ) }

setAttributes( { level: newLevel } ) } /> +

{ __( 'Text Alignment' ) }

{ + return flatMap( blocks, ( block = {} ) => { + if ( block.name === 'core/heading' ) { + return { + ...block, + path, + level: block.attributes.level, + }; + } + return computeOutlineHeadings( block.innerBlocks, [ ...path, block ] ); + } ); +}; + +export const HeadingLevelChecker = ( { blocks = [], title, isTitleSupported, selectedHeadingId } ) => { + const headings = computeOutlineHeadings( blocks ); + + // Iterate headings to find prevHeadingLevel and selectedLevel + let prevHeadingLevel = 1; + let selectedLevel = 1; + let i = 0; + for ( i = 0; i < headings.length; i++ ) { + if ( headings[ i ].clientId === selectedHeadingId ) { + selectedLevel = headings[ i ].level; + if ( i >= 1 ) { + prevHeadingLevel = headings[ i - 1 ].level; + } + } + } + + const titleNode = document.querySelector( '.editor-post-title__input' ); + const hasTitle = isTitleSupported && title && titleNode; + const countByLevel = countBy( headings, 'level' ); + const hasMultipleH1 = countByLevel[ 1 ] > 1; + const isIncorrectLevel = selectedLevel > prevHeadingLevel + 1; + + let msg = ''; + if ( isIncorrectLevel ) { + msg = __( 'This heading level is incorrect.' ); + } else if ( selectedLevel === 1 && hasMultipleH1 ) { + msg = __( 'Multiple H1 headings found.' ); + } else if ( selectedLevel === 1 && hasTitle && ! hasMultipleH1 ) { + msg = __( 'H1 is already used for the post title.' ); + } else { + return null; + } + + // For accessibility + useEffect( () => { + speak( msg ); + }, [ selectedLevel ] ); + + return ( +
+ + { msg } + +
+ ); +}; + +export default compose( + withSelect( ( select ) => { + const { getBlocks } = select( 'core/block-editor' ); + const { getEditedPostAttribute } = select( 'core/editor' ); + const { getPostType } = select( 'core' ); + const postType = getPostType( getEditedPostAttribute( 'type' ) ); + + return { + blocks: getBlocks(), + title: getEditedPostAttribute( 'title' ), + isTitleSupported: get( postType, [ 'supports', 'title' ], false ), + }; + } ) +)( HeadingLevelChecker ); diff --git a/packages/block-library/src/heading/style.native.scss b/packages/block-library/src/heading/style.native.scss index f4801b92a6bc50..ae869bd7f8b037 100644 --- a/packages/block-library/src/heading/style.native.scss +++ b/packages/block-library/src/heading/style.native.scss @@ -2,3 +2,6 @@ .blockText { min-height: $min-height-heading; } +.block-library-heading__heading-level-checker { + margin: 0; +} diff --git a/packages/editor/src/components/document-outline/index.js b/packages/editor/src/components/document-outline/index.js index 5a61503909a88b..d80d2ee8664d3b 100644 --- a/packages/editor/src/components/document-outline/index.js +++ b/packages/editor/src/components/document-outline/index.js @@ -48,7 +48,7 @@ const multipleH1Headings = [ * * @return {Array} An array of heading blocks enhanced with the properties described above. */ -const computeOutlineHeadings = ( blocks = [], path = [] ) => { +export const computeOutlineHeadings = ( blocks = [], path = [] ) => { return flatMap( blocks, ( block = {} ) => { if ( block.name === 'core/heading' ) { return {