Skip to content

Commit

Permalink
Original implementation by @Jackie6 (Rebased + major fixes/tweaks.)
Browse files Browse the repository at this point in the history
Derived from #14889.

Rebase and major fixes by @ZebulanStanphill.

Co-authored-by: Jackie6 <Jackie6@users.noreply.github.com>
  • Loading branch information
ZebulanStanphill and Jackie6 committed Mar 6, 2021
1 parent c24576a commit a28fa40
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 29 deletions.
2 changes: 2 additions & 0 deletions packages/block-library/src/heading/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import HeadingLevelDropdown from './heading-level-dropdown';

function HeadingEdit( {
attributes,
clientId,
setAttributes,
mergeBlocks,
onReplace,
Expand All @@ -43,6 +44,7 @@ function HeadingEdit( {
<BlockControls>
<ToolbarGroup>
<HeadingLevelDropdown
clientId={ clientId }
selectedLevel={ level }
onChange={ ( newLevel ) =>
setAttributes( { level: newLevel } )
Expand Down
4 changes: 4 additions & 0 deletions packages/block-library/src/heading/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@
.block-library-heading-level-toolbar {
border: none;
}

.block-library-heading__heading-level-checker {
margin: 0;
}
110 changes: 110 additions & 0 deletions packages/block-library/src/heading/heading-level-checker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* External dependencies
*/
import { countBy, flatMap, get } from 'lodash';

/**
* WordPress dependencies
*/
import { speak } from '@wordpress/a11y';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { store as coreStore } from '@wordpress/core-data';
import { __ } from '@wordpress/i18n';
import { Notice } from '@wordpress/components';
import { store as editorStore } from '@wordpress/editor';
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;

// For accessibility
useEffect( () => {
if ( isIncorrectLevel ) speak( msg );
}, [ isIncorrectLevel, selectedLevel ] );

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;
}

return (
<div className="block-library-heading__heading-level-checker">
<Notice status="warning" isDismissible={ false }>
{ msg }
</Notice>
</div>
);
};

export default compose(
withSelect( ( select ) => {
const { getBlocks } = select( blockEditorStore );
const { getEditedPostAttribute } = select( editorStore );
const { getPostType } = select( coreStore );
const postType = getPostType( getEditedPostAttribute( 'type' ) );

return {
blocks: getBlocks(),
title: getEditedPostAttribute( 'title' ),
isTitleSupported: get( postType, [ 'supports', 'title' ], false ),
};
} )
)( HeadingLevelChecker );
67 changes: 38 additions & 29 deletions packages/block-library/src/heading/heading-level-dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { DOWN } from '@wordpress/keycodes';
/**
* Internal dependencies
*/
import HeadingLevelChecker from './heading-level-checker';
import HeadingLevelIcon from './heading-level-icon';

const HEADING_LEVELS = [ 1, 2, 3, 4, 5, 6 ];
Expand All @@ -29,6 +30,7 @@ const POPOVER_PROPS = {
*
* @typedef WPHeadingLevelDropdownProps
*
* @property {string} clientId The current block client id.
* @property {number} selectedLevel The chosen heading level.
* @property {(newValue:number)=>any} onChange Callback to run when
* toolbar value is changed.
Expand All @@ -41,7 +43,11 @@ const POPOVER_PROPS = {
*
* @return {WPComponent} The toolbar.
*/
export default function HeadingLevelDropdown( { selectedLevel, onChange } ) {
export default function HeadingLevelDropdown( {
clientId,
selectedLevel,
onChange,
} ) {
return (
<Dropdown
popoverProps={ POPOVER_PROPS }
Expand All @@ -67,34 +73,37 @@ export default function HeadingLevelDropdown( { selectedLevel, onChange } ) {
);
} }
renderContent={ () => (
<Toolbar
className="block-library-heading-level-toolbar"
label={ __( 'Change heading level' ) }
>
<ToolbarGroup
isCollapsed={ false }
controls={ HEADING_LEVELS.map( ( targetLevel ) => {
const isActive = targetLevel === selectedLevel;
return {
icon: (
<HeadingLevelIcon
level={ targetLevel }
isPressed={ isActive }
/>
),
title: sprintf(
// translators: %s: heading level e.g: "1", "2", "3"
__( 'Heading %d' ),
targetLevel
),
isActive,
onClick() {
onChange( targetLevel );
},
};
} ) }
/>
</Toolbar>
<>
<Toolbar
className="block-library-heading-level-toolbar"
label={ __( 'Change heading level' ) }
>
<ToolbarGroup
isCollapsed={ false }
controls={ HEADING_LEVELS.map( ( targetLevel ) => {
const isActive = targetLevel === selectedLevel;
return {
icon: (
<HeadingLevelIcon
level={ targetLevel }
isPressed={ isActive }
/>
),
title: sprintf(
// translators: %s: heading level e.g: "1", "2", "3"
__( 'Heading %d' ),
targetLevel
),
isActive,
onClick() {
onChange( targetLevel );
},
};
} ) }
/>
</Toolbar>
<HeadingLevelChecker selectedHeadingId={ clientId } />
</>
) }
/>
);
Expand Down

0 comments on commit a28fa40

Please sign in to comment.