Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Heading block: add heading level checker #22650

Open
wants to merge 3 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/block-library/src/heading/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ import HeadingLevelDropdown from './heading-level-dropdown';

function HeadingEdit( {
attributes,
clientId,
setAttributes,
mergeBlocks,
onReplace,
mergedStyle,
clientId,
} ) {
const { textAlign, content, level, placeholder } = attributes;
const tagName = 'h' + level;
Expand All @@ -43,6 +43,7 @@ function HeadingEdit( {
<BlockControls>
<ToolbarGroup>
<HeadingLevelDropdown
clientId={ clientId }
selectedLevel={ level }
onChange={ ( newLevel ) =>
setAttributes( { level: newLevel } )
Expand Down
18 changes: 18 additions & 0 deletions packages/block-library/src/heading/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,26 @@
}
}

// Gray the icons for heading level options that may be semantically invalid.
.block-library-heading__heading-level-icon.is-discouraged:not(.is-pressed):not(:hover) {
fill: $gray-600;
}

// 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-dropdown__help {
margin-left: $grid-unit-20;
margin-right: $grid-unit-20;

// Override default paragraph margin to optically balance spacing between it
// and the toolbar above.
margin-top: 0;
}

.block-library-heading__heading-level-warning {
margin: 0;
}
113 changes: 81 additions & 32 deletions packages/block-library/src/heading/heading-level-dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
*/
import {
Dropdown,
ExternalLink,
Toolbar,
ToolbarButton,
ToolbarGroup,
} from '@wordpress/components';
import { useInstanceId } from '@wordpress/compose';
import { __, sprintf } from '@wordpress/i18n';
import { DOWN } from '@wordpress/keycodes';

/**
* Internal dependencies
*/
import HeadingLevelIcon from './heading-level-icon';
import HeadingLevelWarning from './heading-level-warning';
import useHeadingLevelValidator from './use-heading-level-validator';

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

Expand All @@ -29,9 +33,11 @@ const POPOVER_PROPS = {
*
* @typedef WPHeadingLevelDropdownProps
*
* @property {number} selectedLevel The chosen heading level.
* @property {(newValue:number)=>any} onChange Callback to run when
* toolbar value is changed.
* @property {string} clientId The current block client
* id.
* @property {number} selectedLevel The chosen heading level.
* @property {(newValue:number)=>void} onChange Callback to run when
* toolbar value is changed.
*/

/**
Expand All @@ -41,7 +47,21 @@ const POPOVER_PROPS = {
*
* @return {WPComponent} The toolbar.
*/
export default function HeadingLevelDropdown( { selectedLevel, onChange } ) {
export default function HeadingLevelDropdown( {
clientId,
selectedLevel,
onChange,
} ) {
const helpTextId = useInstanceId(
HeadingLevelDropdown,
'block-library-heading__heading-level-dropdown__help'
);

const getLevelValidity = useHeadingLevelValidator( clientId );

const selectedLevelMayBeInvalid =
getLevelValidity( selectedLevel ) !== 'valid';

return (
<Dropdown
popoverProps={ POPOVER_PROPS }
Expand All @@ -67,34 +87,63 @@ 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;
const levelMayBeInvalid =
getLevelValidity( targetLevel ) !== 'valid';

return {
icon: (
<HeadingLevelIcon
level={ targetLevel }
isPressed={ isActive }
isDiscouraged={ levelMayBeInvalid }
/>
),
title: levelMayBeInvalid
? sprintf(
// translators: %d: heading level e.g: "1", "2", "3"
__(
'Heading %d (may be invalid)'
),
targetLevel
)
: sprintf(
// translators: %d: heading level e.g: "1", "2", "3"
__( 'Heading %d' ),
targetLevel
),
// Move tooltips above buttons so they don't overlap
// the help text below.
tooltipPosition: 'top',
isActive,
onClick() {
onChange( targetLevel );
},
};
} ) }
aria-describedby={ helpTextId }
/>
</Toolbar>
<p
id={ helpTextId }
className="block-library-heading__heading-level-dropdown__help"
>
<ExternalLink href="https://www.w3.org/WAI/tutorials/page-structure/headings/">
{ __( 'Learn about heading levels.' ) }
</ExternalLink>
</p>
{ selectedLevelMayBeInvalid && (
<HeadingLevelWarning selectedLevel={ selectedLevel } />
) }
</>
) }
/>
);
Expand Down
25 changes: 22 additions & 3 deletions packages/block-library/src/heading/heading-level-icon.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* WordPress dependencies
*/
Expand All @@ -10,8 +15,12 @@ import { Path, SVG } from '@wordpress/components';
*
* @typedef WPHeadingLevelIconProps
*
* @property {number} level The heading level to show an icon for.
* @property {?boolean} isPressed Whether or not the icon should appear pressed; default: false.
* @property {number} level The heading level to show an icon for.
* @property {boolean} [isDiscouraged] Whether choosing this heading level is
* discouraged due to it possibly being
* semantically incorrect; default: false.
* @property {boolean} [isPressed] Whether or not the icon should appear
* pressed; default: false.
*/

