diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index 37aa57a852d32d..3fe89d0e9df3a0 100644 --- a/packages/block-editor/CHANGELOG.md +++ b/packages/block-editor/CHANGELOG.md @@ -2,10 +2,15 @@ ## Unreleased +### Enhancements + +- Add `HeadingLevelDropdown` component for selecting H1-H6 and paragraph HTML tags from the block toolbar. + ### Bug Fix - Fluid typography: custom font-sizes should use max viewport width ([#51516](https://github.com/WordPress/gutenberg/pull/51516)). + ## 12.3.0 (2023-06-07) ## 12.2.0 (2023-05-24) @@ -22,6 +27,7 @@ ## 11.8.0 (2023-04-12) + ## 11.7.0 (2023-03-29) - `ImageSizeControl`: Update image size label ([#49112](https://github.com/WordPress/gutenberg/pull/49112)). diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 91aa34572648df..72c3d50372e48e 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -545,6 +545,18 @@ _Returns_ - `Object`: Typography block support derived CSS classes & styles. +### HeadingLevelDropdown + +Dropdown for selecting a heading level (1 through 6) or paragraph (0). + +_Parameters_ + +- _props_ `WPHeadingLevelDropdownProps`: Component props. + +_Returns_ + +- `WPComponent`: The toolbar. + ### HeightControl HeightControl renders a linked unit control and range control for adjusting the height of a block. diff --git a/packages/block-editor/src/components/block-heading-level-dropdown/README.md b/packages/block-editor/src/components/block-heading-level-dropdown/README.md new file mode 100644 index 00000000000000..8e0676669b66aa --- /dev/null +++ b/packages/block-editor/src/components/block-heading-level-dropdown/README.md @@ -0,0 +1,52 @@ +# Heading Level Dropdown + +`` Adds a dropdown for selecting H1-H6 and paragraph HTML tags from the block toolbar. +Uses ``. + +## Usage + +```jsx + +import { BlockControls, HeadingLevelDropdown } from '@wordpress/block-editor'; + +const HEADING_LEVELS = [ 0, 1, 2, 3, 4, 5, 6 ]; + +const MyHeadingLevelToolbar = () => ( + + + setAttributes( { tag: newTag } ) + } + /> + +); +``` + +### Props + +#### options + +The list of available HTML tags, passed from the block. + +- Type: `Object` +- Required: no + +#### value + +The chosen HTML tag. + +- Type: `string` +- Required: no + +#### onChange + +Callback to run when toolbar value is changed. + +- Type: `string` +- Required: yes + +## Related components + +Block Editor components are components that can be used to compose the UI of your block editor. Thus, they can only be used under a [`BlockEditorProvider`](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/provider/README.md) in the components tree. diff --git a/packages/block-editor/src/components/block-heading-level-dropdown/heading-level-icon.js b/packages/block-editor/src/components/block-heading-level-dropdown/heading-level-icon.js new file mode 100644 index 00000000000000..7b161d9c01700e --- /dev/null +++ b/packages/block-editor/src/components/block-heading-level-dropdown/heading-level-icon.js @@ -0,0 +1,43 @@ +/** + * WordPress dependencies + */ +import { + headingLevel1, + headingLevel2, + headingLevel3, + headingLevel4, + headingLevel5, + headingLevel6, + paragraph, +} from '@wordpress/icons'; + +/** @typedef {import('@wordpress/element').WPComponent} WPComponent */ + +/** + * HeadingLevelIcon props. + * + * @typedef WPHeadingLevelIconProps + * + * @property {number} level The heading level to show an icon for. + */ + +const LEVEL_TO_PATH = { + 0: paragraph, + 1: headingLevel1, + 2: headingLevel2, + 3: headingLevel3, + 4: headingLevel4, + 5: headingLevel5, + 6: headingLevel6, +}; + +/** + * Heading level icon. + * + * @param {WPHeadingLevelIconProps} props Component props. + * + * @return {?WPComponent} The icon. + */ +export default function HeadingLevelIcon( { level } ) { + return LEVEL_TO_PATH[ level ] ?? null; +} diff --git a/packages/block-library/src/heading/heading-level-dropdown.js b/packages/block-editor/src/components/block-heading-level-dropdown/index.js similarity index 53% rename from packages/block-library/src/heading/heading-level-dropdown.js rename to packages/block-editor/src/components/block-heading-level-dropdown/index.js index d8a4eae0ab34db..40d60b08436cc0 100644 --- a/packages/block-library/src/heading/heading-level-dropdown.js +++ b/packages/block-editor/src/components/block-heading-level-dropdown/index.js @@ -22,27 +22,32 @@ 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 {number} value The chosen heading level. + * @property {number[]} options An array of supported heading levels. + * @property {(newValue:number)=>any} onChange Callback to run when + * toolbar value is changed. */ /** - * Dropdown for selecting a heading level (1 through 6). + * Dropdown for selecting a heading level (1 through 6) or paragraph (0). * * @param {WPHeadingLevelDropdownProps} props Component props. * * @return {WPComponent} The toolbar. */ -export default function HeadingLevelDropdown( { selectedLevel, onChange } ) { +export default function HeadingLevelDropdown( { + options = HEADING_LEVELS, + value, + onChange, +} ) { return ( } - label={ __( 'Change heading level' ) } - controls={ HEADING_LEVELS.map( ( targetLevel ) => { + icon={ } + label={ __( 'Change level' ) } + controls={ options.map( ( targetLevel ) => { { - const isActive = targetLevel === selectedLevel; + const isActive = targetLevel === value; return { icon: ( @@ -51,11 +56,14 @@ export default function HeadingLevelDropdown( { selectedLevel, onChange } ) { isPressed={ isActive } /> ), - label: sprintf( - // translators: %s: heading level e.g: "1", "2", "3" - __( 'Heading %d' ), - targetLevel - ), + label: + targetLevel === 0 + ? __( 'Paragraph' ) + : sprintf( + // translators: %s: heading level e.g: "1", "2", "3" + __( 'Heading %d' ), + targetLevel + ), isActive, onClick() { onChange( targetLevel ); diff --git a/packages/block-library/src/heading/heading-level-dropdown.native.js b/packages/block-editor/src/components/block-heading-level-dropdown/index.native.js similarity index 97% rename from packages/block-library/src/heading/heading-level-dropdown.native.js rename to packages/block-editor/src/components/block-heading-level-dropdown/index.native.js index e24374c86b995c..1ba451ff54b8f4 100644 --- a/packages/block-library/src/heading/heading-level-dropdown.native.js +++ b/packages/block-editor/src/components/block-heading-level-dropdown/index.native.js @@ -57,7 +57,7 @@ export default function HeadingLevelDropdown( { selectedLevel, onChange } ) { controls={ HEADING_LEVELS.map( ( index ) => createLevelControl( index, selectedLevel, onChange ) ) } - label={ __( 'Change heading level' ) } + label={ __( 'Change level' ) } /> ); } diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index fff793b1c5938e..e804771f9ce64a 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -26,6 +26,7 @@ export { default as BlockEdit, useBlockEditContext } from './block-edit'; export { default as BlockIcon } from './block-icon'; export { default as BlockNavigationDropdown } from './block-navigation/dropdown'; export { default as BlockStyles } from './block-styles'; +export { default as HeadingLevelDropdown } from './block-heading-level-dropdown'; export { default as __experimentalBlockVariationPicker } from './block-variation-picker'; export { default as __experimentalBlockPatternSetup } from './block-pattern-setup'; export { default as __experimentalBlockVariationTransforms } from './block-variation-transforms'; diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js index d5b182572e386c..a6ab6bb76d0480 100644 --- a/packages/block-editor/src/components/index.native.js +++ b/packages/block-editor/src/components/index.native.js @@ -28,6 +28,7 @@ export { JustifyContentControl, } from './justify-content-control'; export { default as LineHeightControl } from './line-height-control'; +export { default as HeadingLevelDropdown } from './block-heading-level-dropdown'; export { default as PlainText } from './plain-text'; export { default as RichText, diff --git a/packages/block-library/src/comments-title/edit.js b/packages/block-library/src/comments-title/edit.js index 72e16c88346831..3765a45d4f8bbe 100644 --- a/packages/block-library/src/comments-title/edit.js +++ b/packages/block-library/src/comments-title/edit.js @@ -12,6 +12,7 @@ import { useBlockProps, InspectorControls, store as blockEditorStore, + HeadingLevelDropdown, } from '@wordpress/block-editor'; import { __, _n, sprintf } from '@wordpress/i18n'; import { useEntityProp } from '@wordpress/core-data'; @@ -21,11 +22,6 @@ import { useSelect } from '@wordpress/data'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; -/** - * Internal dependencies - */ -import HeadingLevelDropdown from '../heading/heading-level-dropdown'; - export default function Edit( { attributes: { textAlign, showPostTitle, showCommentsCount, level }, setAttributes, @@ -98,7 +94,7 @@ export default function Edit( { } /> setAttributes( { level: newLevel } ) } diff --git a/packages/block-library/src/heading/edit.js b/packages/block-library/src/heading/edit.js index 3f8fd39ee9caa9..f0e9fd282be3ea 100644 --- a/packages/block-library/src/heading/edit.js +++ b/packages/block-library/src/heading/edit.js @@ -16,12 +16,12 @@ import { RichText, useBlockProps, store as blockEditorStore, + HeadingLevelDropdown, } from '@wordpress/block-editor'; /** * Internal dependencies */ -import HeadingLevelDropdown from './heading-level-dropdown'; import { generateAnchor, setAnchor } from './autogenerate-anchors'; function HeadingEdit( { @@ -92,7 +92,7 @@ function HeadingEdit( { <> setAttributes( { level: newLevel } ) } diff --git a/packages/block-library/src/post-title/edit.js b/packages/block-library/src/post-title/edit.js index 1838339dd16273..92521aa8d86b80 100644 --- a/packages/block-library/src/post-title/edit.js +++ b/packages/block-library/src/post-title/edit.js @@ -12,6 +12,7 @@ import { InspectorControls, useBlockProps, PlainText, + HeadingLevelDropdown, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { ToggleControl, TextControl, PanelBody } from '@wordpress/components'; @@ -22,7 +23,6 @@ import { useEntityProp } from '@wordpress/core-data'; /** * Internal dependencies */ -import HeadingLevelDropdown from '../heading/heading-level-dropdown'; import { useCanEditEntity } from '../utils/hooks'; import { unlock } from '../lock-unlock'; @@ -34,7 +34,7 @@ export default function PostTitleEdit( { context: { postType, postId, queryId }, insertBlocksAfter, } ) { - const TagName = 0 === level ? 'p' : 'h' + level; + const TagName = 'h' + level; const isDescendentOfQueryLoop = Number.isFinite( queryId ); /** * Hack: useCanEditEntity may trigger an OPTIONS request to the REST API via the canUser resolver. @@ -120,7 +120,7 @@ export default function PostTitleEdit( { { blockEditingMode === 'default' && ( setAttributes( { level: newLevel } ) } diff --git a/packages/block-library/src/post-title/index.php b/packages/block-library/src/post-title/index.php index e123a9993304b8..e1d4b255c57733 100644 --- a/packages/block-library/src/post-title/index.php +++ b/packages/block-library/src/post-title/index.php @@ -28,7 +28,7 @@ function render_block_core_post_title( $attributes, $content, $block ) { $tag_name = 'h2'; if ( isset( $attributes['level'] ) ) { - $tag_name = 0 === $attributes['level'] ? 'p' : 'h' . $attributes['level']; + $tag_name = 'h' . $attributes['level']; } if ( isset( $attributes['isLink'] ) && $attributes['isLink'] ) { diff --git a/packages/block-library/src/query-title/edit.js b/packages/block-library/src/query-title/edit.js index da321bead7c0b8..e5fa6f6c396774 100644 --- a/packages/block-library/src/query-title/edit.js +++ b/packages/block-library/src/query-title/edit.js @@ -12,15 +12,11 @@ import { InspectorControls, useBlockProps, Warning, + HeadingLevelDropdown, } from '@wordpress/block-editor'; import { ToggleControl, PanelBody } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -/** - * Internal dependencies - */ -import HeadingLevelDropdown from '../heading/heading-level-dropdown'; - const SUPPORTED_TYPES = [ 'archive', 'search' ]; export default function QueryTitleEdit( { @@ -98,7 +94,7 @@ export default function QueryTitleEdit( { <> setAttributes( { level: newLevel } ) } diff --git a/packages/block-library/src/site-title/edit/index.js b/packages/block-library/src/site-title/edit/index.js index d3224176a7e3a6..ad3136d106557f 100644 --- a/packages/block-library/src/site-title/edit/index.js +++ b/packages/block-library/src/site-title/edit/index.js @@ -15,15 +15,13 @@ import { InspectorControls, BlockControls, useBlockProps, + HeadingLevelDropdown, } from '@wordpress/block-editor'; import { ToggleControl, PanelBody } from '@wordpress/components'; import { createBlock, getDefaultBlockName } from '@wordpress/blocks'; import { decodeEntities } from '@wordpress/html-entities'; -/** - * Internal dependencies - */ -import LevelControl from './level-toolbar'; +const HEADING_LEVELS = [ 0, 1, 2, 3, 4, 5, 6 ]; export default function SiteTitleEdit( { attributes, @@ -95,8 +93,9 @@ export default function SiteTitleEdit( { return ( <> - setAttributes( { level: newLevel } ) } diff --git a/packages/block-library/src/site-title/edit/level-icon.js b/packages/block-library/src/site-title/edit/level-icon.js deleted file mode 100644 index 95295208b6e599..00000000000000 --- a/packages/block-library/src/site-title/edit/level-icon.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * WordPress dependencies - */ -import { SVG, Path } from '@wordpress/components'; -import { paragraph } from '@wordpress/icons'; - -export default function LevelIcon( { level, isPressed = false } ) { - if ( level === 0 ) return paragraph; - 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', - 3: 'M12.1 12.2c.4.3.8.5 1.2.7.4.2.9.3 1.4.3.5 0 1-.1 1.4-.3.3-.1.5-.5.5-.8 0-.2 0-.4-.1-.6-.1-.2-.3-.3-.5-.4-.3-.1-.7-.2-1-.3-.5-.1-1-.1-1.5-.1V9.1c.7.1 1.5-.1 2.2-.4.4-.2.6-.5.6-.9 0-.3-.1-.6-.4-.8-.3-.2-.7-.3-1.1-.3-.4 0-.8.1-1.1.3-.4.2-.7.4-1.1.6l-1.2-1.4c.5-.4 1.1-.7 1.6-.9.5-.2 1.2-.3 1.8-.3.5 0 1 .1 1.6.2.4.1.8.3 1.2.5.3.2.6.5.8.8.2.3.3.7.3 1.1 0 .5-.2.9-.5 1.3-.4.4-.9.7-1.5.9v.1c.6.1 1.2.4 1.6.8.4.4.7.9.7 1.5 0 .4-.1.8-.3 1.2-.2.4-.5.7-.9.9-.4.3-.9.4-1.3.5-.5.1-1 .2-1.6.2-.8 0-1.6-.1-2.3-.4-.6-.2-1.1-.6-1.6-1l1.1-1.4zM7 9H3V5H1v10h2v-4h4v4h2V5H7v4z', - 4: 'M9 15H7v-4H3v4H1V5h2v4h4V5h2v10zm10-2h-1v2h-2v-2h-5v-2l4-6h3v6h1v2zm-3-2V7l-2.8 4H16z', - 5: 'M12.1 12.2c.4.3.7.5 1.1.7.4.2.9.3 1.3.3.5 0 1-.1 1.4-.4.4-.3.6-.7.6-1.1 0-.4-.2-.9-.6-1.1-.4-.3-.9-.4-1.4-.4H14c-.1 0-.3 0-.4.1l-.4.1-.5.2-1-.6.3-5h6.4v1.9h-4.3L14 8.8c.2-.1.5-.1.7-.2.2 0 .5-.1.7-.1.5 0 .9.1 1.4.2.4.1.8.3 1.1.6.3.2.6.6.8.9.2.4.3.9.3 1.4 0 .5-.1 1-.3 1.4-.2.4-.5.8-.9 1.1-.4.3-.8.5-1.3.7-.5.2-1 .3-1.5.3-.8 0-1.6-.1-2.3-.4-.6-.2-1.1-.6-1.6-1-.1-.1 1-1.5 1-1.5zM9 15H7v-4H3v4H1V5h2v4h4V5h2v10z', - 6: 'M9 15H7v-4H3v4H1V5h2v4h4V5h2v10zm8.6-7.5c-.2-.2-.5-.4-.8-.5-.6-.2-1.3-.2-1.9 0-.3.1-.6.3-.8.5l-.6.9c-.2.5-.2.9-.2 1.4.4-.3.8-.6 1.2-.8.4-.2.8-.3 1.3-.3.4 0 .8 0 1.2.2.4.1.7.3 1 .6.3.3.5.6.7.9.2.4.3.8.3 1.3s-.1.9-.3 1.4c-.2.4-.5.7-.8 1-.4.3-.8.5-1.2.6-1 .3-2 .3-3 0-.5-.2-1-.5-1.4-.9-.4-.4-.8-.9-1-1.5-.2-.6-.3-1.3-.3-2.1s.1-1.6.4-2.3c.2-.6.6-1.2 1-1.6.4-.4.9-.7 1.4-.9.6-.3 1.1-.4 1.7-.4.7 0 1.4.1 2 .3.5.2 1 .5 1.4.8 0 .1-1.3 1.4-1.3 1.4zm-2.4 5.8c.2 0 .4 0 .6-.1.2 0 .4-.1.5-.2.1-.1.3-.3.4-.5.1-.2.1-.5.1-.7 0-.4-.1-.8-.4-1.1-.3-.2-.7-.3-1.1-.3-.3 0-.7.1-1 .2-.4.2-.7.4-1 .7 0 .3.1.7.3 1 .1.2.3.4.4.6.2.1.3.3.5.3.2.1.5.2.7.1z', - }; - return ( - - - - ); -} diff --git a/packages/block-library/src/site-title/edit/level-toolbar.js b/packages/block-library/src/site-title/edit/level-toolbar.js deleted file mode 100644 index 704c948d336731..00000000000000 --- a/packages/block-library/src/site-title/edit/level-toolbar.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * WordPress dependencies - */ -import { ToolbarDropdownMenu } from '@wordpress/components'; -import { __, sprintf } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import LevelIcon from './level-icon'; - -export default function LevelControl( { level, onChange } ) { - const allControls = [ 1, 2, 3, 4, 5, 6, 0 ].map( ( currentLevel ) => { - const isActive = currentLevel === level; - return { - icon: , - title: - currentLevel === 0 - ? __( 'Paragraph' ) - : // translators: %s: heading level e.g: "1", "2", "3" - sprintf( __( 'Heading %d' ), currentLevel ), - isActive, - onClick: () => onChange( currentLevel ), - role: 'menuitemradio', - }; - } ); - return ( - } - controls={ allControls } - /> - ); -} diff --git a/packages/e2e-tests/specs/editor/various/rich-text.test.js b/packages/e2e-tests/specs/editor/various/rich-text.test.js index c2098098d9cc73..41b2c7253dfa2e 100644 --- a/packages/e2e-tests/specs/editor/various/rich-text.test.js +++ b/packages/e2e-tests/specs/editor/various/rich-text.test.js @@ -25,8 +25,8 @@ describe( 'RichText', () => { // // See: https://github.com/WordPress/gutenberg/issues/3091 await insertBlock( 'Heading' ); - await page.waitForSelector( '[aria-label="Change heading level"]' ); - await page.click( '[aria-label="Change heading level"]' ); + await page.waitForSelector( '[aria-label="Change level"]' ); + await page.click( '[aria-label="Change level"]' ); await page.click( '[aria-label="Heading 3"]' ); expect( await getEditedPostContent() ).toMatchSnapshot(); diff --git a/test/e2e/specs/site-editor/template-part.spec.js b/test/e2e/specs/site-editor/template-part.spec.js index 5623c1171861ef..1eb5fdba0c70d6 100644 --- a/test/e2e/specs/site-editor/template-part.spec.js +++ b/test/e2e/specs/site-editor/template-part.spec.js @@ -391,7 +391,7 @@ test.describe( 'Template Part', () => { await editor.selectBlocks( siteTitleInGroup ); // Change heading level of the Site Title block. - await editor.clickBlockToolbarButton( 'Change heading level' ); + await editor.clickBlockToolbarButton( 'Change level' ); const Heading3Button = page.getByRole( 'menuitemradio', { name: 'Heading 3', } ); @@ -401,7 +401,7 @@ test.describe( 'Template Part', () => { await pageUtils.pressKeys( 'primary+z' ); await expect( - page.locator( 'role=button[name="Change heading level"i]' ) + page.locator( 'role=button[name="Change level"i]' ) ).toBeFocused(); } ); } );