-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1559fad
commit 8dd9b1d
Showing
4 changed files
with
136 additions
and
96 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
117 changes: 24 additions & 93 deletions
117
packages/block-library/src/heading/heading-level-checker.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,107 +1,38 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { countBy, flatMap, get } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { speak } from '@wordpress/a11y'; | ||
import { __ } from '@wordpress/i18n'; | ||
import { Notice } from '@wordpress/components'; | ||
import { useEffect } from '@wordpress/element'; | ||
import { compose } from '@wordpress/compose'; | ||
import { withSelect } from '@wordpress/data'; | ||
|
||
// copy 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. | ||
* Can be an empty array if the heading is a top-level node (is not nested inside another block). | ||
* level - An integer with the heading level. | ||
* isEmpty - Flag indicating if the heading has no content. | ||
* | ||
* @param {?Array} blocks An array of blocks. | ||
* @param {?Array} path An array of blocks that are ancestors of the blocks passed as blocks. | ||
* | ||
* @return {Array} An array of heading blocks enhanced with the properties described above. | ||
*/ | ||
export const computeOutlineHeadings = ( blocks = [], path = [] ) => { | ||
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; | ||
import { __ } from '@wordpress/i18n'; | ||
|
||
// For accessibility | ||
const INVALID_LEVEL_MESSAGE = __( | ||
'The selected heading level may be invalid. See the content structure tool for more info.' | ||
); | ||
|
||
export default function HeadingLevelChecker( { | ||
selectedLevel, | ||
levelIsInvalid, | ||
} ) { | ||
// 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 ( | ||
<div className="block-library-heading__heading-level-checker"> | ||
<Notice status="warning" isDismissible={ false }> | ||
{ msg } | ||
</Notice> | ||
</div> | ||
<Notice | ||
className="block-library-heading__heading-level-checker-warning" | ||
isDismissible={ false } | ||
status="warning" | ||
> | ||
{ INVALID_LEVEL_MESSAGE } | ||
</Notice> | ||
); | ||
}; | ||
|
||
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 ); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
85 changes: 85 additions & 0 deletions
85
packages/block-library/src/heading/use-is-heading-level-valid.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { flatMap } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useSelect } from '@wordpress/data'; | ||
|
||
// 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. | ||
* Can be an empty array if the heading is a top-level node (is not nested inside another block). | ||
* level - An integer with the heading level. | ||
* isEmpty - Flag indicating if the heading has no content. | ||
* | ||
* @param {?Array} blocks An array of blocks. | ||
* @param {?Array} path An array of blocks that are ancestors of the blocks passed as blocks. | ||
* | ||
* @return {Array} An array of heading blocks enhanced with the properties described above. | ||
*/ | ||
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 { | ||
...block, | ||
path, | ||
level: block.attributes.level, | ||
}; | ||
} | ||
return computeOutlineHeadings( block.innerBlocks, [ ...path, block ] ); | ||
} ); | ||
} | ||
|
||
export default function useIsHeadingLevelValid( | ||
currentBlockClientId, | ||
selectedLevel | ||
) { | ||
const { headings, isTitleSupported, titleIsNotEmpty } = useSelect( | ||
( select ) => { | ||
const { getPostType } = select( 'core' ); | ||
const { getBlocks } = select( 'core/block-editor' ); | ||
const { getEditedPostAttribute } = select( 'core/editor' ); | ||
const postType = getPostType( getEditedPostAttribute( 'type' ) ); | ||
|
||
return { | ||
headings: computeOutlineHeadings( getBlocks() ?? [] ), | ||
isTitleSupported: postType?.supports?.title ?? false, | ||
titleIsNotEmpty: !! getEditedPostAttribute( 'title' ), | ||
}; | ||
}, | ||
[] | ||
); | ||
|
||
// Default the assumed previous level to H1. | ||
let prevLevel = 1; | ||
|
||
const currentHeadingIndex = headings.findIndex( | ||
( { clientId } ) => clientId === currentBlockClientId | ||
); | ||
|
||
// If the current block isn't the first Heading block in the content, set | ||
// prevLevel to the level of the closest Heading block preceding it. | ||
if ( currentHeadingIndex > 0 ) { | ||
prevLevel = headings[ currentHeadingIndex - 1 ].level; | ||
} | ||
|
||
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; | ||
|
||
return levelIsInvalid; | ||
} |