/**
Expand All @@ -21,7 +30,11 @@ import { Path, SVG } from '@wordpress/components';
*
* @return {?WPComponent} The icon.
*/
export default function HeadingLevelIcon( { level, isPressed = false } ) {
export default function HeadingLevelIcon( {
level,
isDiscouraged = false,
isPressed = false,
} ) {
const levelToPath = {
1: 'M9 5h2v10H9v-4H5v4H3V5h2v4h4V5zm6.6 0c-.6.9-1.5 1.7-2.6 2v1h2v7h2V5h-1.4z',
2: 'M7 5h2v10H7v-4H3v4H1V5h2v4h4V5zm8 8c.5-.4.6-.6 1.1-1.1.4-.4.8-.8 1.2-1.3.3-.4.6-.8.9-1.3.2-.4.3-.8.3-1.3 0-.4-.1-.9-.3-1.3-.2-.4-.4-.7-.8-1-.3-.3-.7-.5-1.2-.6-.5-.2-1-.2-1.5-.2-.4 0-.7 0-1.1.1-.3.1-.7.2-1 .3-.3.1-.6.3-.9.5-.3.2-.6.4-.8.7l1.2 1.2c.3-.3.6-.5 1-.7.4-.2.7-.3 1.2-.3s.9.1 1.3.4c.3.3.5.7.5 1.1 0 .4-.1.8-.4 1.1-.3.5-.6.9-1 1.2-.4.4-1 .9-1.6 1.4-.6.5-1.4 1.1-2.2 1.6V15h8v-2H15z',
Expand All @@ -41,6 +54,12 @@ export default function HeadingLevelIcon( { level, isPressed = false } ) {
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
isPressed={ isPressed }
className={ classnames(
'block-library-heading__heading-level-icon',
{
'is-discouraged': isDiscouraged,
}
) }
>
<Path d={ levelToPath[ level ] } />
</SVG>
Expand Down
35 changes: 35 additions & 0 deletions packages/block-library/src/heading/heading-level-warning.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* WordPress dependencies
*/
import { speak } from '@wordpress/a11y';
import { Notice } from '@wordpress/components';
import { useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

const INVALID_LEVEL_MESSAGE = __(
'The chosen heading level may be invalid. See the content structure tool for more info.'
);

const INVALID_LEVEL_MESSAGE_SPOKEN = __(
'The chosen heading level may be invalid. Use proper heading levels to organize your content for visitors and search engines. See the content structure tool for more info.'
);

export default function HeadingLevelWarning( { selectedLevel } ) {
// 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( () => {
speak( INVALID_LEVEL_MESSAGE_SPOKEN );
}, [ selectedLevel ] );

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