diff --git a/plugins/woocommerce-blocks/assets/css/abstracts/_mixins.scss b/plugins/woocommerce-blocks/assets/css/abstracts/_mixins.scss
index 2bf27517e6c..96e1735c2f8 100644
--- a/plugins/woocommerce-blocks/assets/css/abstracts/_mixins.scss
+++ b/plugins/woocommerce-blocks/assets/css/abstracts/_mixins.scss
@@ -176,6 +176,61 @@ $fontSizes: (
-ms-word-break: break-all;
}
+// Add support for content alignment classes
+@mixin with-alignment {
+ // Apply max-width to floated items that have no intrinsic width
+ &.alignleft,
+ &.alignright {
+ max-width: $content-width * 0.5;
+ width: 100%;
+ }
+
+ // Using flexbox without an assigned height property breaks vertical center alignment in IE11.
+ // Appending an empty ::after element tricks IE11 into giving the cover image an implicit height, which sidesteps this issue.
+ &::after {
+ display: block;
+ content: "";
+ font-size: 0;
+ min-height: inherit;
+
+ // IE doesn't support flex so omit that.
+ @supports (position: sticky) {
+ content: none;
+ }
+ }
+
+ // Aligned cover blocks should not use our global alignment rules
+ &.aligncenter,
+ &.alignleft,
+ &.alignright {
+ display: flex;
+ }
+}
+
+// Shows an semi-transparent overlay
+@mixin with-background-dim($opacity: 0.5) {
+ &.has-background-dim {
+ .background-dim__overlay::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background-color: inherit;
+ border-radius: inherit;
+ opacity: $opacity;
+ z-index: 1;
+ }
+ }
+
+ @for $i from 1 through 10 {
+ &.has-background-dim-#{ $i * 10 } .background-dim__overlay::before {
+ opacity: $i * 0.1;
+ }
+ }
+}
+
// Shows a border with the current color and a custom opacity. That can't be achieved
// with normal border because `currentColor` doesn't allow tweaking the opacity, and
// setting the opacity of the entire element would change the children's opacity too.
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/block.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-category/block.js
deleted file mode 100644
index 5e9f167c86f..00000000000
--- a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/block.js
+++ /dev/null
@@ -1,700 +0,0 @@
-/* eslint-disable @wordpress/no-unsafe-wp-apis */
-
-/**
- * External dependencies
- */
-import { useCallback, useEffect, useState } from 'react';
-import { __ } from '@wordpress/i18n';
-import {
- AlignmentToolbar,
- BlockControls,
- InnerBlocks,
- InspectorControls,
- MediaReplaceFlow,
- RichText,
- __experimentalGetSpacingClassesAndStyles as getSpacingClassesAndStyles,
- __experimentalImageEditingProvider as ImageEditingProvider,
- __experimentalImageEditor as ImageEditor,
- __experimentalPanelColorGradientSettings as PanelColorGradientSettings,
- __experimentalUseGradient as useGradient,
-} from '@wordpress/block-editor';
-import {
- Button,
- FocalPointPicker,
- PanelBody,
- Placeholder,
- RangeControl,
- Spinner,
- ToggleControl,
- ToolbarButton,
- ToolbarGroup,
- withSpokenMessages,
- __experimentalToggleGroupControl as ToggleGroupControl,
- __experimentalToggleGroupControlOption as ToggleGroupControlOption,
- TextareaControl,
- ExternalLink,
-} from '@wordpress/components';
-import classnames from 'classnames';
-import { Component } from '@wordpress/element';
-import { withSelect } from '@wordpress/data';
-import { compose, createHigherOrderComponent } from '@wordpress/compose';
-import PropTypes from 'prop-types';
-import { folderStarred } from '@woocommerce/icons';
-import { crop, Icon } from '@wordpress/icons';
-import ProductCategoryControl from '@woocommerce/editor-components/product-category-control';
-import ErrorPlaceholder from '@woocommerce/editor-components/error-placeholder';
-import TextToolbarButton from '@woocommerce/editor-components/text-toolbar-button';
-
-/**
- * Internal dependencies
- */
-import {
- dimRatioToClass,
- getCategoryImageId,
- getCategoryImageSrc,
- calculateBackgroundImagePosition,
-} from './utils';
-import { withCategory } from '../../hocs';
-import { ConstrainedResizable } from '../featured-product/block';
-
-const DEFAULT_EDITOR_SIZE = {
- height: 500,
- width: 500,
-};
-
-/**
- * Component to handle edit mode of "Featured Category".
- *
- * @param {Object} props Incoming props for the component.
- * @param {Object} props.attributes Incoming block attributes.
- * @param {boolean} props.isSelected Whether block is selected or not.
- * @param {function(any):any} props.setAttributes Function for setting new attributes.
- * @param {string} props.error Error message
- * @param {function(any):any} props.getCategory Function for getting category details.
- * @param {boolean} props.isLoading Whether loading or not.
- * @param {Object} props.category The product category object.
- * @param {function(any):any} props.debouncedSpeak Function for delayed speak.
- * @param {function():void} props.triggerUrlUpdate Function to update Shop now button Url.
- */
-const FeaturedCategory = ( {
- attributes,
- isSelected,
- setAttributes,
- error,
- getCategory,
- isLoading,
- category,
- debouncedSpeak,
- triggerUrlUpdate = () => void null,
-} ) => {
- const { mediaId, mediaSrc } = attributes;
-
- const [ isEditingImage, setIsEditingImage ] = useState( false );
- const [ backgroundImageSize, setBackgroundImageSize ] = useState( {} );
- const { setGradient } = useGradient( {
- gradientAttribute: 'overlayGradient',
- customGradientAttribute: 'overlayGradient',
- } );
-
- const backgroundImageSrc = mediaSrc || getCategoryImageSrc( category );
- const backgroundImageId = mediaId || getCategoryImageId( category );
-
- const onResize = useCallback(
- ( _event, _direction, elt ) => {
- setAttributes( { minHeight: parseInt( elt.style.height, 10 ) } );
- },
- [ setAttributes ]
- );
-
- useEffect( () => {
- setIsEditingImage( false );
- }, [ isSelected ] );
-
- const renderApiError = () => (
-
- );
-
- const getBlockControls = () => {
- const { contentAlign, editMode } = attributes;
-
- return (
-
- {
- setAttributes( { contentAlign: nextAlign } );
- } }
- />
-
- { backgroundImageSrc && ! isEditingImage && (
- setIsEditingImage( true ) }
- icon={ crop }
- label={ __(
- 'Edit category image',
- 'woo-gutenberg-products-block'
- ) }
- />
- ) }
- {
- setAttributes( {
- mediaId: media.id,
- mediaSrc: media.url,
- } );
- } }
- allowedTypes={ [ 'image' ] }
- />
- { backgroundImageId && mediaSrc ? (
-
- setAttributes( { mediaId: 0, mediaSrc: '' } )
- }
- >
- { __( 'Reset', 'woo-gutenberg-products-block' ) }
-
- ) : null }
-
-
- setAttributes( { editMode: ! editMode } ),
- isActive: editMode,
- },
- ] }
- />
-
- );
- };
-
- const getInspectorControls = () => {
- const { focalPoint = { x: 0.5, y: 0.5 } } = attributes;
- // FocalPointPicker was introduced in Gutenberg 5.0 (WordPress 5.2),
- // so we need to check if it exists before using it.
- const focalPointPickerExists = typeof FocalPointPicker === 'function';
-
- return (
-
-
-
- setAttributes( { showDesc: ! attributes.showDesc } )
- }
- />
-
- { !! backgroundImageSrc && (
- <>
- { focalPointPickerExists && (
-
-
-
- { __(
- 'Choose “Cover” if you want the image to scale automatically to always fit its container.',
- 'woo-gutenberg-products-block'
- ) }
-
-
- { __(
- 'Note: by choosing “Cover” you will lose the ability to freely move the focal point precisely.',
- 'woo-gutenberg-products-block'
- ) }
-
- >
- }
- label={ __(
- 'Image fit',
- 'woo-gutenberg-products-block'
- ) }
- value={ attributes.imageFit }
- onChange={ ( value ) =>
- setAttributes( {
- imageFit: value,
- } )
- }
- >
-
-
-
-
- setAttributes( {
- focalPoint: value,
- } )
- }
- />
- {
- setAttributes( { alt } );
- } }
- help={
- <>
-
- { __(
- 'Describe the purpose of the image',
- 'woo-gutenberg-products-block'
- ) }
-
- { __(
- 'Leaving it empty will use the category name.',
- 'woo-gutenberg-products-block'
- ) }
- >
- }
- />
-
- ) }
-
- setAttributes( { overlayColor } ),
- onGradientChange: ( overlayGradient ) => {
- setGradient( overlayGradient );
- setAttributes( {
- overlayGradient,
- } );
- },
- label: __(
- 'Color',
- 'woo-gutenberg-products-block'
- ),
- },
- ] }
- >
-
- setAttributes( { dimRatio } )
- }
- min={ 0 }
- max={ 100 }
- step={ 10 }
- required
- />
-
- >
- ) }
-
- );
- };
-
- const renderEditMode = () => {
- const onDone = () => {
- setAttributes( { editMode: false } );
- debouncedSpeak(
- __(
- 'Showing Featured Product block preview.',
- 'woo-gutenberg-products-block'
- )
- );
- };
-
- return (
- }
- label={ __(
- 'Featured Category',
- 'woo-gutenberg-products-block'
- ) }
- className="wc-block-featured-category"
- >
- { __(
- 'Visually highlight a product category and encourage prompt action.',
- 'woo-gutenberg-products-block'
- ) }
-
-
{
- const id = value[ 0 ] ? value[ 0 ].id : 0;
- setAttributes( {
- categoryId: id,
- mediaId: 0,
- mediaSrc: '',
- } );
- triggerUrlUpdate();
- } }
- isSingle
- />
-
-
-
- );
- };
-
- const renderButton = () => {
- const buttonClasses = classnames(
- 'wp-block-button__link',
- 'is-style-fill'
- );
- const buttonStyle = {
- backgroundColor: 'vivid-green-cyan',
- borderRadius: '5px',
- };
- const wrapperStyle = {
- width: '100%',
- };
- return attributes.categoryId === 'preview' ? (
-
-
-
- ) : (
-
- );
- };
-
- const renderCategory = () => {
- const {
- contentAlign,
- dimRatio,
- focalPoint,
- imageFit,
- minHeight,
- overlayColor,
- overlayGradient,
- showDesc,
- style,
- } = attributes;
-
- const classes = classnames(
- 'wc-block-featured-category',
- {
- 'is-selected': isSelected && attributes.productId !== 'preview',
- 'is-loading': ! category && isLoading,
- 'is-not-found': ! category && ! isLoading,
- 'has-background-dim': dimRatio !== 0,
- },
- dimRatioToClass( dimRatio ),
- contentAlign !== 'center' && `has-${ contentAlign }-content`
- );
-
- const containerStyle = {
- borderRadius: style?.border?.radius,
- };
-
- const wrapperStyle = {
- ...getSpacingClassesAndStyles( attributes ).style,
- minHeight,
- };
-
- const backgroundImageStyle = {
- ...calculateBackgroundImagePosition( focalPoint ),
- objectFit: imageFit,
- };
-
- const overlayStyle = {
- background: overlayGradient,
- backgroundColor: overlayColor,
- };
-
- return (
- <>
-
-
-
-
- { backgroundImageSrc && (
-
{
- setBackgroundImageSize( {
- height: e.target?.naturalHeight,
- width: e.target?.naturalWidth,
- } );
- } }
- />
- ) }
-
- { showDesc && (
-
- ) }
-
- { renderButton() }
-
-
-
- >
- );
- };
-
- const renderNoCategory = () => (
- }
- label={ __( 'Featured Category', 'woo-gutenberg-products-block' ) }
- >
- { isLoading ? (
-
- ) : (
- __(
- 'No product category is selected.',
- 'woo-gutenberg-products-block'
- )
- ) }
-
- );
-
- const { editMode } = attributes;
-
- if ( error ) {
- return renderApiError();
- }
-
- if ( editMode ) {
- return renderEditMode();
- }
-
- if ( isEditingImage ) {
- return (
- <>
- {
- setAttributes( { mediaId: id, mediaSrc: url } );
- } }
- isEditing={ isEditingImage }
- onFinishEditing={ () => setIsEditingImage( false ) }
- >
-
-
- >
- );
- }
-
- return (
- <>
- { getBlockControls() }
- { getInspectorControls() }
- { category ? renderCategory() : renderNoCategory() }
- >
- );
-};
-
-FeaturedCategory.propTypes = {
- /**
- * The attributes for this block.
- */
- attributes: PropTypes.object.isRequired,
- /**
- * Whether this block is currently active.
- */
- isSelected: PropTypes.bool.isRequired,
- /**
- * The register block name.
- */
- name: PropTypes.string.isRequired,
- /**
- * A callback to update attributes.
- */
- setAttributes: PropTypes.func.isRequired,
- // from withCategory
- error: PropTypes.object,
- getCategory: PropTypes.func,
- isLoading: PropTypes.bool,
- category: PropTypes.shape( {
- name: PropTypes.node,
- description: PropTypes.node,
- permalink: PropTypes.string,
- } ),
- // from withSpokenMessages
- debouncedSpeak: PropTypes.func.isRequired,
- triggerUrlUpdate: PropTypes.func,
-};
-
-export default compose( [
- withCategory,
- withSpokenMessages,
- withSelect( ( select, { clientId }, { dispatch } ) => {
- const Block = select( 'core/block-editor' ).getBlock( clientId );
- const buttonBlockId = Block?.innerBlocks[ 0 ]?.clientId || '';
- const currentButtonAttributes =
- Block?.innerBlocks[ 0 ]?.attributes || {};
- const updateBlockAttributes = ( attributes ) => {
- if ( buttonBlockId ) {
- dispatch( 'core/block-editor' ).updateBlockAttributes(
- buttonBlockId,
- attributes
- );
- }
- };
- return { updateBlockAttributes, currentButtonAttributes };
- } ),
- createHigherOrderComponent( ( ProductComponent ) => {
- class WrappedComponent extends Component {
- state = {
- doUrlUpdate: false,
- };
- componentDidUpdate() {
- const {
- attributes,
- updateBlockAttributes,
- currentButtonAttributes,
- category,
- } = this.props;
- if (
- this.state.doUrlUpdate &&
- ! attributes.editMode &&
- category?.permalink &&
- currentButtonAttributes?.url &&
- category.permalink !== currentButtonAttributes.url
- ) {
- updateBlockAttributes( {
- ...currentButtonAttributes,
- url: category.permalink,
- } );
- this.setState( { doUrlUpdate: false } );
- }
- }
- triggerUrlUpdate = () => {
- this.setState( { doUrlUpdate: true } );
- };
- render() {
- return (
-
- );
- }
- }
- return WrappedComponent;
- }, 'withUpdateButtonAttributes' ),
-] )( FeaturedCategory );
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/featured-category/edit.tsx
deleted file mode 100644
index 990154b4121..00000000000
--- a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/edit.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * External dependencies
- */
-import { useBlockProps } from '@wordpress/block-editor';
-
-/**
- * Internal dependencies
- */
-import Block from './block';
-
-export const Edit = ( props: unknown ): JSX.Element => {
- const blockProps = useBlockProps();
-
- // The useBlockProps function returns the style with the `color`.
- // We need to remove it to avoid the block to be styled with the color.
- const { color, ...styles } = blockProps.style;
-
- return (
-
-
-
- );
-};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/featured-category/editor.scss
deleted file mode 100644
index 2a5b2726ccb..00000000000
--- a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/editor.scss
+++ /dev/null
@@ -1,15 +0,0 @@
-.wc-block-featured-category {
- background-color: inherit;
-
- .components-resizable-box__handle {
- z-index: 10;
- }
-
- .components-placeholder__label svg {
- fill: currentColor;
- margin-right: 1ch;
- }
-}
-.wc-block-featured-category__selection {
- width: 100%;
-}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/featured-category/index.tsx
deleted file mode 100644
index fef4efb2668..00000000000
--- a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/index.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-/**
- * External dependencies
- */
-import { InnerBlocks } from '@wordpress/block-editor';
-import { registerBlockType } from '@wordpress/blocks';
-import { getSetting } from '@woocommerce/settings';
-import { folderStarred } from '@woocommerce/icons';
-import { Icon } from '@wordpress/icons';
-import { isFeaturePluginBuild } from '@woocommerce/block-settings';
-
-/**
- * Internal dependencies
- */
-import './style.scss';
-import './editor.scss';
-import { example } from './example';
-import { Edit } from './edit';
-import metadata from './block.json';
-
-/**
- * Register and run the "Featured Category" block.
- */
-registerBlockType( metadata, {
- icon: {
- src: (
-
- ),
- },
- attributes: {
- ...metadata.attributes,
- /**
- * A minimum height for the block.
- *
- * Note: if padding is increased, this way the inner content will never
- * overflow, but instead will resize the container.
- *
- * It was decided to change this to make this block more in line with
- * the “Cover” block.
- */
- minHeight: {
- type: 'number',
- default: getSetting( 'default_height', 500 ),
- },
- },
- supports: {
- ...metadata.supports,
- color: {
- background: true,
- text: true,
- ...( isFeaturePluginBuild() && {
- __experimentalDuotone:
- '.wc-block-featured-category__background-image',
- } ),
- },
- spacing: {
- padding: true,
- ...( isFeaturePluginBuild() && {
- __experimentalDefaultControls: {
- padding: true,
- },
- __experimentalSkipSerialization: true,
- } ),
- },
- ...( isFeaturePluginBuild() && {
- __experimentalBorder: {
- color: true,
- radius: true,
- width: true,
- __experimentalSkipSerialization: true,
- },
- } ),
- },
- example,
- /**
- * Renders and manages the block.
- *
- * @param {Object} props Props to pass to block.
- */
- edit: Edit,
-
- /**
- * Block content is rendered in PHP, not via save function.
- */
- save: () => {
- return ;
- },
-} );
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/featured-category/style.scss
deleted file mode 100644
index 0cab1f8b697..00000000000
--- a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/style.scss
+++ /dev/null
@@ -1,186 +0,0 @@
-.wp-block-woocommerce-featured-category {
- background-color: transparent;
- border-color: transparent;
- color: #fff;
- box-sizing: border-box;
-
- .components-resizable-box__container {
- position: absolute !important;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- min-height: 50px;
-
- &:not(.is-resizing) {
- height: auto !important;
- }
- }
-
- // Applying image edits
- .is-applying {
- .components-spinner {
- position: absolute;
- top: 50%;
- left: 50%;
- margin-top: -9px;
- margin-left: -9px;
- }
-
- img {
- opacity: 0.3;
- }
- }
-}
-
-.wc-block-featured-category {
- align-content: center;
- align-items: center;
- background-size: cover;
- background-position: center center;
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- margin: 0;
- overflow: hidden;
- position: relative;
- width: 100%;
-
- .wc-block-featured-category__wrapper {
- align-content: center;
- align-items: center;
- box-sizing: border-box;
- display: flex;
- flex-wrap: wrap;
- height: 100%;
- justify-content: center;
- overflow: hidden;
- }
-
- &.has-left-content {
- justify-content: flex-start;
-
- .wc-block-featured-category__title,
- .wc-block-featured-category__description,
- .wc-block-featured-category__price {
- margin-left: 0;
- text-align: left;
- }
- }
-
- &.has-right-content {
- justify-content: flex-end;
-
- .wc-block-featured-category__title,
- .wc-block-featured-category__description,
- .wc-block-featured-category__price {
- margin-right: 0;
- text-align: right;
- }
- }
-
- .wc-block-featured-category__title,
- .wc-block-featured-category__description,
- .wc-block-featured-category__price {
- color: $white;
- line-height: 1.25;
- margin-bottom: 0;
- text-align: center;
-
- a,
- a:hover,
- a:focus,
- a:active {
- color: $white;
- }
- }
-
- .wc-block-featured-category__title,
- .wc-block-featured-category__description,
- .wc-block-featured-category__price,
- .wc-block-featured-category__link {
- color: inherit;
- width: 100%;
- padding: 0 48px 16px 48px;
- z-index: 1;
- }
-
- .wc-block-featured-category__title {
- margin-top: 0;
-
- div {
- color: inherit;
- }
-
- &::before {
- display: none;
- }
- }
-
- .wc-block-featured-category__description {
- color: inherit;
- p {
- margin: 0;
- }
- }
-
- .wc-block-featured-category__background-image {
- @include absolute-stretch();
- object-fit: none;
- }
-
- .wp-block-button.aligncenter {
- text-align: center;
- }
-
- &.has-background-dim {
- .wc-block-featured-category__overlay::before {
- background: inherit;
- border-radius: inherit;
- content: "";
- position: absolute;
- top: 0;
- left: 0;
- bottom: 0;
- right: 0;
- opacity: 0.5;
- z-index: 1;
- }
- }
-
- @for $i from 1 through 10 {
- &.has-background-dim.has-background-dim-#{ $i * 10 } {
- .wc-block-featured-category__overlay::before {
- opacity: $i * 0.1;
- }
- }
- }
-
- // Apply max-width to floated items that have no intrinsic width
- &.alignleft,
- &.alignright {
- max-width: $content-width * 0.5;
- width: 100%;
- }
-
- // Using flexbox without an assigned height property breaks vertical center alignment in IE11.
- // Appending an empty ::after element tricks IE11 into giving the cover image an implicit height, which sidesteps this issue.
- &::after {
- display: block;
- content: "";
- font-size: 0;
- min-height: inherit;
-
- // IE doesn't support flex so omit that.
- @supports (position: sticky) {
- content: none;
- }
- }
-
- // Aligned cover blocks should not use our global alignment rules
- &.aligncenter,
- &.alignleft,
- &.alignright {
- display: flex;
- }
-}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/utils.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-category/utils.js
deleted file mode 100644
index 831f0756760..00000000000
--- a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/utils.js
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * External dependencies
- */
-import { isObject } from 'lodash';
-
-/**
- * Get the src from a category object, unless null (no image).
- *
- * @param {Object|null} category A product category object from the API.
- * @return {string} The src of the category image.
- */
-function getCategoryImageSrc( category ) {
- if ( category && isObject( category.image ) ) {
- return category.image.src;
- }
- return '';
-}
-
-/**
- * Get the attachment ID from a category object, unless null (no image).
- *
- * @param {Object|null} category A product category object from the API.
- * @return {number} The id of the category image.
- */
-function getCategoryImageId( category ) {
- if ( category && isObject( category.image ) ) {
- return category.image.id;
- }
- return 0;
-}
-
-/**
- * Generate a style object given either a product category image from the API or URL to an image.
- *
- * @param {string} url An image URL.
- * @return {Object} A style object with a backgroundImage set (if a valid image is provided).
- */
-function getBackgroundImageStyles( url ) {
- if ( url ) {
- return { backgroundImage: `url(${ url })` };
- }
- return {};
-}
-
-/**
- * Convert the selected ratio to the correct background class.
- *
- * @param {number} ratio Selected opacity from 0 to 100.
- * @return {string} The class name, if applicable (not used for ratio 0 or 50).
- */
-function dimRatioToClass( ratio ) {
- return ratio === 0 || ratio === 50
- ? null
- : `has-background-dim-${ 10 * Math.round( ratio / 10 ) }`;
-}
-
-export {
- getCategoryImageSrc,
- getCategoryImageId,
- getBackgroundImageStyles,
- dimRatioToClass,
-};
-
-export function calculateBackgroundImagePosition( coords ) {
- if ( ! coords ) return {};
-
- const x = Math.round( coords.x * 100 );
- const y = Math.round( coords.y * 100 );
-
- return {
- objectPosition: `${ x }% ${ y }%`,
- };
-}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/block-controls.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/block-controls.js
new file mode 100644
index 00000000000..5f4c7b8c219
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/block-controls.js
@@ -0,0 +1,116 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import {
+ AlignmentToolbar,
+ BlockControls as BlockControlsWrapper,
+ MediaReplaceFlow,
+} from '@wordpress/block-editor';
+import { ToolbarButton, ToolbarGroup } from '@wordpress/components';
+import { crop } from '@wordpress/icons';
+import TextToolbarButton from '@woocommerce/editor-components/text-toolbar-button';
+
+/**
+ * Internal dependencies
+ */
+import { useBackgroundImage } from './use-background-image';
+
+export const BlockControls = ( {
+ backgroundImageId,
+ backgroundImageSrc,
+ contentAlign,
+ cropLabel,
+ editLabel,
+ editMode,
+ isEditingImage,
+ mediaSrc,
+ setAttributes,
+ setIsEditingImage,
+} ) => {
+ return (
+
+ {
+ setAttributes( { contentAlign: nextAlign } );
+ } }
+ />
+
+ { backgroundImageSrc && ! isEditingImage && (
+ setIsEditingImage( true ) }
+ icon={ crop }
+ label={ cropLabel }
+ />
+ ) }
+ {
+ setAttributes( {
+ mediaId: media.id,
+ mediaSrc: media.url,
+ } );
+ } }
+ allowedTypes={ [ 'image' ] }
+ />
+ { backgroundImageId && mediaSrc ? (
+
+ setAttributes( { mediaId: 0, mediaSrc: '' } )
+ }
+ >
+ { __( 'Reset', 'woo-gutenberg-products-block' ) }
+
+ ) : null }
+
+
+ setAttributes( { editMode: ! editMode } ),
+ isActive: editMode,
+ },
+ ] }
+ />
+
+ );
+};
+
+export const withBlockControls = ( { cropLabel, editLabel } ) => (
+ Component
+) => ( props ) => {
+ const [ isEditingImage, setIsEditingImage ] = props.useEditingImage;
+ const { attributes, category, name, product, setAttributes } = props;
+ const { contentAlign, editMode, mediaId, mediaSrc } = attributes;
+ const item = category || product;
+
+ const { backgroundImageId, backgroundImageSrc } = useBackgroundImage( {
+ item,
+ mediaId,
+ mediaSrc,
+ blockName: name,
+ } );
+
+ return (
+ <>
+
+
+ >
+ );
+};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/call-to-action.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/call-to-action.js
new file mode 100644
index 00000000000..bcab9aec57e
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/call-to-action.js
@@ -0,0 +1,57 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+import { RichText, InnerBlocks } from '@wordpress/block-editor';
+import { __ } from '@wordpress/i18n';
+
+export const CallToAction = ( { itemId, linkText, permalink } ) => {
+ const buttonClasses = classnames(
+ 'wp-block-button__link',
+ 'is-style-fill'
+ );
+ const buttonStyle = {
+ backgroundColor: 'vivid-green-cyan',
+ borderRadius: '5px',
+ };
+ const wrapperStyle = {
+ width: '100%',
+ };
+ return itemId === 'preview' ? (
+
+
+
+ ) : (
+
+ );
+};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/constants.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/constants.js
new file mode 100644
index 00000000000..d96ceb50315
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/constants.js
@@ -0,0 +1,9 @@
+export const DEFAULT_EDITOR_SIZE = {
+ height: 500,
+ width: 500,
+};
+
+export const BLOCK_NAMES = {
+ featuredCategory: 'woocommerce/featured-category',
+ featuredProduct: 'woocommerce/featured-product',
+};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/constrained-resizable.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/constrained-resizable.js
new file mode 100644
index 00000000000..a63d63e57ee
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/constrained-resizable.js
@@ -0,0 +1,44 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+import { useState } from 'react';
+import { ResizableBox } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import { useThrottle } from '../../utils/useThrottle';
+
+export const ConstrainedResizable = ( {
+ className = '',
+ onResize,
+ ...props
+} ) => {
+ const [ isResizing, setIsResizing ] = useState( false );
+
+ const classNames = classnames( className, {
+ 'is-resizing': isResizing,
+ } );
+ const throttledResize = useThrottle(
+ ( event, direction, elt ) => {
+ if ( ! isResizing ) setIsResizing( true );
+ onResize( event, direction, elt );
+ },
+ 50,
+ { leading: true }
+ );
+
+ return (
+ {
+ onResize( ...args );
+ setIsResizing( false );
+ } }
+ { ...props }
+ />
+ );
+};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/edit.tsx
new file mode 100644
index 00000000000..9ee26016b5c
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/edit.tsx
@@ -0,0 +1,21 @@
+/**
+ * External dependencies
+ */
+import { FunctionComponent } from 'react';
+import { useBlockProps } from '@wordpress/block-editor';
+
+export function Edit< T >( Block: FunctionComponent< T > ) {
+ return function WithBlock( props: T ): JSX.Element {
+ const blockProps = useBlockProps();
+
+ // The useBlockProps function returns the style with the `color`.
+ // We need to remove it to avoid the block to be styled with the color.
+ const { color, ...styles } = blockProps.style;
+
+ return (
+
+
+
+ );
+ };
+}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/block.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/block.js
new file mode 100644
index 00000000000..b3436b5c572
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/block.js
@@ -0,0 +1,118 @@
+/**
+ * External dependencies
+ */
+import { withCategory } from '@woocommerce/block-hocs';
+import { withSpokenMessages } from '@wordpress/components';
+import { compose, createHigherOrderComponent } from '@wordpress/compose';
+import { withSelect } from '@wordpress/data';
+import { Component } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { folderStarred } from '@woocommerce/icons';
+
+/**
+ * Internal dependencies
+ */
+import { withBlockControls } from '../block-controls';
+import { withImageEditor } from '../image-editor';
+import { withInspectorControls } from '../inspector-controls';
+import { withApiError } from '../with-api-error';
+import { withEditMode } from '../with-edit-mode';
+import { withEditingImage } from '../with-editing-image';
+import { withFeaturedItem } from '../with-featured-item';
+
+const GENERIC_CONFIG = {
+ icon: folderStarred,
+ label: __( 'Featured Category', 'woo-gutenberg-products-block' ),
+};
+
+const BLOCK_CONTROL_CONFIG = {
+ cropLabel: __( 'Edit category image', 'woo-gutenberg-products-block' ),
+ editLabel: __( 'Edit selected category', 'woo-gutenberg-products-block' ),
+};
+
+const CONTENT_CONFIG = {
+ ...GENERIC_CONFIG,
+ emptyMessage: __(
+ 'No product category is selected.',
+ 'woo-gutenberg-products-block'
+ ),
+};
+
+const EDIT_MODE_CONFIG = {
+ ...GENERIC_CONFIG,
+ description: __(
+ 'Visually highlight a product category and encourage prompt action.',
+ 'woo-gutenberg-products-block'
+ ),
+ editLabel: __(
+ 'Showing Featured Product block preview.',
+ 'woo-gutenberg-products-block'
+ ),
+};
+
+export default compose( [
+ withCategory,
+ withSpokenMessages,
+ withSelect( ( select, { clientId }, { dispatch } ) => {
+ const Block = select( 'core/block-editor' ).getBlock( clientId );
+ const buttonBlockId = Block?.innerBlocks[ 0 ]?.clientId || '';
+ const currentButtonAttributes =
+ Block?.innerBlocks[ 0 ]?.attributes || {};
+ const updateBlockAttributes = ( attributes ) => {
+ if ( buttonBlockId ) {
+ dispatch( 'core/block-editor' ).updateBlockAttributes(
+ buttonBlockId,
+ attributes
+ );
+ }
+ };
+ return { updateBlockAttributes, currentButtonAttributes };
+ } ),
+ createHigherOrderComponent( ( ProductComponent ) => {
+ class WrappedComponent extends Component {
+ state = {
+ doUrlUpdate: false,
+ };
+ componentDidUpdate() {
+ const {
+ attributes,
+ updateBlockAttributes,
+ currentButtonAttributes,
+ category,
+ } = this.props;
+ if (
+ this.state.doUrlUpdate &&
+ ! attributes.editMode &&
+ category?.permalink &&
+ currentButtonAttributes?.url &&
+ category.permalink !== currentButtonAttributes.url
+ ) {
+ updateBlockAttributes( {
+ ...currentButtonAttributes,
+ url: category.permalink,
+ } );
+ this.setState( { doUrlUpdate: false } );
+ }
+ }
+ triggerUrlUpdate = () => {
+ this.setState( { doUrlUpdate: true } );
+ };
+ render() {
+ return (
+
+ );
+ }
+ }
+ return WrappedComponent;
+ }, 'withUpdateButtonAttributes' ),
+ withEditingImage,
+ withEditMode( EDIT_MODE_CONFIG ),
+ withFeaturedItem( CONTENT_CONFIG ),
+ withApiError,
+ withImageEditor,
+ withInspectorControls,
+ withBlockControls( BLOCK_CONTROL_CONFIG ),
+] )( () => <>> );
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/block.json b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/block.json
similarity index 100%
rename from plugins/woocommerce-blocks/assets/js/blocks/featured-category/block.json
rename to plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/block.json
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/editor.scss
new file mode 100644
index 00000000000..9b0eb3b1329
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/editor.scss
@@ -0,0 +1,6 @@
+@import "../style";
+
+.wp-block-woocommerce-featured-category {
+ @extend %with-media-controls;
+ @extend %with-resizable-box;
+}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/example.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/example.js
similarity index 51%
rename from plugins/woocommerce-blocks/assets/js/blocks/featured-category/example.js
rename to plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/example.js
index 341176cd0eb..92f45ba2925 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/featured-category/example.js
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/example.js
@@ -1,18 +1,10 @@
/**
* External dependencies
*/
-import { getSetting } from '@woocommerce/settings';
import { previewCategories } from '@woocommerce/resource-previews';
export const example = {
attributes: {
- alt: '',
- contentAlign: 'center',
- dimRatio: 50,
- editMode: false,
- height: getSetting( 'default_height', 500 ),
- mediaSrc: '',
- showDesc: true,
categoryId: 'preview',
previewCategory: previewCategories[ 0 ],
},
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/index.tsx
new file mode 100644
index 00000000000..75bbcd4c96f
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/index.tsx
@@ -0,0 +1,26 @@
+/**
+ * External dependencies
+ */
+import { folderStarred } from '@woocommerce/icons';
+import { Icon } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+import './editor.scss';
+import Block from './block';
+import metadata from './block.json';
+import { register } from '../register';
+import { example } from './example';
+
+register( Block, example, metadata, {
+ icon: {
+ src: (
+
+ ),
+ },
+} );
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/style.scss
new file mode 100644
index 00000000000..a6688dfa188
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/style.scss
@@ -0,0 +1,9 @@
+@import "../style";
+
+.wp-block-woocommerce-featured-category {
+ @extend %wp-block-featured-item;
+}
+
+.wc-block-featured-category {
+ @include wc-block-featured-item();
+}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/utils.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/utils.js
new file mode 100644
index 00000000000..f5abff54969
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-category/utils.js
@@ -0,0 +1,32 @@
+/**
+ * External dependencies
+ */
+import { isObject } from 'lodash';
+
+/**
+ * Get the src from a category object, unless null (no image).
+ *
+ * @param {Object|null} category A product category object from the API.
+ * @return {string} The src of the category image.
+ */
+function getCategoryImageSrc( category ) {
+ if ( category && isObject( category.image ) ) {
+ return category.image.src;
+ }
+ return '';
+}
+
+/**
+ * Get the attachment ID from a category object, unless null (no image).
+ *
+ * @param {Object|null} category A product category object from the API.
+ * @return {number} The id of the category image.
+ */
+function getCategoryImageId( category ) {
+ if ( category && isObject( category.image ) ) {
+ return category.image.id;
+ }
+ return 0;
+}
+
+export { getCategoryImageSrc, getCategoryImageId };
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/block.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/block.js
new file mode 100644
index 00000000000..2b98b4df9b8
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/block.js
@@ -0,0 +1,118 @@
+/**
+ * External dependencies
+ */
+import { withProduct } from '@woocommerce/block-hocs';
+import { withSpokenMessages } from '@wordpress/components';
+import { compose, createHigherOrderComponent } from '@wordpress/compose';
+import { withSelect } from '@wordpress/data';
+import { Component } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { starEmpty } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import { withBlockControls } from '../block-controls';
+import { withImageEditor } from '../image-editor';
+import { withInspectorControls } from '../inspector-controls';
+import { withApiError } from '../with-api-error';
+import { withEditMode } from '../with-edit-mode';
+import { withEditingImage } from '../with-editing-image';
+import { withFeaturedItem } from '../with-featured-item';
+
+const GENERIC_CONFIG = {
+ icon: starEmpty,
+ label: __( 'Featured Product', 'woo-gutenberg-products-block' ),
+};
+
+const BLOCK_CONTROL_CONFIG = {
+ cropLabel: __( 'Edit product image', 'woo-gutenberg-products-block' ),
+ editLabel: __( 'Edit selected product', 'woo-gutenberg-products-block' ),
+};
+
+const CONTENT_CONFIG = {
+ ...GENERIC_CONFIG,
+ emptyMessage: __(
+ 'No product is selected.',
+ 'woo-gutenberg-products-block'
+ ),
+};
+
+const EDIT_MODE_CONFIG = {
+ ...GENERIC_CONFIG,
+ description: __(
+ 'Visually highlight a product or variation and encourage prompt action',
+ 'woo-gutenberg-products-block'
+ ),
+ editLabel: __(
+ 'Showing Featured Product block preview.',
+ 'woo-gutenberg-products-block'
+ ),
+};
+
+export default compose( [
+ withProduct,
+ withSpokenMessages,
+ withSelect( ( select, { clientId }, { dispatch } ) => {
+ const Block = select( 'core/block-editor' ).getBlock( clientId );
+ const buttonBlockId = Block?.innerBlocks[ 0 ]?.clientId || '';
+ const currentButtonAttributes =
+ Block?.innerBlocks[ 0 ]?.attributes || {};
+ const updateBlockAttributes = ( attributes ) => {
+ if ( buttonBlockId ) {
+ dispatch( 'core/block-editor' ).updateBlockAttributes(
+ buttonBlockId,
+ attributes
+ );
+ }
+ };
+ return { updateBlockAttributes, currentButtonAttributes };
+ } ),
+ createHigherOrderComponent( ( ProductComponent ) => {
+ class WrappedComponent extends Component {
+ state = {
+ doUrlUpdate: false,
+ };
+ componentDidUpdate() {
+ const {
+ attributes,
+ updateBlockAttributes,
+ currentButtonAttributes,
+ product,
+ } = this.props;
+ if (
+ this.state.doUrlUpdate &&
+ ! attributes.editMode &&
+ product?.permalink &&
+ currentButtonAttributes?.url &&
+ product.permalink !== currentButtonAttributes.url
+ ) {
+ updateBlockAttributes( {
+ ...currentButtonAttributes,
+ url: product.permalink,
+ } );
+ this.setState( { doUrlUpdate: false } );
+ }
+ }
+ triggerUrlUpdate = () => {
+ this.setState( { doUrlUpdate: true } );
+ };
+ render() {
+ return (
+
+ );
+ }
+ }
+ return WrappedComponent;
+ }, 'withUpdateButtonAttributes' ),
+ withEditingImage,
+ withEditMode( EDIT_MODE_CONFIG ),
+ withFeaturedItem( CONTENT_CONFIG ),
+ withApiError,
+ withImageEditor,
+ withInspectorControls,
+ withBlockControls( BLOCK_CONTROL_CONFIG ),
+] )( () => <>> );
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/block.json b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/block.json
similarity index 100%
rename from plugins/woocommerce-blocks/assets/js/blocks/featured-product/block.json
rename to plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/block.json
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/editor.scss
new file mode 100644
index 00000000000..39553be112c
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/editor.scss
@@ -0,0 +1,10 @@
+@import "../style";
+
+.wp-block-woocommerce-featured-product {
+ @extend %with-media-controls;
+ @extend %with-resizable-box;
+
+ &__message {
+ margin-bottom: 16px;
+ }
+}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/example.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/example.js
new file mode 100644
index 00000000000..71fbe0c1da0
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/example.js
@@ -0,0 +1,11 @@
+/**
+ * External dependencies
+ */
+import { previewProducts } from '@woocommerce/resource-previews';
+
+export const example = {
+ attributes: {
+ productId: 'preview',
+ previewProduct: previewProducts[ 0 ],
+ },
+};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/index.tsx
new file mode 100644
index 00000000000..0bf5e083f7f
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/index.tsx
@@ -0,0 +1,25 @@
+/**
+ * External dependencies
+ */
+import { Icon, starEmpty } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+import './editor.scss';
+import Block from './block';
+import { register } from '../register';
+import { example } from './example';
+import metadata from './block.json';
+
+register( Block, example, metadata, {
+ icon: {
+ src: (
+
+ ),
+ },
+} );
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/style.scss
new file mode 100644
index 00000000000..5e2a726dba7
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/featured-product/style.scss
@@ -0,0 +1,32 @@
+@import "../style";
+
+.wp-block-woocommerce-featured-product {
+ @extend %wp-block-featured-item;
+ background-color: transparent;
+}
+
+.wc-block-featured-product {
+ @include wc-block-featured-item();
+
+ .wc-block-featured-product__title,
+ .wc-block-featured-product__variation {
+ margin-top: 0;
+ border: 0;
+
+ &::before {
+ display: none;
+ }
+ }
+
+ .wc-block-featured-product__variation {
+ font-style: italic;
+ padding-top: 0;
+ }
+
+ .wc-block-featured-product__description {
+ p {
+ margin: 0;
+ line-height: 1.5;
+ }
+ }
+}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/image-editor.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/image-editor.js
new file mode 100644
index 00000000000..8de0d51ca56
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/image-editor.js
@@ -0,0 +1,85 @@
+/* eslint-disable @wordpress/no-unsafe-wp-apis */
+
+/**
+ * External dependencies
+ */
+import {
+ __experimentalImageEditingProvider as ImageEditingProvider,
+ __experimentalImageEditor as GutenbergImageEditor,
+} from '@wordpress/block-editor';
+
+/**
+ * Internal dependencies
+ */
+import { BLOCK_NAMES, DEFAULT_EDITOR_SIZE } from './constants';
+import { useBackgroundImage } from './use-background-image';
+
+export const ImageEditor = ( {
+ backgroundImageId,
+ backgroundImageSize,
+ backgroundImageSrc,
+ isEditingImage,
+ setAttributes,
+ setIsEditingImage,
+} ) => {
+ return (
+ <>
+ {
+ setAttributes( { mediaId: id, mediaSrc: url } );
+ } }
+ isEditing={ isEditingImage }
+ onFinishEditing={ () => setIsEditingImage( false ) }
+ >
+
+
+ >
+ );
+};
+
+export const withImageEditor = ( Component ) => ( props ) => {
+ const [ isEditingImage, setIsEditingImage ] = props.useEditingImage;
+
+ const { attributes, backgroundImageSize, name, setAttributes } = props;
+ const { mediaId, mediaSrc } = attributes;
+ const item =
+ name === BLOCK_NAMES.featuredProduct ? props.product : props.category;
+
+ const { backgroundImageId, backgroundImageSrc } = useBackgroundImage( {
+ item,
+ mediaId,
+ mediaSrc,
+ blockName: name,
+ } );
+
+ if ( isEditingImage ) {
+ return (
+
+ );
+ }
+
+ return ;
+};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/inspector-controls.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/inspector-controls.js
new file mode 100644
index 00000000000..6faafc1b9c6
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/inspector-controls.js
@@ -0,0 +1,292 @@
+/* eslint-disable @wordpress/no-unsafe-wp-apis */
+
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import {
+ InspectorControls as GutenbergInspectorControls,
+ __experimentalPanelColorGradientSettings as PanelColorGradientSettings,
+ __experimentalUseGradient as useGradient,
+} from '@wordpress/block-editor';
+import {
+ FocalPointPicker,
+ PanelBody,
+ RangeControl,
+ ToggleControl,
+ __experimentalToggleGroupControl as ToggleGroupControl,
+ __experimentalToggleGroupControlOption as ToggleGroupControlOption,
+ TextareaControl,
+ ExternalLink,
+} from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import { useBackgroundImage } from './use-background-image';
+import { BLOCK_NAMES } from './constants';
+
+export const InspectorControls = ( {
+ alt,
+ backgroundImageSrc,
+ contentPanel,
+ dimRatio,
+ focalPoint = { x: 0.5, y: 0.5 },
+ hasParallax,
+ isRepeated,
+ imageFit,
+ overlayColor,
+ overlayGradient,
+ setAttributes,
+ setGradient,
+ showDesc,
+} ) => {
+ // FocalPointPicker was introduced in Gutenberg 5.0 (WordPress 5.2),
+ // so we need to check if it exists before using it.
+ const focalPointPickerExists = typeof FocalPointPicker === 'function';
+
+ const isImgElement = ! isRepeated && ! hasParallax;
+
+ return (
+
+
+ setAttributes( { showDesc: ! showDesc } ) }
+ />
+ { contentPanel }
+
+ { !! backgroundImageSrc && (
+ <>
+ { focalPointPickerExists && (
+
+ {
+ setAttributes( {
+ hasParallax: ! hasParallax,
+ } );
+ } }
+ />
+ {
+ setAttributes( {
+ isRepeated: ! isRepeated,
+ } );
+ } }
+ />
+ { ! isRepeated && (
+
+
+ { __(
+ 'Choose “Cover” if you want the image to scale automatically to always fit its container.',
+ 'woo-gutenberg-products-block'
+ ) }
+
+
+ { __(
+ 'Note: by choosing “Cover” you will lose the ability to freely move the focal point precisely.',
+ 'woo-gutenberg-products-block'
+ ) }
+
+ >
+ }
+ label={ __(
+ 'Image fit',
+ 'woo-gutenberg-products-block'
+ ) }
+ value={ imageFit }
+ onChange={ ( value ) =>
+ setAttributes( {
+ imageFit: value,
+ } )
+ }
+ >
+
+
+
+ ) }
+
+ setAttributes( {
+ focalPoint: value,
+ } )
+ }
+ />
+ { isImgElement && (
+ {
+ setAttributes( { alt: value } );
+ } }
+ help={
+ <>
+
+ { __(
+ 'Describe the purpose of the image',
+ 'woo-gutenberg-products-block'
+ ) }
+
+ >
+ }
+ />
+ ) }
+
+ ) }
+
+ setAttributes( { overlayColor: value } ),
+ onGradientChange: ( value ) => {
+ setGradient( value );
+ setAttributes( {
+ overlayGradient: value,
+ } );
+ },
+ label: __(
+ 'Color',
+ 'woo-gutenberg-products-block'
+ ),
+ },
+ ] }
+ >
+
+ setAttributes( { dimRatio: value } )
+ }
+ min={ 0 }
+ max={ 100 }
+ step={ 10 }
+ required
+ />
+
+ >
+ ) }
+
+ );
+};
+
+export const withInspectorControls = ( Component ) => ( props ) => {
+ const { attributes, name, setAttributes } = props;
+ const {
+ alt,
+ dimRatio,
+ focalPoint,
+ hasParallax,
+ isRepeated,
+ imageFit,
+ mediaId,
+ mediaSrc,
+ overlayColor,
+ overlayGradient,
+ showDesc,
+ showPrice,
+ } = attributes;
+
+ const item =
+ name === BLOCK_NAMES.featuredProduct ? props.product : props.category;
+
+ const { setGradient } = useGradient( {
+ gradientAttribute: 'overlayGradient',
+ customGradientAttribute: 'overlayGradient',
+ } );
+ const { backgroundImageSrc } = useBackgroundImage( {
+ item,
+ mediaId,
+ mediaSrc,
+ blockName: name,
+ } );
+
+ const contentPanel = name === BLOCK_NAMES.featuredProduct && (
+
+ setAttributes( {
+ showPrice: ! showPrice,
+ } )
+ }
+ />
+ );
+
+ return (
+ <>
+
+
+ >
+ );
+};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/register.tsx b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/register.tsx
new file mode 100644
index 00000000000..067b22e30f2
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/register.tsx
@@ -0,0 +1,100 @@
+/**
+ * External dependencies
+ */
+import { FunctionComponent } from 'react';
+import { InnerBlocks } from '@wordpress/block-editor';
+import { BlockConfiguration, registerBlockType } from '@wordpress/blocks';
+import { getSetting } from '@woocommerce/settings';
+import { isFeaturePluginBuild } from '@woocommerce/block-settings';
+
+/**
+ * Internal dependencies
+ */
+import { Edit } from './edit';
+
+export function register(
+ Block: FunctionComponent,
+ example: { attributes: Record< string, unknown > },
+ metadata: BlockConfiguration,
+ settings: Partial< BlockConfiguration >
+): void {
+ const DEFAULT_SETTINGS = {
+ attributes: {
+ ...metadata.attributes,
+ /**
+ * A minimum height for the block.
+ *
+ * Note: if padding is increased, this way the inner content will never
+ * overflow, but instead will resize the container.
+ *
+ * It was decided to change this to make this block more in line with
+ * the “Cover” block.
+ */
+ minHeight: {
+ type: 'number',
+ default: getSetting( 'default_height', 500 ),
+ },
+ },
+ supports: {
+ ...metadata.supports,
+ color: {
+ background: metadata.supports?.color?.background,
+ text: metadata.supports?.color?.text,
+ ...( isFeaturePluginBuild() && {
+ __experimentalDuotone:
+ metadata.supports?.color?.__experimentalDuotone,
+ } ),
+ },
+ spacing: {
+ padding: metadata.supports?.spacing?.padding,
+ ...( isFeaturePluginBuild() && {
+ __experimentalDefaultControls: {
+ padding:
+ metadata.supports?.spacing
+ ?.__experimentalDefaultControls,
+ },
+ __experimentalSkipSerialization:
+ metadata.supports?.spacing
+ ?.__experimentalSkipSerialization,
+ } ),
+ },
+ ...( isFeaturePluginBuild() && {
+ __experimentalBorder: metadata?.supports?.__experimentalBorder,
+ } ),
+ },
+ };
+
+ const DEFAULT_EXAMPLE = {
+ attributes: {
+ alt: '',
+ contentAlign: 'center',
+ dimRatio: 50,
+ editMode: false,
+ hasParallax: false,
+ isRepeated: false,
+ height: getSetting( 'default_height', 500 ),
+ mediaSrc: '',
+ overlayColor: '#000000',
+ showDesc: true,
+ },
+ };
+
+ registerBlockType( metadata, {
+ ...DEFAULT_SETTINGS,
+ example: {
+ ...DEFAULT_EXAMPLE,
+ example,
+ },
+ /**
+ * Renders and manages the block.
+ *
+ * @param {Object} props Props to pass to block.
+ */
+ edit: Edit( Block ),
+ /**
+ * Block content is rendered in PHP, not via save function.
+ */
+ save: () => ,
+ ...settings,
+ } );
+}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/style.scss
similarity index 50%
rename from plugins/woocommerce-blocks/assets/js/blocks/featured-product/style.scss
rename to plugins/woocommerce-blocks/assets/js/blocks/featured-items/style.scss
index caf6872aef9..562d62fb4ed 100644
--- a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/style.scss
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/style.scss
@@ -1,27 +1,12 @@
-.wp-block-woocommerce-featured-product {
- background-color: transparent;
- border-color: transparent;
- color: #fff;
- box-sizing: border-box;
+@mixin with-content-selection {
+ background-color: inherit;
- .components-resizable-box__container {
- position: absolute !important;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- min-height: 50px;
-
- &:not(.is-resizing) {
- height: auto !important;
- }
- }
-
- &.is-repeated {
- background-repeat: repeat;
- background-size: auto;
+ &__selection {
+ width: 100%;
}
+}
+%with-media-controls {
// Applying image edits
.is-applying {
.components-spinner {
@@ -38,11 +23,42 @@
}
}
-.wc-block-featured-product {
+%with-resizable-box {
+ .components-resizable-box__container {
+ position: absolute !important;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ min-height: 50px;
+
+ &:not(.is-resizing) {
+ height: auto !important;
+ }
+ }
+
+ .components-resizable-box__handle {
+ z-index: 10;
+ }
+}
+
+%wp-block-featured-item {
+ background-color: transparent;
+ border-color: transparent;
+ color: #fff;
+ box-sizing: border-box;
+}
+
+@mixin wc-block-featured-item {
+ $block: &;
+
+ @include with-background-dim();
+ @include with-content-selection();
+
align-content: center;
align-items: center;
- background-size: cover;
background-position: center center;
+ background-size: cover;
display: flex;
flex-wrap: wrap;
justify-content: center;
@@ -51,25 +67,13 @@
position: relative;
width: 100%;
- .wc-block-featured-product__wrapper {
- align-content: center;
- align-items: center;
- box-sizing: border-box;
- display: flex;
- flex-wrap: wrap;
- height: 100%;
- width: 100%;
- justify-content: center;
- overflow: hidden;
- }
-
&.has-left-content {
justify-content: flex-start;
- .wc-block-featured-product__title,
- .wc-block-featured-product__variation,
- .wc-block-featured-product__description,
- .wc-block-featured-product__price {
+ #{$block}__description,
+ #{$block}__price,
+ #{$block}__title,
+ #{$block}__variation {
margin-left: 0;
text-align: left;
}
@@ -78,23 +82,27 @@
&.has-right-content {
justify-content: flex-end;
- .wc-block-featured-product__title,
- .wc-block-featured-product__variation,
- .wc-block-featured-product__description,
- .wc-block-featured-product__price {
+ #{$block}__description,
+ #{$block}__price,
+ #{$block}__title,
+ #{$block}__variation {
margin-right: 0;
text-align: right;
}
}
- .wc-block-featured-product__title,
- .wc-block-featured-product__variation,
- .wc-block-featured-product__description,
- .wc-block-featured-product__price {
+ &.is-repeated {
+ background-repeat: repeat;
+ background-size: auto;
+ }
+
+ &__description,
+ &__price,
+ &__title,
+ &__variation {
line-height: 1.25;
margin-bottom: 0;
text-align: center;
- color: inherit;
a,
a:hover,
@@ -104,39 +112,18 @@
}
}
- .wc-block-featured-product__title,
- .wc-block-featured-product__variation,
- .wc-block-featured-product__description,
- .wc-block-featured-product__price,
- .wc-block-featured-product__link {
+ &__description,
+ &__link,
+ &__price,
+ &__title,
+ &__variation {
+ color: inherit;
width: 100%;
- padding: 16px 48px 0 48px;
+ padding: 0 48px 16px 48px;
z-index: 1;
}
- .wc-block-featured-product__title,
- .wc-block-featured-product__variation {
- margin-top: 0;
- border: 0;
-
- &::before {
- display: none;
- }
- }
-
- .wc-block-featured-product__variation {
- font-style: italic;
- padding-top: 0;
- }
-
- .wc-block-featured-product__description {
- p {
- margin: 0;
- line-height: 1.5;
- }
- }
-
- .wc-block-featured-product__background-image {
+ & &__background-image {
@include absolute-stretch();
object-fit: none;
@@ -157,57 +144,39 @@
}
}
+ &__description {
+ color: inherit;
+
+ p {
+ margin: 0;
+ }
+ }
+
+ &__title {
+ margin-top: 0;
+
+ div {
+ color: inherit;
+ }
+
+ &::before {
+ display: none;
+ }
+ }
+
+ &__wrapper {
+ align-content: center;
+ align-items: center;
+ box-sizing: border-box;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+ }
+
.wp-block-button.aligncenter {
text-align: center;
}
-
- &.has-background-dim {
- .wc-block-featured-product__overlay::before {
- background: inherit;
- content: "";
- position: absolute;
- top: 0;
- left: 0;
- bottom: 0;
- right: 0;
- opacity: 0.5;
- z-index: 1;
- }
- }
-
- @for $i from 1 through 10 {
- &.has-background-dim.has-background-dim-#{ $i * 10 } {
- .wc-block-featured-product__overlay::before {
- opacity: $i * 0.1;
- }
- }
- }
-
- // Apply max-width to floated items that have no intrinsic width
- &.alignleft,
- &.alignright {
- max-width: $content-width * 0.5;
- width: 100%;
- }
-
- // Using flexbox without an assigned height property breaks vertical center alignment in IE11.
- // Appending an empty ::after element tricks IE11 into giving the cover image an implicit height, which sidesteps this issue.
- &::after {
- display: block;
- content: "";
- font-size: 0;
- min-height: inherit;
-
- // IE doesn't support flex so omit that.
- @supports (position: sticky) {
- content: none;
- }
- }
-
- // Aligned cover blocks should not use our global alignment rules
- &.aligncenter,
- &.alignleft,
- &.alignright {
- display: flex;
- }
}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/use-background-image.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/use-background-image.js
new file mode 100644
index 00000000000..77d62f6ceb4
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/use-background-image.js
@@ -0,0 +1,48 @@
+/**
+ * External dependencies
+ */
+import {
+ getImageSrcFromProduct,
+ getImageIdFromProduct,
+} from '@woocommerce/utils';
+import { useEffect, useState } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import { BLOCK_NAMES } from './constants';
+import {
+ getCategoryImageSrc,
+ getCategoryImageId,
+} from './featured-category/utils';
+
+export function useBackgroundImage( { blockName, item, mediaId, mediaSrc } ) {
+ const [ backgroundImageId, setBackgroundImageId ] = useState( 0 );
+ const [ backgroundImageSrc, setBackgroundImageSrc ] = useState( '' );
+
+ useEffect( () => {
+ if ( mediaId ) {
+ setBackgroundImageId( mediaId );
+ } else {
+ setBackgroundImageId(
+ blockName === BLOCK_NAMES.featuredProduct
+ ? getImageIdFromProduct( item )
+ : getCategoryImageId( item )
+ );
+ }
+ }, [ blockName, item, mediaId ] );
+
+ useEffect( () => {
+ if ( mediaSrc ) {
+ setBackgroundImageSrc( mediaSrc );
+ } else {
+ setBackgroundImageSrc(
+ blockName === BLOCK_NAMES.featuredProduct
+ ? getImageSrcFromProduct( item )
+ : getCategoryImageSrc( item )
+ );
+ }
+ }, [ blockName, item, mediaSrc ] );
+
+ return { backgroundImageId, backgroundImageSrc };
+}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/utils.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/utils.js
new file mode 100644
index 00000000000..53be26f3a62
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/utils.js
@@ -0,0 +1,87 @@
+export function calculateBackgroundImagePosition( coords ) {
+ if ( ! coords ) return {};
+
+ return {
+ objectPosition: calculatePercentPositionFromCoordinates( coords ),
+ };
+}
+
+export function calculatePercentPositionFromCoordinates( coords ) {
+ if ( ! coords ) return '';
+
+ const x = Math.round( coords.x * 100 );
+ const y = Math.round( coords.y * 100 );
+
+ return `${ x }% ${ y }%`;
+}
+
+/**
+ * Generate the style object of the background image of the block.
+ *
+ * It outputs styles for either an `img` element or a `div` with a background,
+ * depending on what is needed.
+ *
+ * @param {Object} opts Options for the element.
+ * @param {Object} opts.focalPoint X and Y coordinates of the image.
+ * @param {'cover' | 'none'} opts.imageFit How to fit the image in the wrapper.
+ * @param {boolean} opts.isImgElement Whether the rendered background is an `img` element.
+ * @param {boolean} opts.isRepeated Whether the background is repeated (no effect if `isImgElement` is `true`).
+ * @param {string} opts.url The url of the image.
+ *
+ * @return {Object} A style object with a backgroundImage set (if a valid image is provided).
+ */
+export function getBackgroundImageStyles( {
+ focalPoint,
+ imageFit,
+ isImgElement,
+ isRepeated,
+ url,
+} ) {
+ let styles = {};
+
+ if ( isImgElement ) {
+ styles = {
+ ...styles,
+ ...calculateBackgroundImagePosition( focalPoint ),
+ objectFit: imageFit,
+ };
+ } else {
+ styles = {
+ ...styles,
+ ...( url && {
+ backgroundImage: `url(${ url })`,
+ } ),
+ backgroundPosition: calculatePercentPositionFromCoordinates(
+ focalPoint
+ ),
+ ...( ! isRepeated && {
+ backgroundRepeat: 'no-repeat',
+ backgroundSize: imageFit === 'cover' ? imageFit : 'auto',
+ } ),
+ };
+ }
+
+ return styles;
+}
+
+/**
+ * Generates the CSS class prefix for scoping elements to a block.
+ *
+ * @param {string} blockName The name of the block.
+ * @return {string} The prefix for the HTML elements belonging to that block.
+ */
+export function getClassPrefixFromName( blockName ) {
+ return `wc-block-${ blockName.split( '/' )[ 1 ] }`;
+}
+
+/**
+ * Convert the selected ratio to the correct background class.
+ *
+ * @param {number} ratio Selected opacity from 0 to 100.
+ * @return {string} The class name, if applicable (not used for ratio 0 or 50).
+ */
+export function dimRatioToClass( ratio ) {
+ return ratio === 0 || ratio === 50
+ ? null
+ : `has-background-dim-${ 10 * Math.round( ratio / 10 ) }`;
+}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-api-error.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-api-error.js
new file mode 100644
index 00000000000..7e920db80c0
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-api-error.js
@@ -0,0 +1,33 @@
+/**
+ * External dependencies
+ */
+import ErrorPlaceholder from '@woocommerce/editor-components/error-placeholder';
+
+/**
+ * Internal dependencies
+ */
+import { BLOCK_NAMES } from './constants';
+import { getClassPrefixFromName } from './utils';
+
+export const withApiError = ( Component ) => ( props ) => {
+ const { error, isLoading, name } = props;
+
+ const className = getClassPrefixFromName( name );
+ const onRetry =
+ name === BLOCK_NAMES.featuredCategory
+ ? props.getCategory
+ : props.getProduct;
+
+ if ( error ) {
+ return (
+
+ );
+ }
+
+ return ;
+};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-edit-mode.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-edit-mode.js
new file mode 100644
index 00000000000..52eff715c2f
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-edit-mode.js
@@ -0,0 +1,81 @@
+/**
+ * External dependencies
+ */
+import { Placeholder, Icon, Button } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import ProductCategoryControl from '@woocommerce/editor-components/product-category-control';
+import ProductControl from '@woocommerce/editor-components/product-control';
+
+/**
+ * Internal dependencies
+ */
+import { getClassPrefixFromName } from './utils';
+import { BLOCK_NAMES } from './constants';
+
+export const withEditMode = ( { description, editLabel, icon, label } ) => (
+ Component
+) => ( props ) => {
+ const {
+ attributes,
+ debouncedSpeak,
+ name,
+ setAttributes,
+ triggerUrlUpdate = () => void null,
+ } = props;
+
+ const className = getClassPrefixFromName( name );
+
+ const onDone = () => {
+ setAttributes( { editMode: false } );
+ debouncedSpeak( editLabel );
+ };
+
+ if ( attributes.editMode ) {
+ return (
+ }
+ label={ label }
+ className={ className }
+ >
+ { description }
+
+ { name === BLOCK_NAMES.featuredCategory && (
+
{
+ const id = value[ 0 ] ? value[ 0 ].id : 0;
+ setAttributes( {
+ categoryId: id,
+ mediaId: 0,
+ mediaSrc: '',
+ } );
+ triggerUrlUpdate();
+ } }
+ isSingle
+ />
+ ) }
+ { name === BLOCK_NAMES.featuredProduct && (
+ {
+ const id = value[ 0 ] ? value[ 0 ].id : 0;
+ setAttributes( {
+ productId: id,
+ mediaId: 0,
+ mediaSrc: '',
+ } );
+ triggerUrlUpdate();
+ } }
+ />
+ ) }
+
+
+
+ );
+ }
+
+ return ;
+};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-editing-image.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-editing-image.js
new file mode 100644
index 00000000000..e3653d43545
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-editing-image.js
@@ -0,0 +1,20 @@
+/**
+ * External dependencies
+ */
+import { useEffect, useState } from 'react';
+
+export const withEditingImage = ( Component ) => ( props ) => {
+ const [ isEditingImage, setIsEditingImage ] = useState( false );
+ const { isSelected } = props;
+
+ useEffect( () => {
+ setIsEditingImage( false );
+ }, [ isSelected ] );
+
+ return (
+
+ );
+};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-featured-item.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-featured-item.js
new file mode 100644
index 00000000000..9b683060c49
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/featured-items/with-featured-item.js
@@ -0,0 +1,237 @@
+/* eslint-disable @wordpress/no-unsafe-wp-apis */
+
+/**
+ * External dependencies
+ */
+import { __experimentalGetSpacingClassesAndStyles as getSpacingClassesAndStyles } from '@wordpress/block-editor';
+import { Icon, Placeholder, Spinner } from '@wordpress/components';
+import classnames from 'classnames';
+import { isEmpty } from 'lodash';
+import { useCallback, useState } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import { CallToAction } from './call-to-action';
+import { ConstrainedResizable } from './constrained-resizable';
+import { useBackgroundImage } from './use-background-image';
+import {
+ dimRatioToClass,
+ getBackgroundImageStyles,
+ getClassPrefixFromName,
+} from './utils';
+
+export const withFeaturedItem = ( { emptyMessage, icon, label } ) => (
+ Component
+) => ( props ) => {
+ const [ isEditingImage ] = props.useEditingImage;
+
+ const {
+ attributes,
+ category,
+ isLoading,
+ isSelected,
+ name,
+ product,
+ setAttributes,
+ } = props;
+ const { mediaId, mediaSrc } = attributes;
+ const item = category || product;
+ const [ backgroundImageSize, setBackgroundImageSize ] = useState( {} );
+
+ const { backgroundImageSrc } = useBackgroundImage( {
+ item,
+ mediaId,
+ mediaSrc,
+ blockName: name,
+ } );
+
+ const className = getClassPrefixFromName( name );
+
+ const onResize = useCallback(
+ ( _event, _direction, elt ) => {
+ setAttributes( { minHeight: parseInt( elt.style.height, 10 ) } );
+ },
+ [ setAttributes ]
+ );
+
+ const renderButton = () => {
+ const { categoryId, linkText, productId } = attributes;
+
+ return (
+
+ );
+ };
+
+ const renderNoItem = () => (
+ }
+ label={ label }
+ >
+ { isLoading ? : emptyMessage }
+
+ );
+
+ const renderItem = () => {
+ const {
+ contentAlign,
+ dimRatio,
+ focalPoint,
+ hasParallax,
+ isRepeated,
+ imageFit,
+ minHeight,
+ overlayColor,
+ overlayGradient,
+ showDesc,
+ showPrice,
+ style,
+ } = attributes;
+
+ const classes = classnames(
+ className,
+ {
+ 'is-selected':
+ isSelected &&
+ attributes.categoryId !== 'preview' &&
+ attributes.productId !== 'preview',
+ 'is-loading': ! item && isLoading,
+ 'is-not-found': ! item && ! isLoading,
+ 'has-background-dim': dimRatio !== 0,
+ 'is-repeated': isRepeated,
+ },
+ dimRatioToClass( dimRatio ),
+ contentAlign !== 'center' && `has-${ contentAlign }-content`
+ );
+
+ const containerStyle = {
+ borderRadius: style?.border?.radius,
+ };
+
+ const wrapperStyle = {
+ ...getSpacingClassesAndStyles( attributes ).style,
+ minHeight,
+ };
+
+ const isImgElement = ! isRepeated && ! hasParallax;
+
+ const backgroundImageStyle = getBackgroundImageStyles( {
+ focalPoint,
+ imageFit,
+ isImgElement,
+ isRepeated,
+ url: backgroundImageSrc,
+ } );
+
+ const overlayStyle = {
+ background: overlayGradient,
+ backgroundColor: overlayColor,
+ };
+
+ return (
+ <>
+
+
+
+
+ { backgroundImageSrc &&
+ ( isImgElement ? (
+
{
+ setBackgroundImageSize( {
+ height: e.target?.naturalHeight,
+ width: e.target?.naturalWidth,
+ } );
+ } }
+ />
+ ) : (
+
+ ) ) }
+
+ { ! isEmpty( product?.variation ) && (
+
+ ) }
+ { showDesc && (
+
+ ) }
+ { showPrice && (
+
+ ) }
+
+ { renderButton() }
+
+
+
+ >
+ );
+ };
+
+ if ( isEditingImage ) {
+ return (
+
+ );
+ }
+
+ return (
+ <>
+
+ { item ? renderItem() : renderNoItem() }
+ >
+ );
+};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/block.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-product/block.js
deleted file mode 100644
index 263b309343d..00000000000
--- a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/block.js
+++ /dev/null
@@ -1,851 +0,0 @@
-/* eslint-disable @wordpress/no-unsafe-wp-apis */
-
-/**
- * External dependencies
- */
-import { useCallback, useEffect, useState } from 'react';
-import { __ } from '@wordpress/i18n';
-import {
- AlignmentToolbar,
- BlockControls,
- InnerBlocks,
- InspectorControls,
- MediaReplaceFlow,
- RichText,
- __experimentalGetSpacingClassesAndStyles as getSpacingClassesAndStyles,
- __experimentalImageEditingProvider as ImageEditingProvider,
- __experimentalImageEditor as ImageEditor,
- __experimentalPanelColorGradientSettings as PanelColorGradientSettings,
- __experimentalUseGradient as useGradient,
-} from '@wordpress/block-editor';
-import { withSelect } from '@wordpress/data';
-import {
- Button,
- ExternalLink,
- FocalPointPicker,
- PanelBody,
- Placeholder,
- RangeControl,
- ResizableBox,
- Spinner,
- TextareaControl,
- ToggleControl,
- ToolbarButton,
- ToolbarGroup,
- withSpokenMessages,
- __experimentalToggleGroupControl as ToggleGroupControl,
- __experimentalToggleGroupControlOption as ToggleGroupControlOption,
-} from '@wordpress/components';
-import classnames from 'classnames';
-import { Component } from '@wordpress/element';
-import { compose, createHigherOrderComponent } from '@wordpress/compose';
-import { isEmpty } from 'lodash';
-import PropTypes from 'prop-types';
-import ProductControl from '@woocommerce/editor-components/product-control';
-import ErrorPlaceholder from '@woocommerce/editor-components/error-placeholder';
-import TextToolbarButton from '@woocommerce/editor-components/text-toolbar-button';
-import { withProduct } from '@woocommerce/block-hocs';
-import { crop, Icon, starEmpty } from '@wordpress/icons';
-
-/**
- * Internal dependencies
- */
-import {
- backgroundImageStyles,
- calculateImagePosition,
- dimRatioToClass,
-} from './utils';
-import {
- getImageSrcFromProduct,
- getImageIdFromProduct,
-} from '../../utils/products';
-import { useThrottle } from '../../utils/useThrottle';
-
-const DEFAULT_EDITOR_SIZE = {
- height: 500,
- width: 500,
-};
-
-export const ConstrainedResizable = ( {
- className = '',
- onResize,
- ...props
-} ) => {
- const [ isResizing, setIsResizing ] = useState( false );
-
- const classNames = classnames( className, {
- 'is-resizing': isResizing,
- } );
- const throttledResize = useThrottle(
- ( event, direction, elt ) => {
- if ( ! isResizing ) setIsResizing( true );
- onResize( event, direction, elt );
- },
- 50,
- { leading: true }
- );
-
- return (
- {
- onResize( ...args );
- setIsResizing( false );
- } }
- { ...props }
- />
- );
-};
-
-/**
- * Component to handle edit mode of "Featured Product".
- *
- * @param {Object} props Incoming props for the component.
- * @param {Object} props.attributes Incoming block attributes.
- * @param {function(any):any} props.debouncedSpeak Function for delayed speak.
- * @param {string} props.error Error message.
- * @param {function(any):any} props.getProduct Function for getting the product.
- * @param {boolean} props.isLoading Whether product is loading or not.
- * @param {boolean} props.isSelected Whether block is selected or not.
- * @param {Object} props.product Product object.
- * @param {function(any):any} props.setAttributes Setter for attributes.
- * @param {function():any} props.triggerUrlUpdate Function for triggering a url update for product.
- */
-const FeaturedProduct = ( {
- attributes,
- debouncedSpeak,
- error,
- getProduct,
- isLoading,
- isSelected,
- product,
- setAttributes,
- triggerUrlUpdate = () => void null,
-} ) => {
- const { mediaId, mediaSrc } = attributes;
-
- const [ isEditingImage, setIsEditingImage ] = useState( false );
- const [ backgroundImageSize, setBackgroundImageSize ] = useState( {} );
- const { setGradient } = useGradient( {
- gradientAttribute: 'overlayGradient',
- customGradientAttribute: 'overlayGradient',
- } );
-
- const backgroundImageSrc = mediaSrc || getImageSrcFromProduct( product );
- const backgroundImageId = mediaId || getImageIdFromProduct( product );
-
- const onResize = useCallback(
- ( _event, _direction, elt ) => {
- setAttributes( { minHeight: parseInt( elt.style.height, 10 ) } );
- },
- [ setAttributes ]
- );
-
- const renderApiError = () => (
-
- );
-
- useEffect( () => {
- setIsEditingImage( false );
- }, [ isSelected ] );
-
- const renderEditMode = () => {
- const onDone = () => {
- setAttributes( { editMode: false } );
- debouncedSpeak(
- __(
- 'Showing Featured Product block preview.',
- 'woo-gutenberg-products-block'
- )
- );
- };
-
- return (
- <>
- { getBlockControls() }
- }
- label={ __(
- 'Featured Product',
- 'woo-gutenberg-products-block'
- ) }
- className="wc-block-featured-product"
- >
- { __(
- 'Visually highlight a product or variation and encourage prompt action',
- 'woo-gutenberg-products-block'
- ) }
-
-
{
- const id = value[ 0 ] ? value[ 0 ].id : 0;
- setAttributes( {
- productId: id,
- mediaId: 0,
- mediaSrc: '',
- } );
- triggerUrlUpdate();
- } }
- />
-
-
-
- >
- );
- };
-
- const getBlockControls = () => {
- const { contentAlign, editMode } = attributes;
-
- return (
-
- {
- setAttributes( { contentAlign: nextAlign } );
- } }
- />
-
- { backgroundImageSrc && ! isEditingImage && (
- setIsEditingImage( true ) }
- icon={ crop }
- label={ __(
- 'Edit product image',
- 'woo-gutenberg-products-block'
- ) }
- />
- ) }
- {
- setAttributes( {
- mediaId: media.id,
- mediaSrc: media.url,
- } );
- } }
- allowedTypes={ [ 'image' ] }
- />
- { backgroundImageId && mediaSrc ? (
-
- setAttributes( { mediaId: 0, mediaSrc: '' } )
- }
- >
- { __( 'Reset', 'woo-gutenberg-products-block' ) }
-
- ) : null }
-
-
- setAttributes( { editMode: ! editMode } ),
- isActive: editMode,
- },
- ] }
- />
-
- );
- };
-
- const getInspectorControls = () => {
- const url = attributes.mediaSrc || getImageSrcFromProduct( product );
- const {
- focalPoint = { x: 0.5, y: 0.5 },
- hasParallax,
- isRepeated,
- } = attributes;
- // FocalPointPicker was introduced in Gutenberg 5.0 (WordPress 5.2),
- // so we need to check if it exists before using it.
- const focalPointPickerExists = typeof FocalPointPicker === 'function';
-
- const isImgElement = ! isRepeated && ! hasParallax;
-
- return (
- <>
-
-
-
- setAttributes( {
- showDesc: ! attributes.showDesc,
- } )
- }
- />
-
- setAttributes( {
- showPrice: ! attributes.showPrice,
- } )
- }
- />
-
- { !! url && (
- <>
- { focalPointPickerExists && (
-
- {
- setAttributes( {
- hasParallax: ! hasParallax,
- } );
- } }
- />
- {
- setAttributes( {
- isRepeated: ! isRepeated,
- } );
- } }
- />
- { ! isRepeated && (
-
-
- { __(
- 'Choose “Cover” if you want the image to scale automatically to always fit its container.',
- 'woo-gutenberg-products-block'
- ) }
-
-
- { __(
- 'Note: by choosing “Cover” you will lose the ability to freely move the focal point precisely.',
- 'woo-gutenberg-products-block'
- ) }
-
- >
- }
- label={ __(
- 'Image fit',
- 'woo-gutenberg-products-block'
- ) }
- value={ attributes.imageFit }
- onChange={ ( value ) =>
- setAttributes( {
- imageFit: value,
- } )
- }
- >
-
-
-
- ) }
-
- setAttributes( {
- focalPoint: value,
- } )
- }
- />
- { isImgElement && (
- {
- setAttributes( { alt } );
- } }
- help={
- <>
-
- { __(
- 'Describe the purpose of the image',
- 'woo-gutenberg-products-block'
- ) }
-
- { __(
- 'Leaving it empty will use the product name.',
- 'woo-gutenberg-products-block'
- ) }
- >
- }
- />
- ) }
-
- ) }
-
- setAttributes( { overlayColor } ),
- onGradientChange: (
- overlayGradient
- ) => {
- setGradient( overlayGradient );
- setAttributes( {
- overlayGradient,
- } );
- },
- label: __(
- 'Color',
- 'woo-gutenberg-products-block'
- ),
- },
- ] }
- >
-
- setAttributes( { dimRatio } )
- }
- min={ 0 }
- max={ 100 }
- step={ 10 }
- required
- />
-
- >
- ) }
-
-
- >
- );
- };
-
- const renderProduct = () => {
- const {
- contentAlign,
- dimRatio,
- focalPoint,
- hasParallax,
- isRepeated,
- imageFit,
- minHeight,
- overlayColor,
- overlayGradient,
- showDesc,
- showPrice,
- style,
- } = attributes;
-
- const classes = classnames(
- 'wc-block-featured-product',
- dimRatioToClass( dimRatio ),
- {
- 'is-selected': isSelected && attributes.productId !== 'preview',
- 'is-loading': ! product && isLoading,
- 'is-not-found': ! product && ! isLoading,
- 'has-background-dim': dimRatio !== 0,
- 'is-repeated': isRepeated,
- },
- contentAlign !== 'center' && `has-${ contentAlign }-content`
- );
-
- const containerStyle = {
- borderRadius: style?.border?.radius,
- };
-
- const backgroundImageStyle = {
- objectPosition: calculateImagePosition( focalPoint ),
- objectFit: imageFit,
- };
-
- const isImgElement = ! isRepeated && ! hasParallax;
-
- const wrapperStyle = {
- ...getSpacingClassesAndStyles( attributes ).style,
- minHeight,
- };
-
- const backgroundDivStyle = {
- ...( ! isImgElement
- ? {
- ...backgroundImageStyles( backgroundImageSrc ),
- backgroundPosition: calculateImagePosition(
- focalPoint
- ),
- }
- : undefined ),
- ...( ! isRepeated && {
- backgroundRepeat: 'no-repeat',
- backgroundSize: imageFit === 'cover' ? imageFit : 'auto',
- } ),
- };
-
- const overlayStyle = {
- background: overlayGradient,
- backgroundColor: overlayColor,
- };
-
- return (
- <>
-
-
-
-
- { isImgElement && (
-
{
- setBackgroundImageSize( {
- height: e.target?.naturalHeight,
- width: e.target?.naturalWidth,
- } );
- } }
- />
- ) }
- { ! isImgElement && (
-
- ) }
-
- { ! isEmpty( product.variation ) && (
-
- ) }
- { showDesc && (
-
- ) }
- { showPrice && (
-
- ) }
-
- { renderButton() }
-
-
-
- >
- );
- };
-
- const renderButton = () => {
- const buttonClasses = classnames(
- 'wp-block-button__link',
- 'is-style-fill'
- );
- const buttonStyle = {
- backgroundColor: 'vivid-green-cyan',
- borderRadius: '5px',
- };
- const wrapperStyle = {
- width: '100%',
- };
- return attributes.productId === 'preview' ? (
-
-
-
- ) : (
-
- );
- };
-
- const renderNoProduct = () => (
- }
- label={ __( 'Featured Product', 'woo-gutenberg-products-block' ) }
- >
- { isLoading ? (
-
- ) : (
- __( 'No product is selected.', 'woo-gutenberg-products-block' )
- ) }
-
- );
-
- const { editMode } = attributes;
-
- if ( error ) {
- return renderApiError();
- }
-
- if ( editMode ) {
- return renderEditMode();
- }
-
- if ( isEditingImage ) {
- return (
- <>
- {
- setAttributes( { mediaId: id, mediaSrc: url } );
- } }
- isEditing={ isEditingImage }
- onFinishEditing={ () => setIsEditingImage( false ) }
- >
-
-
- >
- );
- }
-
- return (
- <>
- { getBlockControls() }
- { getInspectorControls() }
- { product ? renderProduct() : renderNoProduct() }
- >
- );
-};
-
-FeaturedProduct.propTypes = {
- /**
- * The attributes for this block.
- */
- attributes: PropTypes.object.isRequired,
- /**
- * Whether this block is currently active.
- */
- isSelected: PropTypes.bool.isRequired,
- /**
- * The register block name.
- */
- name: PropTypes.string.isRequired,
- /**
- * A callback to update attributes.
- */
- setAttributes: PropTypes.func.isRequired,
- // from withProduct
- error: PropTypes.object,
- getProduct: PropTypes.func,
- isLoading: PropTypes.bool,
- product: PropTypes.shape( {
- name: PropTypes.node,
- variation: PropTypes.node,
- description: PropTypes.node,
- price_html: PropTypes.node,
- permalink: PropTypes.string,
- } ),
- // from withSpokenMessages
- debouncedSpeak: PropTypes.func.isRequired,
- triggerUrlUpdate: PropTypes.func,
-};
-
-export default compose( [
- withProduct,
- withSpokenMessages,
- withSelect( ( select, { clientId }, { dispatch } ) => {
- const Block = select( 'core/block-editor' ).getBlock( clientId );
- const buttonBlockId = Block?.innerBlocks[ 0 ]?.clientId || '';
- const currentButtonAttributes =
- Block?.innerBlocks[ 0 ]?.attributes || {};
- const updateBlockAttributes = ( attributes ) => {
- if ( buttonBlockId ) {
- dispatch( 'core/block-editor' ).updateBlockAttributes(
- buttonBlockId,
- attributes
- );
- }
- };
- return { updateBlockAttributes, currentButtonAttributes };
- } ),
- createHigherOrderComponent( ( ProductComponent ) => {
- class WrappedComponent extends Component {
- state = {
- doUrlUpdate: false,
- };
-
- componentDidUpdate() {
- const {
- attributes,
- updateBlockAttributes,
- currentButtonAttributes,
- product,
- } = this.props;
- if (
- this.state.doUrlUpdate &&
- ! attributes.editMode &&
- product?.permalink &&
- currentButtonAttributes?.url &&
- product.permalink !== currentButtonAttributes.url
- ) {
- updateBlockAttributes( {
- ...currentButtonAttributes,
- url: product.permalink,
- } );
- this.setState( { doUrlUpdate: false } );
- }
- }
-
- triggerUrlUpdate = () => {
- this.setState( { doUrlUpdate: true } );
- };
-
- render() {
- return (
-
- );
- }
- }
-
- return WrappedComponent;
- }, 'withUpdateButtonAttributes' ),
-] )( FeaturedProduct );
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/featured-product/edit.tsx
deleted file mode 100644
index d8fe566c42f..00000000000
--- a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/edit.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * External dependencies
- */
-import { useBlockProps } from '@wordpress/block-editor';
-
-/**
- * Internal dependencies
- */
-import Block from './block';
-import './editor.scss';
-
-export const Edit = ( props: unknown ): JSX.Element => {
- const blockProps = useBlockProps();
-
- return (
-
-
-
- );
-};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/featured-product/editor.scss
deleted file mode 100644
index acc516bef47..00000000000
--- a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/editor.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-.wc-block-featured-product {
- background-color: inherit;
-
- .components-resizable-box__handle {
- z-index: 10;
- }
-}
-.wc-block-featured-product__message {
- margin-bottom: 16px;
-}
-
-.wc-block-featured-product__selection {
- width: 100%;
-}
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/example.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-product/example.js
deleted file mode 100644
index f57fbc999b2..00000000000
--- a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/example.js
+++ /dev/null
@@ -1,22 +0,0 @@
-/**
- * External dependencies
- */
-import { getSetting } from '@woocommerce/settings';
-import { previewProducts } from '@woocommerce/resource-previews';
-
-export const example = {
- attributes: {
- alt: '',
- contentAlign: 'center',
- dimRatio: 50,
- editMode: false,
- hasParallax: false,
- isRepeated: false,
- height: getSetting( 'default_height', 500 ),
- mediaSrc: '',
- overlayColor: '#000000',
- showDesc: true,
- productId: 'preview',
- previewProduct: previewProducts[ 0 ],
- },
-};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/featured-product/index.tsx
deleted file mode 100644
index 1b5c3b3b287..00000000000
--- a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/index.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-/**
- * External dependencies
- */
-import { InnerBlocks } from '@wordpress/block-editor';
-import { registerBlockType } from '@wordpress/blocks';
-import { getSetting } from '@woocommerce/settings';
-import { isFeaturePluginBuild } from '@woocommerce/block-settings';
-import { Icon, starEmpty } from '@wordpress/icons';
-
-/**
- * Internal dependencies
- */
-import './style.scss';
-import { example } from './example';
-import { Edit } from './edit';
-import metadata from './block.json';
-
-/**
- * Register and run the "Featured Product" block.
- */
-registerBlockType( metadata, {
- icon: {
- src: (
-
- ),
- },
- attributes: {
- ...metadata.attributes,
- /**
- * A minimum height for the block.
- *
- * Note: if padding is increased, this way the inner content will never
- * overflow, but instead will resize the container.
- *
- * It was decided to change this to make this block more in line with
- * the “Cover” block.
- */
- minHeight: {
- type: 'number',
- default: getSetting( 'default_height', 500 ),
- },
- },
- supports: {
- ...metadata.supports,
- color: {
- background: true,
- text: true,
- ...( isFeaturePluginBuild() && {
- __experimentalDuotone:
- '.wc-block-featured-product__background-image',
- } ),
- },
- spacing: {
- padding: true,
- ...( isFeaturePluginBuild() && {
- __experimentalDefaultControls: {
- padding: true,
- },
- __experimentalSkipSerialization: true,
- } ),
- },
- ...( isFeaturePluginBuild() && {
- __experimentalBorder: {
- color: true,
- radius: true,
- width: true,
- __experimentalSkipSerialization: true,
- },
- } ),
- },
- example,
-
- /**
- * Renders and manages the block.
- *
- * @param {Object} props Props to pass to block.
- */
- edit: Edit,
-
- /**
- * Block content is rendered in PHP, not via save function.
- */
- save: () => {
- return ;
- },
-} );
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/utils.js b/plugins/woocommerce-blocks/assets/js/blocks/featured-product/utils.js
deleted file mode 100644
index 82fa814a7b9..00000000000
--- a/plugins/woocommerce-blocks/assets/js/blocks/featured-product/utils.js
+++ /dev/null
@@ -1,24 +0,0 @@
-export function calculateImagePosition( coords ) {
- if ( ! coords ) return {};
-
- const x = Math.round( coords.x * 100 );
- const y = Math.round( coords.y * 100 );
-
- return `${ x }% ${ y }%`;
-}
-
-/**
- * Convert the selected ratio to the correct background class.
- *
- * @param {number} ratio Selected opacity from 0 to 100.
- * @return {string?} The class name, if applicable (not used for ratio 0 or 50).
- */
-export function dimRatioToClass( ratio ) {
- return ratio === 0 || ratio === 50
- ? null
- : `has-background-dim-${ 10 * Math.round( ratio / 10 ) }`;
-}
-
-export function backgroundImageStyles( url ) {
- return { backgroundImage: `url(${ url })` };
-}
diff --git a/plugins/woocommerce-blocks/bin/webpack-configs.js b/plugins/woocommerce-blocks/bin/webpack-configs.js
index 0c824517ad7..90978da1d20 100644
--- a/plugins/woocommerce-blocks/bin/webpack-configs.js
+++ b/plugins/woocommerce-blocks/bin/webpack-configs.js
@@ -264,11 +264,13 @@ const getMainConfig = ( options = {} ) => {
to: './checkout/block.json',
},
{
- from: './assets/js/blocks/featured-category/block.json',
+ from:
+ './assets/js/blocks/featured-items/featured-category/block.json',
to: './featured-category/block.json',
},
{
- from: './assets/js/blocks/featured-product/block.json',
+ from:
+ './assets/js/blocks/featured-items/featured-product/block.json',
to: './featured-product/block.json',
},
{
diff --git a/plugins/woocommerce-blocks/bin/webpack-entries.js b/plugins/woocommerce-blocks/bin/webpack-entries.js
index 903189b85fb..b6e9d239f15 100644
--- a/plugins/woocommerce-blocks/bin/webpack-entries.js
+++ b/plugins/woocommerce-blocks/bin/webpack-entries.js
@@ -20,7 +20,9 @@ const blocks = {
'product-on-sale': {},
'product-top-rated': {},
'products-by-attribute': {},
- 'featured-product': {},
+ 'featured-product': {
+ customDir: 'featured-items/featured-product',
+ },
'all-reviews': {
customDir: 'reviews/all-reviews',
},
@@ -32,7 +34,9 @@ const blocks = {
},
'product-search': {},
'product-tag': {},
- 'featured-category': {},
+ 'featured-category': {
+ customDir: 'featured-items/featured-category',
+ },
'all-products': {
customDir: 'products/all-products',
},
diff --git a/plugins/woocommerce-blocks/src/BlockTypes/FeaturedCategory.php b/plugins/woocommerce-blocks/src/BlockTypes/FeaturedCategory.php
index 12e00b999a7..00e7d7b0393 100644
--- a/plugins/woocommerce-blocks/src/BlockTypes/FeaturedCategory.php
+++ b/plugins/woocommerce-blocks/src/BlockTypes/FeaturedCategory.php
@@ -179,7 +179,7 @@ class FeaturedCategory extends AbstractDynamicBlock {
$overlay_styles = 'background-color: #000000';
}
- return sprintf( '', esc_attr( $overlay_styles ) );
+ return sprintf( '', esc_attr( $overlay_styles ) );
}
/**
diff --git a/plugins/woocommerce-blocks/src/BlockTypes/FeaturedProduct.php b/plugins/woocommerce-blocks/src/BlockTypes/FeaturedProduct.php
index cd10147eec8..1405ab69d50 100644
--- a/plugins/woocommerce-blocks/src/BlockTypes/FeaturedProduct.php
+++ b/plugins/woocommerce-blocks/src/BlockTypes/FeaturedProduct.php
@@ -230,7 +230,7 @@ class FeaturedProduct extends AbstractDynamicBlock {
$overlay_styles = 'background-color: #000000';
}
- return sprintf( '', esc_attr( $overlay_styles ) );
+ return sprintf( '', esc_attr( $overlay_styles ) );
}
/**