diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss
index c945e2bf99a70a..8f232cb0a35e6f 100644
--- a/packages/block-library/src/editor.scss
+++ b/packages/block-library/src/editor.scss
@@ -17,6 +17,7 @@
@import "./classic/editor.scss";
@import "./gallery/editor.scss";
@import "./group/editor.scss";
+@import "./heading/editor.scss";
@import "./html/editor.scss";
@import "./image/editor.scss";
@import "./latest-comments/editor.scss";
diff --git a/packages/block-library/src/heading/edit.js b/packages/block-library/src/heading/edit.js
index 524eee735b57c9..d4a353c1b0ead4 100644
--- a/packages/block-library/src/heading/edit.js
+++ b/packages/block-library/src/heading/edit.js
@@ -3,25 +3,23 @@
*/
import classnames from 'classnames';
-/**
- * Internal dependencies
- */
-import HeadingToolbar from './heading-toolbar';
-
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { PanelBody, __experimentalText as Text } from '@wordpress/components';
import { createBlock } from '@wordpress/blocks';
import {
AlignmentToolbar,
BlockControls,
- InspectorControls,
RichText,
__experimentalBlock as Block,
} from '@wordpress/block-editor';
-import { Platform } from '@wordpress/element';
+import { ToolbarGroup } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import HeadingLevelDropdown from './heading-level-dropdown';
function HeadingEdit( {
attributes,
@@ -36,14 +34,14 @@ function HeadingEdit( {
return (
<>
-
- setAttributes( { level: newLevel } )
- }
- />
+
+
+ setAttributes( { level: newLevel } )
+ }
+ />
+
{
@@ -51,22 +49,6 @@ function HeadingEdit( {
} }
/>
- { Platform.OS === 'web' && (
-
-
- { __( 'Level' ) }
-
- setAttributes( { level: newLevel } )
- }
- />
-
-
- ) }
any} onChange Callback to run when
+ * toolbar value is changed.
+ */
+
+/**
+ * Dropdown for selecting a heading level (1 through 6).
+ *
+ * @param {WPHeadingLevelDropdownProps} props Component props.
+ *
+ * @return {WPComponent} The toolbar.
+ */
+export default function HeadingLevelDropdown( { selectedLevel, onChange } ) {
+ return (
+ {
+ const openOnArrowDown = ( event ) => {
+ if ( ! isOpen && event.keyCode === DOWN ) {
+ event.preventDefault();
+ event.stopPropagation();
+ onToggle();
+ }
+ };
+
+ return (
+ }
+ label={ __( 'Change heading level' ) }
+ onClick={ onToggle }
+ onKeyDown={ openOnArrowDown }
+ showTooltip
+ />
+ );
+ } }
+ renderContent={ () => (
+
+ {
+ const isActive = targetLevel === selectedLevel;
+ return {
+ icon: (
+
+ ),
+ title: sprintf(
+ // translators: %s: heading level e.g: "1", "2", "3"
+ __( 'Heading %d' ),
+ targetLevel
+ ),
+ isActive,
+ onClick() {
+ onChange( targetLevel );
+ },
+ // Temporary workaround for macOS Firefox/Safari issue
+ // where clicking buttons in the heading level toolbar
+ // doesn't work.
+ // TODO: Replace this with a more general solution.
+ // https://github.com/WordPress/gutenberg/pull/20246#pullrequestreview-417338057
+ onMouseDown( event ) {
+ event.preventDefault();
+ event.currentTarget.focus();
+ },
+ };
+ } ) }
+ />
+
+ ) }
+ />
+ );
+}
diff --git a/packages/block-library/src/heading/heading-level-dropdown.native.js b/packages/block-library/src/heading/heading-level-dropdown.native.js
new file mode 100644
index 00000000000000..e24374c86b995c
--- /dev/null
+++ b/packages/block-library/src/heading/heading-level-dropdown.native.js
@@ -0,0 +1,63 @@
+/**
+ * WordPress dependencies
+ */
+import { __, sprintf } from '@wordpress/i18n';
+import { DropdownMenu } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import HeadingLevelIcon from './heading-level-icon';
+
+const HEADING_LEVELS = [ 1, 2, 3, 4, 5, 6 ];
+
+/** @typedef {import('@wordpress/element').WPComponent} WPComponent */
+
+/**
+ * HeadingLevelDropdown props.
+ *
+ * @typedef WPHeadingLevelDropdownProps
+ *
+ * @property {number} selectedLevel The chosen heading level.
+ * @property {(newValue:number)=>any} onChange Callback to run when
+ * toolbar value is changed.
+ */
+
+/**
+ * Dropdown for selecting a heading level (1 through 6).
+ *
+ * @param {WPHeadingLevelDropdownProps} props Component props.
+ *
+ * @return {WPComponent} The toolbar.
+ */
+export default function HeadingLevelDropdown( { selectedLevel, onChange } ) {
+ const createLevelControl = (
+ targetLevel,
+ currentLevel,
+ onChangeCallback
+ ) => {
+ const isActive = targetLevel === currentLevel;
+ return {
+ icon: (
+
+ ),
+ // translators: %s: heading level e.g: "1", "2", "3"
+ title: sprintf( __( 'Heading %d' ), targetLevel ),
+ isActive,
+ onClick: () => onChangeCallback( targetLevel ),
+ };
+ };
+
+ return (
+ }
+ controls={ HEADING_LEVELS.map( ( index ) =>
+ createLevelControl( index, selectedLevel, onChange )
+ ) }
+ label={ __( 'Change heading level' ) }
+ />
+ );
+}
diff --git a/packages/block-library/src/heading/heading-level-icon.js b/packages/block-library/src/heading/heading-level-icon.js
index eef2b3af3b5e7e..b3288d02761612 100644
--- a/packages/block-library/src/heading/heading-level-icon.js
+++ b/packages/block-library/src/heading/heading-level-icon.js
@@ -3,6 +3,24 @@
*/
import { Path, SVG } from '@wordpress/components';
+/** @typedef {import('@wordpress/element').WPComponent} WPComponent */
+
+/**
+ * HeadingLevelIcon props.
+ *
+ * @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.
+ */
+
+/**
+ * Heading level icon.
+ *
+ * @param {WPHeadingLevelIconProps} props Component props.
+ *
+ * @return {?WPComponent} The icon.
+ */
export default function HeadingLevelIcon( { level, isPressed = false } ) {
const levelToPath = {
1: 'M9 5h2v10H9v-4H5v4H3V5h2v4h4V5zm6.6 0c-.6.9-1.5 1.7-2.6 2v1h2v7h2V5h-1.4z',
diff --git a/packages/block-library/src/heading/heading-toolbar.js b/packages/block-library/src/heading/heading-toolbar.js
deleted file mode 100644
index 97cd4f12cfe23c..00000000000000
--- a/packages/block-library/src/heading/heading-toolbar.js
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * External dependencies
- */
-import { range } from 'lodash';
-
-/**
- * WordPress dependencies
- */
-import { __, sprintf } from '@wordpress/i18n';
-import { Component } from '@wordpress/element';
-import { ToolbarGroup } from '@wordpress/components';
-
-/**
- * Internal dependencies
- */
-import HeadingLevelIcon from './heading-level-icon';
-
-class HeadingToolbar extends Component {
- createLevelControl( targetLevel, selectedLevel, onChange ) {
- const isActive = targetLevel === selectedLevel;
- return {
- icon: (
-
- ),
- // translators: %s: heading level e.g: "1", "2", "3"
- title: sprintf( __( 'Heading %d' ), targetLevel ),
- isActive,
- onClick: () => onChange( targetLevel ),
- };
- }
-
- render() {
- const {
- isCollapsed = true,
- minLevel,
- maxLevel,
- selectedLevel,
- onChange,
- } = this.props;
-
- return (
- }
- controls={ range( minLevel, maxLevel ).map( ( index ) =>
- this.createLevelControl( index, selectedLevel, onChange )
- ) }
- label={ __( 'Change heading level' ) }
- />
- );
- }
-}
-
-export default HeadingToolbar;
diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js
index bae36d9885b452..4168d198185ea4 100644
--- a/packages/components/src/index.native.js
+++ b/packages/components/src/index.native.js
@@ -12,6 +12,7 @@ export { default as ColorIndicator } from './color-indicator';
export { default as ColorPalette } from './color-palette';
export { default as Dashicon } from './dashicon';
export { default as Dropdown } from './dropdown';
+export { default as DropdownMenu } from './dropdown-menu';
export { default as Toolbar } from './toolbar';
export { default as ToolbarButton } from './toolbar-button';
export { default as __experimentalToolbarContext } from './toolbar-context';
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 8135962e22556e..1ac2080a1631cb 100644
--- a/packages/e2e-tests/specs/editor/various/rich-text.test.js
+++ b/packages/e2e-tests/specs/editor/various/rich-text.test.js
@@ -7,7 +7,6 @@ import {
insertBlock,
clickBlockAppender,
pressKeyWithModifier,
- openDocumentSettingsSidebar,
} from '@wordpress/e2e-test-utils';
describe( 'RichText', () => {
@@ -23,7 +22,8 @@ describe( 'RichText', () => {
//
// See: https://github.com/WordPress/gutenberg/issues/3091
await insertBlock( 'Heading' );
- await openDocumentSettingsSidebar();
+ await page.waitForSelector( '[aria-label="Change heading level"]' );
+ await page.click( '[aria-label="Change heading level"]' );
await page.click( '[aria-label="Heading 3"]' );
expect( await getEditedPostContent() ).toMatchSnapshot();