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.' );
+	}
+}