diff --git a/lib/client-assets.php b/lib/client-assets.php
index 1f3016019b6dc7..9c05c51667eb7a 100644
--- a/lib/client-assets.php
+++ b/lib/client-assets.php
@@ -647,6 +647,19 @@ function gutenberg_extend_settings_block_patterns( $settings ) {
}
add_filter( 'block_editor_settings', 'gutenberg_extend_settings_block_patterns', 0 );
+/**
+ * Extends block editor settings to determine whether to use custom line height controls.
+ * Currently experimental.
+ *
+ * @param array $settings Default editor settings.
+ *
+ * @return array Filtered editor settings.
+ */
+function gutenberg_extend_settings_custom_line_height( $settings ) {
+ $settings['__experimentalDisableCustomLineHeight'] = get_theme_support( 'disable-custom-line-height' );
+ return $settings;
+}
+add_filter( 'block_editor_settings', 'gutenberg_extend_settings_custom_line_height' );
/*
* Register default patterns if not registered in Core already.
diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js
index 0fd7fb2d4fb09b..0463948da0ce36 100644
--- a/packages/block-editor/src/components/index.js
+++ b/packages/block-editor/src/components/index.js
@@ -34,6 +34,7 @@ export { default as InnerBlocks } from './inner-blocks';
export { default as InspectorAdvancedControls } from './inspector-advanced-controls';
export { default as InspectorControls } from './inspector-controls';
export { default as __experimentalLinkControl } from './link-control';
+export { default as __experimentalLineHeightControl } from './line-height-control';
export { default as MediaReplaceFlow } from './media-replace-flow';
export { default as MediaPlaceholder } from './media-placeholder';
export { default as MediaUpload } from './media-upload';
diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js
index 130cde1c2303bb..a970323a0350d9 100644
--- a/packages/block-editor/src/components/index.native.js
+++ b/packages/block-editor/src/components/index.native.js
@@ -11,6 +11,7 @@ export * from './font-sizes';
export { default as AlignmentToolbar } from './alignment-toolbar';
export { default as InnerBlocks } from './inner-blocks';
export { default as InspectorControls } from './inspector-controls';
+export { default as __experimentalLineHeightControl } from './line-height-control';
export { default as PlainText } from './plain-text';
export {
default as RichText,
diff --git a/packages/block-editor/src/components/line-height-control/index.js b/packages/block-editor/src/components/line-height-control/index.js
new file mode 100644
index 00000000000000..4aa913901a80b7
--- /dev/null
+++ b/packages/block-editor/src/components/line-height-control/index.js
@@ -0,0 +1,88 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { TextControl } from '@wordpress/components';
+import { ZERO } from '@wordpress/keycodes';
+
+/**
+ * Internal dependencies
+ */
+import {
+ BASE_DEFAULT_VALUE,
+ RESET_VALUE,
+ STEP,
+ useIsLineHeightControlsDisabled,
+ isLineHeightDefined,
+} from './utils';
+
+export default function LineHeightControl( { value: lineHeight, onChange } ) {
+ const isDisabled = useIsLineHeightControlsDisabled();
+ const isDefined = isLineHeightDefined( lineHeight );
+
+ // Don't render the controls if disabled by editor settings
+ if ( isDisabled ) {
+ return null;
+ }
+
+ const handleOnKeyDown = ( event ) => {
+ const { keyCode } = event;
+
+ if ( keyCode === ZERO && ! isDefined ) {
+ /**
+ * Prevents the onChange callback from firing, which prevents
+ * the logic from assuming the change was triggered from
+ * an input arrow CLICK.
+ */
+ event.preventDefault();
+ onChange( '0' );
+ }
+ };
+
+ const handleOnChange = ( nextValue ) => {
+ // Set the next value without modification if lineHeight has been defined
+ if ( isDefined ) {
+ onChange( nextValue );
+ return;
+ }
+
+ // Otherwise...
+ /**
+ * The following logic handles the initial up/down arrow CLICK of the
+ * input element. This is so that the next values (from an undefined value state)
+ * are more better suited for line-height rendering.
+ */
+ let adjustedNextValue = nextValue;
+
+ switch ( nextValue ) {
+ case `${ STEP }`:
+ // Increment by step value
+ adjustedNextValue = BASE_DEFAULT_VALUE + STEP;
+ break;
+ case '0':
+ // Decrement by step value
+ adjustedNextValue = BASE_DEFAULT_VALUE - STEP;
+ break;
+ }
+
+ onChange( adjustedNextValue );
+ };
+
+ const value = isDefined ? lineHeight : RESET_VALUE;
+
+ return (
+
+
+
+ );
+}
diff --git a/packages/block-editor/src/components/line-height-control/style.scss b/packages/block-editor/src/components/line-height-control/style.scss
new file mode 100644
index 00000000000000..ad98d76e4a2ed1
--- /dev/null
+++ b/packages/block-editor/src/components/line-height-control/style.scss
@@ -0,0 +1,8 @@
+.block-editor-line-height-control {
+ margin-bottom: 24px;
+
+ input {
+ display: block;
+ max-width: 60px;
+ }
+}
diff --git a/packages/block-editor/src/components/line-height-control/utils.js b/packages/block-editor/src/components/line-height-control/utils.js
new file mode 100644
index 00000000000000..351a35a66a6a0a
--- /dev/null
+++ b/packages/block-editor/src/components/line-height-control/utils.js
@@ -0,0 +1,44 @@
+/**
+ * WordPress dependencies
+ */
+import { useSelect } from '@wordpress/data';
+
+export const BASE_DEFAULT_VALUE = 1.5;
+export const STEP = 0.1;
+/**
+ * There are varying value types within LineHeightControl:
+ *
+ * {undefined} Initial value. No changes from the user.
+ * {string} Input value. Value consumed/outputted by the input. Empty would be ''.
+ * {number} Block attribute type. Input value needs to be converted for attribute setting.
+ *
+ * Note: If the value is undefined, the input requires it to be an empty string ('')
+ * in order to be considered "controlled" by props (rather than internal state).
+ */
+export const RESET_VALUE = '';
+
+/**
+ * Retrieves whether custom lineHeight controls should be disabled from editor settings.
+ *
+ * @return {boolean} Whether lineHeight controls should be disabled.
+ */
+export function useIsLineHeightControlsDisabled() {
+ const isDisabled = useSelect( ( select ) => {
+ const { getSettings } = select( 'core/block-editor' );
+
+ return !! getSettings().__experimentalDisableCustomLineHeight;
+ }, [] );
+
+ return isDisabled;
+}
+
+/**
+ * Determines if the lineHeight attribute has been properly defined.
+ *
+ * @param {any} lineHeight The value to check.
+ *
+ * @return {boolean} Whether the lineHeight attribute is valid.
+ */
+export function isLineHeightDefined( lineHeight ) {
+ return lineHeight !== undefined && lineHeight !== RESET_VALUE;
+}
diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js
index f02476e82caeca..61d85140cf249b 100644
--- a/packages/block-editor/src/hooks/color.js
+++ b/packages/block-editor/src/hooks/color.js
@@ -2,7 +2,6 @@
* External dependencies
*/
import classnames from 'classnames';
-import { pickBy, isEqual, isObject, identity, mapValues } from 'lodash';
/**
* WordPress dependencies
@@ -26,22 +25,10 @@ import PanelColorSettings from '../components/panel-color-settings';
import ContrastChecker from '../components/contrast-checker';
import InspectorControls from '../components/inspector-controls';
import { getBlockDOMNode } from '../utils/dom';
+import { cleanEmptyObject } from './utils';
export const COLOR_SUPPORT_KEY = '__experimentalColor';
-export const cleanEmptyObject = ( object ) => {
- if ( ! isObject( object ) ) {
- return object;
- }
- const cleanedNestedObjects = pickBy(
- mapValues( object, cleanEmptyObject ),
- identity
- );
- return isEqual( cleanedNestedObjects, {} )
- ? undefined
- : cleanedNestedObjects;
-};
-
/**
* Filters registered block settings, extending attributes to include
* `backgroundColor` and `textColor` attribute.
diff --git a/packages/block-editor/src/hooks/line-height.js b/packages/block-editor/src/hooks/line-height.js
new file mode 100644
index 00000000000000..6f813032218118
--- /dev/null
+++ b/packages/block-editor/src/hooks/line-height.js
@@ -0,0 +1,64 @@
+/**
+ * WordPress dependencies
+ */
+import { addFilter } from '@wordpress/hooks';
+import { hasBlockSupport } from '@wordpress/blocks';
+import { createHigherOrderComponent } from '@wordpress/compose';
+import { PanelBody } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import LineHeightControl from '../components/line-height-control';
+import InspectorControls from '../components/inspector-controls';
+import { cleanEmptyObject } from './utils';
+
+export const LINE_HEIGHT_SUPPRT_KEY = '__experimentalLineHeight';
+
+/**
+ * Override the default edit UI to include new inspector controls for block
+ * color, if block defines support.
+ *
+ * @param {Function} BlockEdit Original component
+ * @return {Function} Wrapped component
+ */
+export const withBlockControls = createHigherOrderComponent(
+ ( BlockEdit ) => ( props ) => {
+ const { name: blockName } = props;
+ if ( ! hasBlockSupport( blockName, LINE_HEIGHT_SUPPRT_KEY ) ) {
+ return ;
+ }
+ const { style } = props.attributes;
+ const onChange = ( newLineHeightValue ) => {
+ const newStyle = {
+ ...style,
+ typography: {
+ lineHeight: newLineHeightValue,
+ },
+ };
+ props.setAttributes( {
+ style: cleanEmptyObject( newStyle ),
+ } );
+ };
+
+ return [
+
+
+
+
+ ,
+ ,
+ ];
+ },
+ 'withToolbarControls'
+);
+
+addFilter(
+ 'editor.BlockEdit',
+ 'core/color/with-block-controls',
+ withBlockControls
+);
diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js
index 5eea58cea5b9f4..9ffd3fd34d0ca7 100644
--- a/packages/block-editor/src/hooks/style.js
+++ b/packages/block-editor/src/hooks/style.js
@@ -13,8 +13,9 @@ import { hasBlockSupport } from '@wordpress/blocks';
* Internal dependencies
*/
import { COLOR_SUPPORT_KEY } from './color';
+import { LINE_HEIGHT_SUPPRT_KEY } from './line-height';
-const styleSupportKeys = [ COLOR_SUPPORT_KEY ];
+const styleSupportKeys = [ COLOR_SUPPORT_KEY, LINE_HEIGHT_SUPPRT_KEY ];
const hasStyleSupport = ( blockType ) =>
styleSupportKeys.some( ( key ) => hasBlockSupport( blockType, key ) );
diff --git a/packages/block-editor/src/hooks/test/color.js b/packages/block-editor/src/hooks/test/color.js
index d8644c931a3e58..9c3361ce80c950 100644
--- a/packages/block-editor/src/hooks/test/color.js
+++ b/packages/block-editor/src/hooks/test/color.js
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
-import { cleanEmptyObject } from '../color';
+import { cleanEmptyObject } from '../utils';
describe( 'cleanEmptyObject', () => {
it( 'should remove nested keys', () => {
diff --git a/packages/block-editor/src/hooks/test/anchor.js b/packages/block-editor/src/hooks/test/utils.js
similarity index 100%
rename from packages/block-editor/src/hooks/test/anchor.js
rename to packages/block-editor/src/hooks/test/utils.js
diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js
new file mode 100644
index 00000000000000..dba018f2cfc8aa
--- /dev/null
+++ b/packages/block-editor/src/hooks/utils.js
@@ -0,0 +1,23 @@
+/**
+ * External dependencies
+ */
+import { pickBy, isEqual, isObject, identity, mapValues } from 'lodash';
+
+/**
+ * Removed undefined values from nested object.
+ *
+ * @param {*} object
+ * @return {*} Object cleaned from undefined values
+ */
+export const cleanEmptyObject = ( object ) => {
+ if ( ! isObject( object ) ) {
+ return object;
+ }
+ const cleanedNestedObjects = pickBy(
+ mapValues( object, cleanEmptyObject ),
+ identity
+ );
+ return isEqual( cleanedNestedObjects, {} )
+ ? undefined
+ : cleanedNestedObjects;
+};
diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss
index cdf78fbc8f4981..3f4ae8b3b31369 100644
--- a/packages/block-editor/src/style.scss
+++ b/packages/block-editor/src/style.scss
@@ -27,6 +27,7 @@
@import "./components/contrast-checker/style.scss";
@import "./components/default-block-appender/style.scss";
@import "./components/link-control/style.scss";
+@import "./components/line-height-control/style.scss";
@import "./components/image-size-control/style.scss";
@import "./components/inner-blocks/style.scss";
@import "./components/inserter-list-item/style.scss";
diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json
index ff252ec7760a3a..dc3d4ef895c8e7 100644
--- a/packages/block-library/src/paragraph/block.json
+++ b/packages/block-library/src/paragraph/block.json
@@ -26,7 +26,10 @@
},
"direction": {
"type": "string",
- "enum": [ "ltr", "rtl" ]
+ "enum": [
+ "ltr",
+ "rtl"
+ ]
}
}
}
diff --git a/packages/block-library/src/paragraph/edit.js b/packages/block-library/src/paragraph/edit.js
index cda1879390d2cf..e09aecea77c810 100644
--- a/packages/block-library/src/paragraph/edit.js
+++ b/packages/block-library/src/paragraph/edit.js
@@ -80,13 +80,19 @@ function ParagraphBlock( {
setAttributes,
setFontSize,
} ) {
- const { align, content, dropCap, placeholder, direction } = attributes;
+ const { align, content, direction, dropCap, placeholder } = attributes;
const ref = useRef();
const dropCapMinimumHeight = useDropCapMinimumHeight( dropCap, [
fontSize.size,
] );
+ const styles = {
+ fontSize: fontSize.size ? `${ fontSize.size }px` : undefined,
+ direction,
+ minHeight: dropCapMinimumHeight,
+ };
+
return (
<>
@@ -132,11 +138,7 @@ function ParagraphBlock( {
[ `has-text-align-${ align }` ]: align,
[ fontSize.class ]: fontSize.class,
} ) }
- style={ {
- fontSize: fontSize.size ? fontSize.size + 'px' : undefined,
- direction,
- minHeight: dropCapMinimumHeight,
- } }
+ style={ styles }
value={ content }
onChange={ ( newContent ) =>
setAttributes( { content: newContent } )
diff --git a/packages/block-library/src/paragraph/index.js b/packages/block-library/src/paragraph/index.js
index c04c011c1b0c67..9ce400ec4077d1 100644
--- a/packages/block-library/src/paragraph/index.js
+++ b/packages/block-library/src/paragraph/index.js
@@ -41,6 +41,7 @@ export const settings = {
__unstablePasteTextInline: true,
lightBlockWrapper: true,
__experimentalColor: true,
+ __experimentalLineHeight: true,
},
__experimentalLabel( attributes, { context } ) {
if ( context === 'accessibility' ) {
diff --git a/packages/block-library/src/paragraph/style.scss b/packages/block-library/src/paragraph/style.scss
index f43612c851d1fe..9c0f3d7edc7612 100644
--- a/packages/block-library/src/paragraph/style.scss
+++ b/packages/block-library/src/paragraph/style.scss
@@ -1,6 +1,7 @@
:root p {
background-color: var(--wp--color--background);
color: var(--wp--color--text);
+ line-height: var(--wp--typography--line-height);
}
.is-small-text {
diff --git a/packages/e2e-tests/specs/editor/various/embedding.test.js b/packages/e2e-tests/specs/editor/various/embedding.test.js
index b33a24fd787208..9e30ec116f3c37 100644
--- a/packages/e2e-tests/specs/editor/various/embedding.test.js
+++ b/packages/e2e-tests/specs/editor/various/embedding.test.js
@@ -307,7 +307,7 @@ describe( 'Embedding content', () => {
await page.keyboard.type( 'Hello there!' );
await publishPost();
const postUrl = await page.$eval(
- '[id^=inspector-text-control-]',
+ '.editor-post-publish-panel [id^=inspector-text-control-]',
( el ) => el.value
);
diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js
index 555b7b88f02509..5cbd7751736b93 100644
--- a/packages/editor/src/components/provider/index.js
+++ b/packages/editor/src/components/provider/index.js
@@ -123,6 +123,7 @@ class EditorProvider extends Component {
'__experimentalEnableFullSiteEditingDemo',
'__experimentalGlobalStylesUserEntityId',
'__experimentalGlobalStylesBase',
+ '__experimentalDisableCustomLineHeight',
'gradients',
] ),
mediaUpload: hasUploadPermissions ? mediaUpload : undefined,
diff --git a/packages/editor/src/editor-styles.scss b/packages/editor/src/editor-styles.scss
index f8eee44a116938..a7608813a5fcc2 100644
--- a/packages/editor/src/editor-styles.scss
+++ b/packages/editor/src/editor-styles.scss
@@ -95,7 +95,6 @@ h6 {
p {
font-size: inherit;
- line-height: inherit;
margin-top: $default-block-margin;
margin-bottom: $default-block-margin;
}
diff --git a/packages/keycodes/README.md b/packages/keycodes/README.md
index 7d54480a89b4bd..bb7036ba95de74 100644
--- a/packages/keycodes/README.md
+++ b/packages/keycodes/README.md
@@ -162,6 +162,10 @@ Keycode for TAB key.
Keycode for UP key.
+# **ZERO**
+
+Keycode for ZERO key.
+
diff --git a/packages/keycodes/src/index.js b/packages/keycodes/src/index.js
index 16a002973ec070..fc975909aa4e4d 100644
--- a/packages/keycodes/src/index.js
+++ b/packages/keycodes/src/index.js
@@ -95,6 +95,10 @@ export const COMMAND = 'meta';
* Keycode for SHIFT key.
*/
export const SHIFT = 'shift';
+/**
+ * Keycode for ZERO key.
+ */
+export const ZERO = 48;
/**
* Object that contains functions that return the available modifier