diff --git a/packages/block-library/src/heading/editor.scss b/packages/block-library/src/heading/editor.scss index 05040868113b6..0add23862762f 100644 --- a/packages/block-library/src/heading/editor.scss +++ b/packages/block-library/src/heading/editor.scss @@ -13,6 +13,6 @@ border: none; } -.block-library-heading__heading-level-checker { +.block-library-heading__heading-level-checker-warning { margin: 0; } diff --git a/packages/block-library/src/heading/heading-level-checker.js b/packages/block-library/src/heading/heading-level-checker.js index a206c3251abc9..75ae039eca129 100644 --- a/packages/block-library/src/heading/heading-level-checker.js +++ b/packages/block-library/src/heading/heading-level-checker.js @@ -1,19 +1,22 @@ /** * External dependencies */ -import { countBy, flatMap, get } from 'lodash'; +import { flatMap } from 'lodash'; /** * WordPress dependencies */ import { speak } from '@wordpress/a11y'; -import { __ } from '@wordpress/i18n'; import { Notice } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; import { useEffect } from '@wordpress/element'; -import { compose } from '@wordpress/compose'; -import { withSelect } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; + +const INVALID_LEVEL_MESSAGE = __( + 'The selected heading level may be invalid. See the content structure tool for more info.' +); -// copy from packages/editor/src/components/document-outline/index.js +// Copied from packages/editor/src/components/document-outline/index.js. /** * Returns an array of heading blocks enhanced with the following properties: * path - An array of blocks that are ancestors of the heading starting from a top-level node. @@ -26,7 +29,8 @@ import { withSelect } from '@wordpress/data'; * * @return {Array} An array of heading blocks enhanced with the properties described above. */ -export const computeOutlineHeadings = ( blocks = [], path = [] ) => { +function computeOutlineHeadings( blocks = [], path = [] ) { + // We don't polyfill native JS [].flatMap yet, so we have to use Lodash. return flatMap( blocks, ( block = {} ) => { if ( block.name === 'core/heading' ) { return { @@ -37,71 +41,70 @@ export const computeOutlineHeadings = ( blocks = [], path = [] ) => { } return computeOutlineHeadings( block.innerBlocks, [ ...path, block ] ); } ); -}; +} -export const HeadingLevelChecker = ( { - blocks = [], - title, - isTitleSupported, - selectedHeadingId, -} ) => { - const headings = computeOutlineHeadings( blocks ); +export default function HeadingLevelChecker( { selectedHeadingId } ) { + const { headings, titleIsNotEmpty, isTitleSupported } = useSelect( + ( select ) => { + const { getPostType } = select( 'core' ); + const { getBlocks } = select( 'core/block-editor' ); + const { getEditedPostAttribute } = select( 'core/editor' ); + const postType = getPostType( getEditedPostAttribute( 'type' ) ); - // Iterate headings to find prevHeadingLevel and selectedLevel - let prevHeadingLevel = 1; + return { + headings: computeOutlineHeadings( getBlocks() ?? [] ), + titleIsNotEmpty: !! getEditedPostAttribute( 'title' ), + isTitleSupported: postType?.supports?.title ?? false, + }; + }, + [] + ); + + // Find the heading level of the current block and the level of the closest + // heading preceding it. + let prevLevel = 1; let selectedLevel = 1; - let i = 0; - for ( i = 0; i < headings.length; i++ ) { + for ( let i = 0; i < headings.length; i++ ) { if ( headings[ i ].clientId === selectedHeadingId ) { selectedLevel = headings[ i ].level; if ( i >= 1 ) { - prevHeadingLevel = headings[ i - 1 ].level; + prevLevel = 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; + const titleNode = document.getElementsByClassName( + 'editor-post-title__input' + )[ 0 ]; + const hasTitle = isTitleSupported && titleIsNotEmpty && titleNode; + const hasMultipleH1 = + headings.filter( ( { level } ) => level === 1 ).length > 1; + const levelIsDuplicateH1 = hasMultipleH1 && selectedLevel === 1; + const levelAndPostTitleAreBothH1 = + selectedLevel === 1 && hasTitle && ! hasMultipleH1; + const levelIsTooDeep = selectedLevel > prevLevel + 1; + const levelIsInvalid = + levelIsDuplicateH1 || levelAndPostTitleAreBothH1 || levelIsTooDeep; - // For accessibility + // For accessibility, announce the invalid heading level to screen readers. + // The selectedLevel value is included in the dependency array so that the + // message will be replayed if a new level is selected, but the new level is + // still invalid. useEffect( () => { - if ( isIncorrectLevel ) speak( msg ); - }, [ isIncorrectLevel, selectedLevel ] ); + if ( levelIsInvalid ) speak( INVALID_LEVEL_MESSAGE ); + }, [ selectedLevel, levelIsInvalid ] ); - 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 { + if ( ! levelIsInvalid ) { return null; } return ( -