Commit 1863e2243e4 for woocommerce

commit 1863e2243e43bb7adbc3c85bcf6c1e4c3165707c
Author: Tung Du <dinhtungdu@gmail.com>
Date:   Wed Aug 26 11:39:11 2026 +0700

    Add: Product Filters block overlay controls (#67696)

    * add: product filter overlay controls

    * chore: add product filter overlay changelog

    * refactor: store product filter overlay as enum

    * Revert "refactor: store product filter overlay as enum"

    This reverts commit d1b8c7477382f119af426457dc32d0232d3db0fa.

    * fix: update copy, var name

    * fix: add fallback value

    * fix: simplify svg handling

    * fix: apply overlay position at all breakpoints

    * fix: adjust padding

    * fix: simplify width handling

    * chore: remove unused show more style

    * feat: support configurable product filter viewports

    * feat: add responsive product filter overlay modes

    * test: add product filter assertion messages

    * chore: remove test

    * fix: deprecate showFilterDrawer

    * fix: use guaranteed desktop viewport query

    * chore: update changelog

    * fix: avoid private viewport APIs

    * refactor: simplify responsive filter styles

    * refactor: remove tablet overlay option

    * fix: fall back to mobile viewport

    * refactor: inline responsive filter styles

    * fix: animate product filter overlay closing

    * fix: delay overlay backdrop dismissal

    * fix: synchronize overlay close transitions

diff --git a/docs/block-development/reference/block-references.md b/docs/block-development/reference/block-references.md
index 724de0c989f..3b771d040e6 100644
--- a/docs/block-development/reference/block-references.md
+++ b/docs/block-development/reference/block-references.md
@@ -1240,7 +1240,7 @@ Add a set of filters shoppers can use.
 - **Name:** woocommerce/product-filters
 - **Category:** woocommerce
 - **Supports:** align, color (background, button, heading, text, ~~enableContrastChecker~~), inserter, interactivity, layout (default, ~~allowEditing~~), spacing (blockGap), typography (fontSize)
-- **Attributes:** isPreview, showFilterDrawer
+- **Attributes:** isPreview, overlayMode, overlayPosition

 ## Active Filters - woocommerce/product-filter-active

diff --git a/plugins/woocommerce/changelog/add-product-filter-overlay-controls b/plugins/woocommerce/changelog/add-product-filter-overlay-controls
new file mode 100644
index 00000000000..355b8154f80
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-product-filter-overlay-controls
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add Off, Mobile, and Always overlay modes to Product Filters block.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/__tests__/frontend.test.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/__tests__/frontend.test.ts
index 9dab50c6b15..b950e484098 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/__tests__/frontend.test.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/__tests__/frontend.test.ts
@@ -140,6 +140,28 @@ describe( 'product filters interactivity store', () => {
 		expect( mockRegisteredStore.state.selectableItems ).toEqual( [] );
 	} );

+	it( 'closes from the backdrop but not the dialog', () => {
+		if ( ! mockRegisteredStore ) {
+			throw new Error( 'Product filters store was not registered.' );
+		}
+		const context = { isOverlayOpened: true };
+		const wrapper = document.createElement( 'div' );
+		const dialog = document.createElement( 'div' );
+		mockGetContext.mockReturnValue( context );
+
+		mockRegisteredStore.actions.closeOverlayOnBackdrop( {
+			target: dialog,
+			currentTarget: wrapper,
+		} as unknown as MouseEvent );
+		expect( context.isOverlayOpened ).toBe( true );
+
+		mockRegisteredStore.actions.closeOverlayOnBackdrop( {
+			target: wrapper,
+			currentTarget: wrapper,
+		} as unknown as MouseEvent );
+		expect( context.isOverlayOpened ).toBe( false );
+	} );
+
 	it( 'does not add child-owned index metadata to selectable items', () => {
 		if ( ! mockRegisteredStore ) {
 			throw new Error( 'Product filters store was not registered.' );
@@ -270,7 +292,7 @@ describe( 'product filters interactivity store', () => {
 						actions: {
 							navigate: routerNavigate,
 						},
-					} );
+					} as unknown as typeof import('@wordpress/interactivity-router') );

 					expect( routerNavigate ).toHaveBeenCalledTimes( 1 );
 					const [ navigatedUrl ] = routerNavigate.mock.calls[ 0 ];
@@ -434,7 +456,7 @@ describe( 'product filters interactivity store', () => {
 			expect( firstYield.done ).toBe( false );
 			iterator.next( {
 				actions: { navigate: routerNavigate },
-			} );
+			} as unknown as typeof import('@wordpress/interactivity-router') );

 			expect( mockReload ).not.toHaveBeenCalled();
 			expect( routerNavigate ).toHaveBeenCalledTimes( 1 );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/block.json
index 73d863a6116..b548ce9e3b2 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/block.json
@@ -40,9 +40,13 @@
 			"type": "boolean",
 			"default": false
 		},
-		"showFilterDrawer": {
-			"type": "boolean",
-			"default": true
+		"overlayMode": {
+			"type": "string",
+			"enum": [ "off", "mobile", "always" ]
+		},
+		"overlayPosition": {
+			"type": "string",
+			"enum": [ "left", "right" ]
 		}
 	},
 	"example": {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/deprecated.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/deprecated.tsx
index 3da47a4821f..8d78e1e7e3f 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/deprecated.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/deprecated.tsx
@@ -2,10 +2,12 @@
  * External dependencies
  */
 import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
+import clsx from 'clsx';

 /**
  * Internal dependencies
  */
+import metadata from './block.json';
 import { type BlockAttributes } from './types';
 import { getColorsFromBlockSupports } from './utils/get-colors-from-block-supports';
 import { presetToCssVariable } from './utils/preset-to-css-variable';
@@ -45,6 +47,40 @@ function getProductFiltersCssV1( attributes: BlockAttributes ) {
 	return styles;
 }

+type LegacyDrawerAttributes = BlockAttributes & {
+	showFilterDrawer?: boolean;
+};
+
+const v2 = {
+	attributes: {
+		...metadata.attributes,
+		showFilterDrawer: {
+			type: 'boolean',
+		},
+	},
+	save( { attributes }: { attributes: LegacyDrawerAttributes } ) {
+		const blockProps = useBlockProps.save( {
+			className: clsx( 'wc-block-product-filters', {
+				'is-filter-drawer-disabled':
+					attributes.showFilterDrawer === false,
+			} ),
+		} );
+		const innerBlocksProps = useInnerBlocksProps.save( blockProps );
+		return <div { ...innerBlocksProps } />;
+	},
+	isEligible( attributes: LegacyDrawerAttributes ) {
+		return typeof attributes.showFilterDrawer === 'boolean';
+	},
+	migrate( attributes: LegacyDrawerAttributes ) {
+		const { showFilterDrawer, ...currentAttributes } = attributes;
+
+		return {
+			...currentAttributes,
+			overlayMode: showFilterDrawer === false ? 'off' : 'mobile',
+		};
+	},
+};
+
 const v1 = {
 	save( { attributes }: { attributes: BlockAttributes } ) {
 		const blockProps = useBlockProps.save( {
@@ -56,4 +92,4 @@ const v1 = {
 	},
 };

-export default [ v1 ];
+export default [ v2, v1 ];
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/edit.tsx
index 346f1fb25f8..5d56f4d1126 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/edit.tsx
@@ -7,7 +7,14 @@ import {
 	useBlockProps,
 } from '@wordpress/block-editor';
 import { BlockEditProps, InnerBlockTemplate } from '@wordpress/blocks';
-import { PanelBody, ToggleControl } from '@wordpress/components';
+import type { HTMLAttributes } from 'react';
+import {
+	PanelBody,
+	// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
+	__experimentalToggleGroupControl as ToggleGroupControl,
+	// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
+	__experimentalToggleGroupControlOption as ToggleGroupControlOption,
+} from '@wordpress/components';
 import { __ } from '@wordpress/i18n';
 import { Icon, close } from '@wordpress/icons';
 import { useState } from '@wordpress/element';
@@ -21,6 +28,7 @@ import clsx from 'clsx';
 import './editor.scss';
 import { type BlockAttributes } from './types';
 import { getColorsFromBlockSupports } from './utils/get-colors-from-block-supports';
+import { getOverlayMode, isOverlayMode } from './utils/overlay-mode';
 import { presetToCssVariable } from './utils/preset-to-css-variable';

 const TEMPLATE: InnerBlockTemplate[] = [
@@ -46,7 +54,10 @@ const TEMPLATE: InnerBlockTemplate[] = [
 export const Edit = ( props: BlockEditProps< BlockAttributes > ) => {
 	const { attributes, setAttributes } = props;
 	const { isPreview } = attributes;
-	const showFilterDrawer = attributes.showFilterDrawer !== false;
+	const overlayMode = getOverlayMode( attributes );
+	const hasOverlay = overlayMode !== 'off';
+	const overlayPosition =
+		attributes.overlayPosition === 'right' ? 'right' : 'left';
 	const [ isOpen, setIsOpen ] = useState( false );

 	const globalColors = getSetting< { background?: string; text?: string } >(
@@ -65,7 +76,9 @@ export const Edit = ( props: BlockEditProps< BlockAttributes > ) => {
 	const blockProps = useBlockProps( {
 		className: clsx( 'wc-block-product-filters', {
 			'is-overlay-opened': isOpen,
-			'is-filter-drawer-disabled': ! showFilterDrawer,
+			'is-filter-drawer-disabled': ! hasOverlay,
+			'is-mobile-overlay': overlayMode === 'mobile',
+			'is-overlay-right': hasOverlay && overlayPosition === 'right',
 		} ),
 		style: {
 			'--wc-product-filters-background-color':
@@ -76,7 +89,7 @@ export const Edit = ( props: BlockEditProps< BlockAttributes > ) => {
 				? presetToCssVariable( blockGap )
 				: undefined,
 		},
-	} );
+	} ) as HTMLAttributes< HTMLDivElement >;

 	let filtersContent: JSX.Element;

@@ -86,7 +99,7 @@ export const Edit = ( props: BlockEditProps< BlockAttributes > ) => {
 				<InnerBlocks templateLock={ false } template={ TEMPLATE } />
 			</div>
 		);
-	} else if ( showFilterDrawer ) {
+	} else if ( hasOverlay ) {
 		filtersContent = (
 			<>
 				<button
@@ -108,9 +121,6 @@ export const Edit = ( props: BlockEditProps< BlockAttributes > ) => {
 									className="wc-block-product-filters__close-overlay"
 									onClick={ () => setIsOpen( ! isOpen ) }
 								>
-									<span>
-										{ __( 'Close', 'woocommerce' ) }
-									</span>
 									<Icon icon={ close } />
 								</button>
 							</header>
@@ -147,27 +157,60 @@ export const Edit = ( props: BlockEditProps< BlockAttributes > ) => {
 		<>
 			<InspectorControls>
 				<PanelBody title={ __( 'Settings', 'woocommerce' ) }>
-					<ToggleControl
-						label={ __(
-							'Collapse filters on small screens',
+					<ToggleGroupControl
+						__nextHasNoMarginBottom
+						__next40pxDefaultSize
+						isBlock
+						label={ __( 'Overlay', 'woocommerce' ) }
+						help={ __(
+							'When on, filters are hidden behind a button instead of showing on the page.',
 							'woocommerce'
 						) }
-						help={
-							showFilterDrawer
-								? __(
-										'Shoppers tap a button to open filters.',
-										'woocommerce'
-								  )
-								: __(
-										'Filters are shown directly on the page.',
-										'woocommerce'
-								  )
-						}
-						checked={ showFilterDrawer }
-						onChange={ ( value ) =>
-							setAttributes( { showFilterDrawer: value } )
-						}
-					/>
+						value={ overlayMode }
+						onChange={ ( value ) => {
+							if ( isOverlayMode( value ) ) {
+								setAttributes( { overlayMode: value } );
+							}
+						} }
+					>
+						<ToggleGroupControlOption
+							value="off"
+							label={ __( 'Off', 'woocommerce' ) }
+						/>
+						<ToggleGroupControlOption
+							value="mobile"
+							label={ __( 'Mobile', 'woocommerce' ) }
+						/>
+						<ToggleGroupControlOption
+							value="always"
+							label={ __( 'Always', 'woocommerce' ) }
+						/>
+					</ToggleGroupControl>
+					{ overlayMode !== 'off' && (
+						<ToggleGroupControl
+							__nextHasNoMarginBottom
+							__next40pxDefaultSize
+							isBlock
+							label={ __( 'Overlay position', 'woocommerce' ) }
+							value={ overlayPosition }
+							onChange={ ( value ) => {
+								if ( value === 'left' || value === 'right' ) {
+									setAttributes( {
+										overlayPosition: value,
+									} );
+								}
+							} }
+						>
+							<ToggleGroupControlOption
+								value="left"
+								label={ __( 'Left', 'woocommerce' ) }
+							/>
+							<ToggleGroupControlOption
+								value="right"
+								label={ __( 'Right', 'woocommerce' ) }
+							/>
+						</ToggleGroupControl>
+					) }
 				</PanelBody>
 			</InspectorControls>
 			<div { ...blockProps }>{ filtersContent }</div>
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/frontend.ts
index 06465215a39..9ebb0e83df9 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/frontend.ts
@@ -179,6 +179,11 @@ const productFiltersStore = {
 			const context = getContext< ProductFiltersContext >();
 			context.isOverlayOpened = false;
 		},
+		closeOverlayOnBackdrop: ( event: MouseEvent ) => {
+			if ( event.target === event.currentTarget ) {
+				actions.closeOverlay();
+			}
+		},
 		closeOverlayOnEscape: ( event: KeyboardEvent ) => {
 			const context = getContext< ProductFiltersContext >();
 			if ( context.isOverlayOpened && event.key === 'Escape' ) {
@@ -316,7 +321,6 @@ const productFiltersStore = {
 };

 // Compile-time protocol conformance check.
-// eslint-disable-next-line @typescript-eslint/no-unused-expressions
 productFiltersStore satisfies SelectableItemsParentStore< FilterItemFields >;

 export type ProductFiltersStore = typeof productFiltersStore;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/save.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/save.tsx
index b00280b8b14..a3b3d80bd13 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/save.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/save.tsx
@@ -9,15 +9,17 @@ import clsx from 'clsx';
  */
 import './editor.scss';
 import { type BlockAttributes } from './types';
+import { getOverlayMode } from './utils/overlay-mode';

 export const Save = ( {
 	attributes,
 }: {
 	attributes: BlockAttributes;
 } ): JSX.Element => {
+	const hasOverlay = getOverlayMode( attributes ) !== 'off';
 	const blockProps = useBlockProps.save( {
 		className: clsx( 'wc-block-product-filters', {
-			'is-filter-drawer-disabled': attributes.showFilterDrawer === false,
+			'is-filter-drawer-disabled': ! hasOverlay,
 		} ),
 	} );
 	const innerBlocksProps = useInnerBlocksProps.save( blockProps );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/style.scss
index 5da8faffae7..77e26fcc428 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/style.scss
@@ -1,5 +1,5 @@
 :where(.wc-block-product-filters) {
-	--top-padding: var(--wp-admin--admin-bar--height);
+	--top-padding: var(--wp-admin--admin-bar--height, 0);
 	@include breakpoint("<782px") {
 		--top-padding: var(--adminbar-mobile-padding, 0);
 	}
@@ -35,8 +35,10 @@
 		inset: 0;
 		pointer-events: none;
 		top: var(--top-padding);
+		z-index: 9999;
 		background-color: transparent;
-		transition: background-color 500ms;
+		// Delay the 200ms backdrop fade so it finishes with the 500ms drawer transition.
+		transition: background-color 200ms 300ms;
 	}

 	.wc-block-product-filters__overlay-wrapper {
@@ -47,9 +49,12 @@

 	.wc-block-product-filters__overlay-dialog {
 		position: absolute;
-		inset: 0;
-		transform: translateY(100vh);
-		transition: none;
+		/*rtl:ignore*/
+		inset: 0 auto 0 0;
+		--wc-product-filters-closed-translate-x: -100%;
+		width: min(100%, 480px);
+		transform: translateX(var(--wc-product-filters-closed-translate-x));
+		transition: transform 500ms;
 		background-color: var(
 			--wc-product-filters-overlay-background-color,
 			var(--wc-product-filters-background-color, Canvas)
@@ -64,7 +69,7 @@
 	}

 	.wc-block-product-filters__overlay-header {
-		padding: var(--wp--preset--spacing--30) var(--wp--preset--spacing--40) 0;
+		padding: var(--wp--preset--spacing--30) var(--wp--preset--spacing--30) 0;
 		display: flex;
 		flex-flow: row-reverse;
 	}
@@ -85,12 +90,12 @@
 	}

 	.wc-block-product-filters__overlay-content {
-		padding: 0 var(--wp--preset--spacing--40);
+		padding: 0 var(--wp--preset--spacing--30);
 		overflow-y: scroll;
 	}

 	.wc-block-product-filters__overlay-footer {
-		padding: var(--wp--preset--spacing--30) var(--wp--preset--spacing--40);
+		padding: var(--wp--preset--spacing--30) var(--wp--preset--spacing--30);
 		box-shadow: 0 -4px 8px 0 #0000001a;
 	}

@@ -98,78 +103,34 @@
 		width: 100%;
 	}

+	&.is-overlay-right .wc-block-product-filters__overlay-dialog {
+		/*rtl:ignore*/
+		inset: 0 0 0 auto;
+		--wc-product-filters-closed-translate-x: 100%;
+	}
+
 	&.is-overlay-opened {
 		.wc-block-product-filters__overlay {
-			z-index: 9999;
 			pointer-events: auto;
 			background-color: rgba(95, 95, 95, 0.35);
+			transition-delay: 0s;
 		}

 		.wc-block-product-filters__overlay-dialog {
-			transform: translateY(0);
-			transition: transform 500ms;
+			transform: translateX(0);
 		}
-	}

-	&.is-filter-drawer-disabled {
-		display: flex;
-	}
-
-	@include breakpoint(">600px") {
-		&,
-		&.is-overlay-opened {
-			display: flex;
-
-			.wc-block-product-filters__overlay-header,
-			.wc-block-product-filters__overlay-footer,
-			.wc-block-product-filters__open-overlay {
-				display: none;
-			}
-
-			.wc-block-product-filters__overlay {
-				position: relative;
-				pointer-events: auto;
-				inset: 0;
-				background: inherit;
-				color: inherit;
-				transition: none;
-				flex-grow: 1;
-			}
-			.wc-block-product-filters__overlay-wrapper {
-				width: auto;
-				height: auto;
-				background: inherit;
-				color: inherit;
-			}
+		.wc-block-product-filters__overlay-content .wp-block-group {
+			display: block;

-			.wc-block-product-filters__overlay-dialog {
-				position: relative;
-				transform: none;
-				background: inherit;
-				color: inherit;
-			}
-
-			.wc-block-product-filters__overlay-content {
-				padding: 0;
-				overflow: visible;
-				flex-grow: 1;
-				background: inherit;
-				color: inherit;
+			> div {
+				margin: 20px 0;
 			}
 		}
 	}

-	@include breakpoint("<600px") {
-		.wc-block-product-filters__content,
-		.wc-block-product-filters__overlay-content {
-			.wp-block-group {
-				display: block;
-
-				> div {
-					margin: 20px 0;
-				}
-			}
-		}
+	&.is-filter-drawer-disabled {
+		display: flex;
 	}
 }

@@ -177,23 +138,3 @@
 .wc-block-product-filter--hidden {
 	display: none;
 }
-
-.wc-block-product-filter__show-more {
-	background: none;
-	border: none;
-	font-family: inherit;
-	margin-top: 0.5em;
-	color: inherit;
-	cursor: pointer;
-	text-decoration: underline;
-	padding: 0;
-
-	&:hover {
-		text-decoration: none;
-	}
-
-	&:focus {
-		outline: 1px dotted currentColor;
-		outline-offset: 2px;
-	}
-}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/types.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/types.ts
index 8bf9e802a2e..3e26e516eea 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/types.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/types.ts
@@ -51,10 +51,13 @@ export type ProductFiltersContext = {
 // ----------------------------------------
 // Block props
 // ----------------------------------------
+export type OverlayMode = 'off' | 'mobile' | 'always';
+
 export type BlockAttributes = {
 	productId?: string;
 	isPreview: boolean;
-	showFilterDrawer?: boolean;
+	overlayMode?: OverlayMode;
+	overlayPosition?: 'left' | 'right';
 };

 export type EditProps = BlockEditProps< BlockAttributes >;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/utils/overlay-mode.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/utils/overlay-mode.ts
new file mode 100644
index 00000000000..1dfbd9ef9c7
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/utils/overlay-mode.ts
@@ -0,0 +1,17 @@
+/**
+ * Internal dependencies
+ */
+import type { BlockAttributes, OverlayMode } from '../types';
+
+const OVERLAY_MODES: readonly OverlayMode[] = [ 'off', 'mobile', 'always' ];
+
+export const isOverlayMode = ( value: unknown ): value is OverlayMode =>
+	OVERLAY_MODES.includes( value as OverlayMode );
+
+export const getOverlayMode = ( attributes: BlockAttributes ): OverlayMode => {
+	if ( isOverlayMode( attributes.overlayMode ) ) {
+		return attributes.overlayMode;
+	}
+
+	return 'mobile';
+};
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php
index da219033cad..5f26a44e39c 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php
@@ -40,6 +40,11 @@ class ProductFilters extends AbstractBlock {
 	protected function enqueue_data( array $attributes = array() ) {
 		parent::enqueue_data( $attributes );

+		wp_add_inline_style(
+			generate_block_asset_handle( $this->get_full_block_name(), 'style' ),
+			$this->get_responsive_styles()
+		);
+
 		if ( is_admin() ) {
 			$this->asset_data_registry->add( 'globalStylesColors', wp_get_global_styles( array( 'color' ) ) );
 		}
@@ -54,6 +59,102 @@ class ProductFilters extends AbstractBlock {
 		}
 	}

+	/**
+	 * Resolve the responsive overlay breakpoint.
+	 *
+	 * @return string Responsive overlay breakpoint.
+	 */
+	private function get_overlay_breakpoint() {
+		if ( function_exists( 'gutenberg_get_global_settings' ) ) {
+			$viewport_settings = gutenberg_get_global_settings( array( 'viewport' ) );
+		} else {
+			$viewport_settings = wp_get_global_settings( array( 'viewport' ) );
+		}
+
+		if ( is_array( $viewport_settings ) ) {
+			foreach ( array( 'tablet', 'mobile' ) as $viewport ) {
+				if ( isset( $viewport_settings[ $viewport ] ) && is_string( $viewport_settings[ $viewport ] ) ) {
+					return $viewport_settings[ $viewport ];
+				}
+			}
+		}
+
+		return '782px';
+	}
+
+	/**
+	 * Generate viewport-aware Product Filters styles.
+	 *
+	 * @return string Responsive CSS.
+	 */
+	private function get_responsive_styles() {
+		$overlay_breakpoint = $this->get_overlay_breakpoint();
+
+		return <<<CSS
+@media (width > {$overlay_breakpoint}) {
+	:where(.wc-block-product-filters).is-mobile-overlay {
+		display: flex;
+	}
+
+	:where(.wc-block-product-filters).is-mobile-overlay .wc-block-product-filters__overlay-header,
+	:where(.wc-block-product-filters).is-mobile-overlay .wc-block-product-filters__overlay-footer,
+	:where(.wc-block-product-filters).is-mobile-overlay .wc-block-product-filters__open-overlay {
+		display: none;
+	}
+
+	:where(.wc-block-product-filters).is-mobile-overlay .wc-block-product-filters__overlay {
+		position: relative;
+		pointer-events: auto;
+		inset: 0;
+		z-index: auto;
+		background: inherit;
+		color: inherit;
+		transition: none;
+		flex-grow: 1;
+	}
+
+	:where(.wc-block-product-filters).is-mobile-overlay .wc-block-product-filters__overlay-wrapper {
+		width: auto;
+		height: auto;
+		background: inherit;
+		color: inherit;
+	}
+
+	:where(.wc-block-product-filters).is-mobile-overlay .wc-block-product-filters__overlay-dialog {
+		position: relative;
+		width: auto;
+		transform: none;
+		transition: none;
+		background: inherit;
+		color: inherit;
+	}
+
+	:where(.wc-block-product-filters).is-mobile-overlay .wc-block-product-filters__overlay-content {
+		padding: 0;
+		overflow: visible;
+		flex-grow: 1;
+		background: inherit;
+		color: inherit;
+	}
+}
+CSS;
+	}
+
+	/**
+	 * Resolve the canonical overlay mode with legacy drawer compatibility.
+	 *
+	 * @param array $attributes Block attributes.
+	 * @return string Overlay mode.
+	 */
+	private function get_overlay_mode( $attributes ) {
+		$overlay_mode = $attributes['overlayMode'] ?? null;
+		if ( in_array( $overlay_mode, array( 'off', 'mobile', 'always' ), true ) ) {
+			return $overlay_mode;
+		}
+
+		return isset( $attributes['showFilterDrawer'] ) && false === $attributes['showFilterDrawer'] ? 'off' : 'mobile';
+	}
+
 	/**
 	 * Include and render the block.
 	 *
@@ -111,11 +212,19 @@ class ProductFilters extends AbstractBlock {
 			'forcePageReload' => isset( $block->context['forcePageReload'] ) ? (bool) $block->context['forcePageReload'] : null,
 		);

-		$show_filter_drawer = ! isset( $attributes['showFilterDrawer'] ) || false !== $attributes['showFilterDrawer'];
-		$wrapper_classes    = array( 'wc-block-product-filters' );
-		if ( ! $show_filter_drawer ) {
+		$overlay_mode     = $this->get_overlay_mode( $attributes );
+		$has_overlay      = 'off' !== $overlay_mode;
+		$overlay_position = isset( $attributes['overlayPosition'] ) && 'right' === $attributes['overlayPosition'] ? 'right' : 'left';
+		$wrapper_classes  = array( 'wc-block-product-filters' );
+		if ( ! $has_overlay ) {
 			$wrapper_classes[] = 'is-filter-drawer-disabled';
 		}
+		if ( 'mobile' === $overlay_mode ) {
+			$wrapper_classes[] = 'is-mobile-overlay';
+		}
+		if ( $has_overlay && 'right' === $overlay_position ) {
+			$wrapper_classes[] = 'is-overlay-right';
+		}

 		$wrapper_attributes = array(
 			'class'                         => implode( ' ', $wrapper_classes ),
@@ -126,13 +235,13 @@ class ProductFilters extends AbstractBlock {
 			'style'                         => $this->get_css_variables( $attributes ),
 		);

-		if ( $show_filter_drawer ) {
+		if ( $has_overlay ) {
 			$wrapper_attributes['data-wp-watch--scrolling']         = 'callbacks.scrollLimit';
 			$wrapper_attributes['data-wp-on--keyup']                = 'actions.closeOverlayOnEscape';
 			$wrapper_attributes['data-wp-class--is-overlay-opened'] = 'context.isOverlayOpened';
 		}

-		// TODO: Remove this conditional once the fix is released in WP. https://github.com/woocommerce/gutenberg/pull/4.
+		// Remove this conditional once the fix is released in WP. https://github.com/woocommerce/gutenberg/pull/4.
 		if ( ! isset( $block->context['productCollectionLocation'] ) ) {
 			$wrapper_attributes['data-wp-router-region'] = $this->generate_navigation_id( $block );
 		}
@@ -140,17 +249,19 @@ class ProductFilters extends AbstractBlock {
 		ob_start();
 		?>
 		<div <?php echo get_block_wrapper_attributes( $wrapper_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
-			<?php if ( $show_filter_drawer ) : ?>
+			<?php if ( $has_overlay ) : ?>
 				<button
 					type="button"
 					class="wc-block-product-filters__open-overlay"
 					data-wp-on--click="actions.openOverlay"
 				>
-					<?php echo $this->get_svg_icon( 'filter-icon-2' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
+					<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+						<path d="M10 17.5H14V16H10V17.5ZM6 6V7.5H18V6H6ZM8 12.5H16V11H8V12.5Z" fill="currentColor"/>
+					</svg>
 					<span><?php echo esc_html__( 'Filter products', 'woocommerce' ); ?></span>
 				</button>
 				<div class="wc-block-product-filters__overlay">
-					<div class="wc-block-product-filters__overlay-wrapper">
+					<div class="wc-block-product-filters__overlay-wrapper" data-wp-on--click="actions.closeOverlayOnBackdrop">
 						<div
 							class="wc-block-product-filters__overlay-dialog"
 							role="dialog"
@@ -161,9 +272,11 @@ class ProductFilters extends AbstractBlock {
 									type="button"
 									class="wc-block-product-filters__close-overlay"
 									data-wp-on--click="actions.closeOverlay"
+									title="<?php echo esc_attr( __( 'Close', 'woocommerce' ) ); ?>"
 								>
-									<span><?php echo esc_html__( 'Close', 'woocommerce' ); ?></span>
-									<?php echo $this->get_svg_icon( 'close' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
+									<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false">
+										<path d="M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"></path>
+									</svg>
 								</button>
 							</header>
 							<div class="wc-block-product-filters__overlay-content">
@@ -194,28 +307,6 @@ class ProductFilters extends AbstractBlock {
 		return ob_get_clean();
 	}

-	/**
-	 * Get SVG icon markup for a given icon name.
-	 *
-	 * @param string $name The name of the icon to retrieve.
-	 * @return string SVG markup for the icon, or empty string if icon not found.
-	 */
-	private function get_svg_icon( string $name ) {
-		$icons = array(
-			'close'         => '<path d="M12 13.0607L15.7123 16.773L16.773 15.7123L13.0607 12L16.773 8.28772L15.7123 7.22706L12 10.9394L8.28771 7.22705L7.22705 8.28771L10.9394 12L7.22706 15.7123L8.28772 16.773L12 13.0607Z" fill="currentColor"/>',
-			'filter-icon-2' => '<path d="M10 17.5H14V16H10V17.5ZM6 6V7.5H18V6H6ZM8 12.5H16V11H8V12.5Z" fill="currentColor"/>',
-		);
-
-		if ( ! isset( $icons[ $name ] ) ) {
-			return '';
-		}
-
-		return sprintf(
-			'<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">%s</svg>',
-			$icons[ $name ]
-		);
-	}
-
 	/**
 	 * Get CSS custom properties from block attributes.
 	 *