Skip to content

Commit

Permalink
Lots of improvements.
Browse files Browse the repository at this point in the history
  • Loading branch information
ZebulanStanphill committed Jun 2, 2020
1 parent 1559fad commit 8dd9b1d
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 96 deletions.
12 changes: 11 additions & 1 deletion packages/block-library/src/heading/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,22 @@
min-width: 230px;
}

.block-library-heading__heading-level-dropdown-button-invalid-indicator {
background-color: $alert-yellow;
height: 8px;
width: 8px;
position: absolute;
bottom: 6px;
left: 20px;
border-radius: 50%;
}

// The dropdown already has a border, so we can remove the one on the heading
// level toolbar.
.block-library-heading-level-toolbar {
border: none;
}

.block-library-heading__heading-level-checker {
.block-library-heading__heading-level-checker-warning {
margin: 0;
}
117 changes: 24 additions & 93 deletions packages/block-library/src/heading/heading-level-checker.js
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 );
}
18 changes: 16 additions & 2 deletions packages/block-library/src/heading/heading-level-dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { DOWN } from '@wordpress/keycodes';
*/
import HeadingLevelChecker from './heading-level-checker';
import HeadingLevelIcon from './heading-level-icon';
import useIsHeadingLevelValid from './use-is-heading-level-valid';

const HEADING_LEVELS = [ 1, 2, 3, 4, 5, 6 ];

Expand Down Expand Up @@ -48,6 +49,8 @@ export default function HeadingLevelDropdown( {
selectedLevel,
onChange,
} ) {
const levelIsInvalid = useIsHeadingLevelValid( clientId, selectedLevel );

return (
<Dropdown
popoverProps={ POPOVER_PROPS }
Expand All @@ -64,7 +67,15 @@ export default function HeadingLevelDropdown( {
<ToolbarButton
aria-expanded={ isOpen }
aria-haspopup="true"
icon={ <HeadingLevelIcon level={ selectedLevel } /> }
className="block-library-heading__heading-level-dropdown-button"
icon={
<>
<HeadingLevelIcon level={ selectedLevel } />
{ levelIsInvalid && (
<span className="block-library-heading__heading-level-dropdown-button-invalid-indicator" />
) }
</>
}
label={ __( 'Change heading level' ) }
onClick={ onToggle }
onKeyDown={ openOnArrowDown }
Expand Down Expand Up @@ -113,7 +124,10 @@ export default function HeadingLevelDropdown( {
} ) }
/>
</Toolbar>
<HeadingLevelChecker selectedHeadingId={ clientId } />
<HeadingLevelChecker
levelIsInvalid={ levelIsInvalid }
selectedLevel={ selectedLevel }
/>
</>
) }
/>
Expand Down
85 changes: 85 additions & 0 deletions packages/block-library/src/heading/use-is-heading-level-valid.js
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;
}

0 comments on commit 8dd9b1d

Please sign in to comment.