diff --git a/packages/block-library/src/cover/edit.js b/packages/block-library/src/cover/edit.js
deleted file mode 100644
index a61003e6dc4ff9..00000000000000
--- a/packages/block-library/src/cover/edit.js
+++ /dev/null
@@ -1,762 +0,0 @@
-/**
- * External dependencies
- */
-import classnames from 'classnames';
-import { extend } from 'colord';
-import namesPlugin from 'colord/plugins/names';
-
-/**
- * WordPress dependencies
- */
-import { useEntityProp, store as coreStore } from '@wordpress/core-data';
-import {
- Fragment,
- useEffect,
- useRef,
- useState,
- useMemo,
-} from '@wordpress/element';
-import {
- BaseControl,
- Button,
- ExternalLink,
- FocalPointPicker,
- PanelBody,
- PanelRow,
- RangeControl,
- ResizableBox,
- Spinner,
- TextareaControl,
- ToggleControl,
- ToolbarButton,
- __experimentalUseCustomUnits as useCustomUnits,
- __experimentalToolsPanelItem as ToolsPanelItem,
- __experimentalUnitControl as UnitControl,
- __experimentalParseQuantityAndUnitFromRawValue as parseQuantityAndUnitFromRawValue,
-} from '@wordpress/components';
-import { compose, useInstanceId } from '@wordpress/compose';
-import {
- BlockControls,
- BlockIcon,
- InspectorControls,
- MediaPlaceholder,
- MediaReplaceFlow,
- withColors,
- ColorPalette,
- useBlockProps,
- useSetting,
- useInnerBlocksProps,
- __experimentalUseGradient,
- __experimentalPanelColorGradientSettings as PanelColorGradientSettings,
- __experimentalBlockAlignmentMatrixControl as BlockAlignmentMatrixControl,
- __experimentalBlockFullHeightAligmentControl as FullHeightAlignmentControl,
- store as blockEditorStore,
-} from '@wordpress/block-editor';
-import { __ } from '@wordpress/i18n';
-import { useSelect, useDispatch } from '@wordpress/data';
-import { postFeaturedImage, cover as icon } from '@wordpress/icons';
-import { isBlobURL } from '@wordpress/blob';
-import { store as noticesStore } from '@wordpress/notices';
-
-/**
- * Internal dependencies
- */
-import {
- ALLOWED_MEDIA_TYPES,
- attributesFromMedia,
- IMAGE_BACKGROUND_TYPE,
- VIDEO_BACKGROUND_TYPE,
- COVER_MIN_HEIGHT,
- backgroundImageStyles,
- dimRatioToClass,
- isContentPositionCenter,
- getPositionClassName,
-} from './shared';
-import useCoverIsDark from './use-cover-is-dark';
-
-extend( [ namesPlugin ] );
-
-function getInnerBlocksTemplate( attributes ) {
- return [
- [
- 'core/paragraph',
- {
- align: 'center',
- placeholder: __( 'Write title…' ),
- ...attributes,
- },
- ],
- ];
-}
-
-function CoverHeightInput( {
- onChange,
- onUnitChange,
- unit = 'px',
- value = '',
-} ) {
- const instanceId = useInstanceId( UnitControl );
- const inputId = `block-cover-height-input-${ instanceId }`;
- const isPx = unit === 'px';
-
- const units = useCustomUnits( {
- availableUnits: useSetting( 'spacing.units' ) || [
- 'px',
- 'em',
- 'rem',
- 'vw',
- 'vh',
- ],
- defaultValues: { px: 430, '%': 20, em: 20, rem: 20, vw: 20, vh: 50 },
- } );
-
- const handleOnChange = ( unprocessedValue ) => {
- const inputValue =
- unprocessedValue !== ''
- ? parseFloat( unprocessedValue )
- : undefined;
-
- if ( isNaN( inputValue ) && inputValue !== undefined ) {
- return;
- }
- onChange( inputValue );
- };
-
- const computedValue = useMemo( () => {
- const [ parsedQuantity ] = parseQuantityAndUnitFromRawValue( value );
- return [ parsedQuantity, unit ].join( '' );
- }, [ unit, value ] );
-
- const min = isPx ? COVER_MIN_HEIGHT : 0;
-
- return (
-
-
-
- );
-}
-
-const RESIZABLE_BOX_ENABLE_OPTION = {
- top: false,
- right: false,
- bottom: true,
- left: false,
- topRight: false,
- bottomRight: false,
- bottomLeft: false,
- topLeft: false,
-};
-
-function ResizableCover( {
- className,
- onResizeStart,
- onResize,
- onResizeStop,
- ...props
-} ) {
- const [ isResizing, setIsResizing ] = useState( false );
-
- return (
- {
- onResizeStart( elt.clientHeight );
- onResize( elt.clientHeight );
- } }
- onResize={ ( _event, _direction, elt ) => {
- onResize( elt.clientHeight );
- if ( ! isResizing ) {
- setIsResizing( true );
- }
- } }
- onResizeStop={ ( _event, _direction, elt ) => {
- onResizeStop( elt.clientHeight );
- setIsResizing( false );
- } }
- { ...props }
- />
- );
-}
-
-function mediaPosition( { x, y } ) {
- return `${ Math.round( x * 100 ) }% ${ Math.round( y * 100 ) }%`;
-}
-
-/**
- * Is the URL a temporary blob URL? A blob URL is one that is used temporarily while
- * the media (image or video) is being uploaded and will not have an id allocated yet.
- *
- * @param {number} id The id of the media.
- * @param {string} url The url of the media.
- *
- * @return {boolean} Is the URL a Blob URL.
- */
-const isTemporaryMedia = ( id, url ) => ! id && isBlobURL( url );
-
-function CoverPlaceholder( {
- disableMediaButtons = false,
- children,
- onSelectMedia,
- onError,
- style,
-} ) {
- return (
- }
- labels={ {
- title: __( 'Cover' ),
- instructions: __(
- 'Drag and drop onto this block, upload, or select existing media from your library.'
- ),
- } }
- onSelect={ onSelectMedia }
- accept="image/*,video/*"
- allowedTypes={ ALLOWED_MEDIA_TYPES }
- disableMediaButtons={ disableMediaButtons }
- onError={ onError }
- style={ style }
- >
- { children }
-
- );
-}
-
-function CoverEdit( {
- attributes,
- clientId,
- isSelected,
- overlayColor,
- setAttributes,
- setOverlayColor,
- toggleSelection,
- context: { postId, postType },
-} ) {
- const {
- contentPosition,
- id,
- useFeaturedImage,
- dimRatio,
- focalPoint,
- hasParallax,
- isDark,
- isRepeated,
- minHeight,
- minHeightUnit,
- alt,
- allowedBlocks,
- templateLock,
- } = attributes;
-
- const [ featuredImage ] = useEntityProp(
- 'postType',
- postType,
- 'featured_media',
- postId
- );
-
- const media = useSelect(
- ( select ) =>
- featuredImage &&
- select( coreStore ).getMedia( featuredImage, { context: 'view' } ),
- [ featuredImage ]
- );
- const mediaUrl = media?.source_url;
-
- // instead of destructuring the attributes
- // we define the url and background type
- // depending on the value of the useFeaturedImage flag
- // to preview in edit the dynamic featured image
- const url = useFeaturedImage ? mediaUrl : attributes.url;
- const backgroundType = useFeaturedImage
- ? IMAGE_BACKGROUND_TYPE
- : attributes.backgroundType;
-
- const { __unstableMarkNextChangeAsNotPersistent } = useDispatch(
- blockEditorStore
- );
- const { createErrorNotice } = useDispatch( noticesStore );
- const {
- gradientClass,
- gradientValue,
- setGradient,
- } = __experimentalUseGradient();
- const onSelectMedia = attributesFromMedia( setAttributes, dimRatio );
- const isUploadingMedia = isTemporaryMedia( id, url );
-
- const [ prevMinHeightValue, setPrevMinHeightValue ] = useState( minHeight );
- const [ prevMinHeightUnit, setPrevMinHeightUnit ] = useState(
- minHeightUnit
- );
- const isMinFullHeight = minHeightUnit === 'vh' && minHeight === 100;
-
- const toggleMinFullHeight = () => {
- if ( isMinFullHeight ) {
- // If there aren't previous values, take the default ones.
- if ( prevMinHeightUnit === 'vh' && prevMinHeightValue === 100 ) {
- return setAttributes( {
- minHeight: undefined,
- minHeightUnit: undefined,
- } );
- }
-
- // Set the previous values of height.
- return setAttributes( {
- minHeight: prevMinHeightValue,
- minHeightUnit: prevMinHeightUnit,
- } );
- }
-
- setPrevMinHeightValue( minHeight );
- setPrevMinHeightUnit( minHeightUnit );
-
- // Set full height.
- return setAttributes( {
- minHeight: 100,
- minHeightUnit: 'vh',
- } );
- };
-
- const toggleParallax = () => {
- setAttributes( {
- hasParallax: ! hasParallax,
- ...( ! hasParallax ? { focalPoint: undefined } : {} ),
- } );
- };
-
- const toggleIsRepeated = () => {
- setAttributes( {
- isRepeated: ! isRepeated,
- } );
- };
-
- const toggleUseFeaturedImage = () => {
- setAttributes( {
- id: undefined,
- url: undefined,
- useFeaturedImage: ! useFeaturedImage,
- dimRatio: dimRatio === 100 ? 50 : dimRatio,
- backgroundType: useFeaturedImage
- ? IMAGE_BACKGROUND_TYPE
- : undefined,
- } );
- };
-
- const onUploadError = ( message ) => {
- createErrorNotice( Array.isArray( message ) ? message[ 2 ] : message, {
- type: 'snackbar',
- } );
- };
-
- const isDarkElement = useRef();
- const isCoverDark = useCoverIsDark(
- url,
- dimRatio,
- overlayColor.color,
- isDarkElement
- );
-
- useEffect( () => {
- // This side-effect should not create an undo level.
- __unstableMarkNextChangeAsNotPersistent();
- setAttributes( { isDark: isCoverDark } );
- }, [ isCoverDark ] );
-
- const isImageBackground = IMAGE_BACKGROUND_TYPE === backgroundType;
- const isVideoBackground = VIDEO_BACKGROUND_TYPE === backgroundType;
-
- const minHeightWithUnit =
- minHeight && minHeightUnit
- ? `${ minHeight }${ minHeightUnit }`
- : minHeight;
-
- const isImgElement = ! ( hasParallax || isRepeated );
-
- const style = {
- ...( isImageBackground && ! isImgElement
- ? backgroundImageStyles( url )
- : undefined ),
- minHeight: minHeightWithUnit || undefined,
- };
-
- const bgStyle = { backgroundColor: overlayColor.color };
- const mediaStyle = {
- objectPosition:
- focalPoint && isImgElement
- ? mediaPosition( focalPoint )
- : undefined,
- };
-
- const hasBackground = !! ( url || overlayColor.color || gradientValue );
- const showFocalPointPicker =
- isVideoBackground ||
- ( isImageBackground && ( ! hasParallax || isRepeated ) );
-
- const imperativeFocalPointPreview = ( value ) => {
- const [ styleOfRef, property ] = isDarkElement.current
- ? [ isDarkElement.current.style, 'objectPosition' ]
- : [ ref.current.style, 'backgroundPosition' ];
- styleOfRef[ property ] = mediaPosition( value );
- };
-
- const hasInnerBlocks = useSelect(
- ( select ) =>
- select( blockEditorStore ).getBlock( clientId ).innerBlocks.length >
- 0,
- [ clientId ]
- );
-
- const controls = (
- <>
-
-
- setAttributes( {
- contentPosition: nextPosition,
- } )
- }
- isDisabled={ ! hasInnerBlocks }
- />
-
-
-
-
- { ! useFeaturedImage && (
-
- ) }
-
-
- { !! url && (
-
- { isImageBackground && (
-
-
-
-
-
- ) }
- { showFocalPointPicker && (
-
- setAttributes( {
- focalPoint: newFocalPoint,
- } )
- }
- />
- ) }
- { ! useFeaturedImage &&
- url &&
- isImageBackground &&
- isImgElement && (
-
- setAttributes( { alt: newAlt } )
- }
- help={
- <>
-
- { __(
- 'Describe the purpose of the image'
- ) }
-
- { __(
- 'Leave empty if the image is purely decorative.'
- ) }
- >
- }
- />
- ) }
-
-
-
-
- ) }
-
-
- setAttributes( {
- dimRatio: newDimRation,
- } )
- }
- min={ 0 }
- max={ 100 }
- step={ 10 }
- required
- />
-
-
-
- !! minHeight }
- label={ __( 'Minimum height' ) }
- onDeselect={ () =>
- setAttributes( {
- minHeight: undefined,
- minHeightUnit: undefined,
- } )
- }
- resetAllFilter={ () => ( {
- minHeight: undefined,
- minHeightUnit: undefined,
- } ) }
- isShownByDefault={ true }
- panelId={ clientId }
- >
-
- setAttributes( { minHeight: newMinHeight } )
- }
- onUnitChange={ ( nextUnit ) =>
- setAttributes( {
- minHeightUnit: nextUnit,
- } )
- }
- />
-
-
- >
- );
-
- const ref = useRef();
- const blockProps = useBlockProps( { ref } );
-
- // Check for fontSize support before we pass a fontSize attribute to the innerBlocks.
- const hasFontSizes = !! useSetting( 'typography.fontSizes' )?.length;
- const innerBlocksTemplate = getInnerBlocksTemplate( {
- fontSize: hasFontSizes ? 'large' : undefined,
- } );
-
- const innerBlocksProps = useInnerBlocksProps(
- {
- className: 'wp-block-cover__inner-container',
- },
- {
- template: innerBlocksTemplate,
- templateInsertUpdatesSelection: true,
- allowedBlocks,
- templateLock,
- }
- );
-
- if ( ! hasInnerBlocks && ! hasBackground ) {
- return (
- <>
- { controls }
-
-
-
-
-
-
-
{
- setAttributes( { minHeightUnit: 'px' } );
- toggleSelection( false );
- } }
- onResize={ ( value ) => {
- setAttributes( { minHeight: value } );
- } }
- onResizeStop={ ( newMinHeight ) => {
- toggleSelection( true );
- setAttributes( { minHeight: newMinHeight } );
- } }
- showHandle={ isSelected }
- />
-
- >
- );
- }
-
- const classes = classnames(
- {
- 'is-dark-theme': isDark,
- 'is-light': ! isDark,
- 'is-transient': isUploadingMedia,
- 'has-parallax': hasParallax,
- 'is-repeated': isRepeated,
- 'has-custom-content-position': ! isContentPositionCenter(
- contentPosition
- ),
- },
- getPositionClassName( contentPosition )
- );
-
- return (
- <>
- { controls }
-
-
{
- setAttributes( { minHeightUnit: 'px' } );
- toggleSelection( false );
- } }
- onResize={ ( value ) => {
- setAttributes( { minHeight: value } );
- } }
- onResizeStop={ ( newMinHeight ) => {
- toggleSelection( true );
- setAttributes( { minHeight: newMinHeight } );
- } }
- showHandle={ isSelected }
- />
-
-
-
- { url && isImageBackground && isImgElement && (
-
- ) }
- { url && isVideoBackground && (
-
- ) }
- { isUploadingMedia && }
-
-
-
- >
- );
-}
-
-export default compose( [
- withColors( { overlayColor: 'background-color' } ),
-] )( CoverEdit );
diff --git a/packages/block-library/src/cover/edit/block-controls.js b/packages/block-library/src/cover/edit/block-controls.js
new file mode 100644
index 00000000000000..aa22ad5f239ec1
--- /dev/null
+++ b/packages/block-library/src/cover/edit/block-controls.js
@@ -0,0 +1,119 @@
+/**
+ * WordPress dependencies
+ */
+import { useState } from '@wordpress/element';
+import { ToolbarButton } from '@wordpress/components';
+
+import {
+ BlockControls,
+ MediaReplaceFlow,
+ __experimentalBlockAlignmentMatrixControl as BlockAlignmentMatrixControl,
+ __experimentalBlockFullHeightAligmentControl as FullHeightAlignmentControl,
+} from '@wordpress/block-editor';
+import { __ } from '@wordpress/i18n';
+import { postFeaturedImage } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import { ALLOWED_MEDIA_TYPES, IMAGE_BACKGROUND_TYPE } from '../shared';
+
+export default function CoverBlockControls( {
+ attributes,
+ setAttributes,
+ onSelectMedia,
+ currentSettings,
+} ) {
+ const {
+ contentPosition,
+ id,
+ useFeaturedImage,
+ dimRatio,
+ minHeight,
+ minHeightUnit,
+ } = attributes;
+ const { hasInnerBlocks, url } = currentSettings;
+
+ const [ prevMinHeightValue, setPrevMinHeightValue ] = useState( minHeight );
+ const [ prevMinHeightUnit, setPrevMinHeightUnit ] = useState(
+ minHeightUnit
+ );
+ const isMinFullHeight = minHeightUnit === 'vh' && minHeight === 100;
+ const toggleMinFullHeight = () => {
+ if ( isMinFullHeight ) {
+ // If there aren't previous values, take the default ones.
+ if ( prevMinHeightUnit === 'vh' && prevMinHeightValue === 100 ) {
+ return setAttributes( {
+ minHeight: undefined,
+ minHeightUnit: undefined,
+ } );
+ }
+
+ // Set the previous values of height.
+ return setAttributes( {
+ minHeight: prevMinHeightValue,
+ minHeightUnit: prevMinHeightUnit,
+ } );
+ }
+
+ setPrevMinHeightValue( minHeight );
+ setPrevMinHeightUnit( minHeightUnit );
+
+ // Set full height.
+ return setAttributes( {
+ minHeight: 100,
+ minHeightUnit: 'vh',
+ } );
+ };
+
+ const toggleUseFeaturedImage = () => {
+ setAttributes( {
+ id: undefined,
+ url: undefined,
+ useFeaturedImage: ! useFeaturedImage,
+ dimRatio: dimRatio === 100 ? 50 : dimRatio,
+ backgroundType: useFeaturedImage
+ ? IMAGE_BACKGROUND_TYPE
+ : undefined,
+ } );
+ };
+ return (
+ <>
+
+
+ setAttributes( {
+ contentPosition: nextPosition,
+ } )
+ }
+ isDisabled={ ! hasInnerBlocks }
+ />
+
+
+
+
+ { ! useFeaturedImage && (
+
+ ) }
+
+ >
+ );
+}
diff --git a/packages/block-library/src/cover/edit/cover-placeholder.js b/packages/block-library/src/cover/edit/cover-placeholder.js
new file mode 100644
index 00000000000000..2f3c070a90f36d
--- /dev/null
+++ b/packages/block-library/src/cover/edit/cover-placeholder.js
@@ -0,0 +1,39 @@
+/**
+ * WordPress dependencies
+ */
+import { BlockIcon, MediaPlaceholder } from '@wordpress/block-editor';
+import { __ } from '@wordpress/i18n';
+import { cover as icon } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import { ALLOWED_MEDIA_TYPES } from '../shared';
+
+export default function CoverPlaceholder( {
+ disableMediaButtons = false,
+ children,
+ onSelectMedia,
+ onError,
+ style,
+} ) {
+ return (
+ }
+ labels={ {
+ title: __( 'Cover' ),
+ instructions: __(
+ 'Drag and drop onto this block, upload, or select existing media from your library.'
+ ),
+ } }
+ onSelect={ onSelectMedia }
+ accept="image/*,video/*"
+ allowedTypes={ ALLOWED_MEDIA_TYPES }
+ disableMediaButtons={ disableMediaButtons }
+ onError={ onError }
+ style={ style }
+ >
+ { children }
+
+ );
+}
diff --git a/packages/block-library/src/cover/edit/index.js b/packages/block-library/src/cover/edit/index.js
new file mode 100644
index 00000000000000..513e222294dbd9
--- /dev/null
+++ b/packages/block-library/src/cover/edit/index.js
@@ -0,0 +1,383 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+import { extend } from 'colord';
+import namesPlugin from 'colord/plugins/names';
+
+/**
+ * WordPress dependencies
+ */
+import { useEntityProp, store as coreStore } from '@wordpress/core-data';
+import { useEffect, useRef } from '@wordpress/element';
+import { Spinner } from '@wordpress/components';
+import { compose } from '@wordpress/compose';
+import {
+ withColors,
+ ColorPalette,
+ useBlockProps,
+ useSetting,
+ useInnerBlocksProps,
+ __experimentalUseGradient,
+ store as blockEditorStore,
+} from '@wordpress/block-editor';
+import { __ } from '@wordpress/i18n';
+import { useSelect, useDispatch } from '@wordpress/data';
+import { isBlobURL } from '@wordpress/blob';
+import { store as noticesStore } from '@wordpress/notices';
+
+/**
+ * Internal dependencies
+ */
+import {
+ attributesFromMedia,
+ IMAGE_BACKGROUND_TYPE,
+ VIDEO_BACKGROUND_TYPE,
+ backgroundImageStyles,
+ dimRatioToClass,
+ isContentPositionCenter,
+ getPositionClassName,
+ mediaPosition,
+} from '../shared';
+import useCoverIsDark from './use-cover-is-dark';
+import CoverInspectorControls from './inspector-controls';
+import CoverBlockControls from './block-controls';
+import CoverPlaceholder from './cover-placeholder';
+import ResizableCover from './resizeable-cover';
+
+extend( [ namesPlugin ] );
+
+function getInnerBlocksTemplate( attributes ) {
+ return [
+ [
+ 'core/paragraph',
+ {
+ align: 'center',
+ placeholder: __( 'Write title…' ),
+ ...attributes,
+ },
+ ],
+ ];
+}
+
+/**
+ * Is the URL a temporary blob URL? A blob URL is one that is used temporarily while
+ * the media (image or video) is being uploaded and will not have an id allocated yet.
+ *
+ * @param {number} id The id of the media.
+ * @param {string} url The url of the media.
+ *
+ * @return {boolean} Is the URL a Blob URL.
+ */
+const isTemporaryMedia = ( id, url ) => ! id && isBlobURL( url );
+
+function CoverEdit( {
+ attributes,
+ clientId,
+ isSelected,
+ overlayColor,
+ setAttributes,
+ setOverlayColor,
+ toggleSelection,
+ context: { postId, postType },
+} ) {
+ const {
+ contentPosition,
+ id,
+ useFeaturedImage,
+ dimRatio,
+ focalPoint,
+ hasParallax,
+ isDark,
+ isRepeated,
+ minHeight,
+ minHeightUnit,
+ alt,
+ allowedBlocks,
+ templateLock,
+ } = attributes;
+
+ const [ featuredImage ] = useEntityProp(
+ 'postType',
+ postType,
+ 'featured_media',
+ postId
+ );
+
+ const media = useSelect(
+ ( select ) =>
+ featuredImage &&
+ select( coreStore ).getMedia( featuredImage, { context: 'view' } ),
+ [ featuredImage ]
+ );
+ const mediaUrl = media?.source_url;
+
+ // instead of destructuring the attributes
+ // we define the url and background type
+ // depending on the value of the useFeaturedImage flag
+ // to preview in edit the dynamic featured image
+ const url = useFeaturedImage ? mediaUrl : attributes.url;
+ const backgroundType = useFeaturedImage
+ ? IMAGE_BACKGROUND_TYPE
+ : attributes.backgroundType;
+
+ const { __unstableMarkNextChangeAsNotPersistent } = useDispatch(
+ blockEditorStore
+ );
+ const { createErrorNotice } = useDispatch( noticesStore );
+ const { gradientClass, gradientValue } = __experimentalUseGradient();
+ const onSelectMedia = attributesFromMedia( setAttributes, dimRatio );
+ const isUploadingMedia = isTemporaryMedia( id, url );
+
+ const onUploadError = ( message ) => {
+ createErrorNotice( Array.isArray( message ) ? message[ 2 ] : message, {
+ type: 'snackbar',
+ } );
+ };
+
+ const mediaElement = useRef();
+ const isCoverDark = useCoverIsDark(
+ url,
+ dimRatio,
+ overlayColor.color,
+ mediaElement
+ );
+
+ useEffect( () => {
+ // This side-effect should not create an undo level.
+ __unstableMarkNextChangeAsNotPersistent();
+ setAttributes( { isDark: isCoverDark } );
+ }, [ isCoverDark ] );
+
+ const isImageBackground = IMAGE_BACKGROUND_TYPE === backgroundType;
+ const isVideoBackground = VIDEO_BACKGROUND_TYPE === backgroundType;
+
+ const minHeightWithUnit =
+ minHeight && minHeightUnit
+ ? `${ minHeight }${ minHeightUnit }`
+ : minHeight;
+
+ const isImgElement = ! ( hasParallax || isRepeated );
+
+ const style = {
+ ...( isImageBackground && ! isImgElement
+ ? backgroundImageStyles( url )
+ : undefined ),
+ minHeight: minHeightWithUnit || undefined,
+ };
+
+ const bgStyle = { backgroundColor: overlayColor.color };
+ const mediaStyle = {
+ objectPosition:
+ focalPoint && isImgElement
+ ? mediaPosition( focalPoint )
+ : undefined,
+ };
+
+ const hasBackground = !! ( url || overlayColor.color || gradientValue );
+
+ const hasInnerBlocks = useSelect(
+ ( select ) =>
+ select( blockEditorStore ).getBlock( clientId ).innerBlocks.length >
+ 0,
+ [ clientId ]
+ );
+
+ const ref = useRef();
+ const blockProps = useBlockProps( { ref } );
+
+ // Check for fontSize support before we pass a fontSize attribute to the innerBlocks.
+ const hasFontSizes = !! useSetting( 'typography.fontSizes' )?.length;
+ const innerBlocksTemplate = getInnerBlocksTemplate( {
+ fontSize: hasFontSizes ? 'large' : undefined,
+ } );
+
+ const innerBlocksProps = useInnerBlocksProps(
+ {
+ className: 'wp-block-cover__inner-container',
+ },
+ {
+ template: innerBlocksTemplate,
+ templateInsertUpdatesSelection: true,
+ allowedBlocks,
+ templateLock,
+ }
+ );
+
+ const currentSettings = {
+ isVideoBackground,
+ isImageBackground,
+ mediaElement,
+ hasInnerBlocks,
+ url,
+ isImgElement,
+ overlayColor,
+ };
+
+ if ( ! hasInnerBlocks && ! hasBackground ) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
{
+ setAttributes( { minHeightUnit: 'px' } );
+ toggleSelection( false );
+ } }
+ onResize={ ( value ) => {
+ setAttributes( { minHeight: value } );
+ } }
+ onResizeStop={ ( newMinHeight ) => {
+ toggleSelection( true );
+ setAttributes( { minHeight: newMinHeight } );
+ } }
+ showHandle={ isSelected }
+ />
+
+ >
+ );
+ }
+
+ const classes = classnames(
+ {
+ 'is-dark-theme': isDark,
+ 'is-light': ! isDark,
+ 'is-transient': isUploadingMedia,
+ 'has-parallax': hasParallax,
+ 'is-repeated': isRepeated,
+ 'has-custom-content-position': ! isContentPositionCenter(
+ contentPosition
+ ),
+ },
+ getPositionClassName( contentPosition )
+ );
+
+ return (
+ <>
+
+
+
+
{
+ setAttributes( { minHeightUnit: 'px' } );
+ toggleSelection( false );
+ } }
+ onResize={ ( value ) => {
+ setAttributes( { minHeight: value } );
+ } }
+ onResizeStop={ ( newMinHeight ) => {
+ toggleSelection( true );
+ setAttributes( { minHeight: newMinHeight } );
+ } }
+ showHandle={ isSelected }
+ />
+
+
+
+ { url && isImageBackground && isImgElement && (
+
+ ) }
+ { url && isVideoBackground && (
+
+ ) }
+ { isUploadingMedia && }
+
+
+
+ >
+ );
+}
+
+export default compose( [
+ withColors( { overlayColor: 'background-color' } ),
+] )( CoverEdit );
diff --git a/packages/block-library/src/cover/edit/inspector-controls.js b/packages/block-library/src/cover/edit/inspector-controls.js
new file mode 100644
index 00000000000000..b060843c8e6132
--- /dev/null
+++ b/packages/block-library/src/cover/edit/inspector-controls.js
@@ -0,0 +1,286 @@
+/**
+ * WordPress dependencies
+ */
+import { Fragment, useMemo } from '@wordpress/element';
+import {
+ BaseControl,
+ Button,
+ ExternalLink,
+ FocalPointPicker,
+ PanelBody,
+ PanelRow,
+ RangeControl,
+ TextareaControl,
+ ToggleControl,
+ __experimentalUseCustomUnits as useCustomUnits,
+ __experimentalToolsPanelItem as ToolsPanelItem,
+ __experimentalUnitControl as UnitControl,
+ __experimentalParseQuantityAndUnitFromRawValue as parseQuantityAndUnitFromRawValue,
+} from '@wordpress/components';
+import { useInstanceId } from '@wordpress/compose';
+import {
+ InspectorControls,
+ useSetting,
+ __experimentalUseGradient,
+ __experimentalPanelColorGradientSettings as PanelColorGradientSettings,
+} from '@wordpress/block-editor';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import { COVER_MIN_HEIGHT, mediaPosition } from '../shared';
+
+function CoverHeightInput( {
+ onChange,
+ onUnitChange,
+ unit = 'px',
+ value = '',
+} ) {
+ const instanceId = useInstanceId( UnitControl );
+ const inputId = `block-cover-height-input-${ instanceId }`;
+ const isPx = unit === 'px';
+
+ const units = useCustomUnits( {
+ availableUnits: useSetting( 'spacing.units' ) || [
+ 'px',
+ 'em',
+ 'rem',
+ 'vw',
+ 'vh',
+ ],
+ defaultValues: { px: 430, '%': 20, em: 20, rem: 20, vw: 20, vh: 50 },
+ } );
+
+ const handleOnChange = ( unprocessedValue ) => {
+ const inputValue =
+ unprocessedValue !== ''
+ ? parseFloat( unprocessedValue )
+ : undefined;
+
+ if ( isNaN( inputValue ) && inputValue !== undefined ) {
+ return;
+ }
+ onChange( inputValue );
+ };
+
+ const computedValue = useMemo( () => {
+ const [ parsedQuantity ] = parseQuantityAndUnitFromRawValue( value );
+ return [ parsedQuantity, unit ].join( '' );
+ }, [ unit, value ] );
+
+ const min = isPx ? COVER_MIN_HEIGHT : 0;
+
+ return (
+
+
+
+ );
+}
+export default function CoverInspectorControls( {
+ attributes,
+ setAttributes,
+ clientId,
+ setOverlayColor,
+ coverRef,
+ currentSettings,
+} ) {
+ const {
+ useFeaturedImage,
+ dimRatio,
+ focalPoint,
+ hasParallax,
+ isRepeated,
+ minHeight,
+ minHeightUnit,
+ alt,
+ } = attributes;
+ const {
+ isVideoBackground,
+ isImageBackground,
+ mediaElement,
+ url,
+ isImgElement,
+ overlayColor,
+ } = currentSettings;
+
+ const { gradientValue, setGradient } = __experimentalUseGradient();
+
+ const toggleParallax = () => {
+ setAttributes( {
+ hasParallax: ! hasParallax,
+ ...( ! hasParallax ? { focalPoint: undefined } : {} ),
+ } );
+ };
+
+ const toggleIsRepeated = () => {
+ setAttributes( {
+ isRepeated: ! isRepeated,
+ } );
+ };
+
+ const showFocalPointPicker =
+ isVideoBackground ||
+ ( isImageBackground && ( ! hasParallax || isRepeated ) );
+
+ const imperativeFocalPointPreview = ( value ) => {
+ const [ styleOfRef, property ] = mediaElement.current
+ ? [ mediaElement.current.style, 'objectPosition' ]
+ : [ coverRef.current.style, 'backgroundPosition' ];
+ styleOfRef[ property ] = mediaPosition( value );
+ };
+ return (
+ <>
+
+ { !! url && (
+
+ { isImageBackground && (
+
+
+
+
+
+ ) }
+ { showFocalPointPicker && (
+
+ setAttributes( {
+ focalPoint: newFocalPoint,
+ } )
+ }
+ />
+ ) }
+ { ! useFeaturedImage &&
+ url &&
+ isImageBackground &&
+ isImgElement && (
+
+ setAttributes( { alt: newAlt } )
+ }
+ help={
+ <>
+
+ { __(
+ 'Describe the purpose of the image'
+ ) }
+
+ { __(
+ 'Leave empty if the image is purely decorative.'
+ ) }
+ >
+ }
+ />
+ ) }
+
+
+
+
+ ) }
+
+
+ setAttributes( {
+ dimRatio: newDimRation,
+ } )
+ }
+ min={ 0 }
+ max={ 100 }
+ step={ 10 }
+ required
+ />
+
+
+
+ !! minHeight }
+ label={ __( 'Minimum height' ) }
+ onDeselect={ () =>
+ setAttributes( {
+ minHeight: undefined,
+ minHeightUnit: undefined,
+ } )
+ }
+ resetAllFilter={ () => ( {
+ minHeight: undefined,
+ minHeightUnit: undefined,
+ } ) }
+ isShownByDefault={ true }
+ panelId={ clientId }
+ >
+
+ setAttributes( { minHeight: newMinHeight } )
+ }
+ onUnitChange={ ( nextUnit ) =>
+ setAttributes( {
+ minHeightUnit: nextUnit,
+ } )
+ }
+ />
+
+
+ >
+ );
+}
diff --git a/packages/block-library/src/cover/edit/resizeable-cover.js b/packages/block-library/src/cover/edit/resizeable-cover.js
new file mode 100644
index 00000000000000..13d5d85395e16c
--- /dev/null
+++ b/packages/block-library/src/cover/edit/resizeable-cover.js
@@ -0,0 +1,55 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
+/**
+ * WordPress dependencies
+ */
+import { useState } from '@wordpress/element';
+import { ResizableBox } from '@wordpress/components';
+
+const RESIZABLE_BOX_ENABLE_OPTION = {
+ top: false,
+ right: false,
+ bottom: true,
+ left: false,
+ topRight: false,
+ bottomRight: false,
+ bottomLeft: false,
+ topLeft: false,
+};
+
+export default function ResizableCover( {
+ className,
+ onResizeStart,
+ onResize,
+ onResizeStop,
+ ...props
+} ) {
+ const [ isResizing, setIsResizing ] = useState( false );
+
+ return (
+ {
+ onResizeStart( elt.clientHeight );
+ onResize( elt.clientHeight );
+ } }
+ onResize={ ( _event, _direction, elt ) => {
+ onResize( elt.clientHeight );
+ if ( ! isResizing ) {
+ setIsResizing( true );
+ }
+ } }
+ onResizeStop={ ( _event, _direction, elt ) => {
+ onResizeStop( elt.clientHeight );
+ setIsResizing( false );
+ } }
+ { ...props }
+ />
+ );
+}
diff --git a/packages/block-library/src/cover/use-cover-is-dark.js b/packages/block-library/src/cover/edit/use-cover-is-dark.js
similarity index 100%
rename from packages/block-library/src/cover/use-cover-is-dark.js
rename to packages/block-library/src/cover/edit/use-cover-is-dark.js
diff --git a/packages/block-library/src/cover/shared.js b/packages/block-library/src/cover/shared.js
index 6ef966da626c29..ecedc70a175569 100644
--- a/packages/block-library/src/cover/shared.js
+++ b/packages/block-library/src/cover/shared.js
@@ -106,3 +106,7 @@ export function getPositionClassName( contentPosition ) {
return POSITION_CLASSNAMES[ contentPosition ];
}
+
+export function mediaPosition( { x, y } ) {
+ return `${ Math.round( x * 100 ) }% ${ Math.round( y * 100 ) }%`;
+}
diff --git a/packages/block-library/src/cover/test/block-controls.js b/packages/block-library/src/cover/test/block-controls.js
new file mode 100644
index 00000000000000..ea14dfb38d7b73
--- /dev/null
+++ b/packages/block-library/src/cover/test/block-controls.js
@@ -0,0 +1,62 @@
+/**
+ * External dependencies
+ */
+import { render, screen, fireEvent } from '@testing-library/react';
+
+// Need to mock the BlockControls wrapper as this requires a slot to run
+// so can't be easily unit tested.
+jest.mock( '@wordpress/block-editor', () => ( {
+ ...jest.requireActual( '@wordpress/block-editor' ),
+ BlockControls: ( { children } ) => { children }
,
+} ) );
+
+/**
+ * Internal dependencies
+ */
+import CoverBlockControls from '../edit/block-controls';
+
+const setAttributes = jest.fn();
+const onSelectMedia = jest.fn();
+
+const currentSettings = { hasInnerBlocks: true, url: undefined };
+const defaultAttributes = {
+ contentPosition: undefined,
+ id: 1,
+ useFeaturedImage: false,
+ dimRatio: 50,
+ minHeight: 300,
+ minHeightUnit: 'px',
+};
+const defaultProps = {
+ attributes: defaultAttributes,
+ currentSettings,
+ setAttributes,
+ onSelectMedia,
+};
+
+beforeEach( () => {
+ setAttributes.mockClear();
+ onSelectMedia.mockClear();
+} );
+
+describe( 'Cover block controls', () => {
+ describe( 'Full height toggle', () => {
+ test( 'displays toggle full height button toggled off if minHeight not 100vh', () => {
+ render( );
+ expect(
+ screen.getByRole( 'button', {
+ pressed: false,
+ name: 'Toggle full height',
+ } )
+ ).toBeInTheDocument();
+ } );
+ test( 'sets minHeight attributes to 100vh when clicked', () => {
+ render( );
+ fireEvent.click( screen.getByLabelText( 'Toggle full height' ) );
+ expect( setAttributes ).toHaveBeenCalledWith( {
+ minHeight: 100,
+ minHeightUnit: 'vh',
+ } );
+ } );
+ } );
+} );