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.
*