Commit b88541ea46d for woocommerce
commit b88541ea46db044deb2ef4af0df1404c0878b247
Author: Karol Manijak <20098064+kmanijak@users.noreply.github.com>
Date: Tue Oct 6 20:46:42 2026 +0200
Make Featured Category customisable (#69016)
* Add Core design supports to Featured blocks
* Add changelogs for Featured block design supports
* Simplify Featured block design support rendering
* Simplify Featured block changelog entries
* Restore default padding inside featured blocks
* Prevent default gaps in existing featured blocks
* Restore wide and full alignment options for featured blocks
* Remove heading color support from featured blocks
* Fix duplicated styles on featured block backgrounds
* Fix title color inheritance in featured blocks
* Fix block refs in featured item placeholders
* Fix category description spacing in the editor
* Restore top spacing in featured blocks
* Remove redundant featured block wrapper class
* Simplify featured block layout class removal
* Simplify typography supports for featured blocks
* Update featured block tests for supported typography
* Remove unused featured block style configuration
* Remove redundant featured block registration settings
* Remove redundant featured block anchor output
* Prefer Core minimum height over saved featured block height
* Fix full-width featured block sizing
* Preserve featured block spacing with custom vertical alignment (#69372)
diff --git a/docs/block-development/reference/block-references.md b/docs/block-development/reference/block-references.md
index 669d7fb4bfa..204fb548ac6 100644
--- a/docs/block-development/reference/block-references.md
+++ b/docs/block-development/reference/block-references.md
@@ -707,8 +707,8 @@ Visually highlight a product category and encourage prompt action.
- **Name:** woocommerce/featured-category
- **Category:** woocommerce
-- **Supports:** align (full, wide), ariaLabel, color (background, text), filter (duotone), interactivity (clientNavigation), spacing (padding), ~~html~~
-- **Attributes:** alt, categoryId, contentAlign, dimRatio, focalPoint, hasParallax, imageFit, isRepeated, linkText, mediaId, mediaSrc, minHeight, overlayColor, overlayGradient, previewCategory
+- **Supports:** align (full, wide), allowedBlocks, anchor, ariaLabel, color (background, gradients, link, text, ~~enableContrastChecker~~), dimensions (aspectRatio, minHeight), filter (duotone), interactivity (clientNavigation), shadow, spacing (blockGap, margin, padding), typography (), ~~html~~
+- **Attributes:** alt, anchor, categoryId, contentAlign, dimRatio, focalPoint, hasParallax, imageFit, isRepeated, linkText, mediaId, mediaSrc, minHeight, overlayColor, overlayGradient, previewCategory, verticalAlignment
## Featured Product - woocommerce/featured-product
@@ -716,8 +716,8 @@ Highlight a product or variation.
- **Name:** woocommerce/featured-product
- **Category:** woocommerce
-- **Supports:** align (full, wide), ariaLabel, color (background, text), filter (duotone), interactivity (clientNavigation), multiple, spacing (padding), ~~html~~
-- **Attributes:** alt, contentAlign, dimRatio, focalPoint, hasParallax, imageFit, isRepeated, linkText, mediaId, mediaSrc, minHeight, overlayColor, overlayGradient, previewProduct, productId
+- **Supports:** align (full, wide), allowedBlocks, anchor, ariaLabel, color (background, gradients, link, text, ~~enableContrastChecker~~), dimensions (aspectRatio, minHeight), filter (duotone), interactivity (clientNavigation), multiple, shadow, spacing (blockGap, margin, padding), typography (), ~~html~~
+- **Attributes:** alt, anchor, contentAlign, dimRatio, focalPoint, hasParallax, imageFit, isRepeated, linkText, mediaId, mediaSrc, minHeight, overlayColor, overlayGradient, previewProduct, productId, verticalAlignment
## Filter Block - woocommerce/filter-wrapper
diff --git a/plugins/woocommerce/changelog/add-featured-block-design-supports b/plugins/woocommerce/changelog/add-featured-block-design-supports
new file mode 100644
index 00000000000..e7c5fe2c8be
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-featured-block-design-supports
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Support richer styling in Featured Product and Featured Category blocks.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/category-description/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/category-description/edit.tsx
index 425753d6ecc..138ea5127a5 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/category-description/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/category-description/edit.tsx
@@ -82,23 +82,28 @@ export default function Edit( { attributes, setAttributes, context }: Props ) {
} );
let descriptionElement = (
- <p { ...blockProps }>{ __( 'Category description', 'woocommerce' ) }</p>
+ <div { ...blockProps }>
+ <p>{ __( 'Category description', 'woocommerce' ) }</p>
+ </div>
);
if ( termId || isPreviewMode ) {
descriptionElement = userCanEdit ? (
- <PlainText
- tagName="p"
- placeholder={ __( 'No description', 'woocommerce' ) as string }
- value={ displayRawDescription }
- onChange={ ( v: string ) =>
- ( setDescription as ( v: string ) => void )( v )
- }
- __experimentalVersion={ 2 }
- { ...blockProps }
- />
+ <div { ...blockProps }>
+ <PlainText
+ tagName="p"
+ placeholder={
+ __( 'No description', 'woocommerce' ) as string
+ }
+ value={ displayRawDescription }
+ onChange={ ( v: string ) =>
+ ( setDescription as ( v: string ) => void )( v )
+ }
+ __experimentalVersion={ 2 }
+ />
+ </div>
) : (
- <p
+ <div
{ ...blockProps }
dangerouslySetInnerHTML={ {
__html: displayFullDescription,
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/block-controls.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/block-controls.tsx
index d3067594d29..5fcf7d42b84 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/block-controls.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/block-controls.tsx
@@ -4,6 +4,7 @@
import { __ } from '@wordpress/i18n';
import {
AlignmentToolbar,
+ BlockVerticalAlignmentToolbar,
BlockControls as BlockControlsWrapper,
MediaReplaceFlow,
} from '@wordpress/block-editor';
@@ -49,6 +50,7 @@ type WithBlockControlsProps< T extends EditorBlock< T > > =
type BlockControlRequiredAttributes = {
contentAlign: BlockAlignment;
+ verticalAlignment?: 'top' | 'center' | 'bottom' | undefined;
mediaId: number;
mediaSrc: string;
};
@@ -58,6 +60,7 @@ interface BlockControlsProps {
backgroundImageId: number;
backgroundImageSrc: string;
contentAlign: BlockAlignment;
+ verticalAlignment?: 'top' | 'center' | 'bottom' | undefined;
cropLabel: string;
editLabel: string;
editMode: boolean;
@@ -78,6 +81,7 @@ export const BlockControls = ( {
backgroundImageId,
backgroundImageSrc,
contentAlign,
+ verticalAlignment,
cropLabel,
editLabel,
editMode,
@@ -89,6 +93,12 @@ export const BlockControls = ( {
}: BlockControlsProps ) => {
return (
<BlockControlsWrapper>
+ <BlockVerticalAlignmentToolbar
+ value={ verticalAlignment || 'center' }
+ onChange={ ( nextAlignment ) =>
+ setAttributes( { verticalAlignment: nextAlignment } )
+ }
+ />
<AlignmentToolbar
value={ contentAlign }
onChange={ ( nextAlign: BlockAlignment ) => {
@@ -148,7 +158,8 @@ export const withBlockControls =
const [ isEditingImage, setIsEditingImage ] = props.useEditingImage;
const [ editMode, setEditMode ] = props.useEditMode;
const { attributes, category, name, product, setAttributes } = props;
- const { contentAlign, mediaId, mediaSrc } = attributes;
+ const { contentAlign, verticalAlignment, mediaId, mediaSrc } =
+ attributes;
const item = category || product;
const { backgroundImageId, backgroundImageSrc } = useBackgroundImage( {
@@ -165,6 +176,7 @@ export const withBlockControls =
backgroundImageId={ backgroundImageId }
backgroundImageSrc={ backgroundImageSrc }
contentAlign={ contentAlign }
+ verticalAlignment={ verticalAlignment }
cropLabel={ cropLabel }
editLabel={ editLabel }
editMode={ editMode }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/edit.tsx
index 7c95a49bc00..526543bb1e6 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/edit.tsx
@@ -2,20 +2,12 @@
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
-import type { FunctionComponent } from 'react';
+import type { FunctionComponent, JSX } from 'react';
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 (
- <div { ...blockProps } style={ styles }>
- <Block { ...props } />
- </div>
- );
+ return <Block { ...props } blockProps={ blockProps } />;
};
}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/featured-category/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/featured-category/block.json
index dc8ce2b1b5a..31363278d6f 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/featured-category/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/featured-category/block.json
@@ -12,7 +12,10 @@
"ariaLabel": true,
"color": {
"background": true,
- "text": true
+ "text": true,
+ "gradients": true,
+ "link": true,
+ "enableContrastChecker": false
},
"html": false,
"filter": {
@@ -20,24 +23,42 @@
},
"spacing": {
"padding": true,
+ "margin": [ "top", "bottom" ],
+ "blockGap": true,
"__experimentalDefaultControls": {
- "padding": true
- },
- "__experimentalSkipSerialization": true
+ "padding": true,
+ "blockGap": true
+ }
},
"__experimentalBorder": {
"color": true,
"radius": true,
+ "style": true,
"width": true,
"__experimentalDefaultControls": {
"color": true,
"radius": true,
+ "style": true,
"width": true
- },
- "__experimentalSkipSerialization": true
- }
+ }
+ },
+ "anchor": true,
+ "shadow": true,
+ "typography": {
+ "__experimentalTextTransform": true,
+ "__experimentalTextDecoration": true
+ },
+ "dimensions": {
+ "aspectRatio": true,
+ "minHeight": true
+ },
+ "allowedBlocks": true
},
"attributes": {
+ "verticalAlignment": {
+ "type": "string",
+ "enum": [ "top", "center", "bottom" ]
+ },
"alt": {
"type": "string",
"default": ""
@@ -78,8 +99,7 @@
"default": ""
},
"minHeight": {
- "type": "number",
- "default": 500
+ "type": "number"
},
"linkText": {
"default": "Shop now",
@@ -98,6 +118,9 @@
"previewCategory": {
"type": "object",
"default": null
+ },
+ "anchor": {
+ "type": "string"
}
},
"selectors": {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/featured-category/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/featured-category/style.scss
index a6688dfa188..ea6ddb98e72 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/featured-category/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/featured-category/style.scss
@@ -1,6 +1,6 @@
@import "../style";
-.wp-block-woocommerce-featured-category {
+:where(.wp-block-woocommerce-featured-category) {
@extend %wp-block-featured-item;
}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/featured-product/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/featured-product/block.json
index 535560cff4e..1fe753ad365 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/featured-product/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/featured-product/block.json
@@ -16,29 +16,50 @@
},
"color": {
"background": true,
- "text": true
+ "text": true,
+ "gradients": true,
+ "link": true,
+ "enableContrastChecker": false
},
"spacing": {
"padding": true,
+ "margin": [ "top", "bottom" ],
+ "blockGap": true,
"__experimentalDefaultControls": {
- "padding": true
- },
- "__experimentalSkipSerialization": true
+ "padding": true,
+ "blockGap": true
+ }
},
"__experimentalBorder": {
"color": true,
"radius": true,
+ "style": true,
"width": true,
"__experimentalDefaultControls": {
"color": true,
"radius": true,
+ "style": true,
"width": true
- },
- "__experimentalSkipSerialization": true
+ }
+ },
+ "multiple": true,
+ "anchor": true,
+ "shadow": true,
+ "typography": {
+ "__experimentalTextTransform": true,
+ "__experimentalTextDecoration": true
},
- "multiple": true
+ "dimensions": {
+ "aspectRatio": true,
+ "minHeight": true
+ },
+ "allowedBlocks": true
},
"attributes": {
+ "verticalAlignment": {
+ "type": "string",
+ "enum": [ "top", "center", "bottom" ]
+ },
"alt": {
"type": "string",
"default": ""
@@ -79,8 +100,7 @@
"default": ""
},
"minHeight": {
- "type": "number",
- "default": 500
+ "type": "number"
},
"linkText": {
"type": "string",
@@ -99,6 +119,9 @@
"previewProduct": {
"type": "object",
"default": null
+ },
+ "anchor": {
+ "type": "string"
}
},
"selectors": {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/featured-product/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/featured-product/style.scss
index 5e2a726dba7..294c02704d9 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/featured-product/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/featured-product/style.scss
@@ -1,6 +1,6 @@
@import "../style";
-.wp-block-woocommerce-featured-product {
+:where(.wp-block-woocommerce-featured-product) {
@extend %wp-block-featured-item;
background-color: transparent;
}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/register.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/register.tsx
index 7e03e6dfc3b..e9fcac7b902 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/register.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/register.tsx
@@ -1,6 +1,3 @@
-// Disabling because of `__experimental` property names.
-/* eslint-disable @typescript-eslint/naming-convention */
-
/**
* External dependencies
*/
@@ -15,78 +12,12 @@ import type { BlockConfiguration } from '@wordpress/blocks';
*/
import { Edit } from './edit';
-type CSSDirections = 'top' | 'right' | 'bottom' | 'left';
-
-interface ExtendedBlockSupports {
- supports: {
- color?: {
- background: string;
- gradients: boolean;
- link: boolean;
- text: string;
- };
- spacing?: {
- margin: boolean | CSSDirections[];
- padding: boolean | CSSDirections[];
- __experimentalDefaultControls?: {
- margin?: boolean;
- padding?: boolean;
- };
- __experimentalSkipSerialization?: boolean;
- };
- __experimentalBorder?: {
- color: boolean;
- radius: boolean;
- width: boolean;
- __experimentalSkipSerialization?: boolean;
- };
- };
-}
-
export function register(
Block: FunctionComponent,
example: { attributes: Record< string, unknown > },
- metadata: BlockConfiguration & ExtendedBlockSupports,
+ 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( 'defaultHeight', 500 ),
- },
- },
- supports: {
- ...metadata.supports,
- color: {
- background: metadata.supports?.color?.background,
- text: metadata.supports?.color?.text,
- },
- spacing: {
- padding: metadata.supports?.spacing?.padding,
-
- __experimentalDefaultControls: {
- padding:
- metadata.supports?.spacing
- ?.__experimentalDefaultControls,
- },
- __experimentalSkipSerialization:
- metadata.supports?.spacing?.__experimentalSkipSerialization,
- },
- __experimentalBorder: metadata?.supports?.__experimentalBorder,
- },
- };
-
const DEFAULT_EXAMPLE = {
attributes: {
alt: '',
@@ -102,7 +33,6 @@ export function register(
};
registerBlockType( metadata, {
- ...DEFAULT_SETTINGS,
example: {
...DEFAULT_EXAMPLE,
...example,
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/style.scss
index a77412b6538..1a55efcc561 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/style.scss
@@ -55,18 +55,23 @@
@include with-background-dim();
@include with-content-selection();
- align-content: center;
- align-items: center;
+ align-content: normal;
+ align-items: stretch;
background-position: center center;
background-size: cover;
- display: flex;
- flex-wrap: wrap;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
justify-content: center;
margin: 0 auto;
+ min-height: var(--wc-featured-item-min-height, 500px);
overflow: hidden;
position: relative;
width: 100%;
+ &.alignfull {
+ width: auto;
+ }
+
&.has-left-content {
justify-content: flex-start;
@@ -113,7 +118,6 @@
}
&__description,
- &__inner-blocks,
&__link,
&__price,
&__title,
@@ -145,6 +149,24 @@
}
}
+ &__inner-blocks {
+ box-sizing: border-box;
+ color: inherit;
+ width: 100%;
+ padding: 0 48px 16px;
+ z-index: 1;
+
+ &.has-custom-gap {
+ display: flex;
+ flex-direction: column;
+ gap: var(--wc-featured-item-block-gap);
+
+ > * {
+ margin-block: 0;
+ }
+ }
+ }
+
&__description {
color: inherit;
@@ -154,8 +176,7 @@
}
&__inner-blocks {
- .wp-block-post-title,
- .wp-block-woocommerce-category-title {
+ :where(.wp-block-post-title, .wp-block-woocommerce-category-title) {
&,
a {
color: inherit;
@@ -164,7 +185,6 @@
}
& &__title {
- color: inherit;
margin-top: 0;
div {
@@ -185,7 +205,15 @@
justify-content: center;
overflow: hidden;
width: 100%;
- height: 100%;
+ height: auto;
+ }
+
+ &.is-vertically-aligned-top > &__wrapper {
+ align-content: flex-start;
+ }
+
+ &.is-vertically-aligned-bottom > &__wrapper {
+ align-content: flex-end;
}
.wp-block-button.aligncenter {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/with-edit-mode.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/with-edit-mode.tsx
index f712b3ba628..fb84f126d1c 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/with-edit-mode.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/with-edit-mode.tsx
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
+import type { useBlockProps } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
import { usePreviewMode } from '@woocommerce/base-hooks';
import type { ComponentType } from 'react';
@@ -43,6 +44,7 @@ type EditModeRequiredAttributes = {
};
interface EditModeRequiredProps< T > {
+ blockProps: ReturnType< typeof useBlockProps >;
attributes: EditModeRequiredAttributes & EditorBlock< T >[ 'attributes' ];
clientId: string;
effectiveCategoryId?: number;
@@ -138,74 +140,80 @@ export const withEditMode =
if ( editMode && canEditItem ) {
return (
- <Placeholder
- icon={ <Icon icon={ icon } /> }
- label={ label }
- className={ className }
- >
- <HStack alignment="center">
- { isDeleted ? (
- <Icon
- icon={ info }
- className="wc-blocks-featured-items__orange-info-icon"
- />
- ) : (
- <Icon icon={ info } />
- ) }
- <Text>
- { isDeleted
- ? getInvalidItemDescription( name )
- : description }
- </Text>
- </HStack>
- <div className={ `${ className }__selection` }>
- { name === BLOCK_NAMES.featuredCategory && (
- <ProductCategoryControl
- selected={
- selectedOptions?.categoryId
- ? [ selectedOptions.categoryId ]
- : []
- }
- onChange={ (
- value: ProductCategoryResponseItem[] = []
- ) => {
- const id = value[ 0 ] ? value[ 0 ].id : 0;
- setSelectedOptions( {
- categoryId: id,
- mediaId: 0,
- mediaSrc: '',
- } );
- triggerUrlUpdate();
- } }
- isSingle
- />
- ) }
- { name === BLOCK_NAMES.featuredProduct && (
- <ProductControl
- selected={
- selectedOptions?.productId
- ? [ selectedOptions.productId ]
- : []
- }
- showVariations
- onChange={ (
- value: ProductResponseItem[] = []
- ) => {
- const id = value[ 0 ] ? value[ 0 ].id : 0;
- setSelectedOptions( {
- productId: id,
- mediaId: 0,
- mediaSrc: '',
- } );
- triggerUrlUpdate();
- } }
- />
- ) }
- <Button variant="primary" onClick={ onDone }>
- { __( 'Done', 'woocommerce' ) }
- </Button>
- </div>
- </Placeholder>
+ <div { ...props.blockProps }>
+ <Placeholder
+ icon={ <Icon icon={ icon } /> }
+ label={ label }
+ className={ className }
+ >
+ <HStack alignment="center">
+ { isDeleted ? (
+ <Icon
+ icon={ info }
+ className="wc-blocks-featured-items__orange-info-icon"
+ />
+ ) : (
+ <Icon icon={ info } />
+ ) }
+ <Text>
+ { isDeleted
+ ? getInvalidItemDescription( name )
+ : description }
+ </Text>
+ </HStack>
+ <div className={ `${ className }__selection` }>
+ { name === BLOCK_NAMES.featuredCategory && (
+ <ProductCategoryControl
+ selected={
+ selectedOptions?.categoryId
+ ? [ selectedOptions.categoryId ]
+ : []
+ }
+ onChange={ (
+ value: ProductCategoryResponseItem[] = []
+ ) => {
+ const id = value[ 0 ]
+ ? value[ 0 ].id
+ : 0;
+ setSelectedOptions( {
+ categoryId: id,
+ mediaId: 0,
+ mediaSrc: '',
+ } );
+ triggerUrlUpdate();
+ } }
+ isSingle
+ />
+ ) }
+ { name === BLOCK_NAMES.featuredProduct && (
+ <ProductControl
+ selected={
+ selectedOptions?.productId
+ ? [ selectedOptions.productId ]
+ : []
+ }
+ showVariations
+ onChange={ (
+ value: ProductResponseItem[] = []
+ ) => {
+ const id = value[ 0 ]
+ ? value[ 0 ].id
+ : 0;
+ setSelectedOptions( {
+ productId: id,
+ mediaId: 0,
+ mediaSrc: '',
+ } );
+ triggerUrlUpdate();
+ } }
+ />
+ ) }
+ <Button variant="primary" onClick={ onDone }>
+ { __( 'Done', 'woocommerce' ) }
+ </Button>
+ </div>
+ </Placeholder>
+ </div>
);
}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/with-featured-item.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/with-featured-item.tsx
index 8bee87aebd1..1bde796d976 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/with-featured-item.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/with-featured-item.tsx
@@ -3,11 +3,18 @@
*/
import type { BlockAlignment } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
-import type { ComponentType, Dispatch, SetStateAction } from 'react';
+import type {
+ ComponentType,
+ Dispatch,
+ SetStateAction,
+ CSSProperties,
+} from 'react';
+import { getSetting } from '@woocommerce/settings';
import { ProductResponseItem } from '@woocommerce/types';
import { Icon, Placeholder, Spinner } from '@wordpress/components';
import { ProductDataContextProvider } from '@woocommerce/shared-context';
import clsx from 'clsx';
+import { useMergeRefs } from '@wordpress/compose';
import {
useCallback,
useState,
@@ -16,8 +23,12 @@ import {
useMemo,
} from '@wordpress/element';
import { WP_REST_API_Category } from 'wp-types';
-import { useStyleProps } from '@woocommerce/base-hooks';
-import { InnerBlocks, BlockContextProvider } from '@wordpress/block-editor';
+import {
+ BlockContextProvider,
+ getSpacingPresetCssVar,
+ useInnerBlocksProps,
+ useBlockProps,
+} from '@wordpress/block-editor';
/**
* Internal dependencies
@@ -43,6 +54,7 @@ interface WithFeaturedItemConfig extends GenericBlockUIConfig {
export interface FeaturedItemRequiredAttributes {
contentAlign: BlockAlignment;
+ verticalAlignment?: 'top' | 'center' | 'bottom' | undefined;
dimRatio: number;
focalPoint: { x: number; y: number };
hasParallax: boolean;
@@ -57,7 +69,12 @@ export interface FeaturedItemRequiredAttributes {
showDesc: boolean;
showPrice: boolean;
backgroundColor: string | undefined;
- style: { color: { background: string } };
+ style: {
+ color: { background: string };
+ dimensions?: { aspectRatio?: string; minHeight?: string };
+ spacing?: { blockGap?: string | { top?: string; left?: string } };
+ };
+ allowedBlocks?: string[];
backgroundColorVisibilityStatus: {
isBackgroundVisible: boolean;
message?: string | null;
@@ -78,6 +95,7 @@ interface FeaturedProductRequiredAttributes
}
interface FeaturedItemRequiredProps< T > {
+ blockProps: ReturnType< typeof useBlockProps >;
attributes: (
| FeaturedCategoryRequiredAttributes
| FeaturedProductRequiredAttributes
@@ -112,6 +130,40 @@ type FeaturedItemProps< T extends EditorBlock< T > > =
| ( T & FeaturedCategoryProps< T > )
| ( T & FeaturedProductProps< T > );
+function FeaturedItemInnerBlocks( {
+ className,
+ template,
+ attributes,
+}: {
+ className: string;
+ template: NonNullable<
+ Parameters< typeof useInnerBlocksProps >[ 1 ]
+ >[ 'template' ];
+ attributes: FeaturedItemRequiredAttributes;
+} ) {
+ const spacing = attributes.style?.spacing?.blockGap;
+ const blockGap = typeof spacing === 'object' ? spacing?.top : spacing;
+ const hasBlockGap =
+ blockGap !== undefined && blockGap !== null && blockGap !== '';
+ const innerProps = useInnerBlocksProps(
+ {
+ className: clsx( className, { 'has-custom-gap': hasBlockGap } ),
+ style: {
+ '--wc-featured-item-block-gap': hasBlockGap
+ ? getSpacingPresetCssVar( String( blockGap ) )
+ : undefined,
+ } as CSSProperties,
+ },
+ {
+ template,
+ templateLock: false,
+ allowedBlocks: attributes.allowedBlocks,
+ orientation: 'vertical',
+ }
+ );
+ return <div { ...innerProps } />;
+}
+
export const withFeaturedItem =
( {
emptyMessage,
@@ -148,6 +200,10 @@ export const withFeaturedItem =
blockName: name,
} );
const featuredProductParentRef = useRef( null );
+ const containerRef = useMergeRefs( [
+ props.blockProps.ref,
+ featuredProductParentRef,
+ ] );
const [ parentContainerDimension, setParentContainerDimension ] =
useState< BgImageDimensions >( { height: 0, width: 0 } );
@@ -242,12 +298,11 @@ export const withFeaturedItem =
product={ product }
isLoading={ isLoading }
>
- <div className={ `${ className }__inner-blocks` }>
- <InnerBlocks
- template={ innerBlocksTemplate }
- templateLock={ false }
- />
- </div>
+ <FeaturedItemInnerBlocks
+ className={ `${ className }__inner-blocks` }
+ template={ innerBlocksTemplate }
+ attributes={ attributes }
+ />
</ProductDataContextProvider>
</BlockContextProvider>
);
@@ -264,31 +319,30 @@ export const withFeaturedItem =
taxonomy: 'product_cat',
} }
>
- <div className={ `${ className }__inner-blocks` }>
- <InnerBlocks
- template={ FEATURED_CATEGORY_DEFAULT_TEMPLATE(
- category,
- ! attributes.categoryId
- ) }
- templateLock={ false }
- />
- </div>
+ <FeaturedItemInnerBlocks
+ className={ `${ className }__inner-blocks` }
+ template={ FEATURED_CATEGORY_DEFAULT_TEMPLATE(
+ category,
+ ! attributes.categoryId
+ ) }
+ attributes={ attributes }
+ />
</BlockContextProvider>
);
};
const renderNoItem = () => (
- <Placeholder
- className={ className }
- icon={ <Icon icon={ icon } /> }
- label={ label }
- >
- { isLoading ? <Spinner /> : renderNoItemContent() }
- </Placeholder>
+ <div { ...props.blockProps }>
+ <Placeholder
+ className={ className }
+ icon={ <Icon icon={ icon } /> }
+ label={ label }
+ >
+ { isLoading ? <Spinner /> : renderNoItemContent() }
+ </Placeholder>
+ </div>
);
- const styleProps = useStyleProps( attributes );
-
const renderItem = () => {
const {
contentAlign,
@@ -299,7 +353,6 @@ export const withFeaturedItem =
overlayColor,
overlayGradient,
style,
- textColor,
} = attributes;
const containerClass = clsx(
@@ -316,18 +369,20 @@ export const withFeaturedItem =
},
dimRatioToClass( dimRatio ),
contentAlign !== 'center' && `has-${ contentAlign }-content`,
- styleProps.className
+ props.blockProps.className,
+ attributes.verticalAlignment &&
+ `is-vertically-aligned-${ attributes.verticalAlignment }`
);
- const containerStyle: React.CSSProperties = {
- borderRadius: style?.border?.radius,
- color: textColor
- ? `var(--wp--preset--color--${ textColor })`
- : style?.color?.text,
- boxSizing: 'border-box',
- minHeight,
- ...styleProps.style,
- };
+ const hasAspectRatio = !! style?.dimensions?.aspectRatio;
+ const containerStyle = {
+ '--wc-featured-item-min-height': `${ getSetting(
+ 'defaultHeight',
+ 500
+ ) }px`,
+ minHeight: hasAspectRatio ? undefined : minHeight,
+ ...props.blockProps.style,
+ } as CSSProperties;
const isImgElement = ! isRepeated && ! hasParallax;
@@ -346,17 +401,26 @@ export const withFeaturedItem =
return (
<>
- <ConstrainedResizable
- enable={ { bottom: true } }
- onResize={ onResize }
- showHandle={ isSelected }
- style={ { minHeight } }
- />
<div
+ { ...props.blockProps }
className={ containerClass }
- ref={ featuredProductParentRef }
+ ref={ containerRef }
style={ containerStyle }
>
+ <ConstrainedResizable
+ enable={ { bottom: true } }
+ onResize={ onResize }
+ showHandle={
+ isSelected &&
+ ! hasAspectRatio &&
+ ! style?.dimensions?.minHeight
+ }
+ style={ {
+ minHeight: hasAspectRatio
+ ? undefined
+ : minHeight,
+ } }
+ />
<div className={ `${ className }__wrapper` }>
<div
className="background-dim__overlay"
@@ -402,13 +466,15 @@ export const withFeaturedItem =
if ( isEditingImage ) {
return (
- <Component
- { ...props }
- backgroundImageSize={ backgroundImageSize }
- backgroundColorVisibilityStatus={
- backgroundColorVisibilityStatus
- }
- />
+ <div { ...props.blockProps }>
+ <Component
+ { ...props }
+ backgroundImageSize={ backgroundImageSize }
+ backgroundColorVisibilityStatus={
+ backgroundColorVisibilityStatus
+ }
+ />
+ </div>
);
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedItem.php b/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedItem.php
index 24b693ddafb..3115d0161cc 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedItem.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedItem.php
@@ -2,8 +2,6 @@
namespace Automattic\WooCommerce\Blocks\BlockTypes;
-use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
-
/**
* FeaturedItem class.
*/
@@ -24,22 +22,6 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
'align' => 'none',
);
- /**
- * Global style enabled for this block.
- *
- * @var array
- */
- protected $global_style_wrapper = array(
- 'background_color',
- 'border_color',
- 'border_radius',
- 'border_width',
- 'font_size',
- 'padding',
- 'text_color',
- 'extra_classes',
- );
-
/**
* Initialize the block.
*/
@@ -250,7 +232,14 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
$styles = $this->get_styles( $attributes );
$classes = $this->get_classes( $attributes );
- $output = sprintf( '<div class="%1$s wp-block-woocommerce-%2$s" style="%3$s">', esc_attr( trim( $classes ) ), $this->block_name, esc_attr( $styles ) );
+ $wrapper_attributes = get_block_wrapper_attributes(
+ array(
+ 'class' => $classes,
+ 'style' => $styles,
+ )
+ );
+
+ $output = '<div ' . $wrapper_attributes . '>';
$output .= sprintf( '<div class="wc-block-%s__wrapper">', $this->block_name );
$output .= $this->render_overlay( $attributes );
@@ -273,7 +262,17 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
$output .= $this->render_attributes( $item, $attributes );
if ( ! empty( $content ) ) {
- $output .= sprintf( '<div class="wc-block-%s__inner-blocks">%s</div>', $this->block_name, $content );
+ $block_gap = $attributes['style']['spacing']['blockGap'] ?? null;
+ $block_gap = is_array( $block_gap ) ? ( $block_gap['top'] ?? null ) : $block_gap;
+ $gap_styles = wp_style_engine_get_styles( array( 'spacing' => array( 'margin' => array( 'top' => null !== $block_gap ? (string) $block_gap : null ) ) ) );
+ $gap = $gap_styles['declarations']['margin-top'] ?? null;
+ $output .= sprintf(
+ '<div class="wc-block-%1$s__inner-blocks%2$s"%3$s>%4$s</div>',
+ $this->block_name,
+ null !== $gap ? ' has-custom-gap' : '',
+ null !== $gap ? ' style="' . esc_attr( '--wc-featured-item-block-gap:' . $gap ) . '"' : '',
+ $content
+ );
}
$output .= '</div>';
@@ -366,9 +365,6 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
);
}
- $global_style_style = StyleAttributesUtils::get_styles_by_attributes( $attributes, $this->global_style_wrapper );
- $style .= $global_style_style;
-
return $style;
}
@@ -434,29 +430,24 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
}
/**
- * Get the styles for the wrapper element (background image, color).
+ * Get Featured-specific height styles for the block wrapper.
*
* @param array $attributes Block attributes. Default empty array.
* @return string
*/
public function get_styles( $attributes ) {
- $style = '';
+ $style = sprintf( '--wc-featured-item-min-height:%dpx;', wc_get_theme_support( 'featured_block::default_height', 500 ) );
- $min_height = $attributes['minHeight'] ?? wc_get_theme_support( 'featured_block::default_height', 500 );
-
- if ( isset( $attributes['minHeight'] ) ) {
- $style .= sprintf( 'min-height:%dpx;', intval( $min_height ) );
+ if ( isset( $attributes['minHeight'] ) && ! isset( $attributes['style']['dimensions']['minHeight'] ) ) {
+ $style .= sprintf( 'min-height:%dpx;', intval( $attributes['minHeight'] ) );
}
- $global_style_style = StyleAttributesUtils::get_styles_by_attributes( $attributes, $this->global_style_wrapper );
- $style .= $global_style_style;
-
return $style;
}
/**
- * Get class names for the block container.
+ * Get Featured-specific class names for the block container.
*
* @param array $attributes Block attributes. Default empty array.
* @return string
@@ -464,10 +455,6 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
public function get_classes( $attributes ) {
$classes = array( 'wc-block-' . $this->block_name );
- if ( isset( $attributes['align'] ) ) {
- $classes[] = "align{$attributes['align']}";
- }
-
if ( isset( $attributes['dimRatio'] ) && ( 0 !== $attributes['dimRatio'] ) ) {
$classes[] = 'has-background-dim';
@@ -480,9 +467,9 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
$classes[] = "has-{$attributes['contentAlign']}-content";
}
- $global_style_classes = StyleAttributesUtils::get_classes_by_attributes( $attributes, $this->global_style_wrapper );
-
- $classes[] = $global_style_classes;
+ if ( in_array( $attributes['verticalAlignment'] ?? '', array( 'top', 'center', 'bottom' ), true ) ) {
+ $classes[] = 'is-vertically-aligned-' . $attributes['verticalAlignment'];
+ }
return implode( ' ', $classes );
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/FeaturedItemSupportsTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/FeaturedItemSupportsTest.php
new file mode 100644
index 00000000000..d5a80f218bb
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/FeaturedItemSupportsTest.php
@@ -0,0 +1,184 @@
+<?php
+/**
+ * Featured item block support rendering tests.
+ *
+ * @package WooCommerce\Tests\Blocks
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;
+
+use WC_Helper_Product;
+use WC_Unit_Test_Case;
+use WP_HTML_Tag_Processor;
+
+/**
+ * Tests the shared Featured Category and Featured Product support renderer.
+ */
+class FeaturedItemSupportsTest extends WC_Unit_Test_Case {
+ /**
+ * @testdox Design supports and vertical alignment render without changing the inner flow layout.
+ *
+ * @testWith ["category", "top"]
+ * ["category", "center"]
+ * ["category", "bottom"]
+ * ["product", "top"]
+ * ["product", "center"]
+ * ["product", "bottom"]
+ * @param string $kind Featured item kind.
+ * @param string $alignment Vertical content alignment.
+ */
+ public function test_support_styles_and_layout_target( string $kind, string $alignment ): void {
+ $attributes = $this->get_item_attributes( $kind );
+ $attributes['anchor'] = 'featured-anchor';
+ $attributes['verticalAlignment'] = $alignment;
+ $attributes['style'] = array(
+ 'color' => array( 'text' => '#123456' ),
+ 'border' => array(
+ 'style' => 'dashed',
+ 'width' => '3px',
+ ),
+ 'typography' => array(
+ 'textTransform' => 'uppercase',
+ 'textDecoration' => 'underline',
+ ),
+ 'dimensions' => array( 'aspectRatio' => '16/9' ),
+ 'shadow' => '2px 3px 4px #000000',
+ 'spacing' => array(
+ 'margin' => array( 'top' => '20px' ),
+ 'blockGap' => '12px',
+ ),
+ );
+ $html = $this->render_item( $kind, $attributes );
+ $tags = new WP_HTML_Tag_Processor( $html );
+ $this->assertTrue( $tags->next_tag() );
+ $this->assertSame( 'featured-anchor', $tags->get_attribute( 'id' ) );
+ $this->assertFalse( $tags->has_class( 'is-layout-flex' ) );
+ $this->assertTrue( $tags->has_class( 'is-vertically-aligned-' . $alignment ) );
+ foreach ( array( 'color:#123456', 'border-style:dashed', 'text-transform:uppercase', 'text-decoration:underline', 'aspect-ratio:16/9', 'min-height:unset', 'box-shadow:2px 3px 4px #000000', 'margin-top:20px' ) as $style ) {
+ $this->assertStringContainsString( $style, $tags->get_attribute( 'style' ) );
+ }
+ $this->assertStringNotContainsString( 'gap:', $tags->get_attribute( 'style' ) );
+ $this->assertTrue( $tags->next_tag( array( 'class_name' => 'wc-block-featured-' . $kind . '__inner-blocks' ) ) );
+ $this->assertFalse( $tags->has_class( 'is-layout-flex' ) );
+ $this->assertTrue( $tags->has_class( 'has-custom-gap' ) );
+ $this->assertStringContainsString( '--wc-featured-item-block-gap:12px', $tags->get_attribute( 'style' ) );
+ }
+
+ /**
+ * @testdox Explicit zero spacing is distinguished from untouched inner-block margins.
+ *
+ * @testWith [null, false]
+ * ["0", true]
+ * [0, true]
+ * @param mixed $gap Saved block spacing.
+ * @param bool $has_custom_gap Whether explicit spacing should be rendered.
+ */
+ public function test_explicit_zero_spacing( $gap, bool $has_custom_gap ): void {
+ $attributes = $this->get_item_attributes( 'category' );
+ $attributes['style']['spacing']['blockGap'] = $gap;
+ $tags = new WP_HTML_Tag_Processor( $this->render_item( 'category', $attributes ) );
+ $this->assertTrue( $tags->next_tag( array( 'class_name' => 'wc-block-featured-category__inner-blocks' ) ) );
+ $this->assertSame( $has_custom_gap, $tags->has_class( 'has-custom-gap' ) );
+ $this->assertSame( $has_custom_gap ? '--wc-featured-item-block-gap:0' : null, $tags->get_attribute( 'style' ) );
+ }
+
+ /**
+ * @testdox Core renders preset styles and wrapper classes for modern and legacy Featured blocks.
+ *
+ * @testWith ["category", false]
+ * ["category", true]
+ * ["product", false]
+ * ["product", true]
+ * @param string $kind Featured item kind.
+ * @param bool $legacy Whether to render legacy markup.
+ */
+ public function test_core_renders_presets( string $kind, bool $legacy ): void {
+ $attributes = array_merge(
+ $this->get_item_attributes( $kind ),
+ array(
+ 'textColor' => 'vivid-red',
+ 'backgroundColor' => 'black',
+ 'gradient' => 'vivid-cyan-blue-to-vivid-purple',
+ 'borderColor' => 'white',
+ 'align' => 'wide',
+ 'className' => 'custom-featured',
+ )
+ );
+ if ( $legacy ) {
+ $attributes['editMode'] = false;
+ }
+ $tags = new WP_HTML_Tag_Processor( $this->render_item( $kind, $attributes ) );
+ $this->assertTrue( $tags->next_tag() );
+ foreach ( array( 'has-vivid-red-color', 'has-black-background-color', 'has-vivid-cyan-blue-to-vivid-purple-gradient-background', 'has-white-border-color', 'alignwide', 'custom-featured' ) as $class ) {
+ $this->assertTrue( $tags->has_class( $class ), $class . ' should be rendered by Core block supports.' );
+ }
+ }
+
+ /**
+ * @testdox Existing posts keep their height and do not acquire flow spacing.
+ *
+ * @testWith ["category"]
+ * ["product"]
+ * @param string $kind Featured item kind.
+ */
+ public function test_legacy_rendering_keeps_content_and_height( string $kind ): void {
+ $attributes = $this->get_item_attributes( $kind );
+ $attributes['editMode'] = false;
+ $attributes['minHeight'] = 620;
+ $attributes['showDesc'] = true;
+ $html = $this->render_item( $kind, $attributes );
+ $this->assertStringContainsString( 'min-height:620px', $html );
+ $this->assertStringContainsString( 'wc-block-featured-' . $kind . '__title', $html );
+ $this->assertStringNotContainsString( 'is-layout-flow', $html );
+ $this->assertStringNotContainsString( 'is-layout-flex', $html );
+ $this->assertStringContainsString( 'Saved content', $html );
+ }
+
+ /**
+ * @testdox Terms Query context continues to supply the category and inner-block title.
+ */
+ public function test_inherited_category_with_supports(): void {
+ $attributes = $this->get_item_attributes( 'category' );
+ $block = new \WP_Block(
+ parse_blocks( '<!-- wp:woocommerce/featured-category {"style":{"dimensions":{"aspectRatio":"1"}}} --><!-- wp:woocommerce/category-title /--><!-- /wp:woocommerce/featured-category -->' )[0],
+ array(
+ 'termId' => $attributes['categoryId'],
+ 'termTaxonomy' => 'product_cat',
+ )
+ );
+ $html = $block->render();
+ $this->assertStringContainsString( 'Featured category', $html );
+ $this->assertStringContainsString( 'aspect-ratio:1', $html );
+ }
+
+ /**
+ * Create the selected entity without an image.
+ *
+ * @param string $kind Featured item kind.
+ * @return array
+ */
+ private function get_item_attributes( string $kind ): array {
+ $id = 'category' === $kind
+ ? $this->factory->term->create(
+ array(
+ 'taxonomy' => 'product_cat',
+ 'name' => 'Featured category',
+ )
+ )
+ : WC_Helper_Product::create_simple_product()->get_id();
+ return array( $kind . 'Id' => $id );
+ }
+
+ /**
+ * Render saved markup through all Core block support filters.
+ *
+ * @param string $kind Featured item kind.
+ * @param array $attributes Block attributes.
+ * @return string
+ */
+ private function render_item( string $kind, array $attributes ): string {
+ return do_blocks( '<!-- wp:woocommerce/featured-' . $kind . ' ' . wp_json_encode( $attributes ) . ' --><!-- wp:paragraph --><p>Saved content</p><!-- /wp:paragraph --><!-- /wp:woocommerce/featured-' . $kind . ' -->' );
+ }
+}