Commit ed9c0b9c6fc for woocommerce
commit ed9c0b9c6fcec589f645bbe1c637b939f1271d4e
Author: Karol Manijak <20098064+kmanijak@users.noreply.github.com>
Date: Thu Sep 17 17:14:24 2026 +0200
Make Featured Category and Inner blocks compatible with core Terms Query block (#68613)
* Add Terms Query support to WooCommerce category blocks
* Add changelog entries for category block Terms Query support
* Reduce category Terms Query tests to essential regressions
* Remove obsolete FeaturedItem PHPStan baseline entries
* Simplify category test fixture
* Consolidate category Terms Query changelog
* Fix category block previews and loading in Terms Query
* Hide category selection and loading placeholders for inherited categories
* Remove redundant category block context declarations
* Improve category context test expectations
* Keep inherited categories non-editable after loading failures
diff --git a/plugins/woocommerce/changelog/category-blocks-terms-query b/plugins/woocommerce/changelog/category-blocks-terms-query
new file mode 100644
index 00000000000..dc94d4aa540
--- /dev/null
+++ b/plugins/woocommerce/changelog/category-blocks-terms-query
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Make Category Title, Description and Button compatible with core Terms Query block
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/category-description/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/category-description/block.json
index ea11b21eec8..e183887c743 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/category-description/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/category-description/block.json
@@ -23,5 +23,5 @@
},
"typography": true
},
- "usesContext": [ "termId", "termTaxonomy" ]
+ "usesContext": [ "termId", "termTaxonomy", "taxonomy" ]
}
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 160af45fce1..425753d6ecc 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
@@ -22,35 +22,39 @@ interface Props {
context: {
termId?: number;
termTaxonomy?: string;
+ taxonomy?: string;
};
}
export default function Edit( { attributes, setAttributes, context }: Props ) {
const { textAlign } = attributes;
- const { termId, termTaxonomy } = context;
+ const { termId, termTaxonomy, taxonomy } = context;
+ const effectiveTaxonomy = termTaxonomy || taxonomy || 'product_cat';
const userCanEdit = useSelect(
( select ) => {
- if ( ! termId ) return false;
+ if ( ! termId ) {
+ return false;
+ }
// This use actually reflects the use seen in `core/post-title` block.
return select( coreStore ).canUser( 'update', {
kind: 'taxonomy',
- name: termTaxonomy || 'product_cat',
+ name: effectiveTaxonomy,
id: termId,
} );
},
- [ termId, termTaxonomy ]
+ [ termId, effectiveTaxonomy ]
);
const [ rawDescription = '', setDescription, fullDescription ] =
useEntityProp(
'taxonomy',
- termTaxonomy || 'product_cat',
+ effectiveTaxonomy,
'description',
- String( termId )
+ termId ? String( termId ) : undefined
);
- const isPreviewMode = usePreviewMode();
+ const isPreviewMode = usePreviewMode() && ! termId;
let displayRawDescription = '';
if ( isPreviewMode ) {
@@ -62,6 +66,8 @@ export default function Edit( { attributes, setAttributes, context }: Props ) {
let displayFullDescription = '';
if ( isPreviewMode ) {
displayFullDescription = previewCategories[ 0 ].description;
+ } else if ( typeof fullDescription === 'string' ) {
+ displayFullDescription = fullDescription;
} else if (
typeof fullDescription === 'object' &&
fullDescription !== null &&
@@ -79,7 +85,7 @@ export default function Edit( { attributes, setAttributes, context }: Props ) {
<p { ...blockProps }>{ __( 'Category description', 'woocommerce' ) }</p>
);
- if ( termId ) {
+ if ( termId || isPreviewMode ) {
descriptionElement = userCanEdit ? (
<PlainText
tagName="p"
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/category-title/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/category-title/block.json
index 7d718ae4f8f..904ccf2173a 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/category-title/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/category-title/block.json
@@ -39,5 +39,5 @@
},
"typography": true
},
- "usesContext": [ "termId", "termTaxonomy" ]
+ "usesContext": [ "termId", "termTaxonomy", "taxonomy" ]
}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/category-title/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/category-title/edit.tsx
index 1c1d5321f82..03d6f7ba613 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/category-title/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/category-title/edit.tsx
@@ -6,6 +6,8 @@ import { store as coreStore, useEntityProp } from '@wordpress/core-data';
import { useSelect } from '@wordpress/data';
import { createElement, forwardRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
+import { decodeEntities } from '@wordpress/html-entities';
+import { escapeHTML } from '@wordpress/escape-html';
import { WP_REST_API_Category } from 'wp-types';
import {
AlignmentControl,
@@ -38,6 +40,7 @@ interface Props {
context: {
termId?: number;
termTaxonomy?: string;
+ taxonomy?: string;
};
}
@@ -64,39 +67,44 @@ export default function Edit( { attributes, setAttributes, context }: Props ) {
level === 0 ? 'p' : `h${ level }`
) as keyof JSX.IntrinsicElements;
- const { termId, termTaxonomy } = context;
+ const { termId, termTaxonomy, taxonomy } = context;
+ const effectiveTaxonomy = termTaxonomy || taxonomy || 'product_cat';
const userCanEdit = useSelect(
( select ) => {
- if ( ! termId ) return false;
+ if ( ! termId ) {
+ return false;
+ }
// This use actually reflects the use seen in `core/post-title` block.
return select( coreStore ).canUser( 'update', {
kind: 'taxonomy',
- name: termTaxonomy || 'product_cat',
+ name: effectiveTaxonomy,
id: termId,
} );
},
- [ termId, termTaxonomy ]
+ [ termId, effectiveTaxonomy ]
);
- const isPreviewMode = usePreviewMode();
+ const isPreviewMode = usePreviewMode() && ! termId;
const [ rawTitle = '', setTitle, fullTitle ] = useEntityProp(
'taxonomy',
- termTaxonomy || 'product_cat',
+ effectiveTaxonomy,
'name',
termId ? String( termId ) : undefined
);
let displayRawTitle = '';
if ( isPreviewMode ) {
- displayRawTitle = previewCategories[ 0 ].description;
+ displayRawTitle = previewCategories[ 0 ].name;
} else if ( typeof rawTitle === 'string' ) {
displayRawTitle = rawTitle;
}
let displayFullTitle = '';
if ( isPreviewMode ) {
- displayFullTitle = previewCategories[ 0 ].description;
+ displayFullTitle = escapeHTML( previewCategories[ 0 ].name );
+ } else if ( typeof fullTitle === 'string' ) {
+ displayFullTitle = escapeHTML( decodeEntities( fullTitle ) );
} else if (
typeof fullTitle === 'object' &&
fullTitle !== null &&
@@ -108,18 +116,20 @@ export default function Edit( { attributes, setAttributes, context }: Props ) {
const link = useSelect(
( select ) => {
- if ( ! termId ) return undefined;
+ if ( ! termId ) {
+ return undefined;
+ }
const record = select(
coreStore
).getEntityRecord< WP_REST_API_Category >(
'taxonomy',
- termTaxonomy || 'product_cat',
+ effectiveTaxonomy,
termId
);
return record?.link;
},
- [ termId, termTaxonomy ]
+ [ termId, effectiveTaxonomy ]
);
const blockProps = useBlockProps( {
@@ -132,7 +142,7 @@ export default function Edit( { attributes, setAttributes, context }: Props ) {
__( 'Category title', 'woocommerce' )
) as JSX.Element;
- if ( termId ) {
+ if ( termId || isPreviewMode ) {
titleElement = userCanEdit ? (
<PlainText
tagName={ TagName }
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 a7e0d769d71..d3067594d29 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
@@ -28,6 +28,7 @@ interface WithBlockControlsRequiredProps< T > {
setAttributes: ( attrs: Partial< BlockControlRequiredAttributes > ) => void;
useEditingImage: [ boolean, Dispatch< SetStateAction< boolean > > ];
useEditMode: [ boolean, Dispatch< SetStateAction< boolean > > ];
+ canEditItem: boolean;
}
interface WithBlockControlsCategoryProps< T >
@@ -53,6 +54,7 @@ type BlockControlRequiredAttributes = {
};
interface BlockControlsProps {
+ canEditItem?: boolean;
backgroundImageId: number;
backgroundImageSrc: string;
contentAlign: BlockAlignment;
@@ -72,6 +74,7 @@ interface BlockControlsConfiguration extends GenericBlockUIConfig {
}
export const BlockControls = ( {
+ canEditItem = true,
backgroundImageId,
backgroundImageSrc,
contentAlign,
@@ -122,16 +125,18 @@ export const BlockControls = ( {
</ToolbarButton>
) : null }
</ToolbarGroup>
- <ToolbarGroup
- controls={ [
- {
- icon: 'edit',
- title: editLabel,
- onClick: () => setEditMode( ! editMode ),
- isActive: editMode,
- },
- ] }
- />
+ { canEditItem && (
+ <ToolbarGroup
+ controls={ [
+ {
+ icon: 'edit',
+ title: editLabel,
+ onClick: () => setEditMode( ! editMode ),
+ isActive: editMode,
+ },
+ ] }
+ />
+ ) }
</BlockControlsWrapper>
);
};
@@ -156,6 +161,7 @@ export const withBlockControls =
return (
<>
<BlockControls
+ canEditItem={ props.canEditItem }
backgroundImageId={ backgroundImageId }
backgroundImageSrc={ backgroundImageSrc }
contentAlign={ contentAlign }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/constants.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/constants.ts
index c4d2382b3b0..7e8af19238c 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/constants.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/featured-items/constants.ts
@@ -22,7 +22,8 @@ export const BLOCK_NAMES = {
} as const;
export const FEATURED_CATEGORY_DEFAULT_TEMPLATE = (
- category: WP_REST_API_Category
+ category: WP_REST_API_Category,
+ inheritCategory = false
): InnerBlockTemplate[] => [
[ 'woocommerce/category-title', { level: 2, textAlign: 'center' } ],
[ 'woocommerce/category-description', { textAlign: 'center' } ],
@@ -40,6 +41,16 @@ export const FEATURED_CATEGORY_DEFAULT_TEMPLATE = (
{
text: __( 'Shop now', 'woocommerce' ),
url: category.permalink,
+ ...( inheritCategory && {
+ metadata: {
+ bindings: {
+ url: {
+ source: 'core/term-data',
+ args: { field: 'link' },
+ },
+ },
+ },
+ } ),
},
],
],
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 d31fb538187..dc8ce2b1b5a 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
@@ -108,9 +108,5 @@
"textdomain": "woocommerce",
"apiVersion": 3,
"$schema": "https://schemas.wp.org/trunk/block.json",
- "usesContext": [ "termId", "termTaxonomy" ],
- "providesContext": {
- "termId": "categoryId",
- "termTaxonomy": "termTaxonomy"
- }
+ "usesContext": [ "termId", "termTaxonomy", "taxonomy" ]
}
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 badda60a1c4..f712b3ba628 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
@@ -45,6 +45,7 @@ type EditModeRequiredAttributes = {
interface EditModeRequiredProps< T > {
attributes: EditModeRequiredAttributes & EditorBlock< T >[ 'attributes' ];
clientId: string;
+ effectiveCategoryId?: number;
debouncedSpeak: ( label: string ) => void;
setAttributes: ( attrs: Partial< EditModeRequiredAttributes > ) => void;
triggerUrlUpdate: () => void;
@@ -61,11 +62,13 @@ export const withEditMode =
( props: EditModeProps< T > ) => {
const {
attributes,
+ effectiveCategoryId,
debouncedSpeak,
name,
setAttributes,
triggerUrlUpdate = () => void null,
error,
+ isLoading: isItemLoading,
} = props;
const className = getClassPrefixFromName( name );
@@ -78,7 +81,13 @@ export const withEditMode =
const hasFeaturedItemId =
( name === BLOCK_NAMES.featuredProduct && attributes.productId ) ||
- ( name === BLOCK_NAMES.featuredCategory && attributes.categoryId );
+ ( name === BLOCK_NAMES.featuredCategory &&
+ ( attributes.categoryId || effectiveCategoryId ) );
+ const canEditItem = ! (
+ name === BLOCK_NAMES.featuredCategory &&
+ ! attributes.categoryId &&
+ effectiveCategoryId
+ );
// Only show edit mode for newly inserted blocks without existing selection
const [ editMode, setEditMode ] = useState< boolean >(
@@ -96,17 +105,22 @@ export const withEditMode =
const itemId =
name === BLOCK_NAMES.featuredProduct
? attributes?.productId
- : attributes?.categoryId;
+ : attributes?.categoryId || effectiveCategoryId;
- const { status, isDeleted, isLoading } = useFeaturedItemStatus( {
+ const {
+ status,
+ isDeleted,
+ isLoading: isStatusLoading,
+ } = useFeaturedItemStatus( {
itemId,
itemType: name,
} );
+ const isLoading = isItemLoading || isStatusLoading;
const isPreviewMode = usePreviewMode();
useEffect( () => {
- if ( isPreviewMode ) {
+ if ( isPreviewMode || ! canEditItem ) {
return;
}
@@ -120,9 +134,9 @@ export const withEditMode =
setEditMode( currEditModeValue );
}
}
- }, [ status, isDeleted, name, isLoading, isPreviewMode ] );
+ }, [ status, isDeleted, name, isLoading, isPreviewMode, canEditItem ] );
- if ( editMode ) {
+ if ( editMode && canEditItem ) {
return (
<Placeholder
icon={ <Icon icon={ icon } /> }
@@ -198,6 +212,7 @@ export const withEditMode =
return (
<Component
{ ...props }
+ canEditItem={ canEditItem }
isLoading={ isLoading }
error={ isLoading ? null : error }
useEditMode={ [ editMode, setEditMode ] }
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 ab35e3b0bcb..8bee87aebd1 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
@@ -1,9 +1,8 @@
-/* eslint-disable @wordpress/no-unsafe-wp-apis */
-
/**
* External dependencies
*/
import type { BlockAlignment } from '@wordpress/blocks';
+import { __ } from '@wordpress/i18n';
import type { ComponentType, Dispatch, SetStateAction } from 'react';
import { ProductResponseItem } from '@woocommerce/types';
import { Icon, Placeholder, Spinner } from '@wordpress/components';
@@ -93,6 +92,7 @@ interface FeaturedItemRequiredProps< T > {
textColor?: string;
};
isLoading: boolean;
+ canEditItem: boolean;
setAttributes: ( attrs: Partial< FeaturedItemRequiredAttributes > ) => void;
useEditingImage: [ boolean, Dispatch< SetStateAction< boolean > > ];
useEditMode: [ boolean, Dispatch< SetStateAction< boolean > > ];
@@ -126,6 +126,7 @@ export const withFeaturedItem =
const {
attributes,
+ canEditItem,
category,
isLoading,
isSelected,
@@ -163,7 +164,9 @@ export const withFeaturedItem =
const element =
featuredProductParentRef.current as HTMLElement | null;
- if ( ! element ) return;
+ if ( ! element ) {
+ return;
+ }
observer.observe( element );
}
@@ -200,7 +203,18 @@ export const withFeaturedItem =
[ setAttributes ]
);
- const renderNoItemButton = () => {
+ const renderNoItemContent = () => {
+ if ( ! canEditItem ) {
+ return (
+ <p>
+ { __(
+ 'No product category is available.',
+ 'woocommerce'
+ ) }
+ </p>
+ );
+ }
+
return (
<>
<p>{ emptyMessage }</p>
@@ -242,14 +256,19 @@ export const withFeaturedItem =
return (
<BlockContextProvider
value={ {
- termId: category.term_id,
+ termId:
+ attributes.categoryId === 'preview'
+ ? undefined
+ : category.id,
termTaxonomy: 'product_cat',
+ taxonomy: 'product_cat',
} }
>
<div className={ `${ className }__inner-blocks` }>
<InnerBlocks
template={ FEATURED_CATEGORY_DEFAULT_TEMPLATE(
- category
+ category,
+ ! attributes.categoryId
) }
templateLock={ false }
/>
@@ -264,7 +283,7 @@ export const withFeaturedItem =
icon={ <Icon icon={ icon } /> }
label={ label }
>
- { isLoading ? <Spinner /> : renderNoItemButton() }
+ { isLoading ? <Spinner /> : renderNoItemContent() }
</Placeholder>
);
@@ -377,6 +396,10 @@ export const withFeaturedItem =
);
};
+ if ( ! item && isLoading && ! canEditItem ) {
+ return null;
+ }
+
if ( isEditingImage ) {
return (
<Component
diff --git a/plugins/woocommerce/client/blocks/assets/js/hocs/test/with-category.jsx b/plugins/woocommerce/client/blocks/assets/js/hocs/test/with-category.jsx
index 4acef8a0e42..9ee6d54dca1 100644
--- a/plugins/woocommerce/client/blocks/assets/js/hocs/test/with-category.jsx
+++ b/plugins/woocommerce/client/blocks/assets/js/hocs/test/with-category.jsx
@@ -108,6 +108,35 @@ describe( 'withCategory Component', () => {
} );
} );
+ it.each( [
+ [ {}, { termId: 42, taxonomy: 'product_cat' }, [ [ 42 ] ] ],
+ [
+ {},
+ { termId: 42, termTaxonomy: 'product_cat', taxonomy: 'category' },
+ [ [ 42 ] ],
+ ],
+ [
+ { categoryId: 7 },
+ { termId: 42, taxonomy: 'product_cat' },
+ [ [ 7 ] ],
+ ],
+ [ {}, { termId: 42, taxonomy: 'category' }, [] ],
+ ] )(
+ 'resolves category selection %j with context %j',
+ async ( selectedAttributes, context, expectedCalls ) => {
+ mockUtils.getCategory.mockResolvedValue( mockCategory );
+ await renderComponent( {
+ attributes: selectedAttributes,
+ context,
+ } );
+
+ expect( mockUtils.getCategory.mock.calls ).toEqual( expectedCalls );
+ expect( lastProps.effectiveCategoryId ).toBe(
+ expectedCalls[ 0 ]?.[ 0 ]
+ );
+ }
+ );
+
describe( 'when the API returns an error', () => {
const error = { message: 'There was an error.' };
const formattedError = { message: 'There was an error.', type: 'api' };
diff --git a/plugins/woocommerce/client/blocks/assets/js/hocs/with-category.js b/plugins/woocommerce/client/blocks/assets/js/hocs/with-category.js
index 58d7f50d08c..36dd564cecd 100644
--- a/plugins/woocommerce/client/blocks/assets/js/hocs/with-category.js
+++ b/plugins/woocommerce/client/blocks/assets/js/hocs/with-category.js
@@ -19,14 +19,16 @@ const withCategory = createHigherOrderComponent( ( OriginalComponent ) => {
return class WrappedComponent extends Component {
constructor() {
super( ...arguments );
+ const categoryId = this.getCategoryId();
this.state = {
error: null,
- loading: false,
+ loading: !! categoryId && categoryId !== 'preview',
category:
this.props.attributes.categoryId === 'preview'
? this.props.attributes.previewCategory
: null,
};
+ this.getCategoryId = this.getCategoryId.bind( this );
this.loadCategory = this.loadCategory.bind( this );
}
@@ -36,15 +38,28 @@ const withCategory = createHigherOrderComponent( ( OriginalComponent ) => {
componentDidUpdate( prevProps ) {
if (
- prevProps.attributes.categoryId !==
- this.props.attributes.categoryId
+ this.getCategoryId( prevProps ) !==
+ this.getCategoryId( this.props )
) {
this.loadCategory();
}
}
+ getCategoryId( props = this.props ) {
+ const { categoryId } = props.attributes;
+ if ( categoryId ) {
+ return categoryId;
+ }
+
+ const taxonomy =
+ props.context?.termTaxonomy || props.context?.taxonomy;
+ const shouldUseContext = taxonomy === 'product_cat';
+
+ return shouldUseContext ? props.context?.termId : undefined;
+ }
+
loadCategory() {
- const { categoryId } = this.props.attributes;
+ const categoryId = this.getCategoryId();
if ( categoryId === 'preview' ) {
return;
@@ -82,6 +97,7 @@ const withCategory = createHigherOrderComponent( ( OriginalComponent ) => {
getCategory={ this.loadCategory }
isLoading={ loading }
category={ category }
+ effectiveCategoryId={ this.getCategoryId() }
/>
);
}
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 6fdc10cebca..896874daa16 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -49924,18 +49924,6 @@ parameters:
count: 1
path: src/Blocks/BlockTypes/FeaturedItem.php
- -
- message: '#^Parameter \$block of method Automattic\\WooCommerce\\Blocks\\BlockTypes\\FeaturedItem\:\:render\(\) has invalid type Automattic\\WooCommerce\\Blocks\\BlockTypes\\WP_Block\.$#'
- identifier: class.notFound
- count: 1
- path: src/Blocks/BlockTypes/FeaturedItem.php
-
- -
- message: '#^Parameter \$parent_block of method Automattic\\WooCommerce\\Blocks\\BlockTypes\\FeaturedItem\:\:update_context\(\) has invalid type Automattic\\WooCommerce\\Blocks\\BlockTypes\\WP_Block\.$#'
- identifier: class.notFound
- count: 1
- path: src/Blocks/BlockTypes/FeaturedItem.php
-
-
message: '#^Property Automattic\\WooCommerce\\Blocks\\BlockTypes\\FeaturedItem\:\:\$current_item \(WC_Product\|WP_Term\|null\) does not accept WC_Product\|false\|null\.$#'
identifier: assign.propertyType
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/CategoryDescription.php b/plugins/woocommerce/src/Blocks/BlockTypes/CategoryDescription.php
index a88c83e6e7e..c49dd4743b4 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/CategoryDescription.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/CategoryDescription.php
@@ -24,7 +24,7 @@ class CategoryDescription extends AbstractBlock {
*/
protected function render( $attributes, $content, $block ) {
$term_id = $block->context['termId'] ?? 0;
- $term_taxonomy = $block->context['termTaxonomy'] ?? 'product_cat';
+ $term_taxonomy = $block->context['termTaxonomy'] ?? $block->context['taxonomy'] ?? 'product_cat';
$text_align = isset( $attributes['textAlign'] ) ? sanitize_key( $attributes['textAlign'] ) : '';
@@ -60,15 +60,6 @@ class CategoryDescription extends AbstractBlock {
);
}
- /**
- * Register the context used by this block.
- *
- * @return array
- */
- protected function get_block_type_uses_context() {
- return [ 'termId', 'termTaxonomy' ];
- }
-
/**
* Disable the frontend script for this block.
*
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/CategoryTitle.php b/plugins/woocommerce/src/Blocks/BlockTypes/CategoryTitle.php
index 8c939dce34a..c7292da34a4 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/CategoryTitle.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/CategoryTitle.php
@@ -24,7 +24,7 @@ class CategoryTitle extends AbstractBlock {
*/
protected function render( $attributes, $content, $block ) {
$term_id = $block->context['termId'] ?? 0;
- $term_taxonomy = $block->context['termTaxonomy'] ?? 'product_cat';
+ $term_taxonomy = $block->context['termTaxonomy'] ?? $block->context['taxonomy'] ?? 'product_cat';
$level = isset( $attributes['level'] ) ? max( 0, min( 6, intval( $attributes['level'] ) ) ) : 2;
$text_align = isset( $attributes['textAlign'] ) ? sanitize_key( $attributes['textAlign'] ) : '';
@@ -73,15 +73,6 @@ class CategoryTitle extends AbstractBlock {
return $title_html;
}
- /**
- * Register the context used by this block.
- *
- * @return array
- */
- protected function get_block_type_uses_context() {
- return [ 'termId', 'termTaxonomy' ];
- }
-
/**
* Disable the frontend script for this block.
*
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedCategory.php b/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedCategory.php
index 9cef5d7fff1..7380479396b 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedCategory.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedCategory.php
@@ -30,6 +30,63 @@ class FeaturedCategory extends FeaturedItem {
);
}
+ /**
+ * Render the selected category or the product category inherited from context.
+ *
+ * @since 11.2.0
+ *
+ * @param array $attributes Block attributes.
+ * @param string $content Block content.
+ * @param \WP_Block $block Block instance.
+ * @return string
+ */
+ protected function render( $attributes, $content, $block ) {
+ $attributes['categoryId'] = self::resolve_category_id( $attributes, $block->context );
+
+ return parent::render( $attributes, $content, $block );
+ }
+
+ /**
+ * Pass the resolved product category to inner blocks.
+ *
+ * @since 11.2.0
+ *
+ * @param array $context Block context.
+ * @param array $parsed_block Block attributes.
+ * @param \WP_Block|null $parent_block Parent block instance.
+ * @return array Updated block context.
+ */
+ public function update_context( $context, $parsed_block, $parent_block ) {
+ $context = parent::update_context( $context, $parsed_block, $parent_block );
+
+ if ( is_array( $context ) && $parent_block instanceof \WP_Block && 'woocommerce/featured-category' === $parent_block->name ) {
+ $category_id = self::resolve_category_id( $parent_block->attributes, $parent_block->context );
+ if ( $category_id ) {
+ $context['termId'] = $category_id;
+ $context['termTaxonomy'] = 'product_cat';
+ $context['taxonomy'] = 'product_cat';
+ }
+ }
+
+ return $context;
+ }
+
+ /**
+ * Resolve the selected or inherited product category ID.
+ *
+ * @param array $attributes Block attributes.
+ * @param array $context Block context.
+ * @return int
+ */
+ private static function resolve_category_id( array $attributes, array $context ): int {
+ if ( ! empty( $attributes['categoryId'] ) ) {
+ return absint( $attributes['categoryId'] );
+ }
+
+ $taxonomy = $context['termTaxonomy'] ?? $context['taxonomy'] ?? '';
+ return 'product_cat' === $taxonomy ? absint( $context['termId'] ?? 0 ) : 0;
+ }
+
/**
* Returns the featured category.
*
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedItem.php b/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedItem.php
index d9d065b0e54..24b693ddafb 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedItem.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedItem.php
@@ -134,9 +134,9 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
/**
* Update context for inner blocks to provide postId and postType.
*
- * @param array $context Block context.
- * @param array $parsed_block Block attributes.
- * @param WP_Block $parent_block Block instance.
+ * @param array $context Block context.
+ * @param array $parsed_block Block attributes.
+ * @param \WP_Block|null $parent_block Block instance.
*
* @return array Updated block context.
*/
@@ -231,9 +231,9 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
/**
* Render the featured item block.
*
- * @param array $attributes Block attributes.
- * @param string $content Block content.
- * @param WP_Block $block Block instance.
+ * @param array $attributes Block attributes.
+ * @param string $content Block content.
+ * @param \WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/CategoryTermContextTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/CategoryTermContextTest.php
new file mode 100644
index 00000000000..8b5baff26e4
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/CategoryTermContextTest.php
@@ -0,0 +1,105 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;
+
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for category blocks using term context.
+ */
+class CategoryTermContextTest extends WC_Unit_Test_Case {
+ /**
+ * @testdox Should read Core taxonomy context while preserving legacy context precedence.
+ * @testWith ["category-title", {"taxonomy":"category"}, "Context content: category-title"]
+ * ["category-description", {"taxonomy":"category"}, "Context content: category-description"]
+ * ["category-title", {"termTaxonomy":"category", "taxonomy":"product_cat"}, "Context content: category-title"]
+ * ["category-description", {"termTaxonomy":"category", "taxonomy":"product_cat"}, "Context content: category-description"]
+ *
+ * @param string $name Block name without the namespace.
+ * @param array $context Available block context.
+ * @param string $expected_text Expected category text.
+ */
+ public function test_category_text_context( string $name, array $context, string $expected_text ): void {
+ $context['termId'] = self::factory()->term->create(
+ array(
+ 'taxonomy' => 'category',
+ 'name' => 'Context content: category-title',
+ 'description' => 'Context content: category-description',
+ )
+ );
+ $sut = new \WP_Block( parse_blocks( '<!-- wp:woocommerce/' . $name . ' /-->' )[0], $context );
+
+ $this->assertStringContainsString( $expected_text, $sut->render(), 'The block should render the correct term field from the effective taxonomy.' );
+ }
+
+ /**
+ * @testdox Should resolve category text and bound links without overwriting custom URLs.
+ * @testWith [false, "taxonomy", true]
+ * [false, "termTaxonomy", true]
+ * [true, "taxonomy", true]
+ * [false, "taxonomy", false]
+ * [true, "taxonomy", false]
+ *
+ * @param bool $selected Whether the category is selected explicitly.
+ * @param string $taxonomy_key Context key supplying the taxonomy.
+ * @param bool $bound Whether the button uses the term-data binding.
+ */
+ public function test_featured_category_context( bool $selected, string $taxonomy_key, bool $bound ): void {
+ if ( $bound && ! get_block_bindings_source( 'core/term-data' ) ) {
+ $this->markTestSkipped( 'Core term-data bindings are not available.' );
+ }
+
+ $selected_id = self::factory()->term->create(
+ array(
+ 'taxonomy' => 'product_cat',
+ 'name' => 'Selected category',
+ )
+ );
+ $term_ids = self::factory()->term->create_many(
+ 2,
+ array(
+ 'taxonomy' => 'product_cat',
+ 'description' => 'Inherited description',
+ )
+ );
+ $binding = $bound ? '{"metadata":{"bindings":{"url":{"source":"core/term-data","args":{"field":"link"}}}}}' : '{}';
+ $markup = '<!-- wp:woocommerce/featured-category --><!-- wp:woocommerce/category-title /--><!-- wp:woocommerce/category-description /--><!-- wp:buttons --><div class="wp-block-buttons"><!-- wp:button ' . $binding . ' --><div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://example.com/custom">Shop now</a></div><!-- /wp:button --></div><!-- /wp:buttons --><!-- /wp:woocommerce/featured-category -->';
+ $parsed = parse_blocks( $markup )[0];
+
+ $parsed['attrs']['categoryId'] = $selected ? $selected_id : 0;
+
+ foreach ( $term_ids as $term_id ) {
+ $sut = new \WP_Block(
+ $parsed,
+ array(
+ 'termId' => $term_id,
+ $taxonomy_key => 'product_cat',
+ )
+ );
+ $output = $sut->render();
+ $expected_term = get_term( $selected ? $selected_id : $term_id, 'product_cat' );
+ $expected_url = $bound ? get_term_link( $expected_term ) : 'https://example.com/custom';
+
+ $this->assertStringContainsString( esc_html( $expected_term->name ) . '</h2>', $output, 'The title should follow the selected or inherited category.' );
+ $this->assertStringContainsString( 'href="' . esc_url( $expected_url ) . '"', $output, 'Bound links should follow each category; custom URLs should remain unchanged.' );
+ $this->assertSame( ! $selected, str_contains( $output, 'Inherited description' ), 'The description should use the same category as the title.' );
+ }
+ }
+
+ /**
+ * @testdox Should reject inherited terms from other taxonomies.
+ */
+ public function test_featured_category_taxonomy_guard(): void {
+ $term_id = self::factory()->term->create( array( 'taxonomy' => 'product_cat' ) );
+ $sut = new \WP_Block(
+ parse_blocks( '<!-- wp:woocommerce/featured-category /-->' )[0],
+ array(
+ 'termId' => $term_id,
+ 'taxonomy' => 'category',
+ )
+ );
+
+ $this->assertSame( '', $sut->render(), 'A term from another taxonomy must not be treated as a product category.' );
+ }
+}