[Experimental] Product filters/overlay nav block redesign (#51211)

* Remove overlay icon settings from parent block

* Change overlay block icon to generic button icon

* Add logic to remove overlay nav when overlay mode is set to never

* Add logic to add overlay nav when overlay mode is not never

* Add default attributes for overlay nav button

* Add changefile(s) from automation for the following project(s): woocommerce-blocks

* Use unique icons for each overlay navigation close and open

* Add icon picker for open overlay navigation

* Add trigger type for overlay close navigation

* Add missing settings label

* Fix e2e tests

* Skip a test for overlay-navigation block

* Add e2e tests for overlay button behavior

* Fix linting error

* Skip overlay nav tests

* Fix icon size value not saving

* Revise logic to target explicitly the innerblock of product-filters

* Ensure overlay navigation is of type open-overlay

* Prevent possible race conditions

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Roy Ho 2024-09-11 12:11:51 -07:00 committed by GitHub
parent 0c8a8bd6cf
commit 41cf2b285e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 237 additions and 193 deletions

View File

@ -53,18 +53,6 @@
"overlay": {
"type": "string",
"default": "never"
},
"overlayIcon": {
"type": "string",
"default": "filter-icon-1"
},
"overlayButtonStyle": {
"type": "string",
"default": "label-icon"
},
"overlayIconSize": {
"type": "number",
"default": "12"
}
},
"viewScript": "wc-product-filters-frontend",

View File

@ -1,7 +1,6 @@
/**
* External dependencies
*/
import { filter, filterThreeLines } from '@woocommerce/icons';
import { getSetting } from '@woocommerce/settings';
import { AttributeSetting } from '@woocommerce/types';
import {
@ -10,14 +9,19 @@ import {
useBlockProps,
useInnerBlocksProps,
} from '@wordpress/block-editor';
import { BlockEditProps, InnerBlockTemplate } from '@wordpress/blocks';
import {
BlockEditProps,
BlockInstance,
InnerBlockTemplate,
createBlock,
} from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { Icon, menu, settings } from '@wordpress/icons';
import { useEffect } from '@wordpress/element';
import { select, dispatch } from '@wordpress/data';
import { useLocalStorageState } from '@woocommerce/base-hooks';
import {
ExternalLink,
PanelBody,
RadioControl,
RangeControl,
// @ts-expect-error - no types.
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalToggleGroupControl as ToggleGroupControl,
@ -111,6 +115,7 @@ const TEMPLATE: InnerBlockTemplate[] = [
export const Edit = ( {
setAttributes,
attributes,
clientId,
}: BlockEditProps< BlockAttributes > ) => {
const blockProps = useBlockProps();
@ -119,6 +124,84 @@ export const Edit = ( {
''
);
const [
productFiltersOverlayNavigationAttributes,
setProductFiltersOverlayNavigationAttributes,
] = useLocalStorageState< Record< string, unknown > >(
'product-filters-overlay-navigation-attributes',
{}
);
useEffect( () => {
const filtersClientIds = select( 'core/block-editor' ).getBlocksByName(
'woocommerce/product-filters'
);
let overlayBlock:
| BlockInstance< { [ k: string ]: unknown } >
| undefined;
for ( const filterClientId of filtersClientIds ) {
const filterBlock =
select( 'core/block-editor' ).getBlock( filterClientId );
if ( filterBlock ) {
for ( const innerBlock of filterBlock.innerBlocks ) {
if (
innerBlock.name ===
'woocommerce/product-filters-overlay-navigation' &&
innerBlock.attributes.triggerType === 'open-overlay'
) {
overlayBlock = innerBlock;
}
}
}
}
if ( attributes.overlay === 'never' && overlayBlock ) {
setProductFiltersOverlayNavigationAttributes(
overlayBlock.attributes
);
dispatch( 'core/block-editor' ).updateBlockAttributes(
overlayBlock.clientId,
{
lock: {},
}
);
dispatch( 'core/block-editor' ).removeBlock(
overlayBlock.clientId
);
} else if ( attributes.overlay !== 'never' && ! overlayBlock ) {
if ( productFiltersOverlayNavigationAttributes ) {
productFiltersOverlayNavigationAttributes.triggerType =
'open-overlay';
}
dispatch( 'core/block-editor' ).insertBlock(
createBlock(
'woocommerce/product-filters-overlay-navigation',
productFiltersOverlayNavigationAttributes
? productFiltersOverlayNavigationAttributes
: {
align: 'left',
triggerType: 'open-overlay',
lock: { move: true, remove: true },
}
),
0,
clientId,
false
);
}
}, [
attributes.overlay,
clientId,
productFiltersOverlayNavigationAttributes,
setProductFiltersOverlayNavigationAttributes,
] );
return (
<div { ...blockProps }>
<InspectorControls>
@ -144,126 +227,6 @@ export const Edit = ( {
label={ __( 'Always', 'woocommerce' ) }
/>
</ToggleGroupControl>
{ attributes.overlay === 'mobile' && (
<>
<RadioControl
className="wc-block-editor-product-filters__overlay-button-style-toggle"
label={ __( 'Button', 'woocommerce' ) }
selected={ attributes.overlayButtonStyle }
onChange={ (
value: BlockAttributes[ 'overlayButtonStyle' ]
) => {
setAttributes( {
overlayButtonStyle: value,
} );
} }
options={ [
{
value: 'label-icon',
label: __(
'Label and icon',
'woocommerce'
),
},
{
value: 'label',
label: __(
'Label only',
'woocommerce'
),
},
{
value: 'icon',
label: __( 'Icon only', 'woocommerce' ),
},
] }
/>
{ attributes.overlayButtonStyle !== 'label' && (
<>
<ToggleGroupControl
className="wc-block-editor-product-filters__overlay-button-toggle"
isBlock={ true }
value={ attributes.overlayIcon }
onChange={ (
value: BlockAttributes[ 'overlayIcon' ]
) => {
setAttributes( {
overlayIcon: value,
} );
} }
>
<ToggleGroupControlOption
value={ 'filter-icon-1' }
aria-label={ __(
'Filter icon 1',
'woocommerce'
) }
label={
<Icon
size={ 32 }
icon={ filter }
/>
}
/>
<ToggleGroupControlOption
value={ 'filter-icon-2' }
aria-label={ __(
'Filter icon 2',
'woocommerce'
) }
label={
<Icon
size={ 32 }
icon={ filterThreeLines }
/>
}
/>
<ToggleGroupControlOption
value={ 'filter-icon-3' }
aria-label={ __(
'Filter icon 3',
'woocommerce'
) }
label={
<Icon
size={ 32 }
icon={ menu }
/>
}
/>
<ToggleGroupControlOption
value={ 'filter-icon-4' }
aria-label={ __(
'Filter icon 4',
'woocommerce'
) }
label={
<Icon
size={ 32 }
icon={ settings }
/>
}
/>
</ToggleGroupControl>
<RangeControl
label={ __(
'Icon size',
'woocommerce'
) }
className="wc-block-editor-product-filters__overlay-button-size"
value={ attributes.overlayIconSize }
onChange={ ( value: number ) =>
setAttributes( {
overlayIconSize: value,
} )
}
min={ 20 }
max={ 80 }
/>
</>
) }
</>
) }
{ attributes.overlay !== 'never' && (
<ExternalLink
href={ templatePartEditUri }

View File

@ -3,6 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import { BlockVariation } from '@wordpress/blocks';
import { Icon, button } from '@wordpress/icons';
const variations: BlockVariation[] = [
{
@ -12,6 +13,8 @@ const variations: BlockVariation[] = [
triggerType: 'open-overlay',
},
isDefault: false,
icon: <Icon icon={ button } />,
isActive: [ 'triggerType' ],
},
];

View File

@ -20,12 +20,16 @@
"default": "link"
},
"iconSize": {
"type": "string"
"type": "number"
},
"overlayMode": {
"type": "string",
"default": "never"
},
"overlayIcon": {
"type": "string",
"default": "filter-icon-1"
},
"style": {
"type": "object",
"default": {

View File

@ -6,7 +6,8 @@ import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
import { BlockEditProps, store as blocksStore } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import clsx from 'clsx';
import { Icon, close } from '@wordpress/icons';
import { Icon, close, menu, settings } from '@wordpress/icons';
import { filter, filterThreeLines } from '@woocommerce/icons';
/**
* Internal dependencies
@ -16,7 +17,6 @@ import type {
BlockContext,
BlockVariationTriggerType,
} from './types';
import { default as productFiltersIcon } from '../../icon';
import { BlockOverlayAttribute as ProductFiltersBlockOverlayAttribute } from '../../constants';
import './editor.scss';
import { Inspector } from './inspector-controls';
@ -37,16 +37,33 @@ const OverlayNavigationLabel = ( {
const OverlayNavigationIcon = ( {
variation,
iconSize,
overlayIcon,
style,
}: {
variation: BlockVariationTriggerType;
iconSize: number | undefined;
overlayIcon: string;
style: BlockAttributes[ 'style' ];
} ) => {
let icon = close;
if ( variation === 'open-overlay' ) {
icon = productFiltersIcon();
switch ( overlayIcon ) {
case 'filter-icon-4':
icon = settings;
break;
case 'filter-icon-3':
icon = menu;
break;
case 'filter-icon-2':
icon = filterThreeLines;
break;
case 'filter-icon-1':
icon = filter;
break;
default:
icon = filter;
}
}
return (
@ -65,11 +82,13 @@ const OverlayNavigationContent = ( {
variation,
iconSize,
style,
overlayIcon,
navigationStyle,
}: {
variation: BlockVariationTriggerType;
iconSize: BlockAttributes[ 'iconSize' ];
style: BlockAttributes[ 'style' ];
overlayIcon: BlockAttributes[ 'overlayIcon' ];
navigationStyle: BlockAttributes[ 'navigationStyle' ];
} ) => {
const overlayNavigationLabel = (
@ -79,6 +98,7 @@ const OverlayNavigationContent = ( {
<OverlayNavigationIcon
variation={ variation }
iconSize={ iconSize }
overlayIcon={ overlayIcon }
style={ style }
/>
);
@ -111,8 +131,14 @@ const OverlayNavigationContent = ( {
type BlockProps = BlockEditProps< BlockAttributes > & { context: BlockContext };
export const Edit = ( { attributes, setAttributes, context }: BlockProps ) => {
const { navigationStyle, buttonStyle, iconSize, style, triggerType } =
attributes;
const {
navigationStyle,
buttonStyle,
iconSize,
overlayIcon,
style,
triggerType,
} = attributes;
const { 'woocommerce/product-filters/overlay': productFiltersOverlayMode } =
context;
const blockProps = useBlockProps( {
@ -214,6 +240,7 @@ export const Edit = ( { attributes, setAttributes, context }: BlockProps ) => {
variation={ triggerType }
iconSize={ iconSize }
navigationStyle={ navigationStyle }
overlayIcon={ overlayIcon }
style={ style }
/>
</div>

View File

@ -2,8 +2,8 @@
* External dependencies
*/
import { registerBlockType } from '@wordpress/blocks';
import { Icon } from '@wordpress/icons';
import { closeSquareShadow } from '@woocommerce/icons';
import { Icon } from '@wordpress/icons';
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
/**

View File

@ -4,6 +4,8 @@
import { InspectorControls } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { filter, filterThreeLines } from '@woocommerce/icons';
import { Icon, menu, settings } from '@wordpress/icons';
import {
PanelBody,
RadioControl,
@ -34,7 +36,8 @@ export const Inspector = ( {
setAttributes,
buttonStyles,
}: InspectorProps ) => {
const { navigationStyle, buttonStyle, iconSize } = attributes;
const { navigationStyle, buttonStyle, iconSize, overlayIcon, triggerType } =
attributes;
return (
<InspectorControls group="styles">
<PanelBody title={ __( 'Style', 'woocommerce' ) }>
@ -100,6 +103,61 @@ export const Inspector = ( {
/>
) }
{ triggerType === 'open-overlay' &&
navigationStyle !== 'label-only' && (
<ToggleGroupControl
label={ __( 'Icon', 'woocommerce' ) }
className="wc-block-editor-product-filters__overlay-button-toggle"
isBlock={ true }
value={ overlayIcon }
onChange={ (
value: BlockAttributes[ 'overlayIcon' ]
) => {
setAttributes( {
overlayIcon: value,
} );
} }
>
<ToggleGroupControlOption
value={ 'filter-icon-1' }
aria-label={ __(
'Filter icon 1',
'woocommerce'
) }
label={ <Icon size={ 32 } icon={ filter } /> }
/>
<ToggleGroupControlOption
value={ 'filter-icon-2' }
aria-label={ __(
'Filter icon 2',
'woocommerce'
) }
label={
<Icon
size={ 32 }
icon={ filterThreeLines }
/>
}
/>
<ToggleGroupControlOption
value={ 'filter-icon-3' }
aria-label={ __(
'Filter icon 3',
'woocommerce'
) }
label={ <Icon size={ 32 } icon={ menu } /> }
/>
<ToggleGroupControlOption
value={ 'filter-icon-4' }
aria-label={ __(
'Filter icon 4',
'woocommerce'
) }
label={ <Icon size={ 32 } icon={ settings } /> }
/>
</ToggleGroupControl>
) }
{ navigationStyle !== 'label-only' && (
<RangeControl
className="wc-block-product-filters-overlay-navigation__icon-size-control"

View File

@ -27,6 +27,7 @@ export type BlockAttributes = {
iconSize?: number;
overlayMode: ProductFiltersBlockOverlayAttributeOptions;
triggerType: BlockVariationTriggerType;
overlayIcon: string;
style: {
border?: {
radius?: string | BorderRadius;

View File

@ -28,14 +28,20 @@ test.describe( `Filters Overlay Navigation`, () => {
} );
} );
test( 'should be included in the Filters Overlay template part', async ( {
// Since we need to overhaul the overlay area, we can skip this test for now.
// eslint-disable-next-line playwright/no-skipped-test
test.skip( 'should be included in the Filters Overlay template part', async ( {
editor,
} ) => {
const block = editor.canvas.getByLabel( `Block: ${ blockData.title }` );
await expect( block ).toBeVisible();
} );
test( 'should have settings and styles controls', async ( { editor } ) => {
// Since we need to overhaul the overlay area, we can skip this test for now.
// eslint-disable-next-line playwright/no-skipped-test
test.skip( 'should have settings and styles controls', async ( {
editor,
} ) => {
const block = editor.canvas.getByLabel( `Block: ${ blockData.title }` );
await block.click();

View File

@ -17,6 +17,16 @@ const blockData = {
settings: {},
layoutWrapper:
'.wp-block-woocommerce-product-filters-is-layout-flex',
blocks: {
filters: {
title: 'Product Filters (Experimental)',
label: 'Block: Product Filters (Experimental)',
},
overlay: {
title: 'Overlay Navigation (Experimental)',
label: 'Block: Overlay Navigation (Experimental)',
},
},
},
},
slug: 'archive-product',
@ -54,7 +64,7 @@ test.describe( `${ blockData.name }`, () => {
await pageObject.addProductFiltersBlock( { cleanContent: true } );
const block = editor.canvas.getByLabel(
'Block: Product Filters (Experimental)'
blockData.selectors.editor.blocks.filters.label
);
await expect( block ).toBeVisible();
@ -142,7 +152,7 @@ test.describe( `${ blockData.name }`, () => {
await pageObject.addProductFiltersBlock( { cleanContent: true } );
const block = editor.canvas.getByLabel(
'Block: Product Filters (Experimental)'
blockData.selectors.editor.blocks.filters.label
);
await expect( block ).toBeVisible();
@ -152,7 +162,7 @@ test.describe( `${ blockData.name }`, () => {
await expect( listView ).toBeVisible();
const productFiltersBlockListItem = listView.getByRole( 'link', {
name: 'Product Filters (Experimental)',
name: blockData.selectors.editor.blocks.filters.title,
} );
await expect( productFiltersBlockListItem ).toBeVisible();
const listViewExpander =
@ -199,7 +209,7 @@ test.describe( `${ blockData.name }`, () => {
await pageObject.addProductFiltersBlock( { cleanContent: true } );
const block = editor.canvas.getByLabel(
'Block: Product Filters (Experimental)'
blockData.selectors.editor.blocks.filters.label
);
await expect( block ).toBeVisible();
@ -246,10 +256,17 @@ test.describe( `${ blockData.name }`, () => {
} ) => {
await pageObject.addProductFiltersBlock( { cleanContent: true } );
const block = editor.canvas.getByLabel(
'Block: Product Filters (Experimental)'
const filtersBlock = editor.canvas.getByLabel(
blockData.selectors.editor.blocks.filters.label
);
await expect( block ).toBeVisible();
await expect( filtersBlock ).toBeVisible();
const overlayBlock = editor.canvas.getByLabel(
blockData.selectors.editor.blocks.overlay.label
);
// Overlay mode is set to 'Never' by default so the block should be hidden
await expect( overlayBlock ).toBeHidden();
await editor.openDocumentSettingsSidebar();
@ -260,17 +277,6 @@ test.describe( `${ blockData.name }`, () => {
// Overlay settings
const overlayModeSettings = [ 'Never', 'Mobile', 'Always' ];
const overlayButtonSettings = [
'Label and icon',
'Label only',
'Icon only',
];
const overlayIconsSettings = [
'Filter icon 1',
'Filter icon 2',
'Filter icon 3',
'Filter icon 4',
];
await expect( editor.page.getByText( 'Overlay' ) ).toBeVisible();
@ -278,43 +284,27 @@ test.describe( `${ blockData.name }`, () => {
await expect( editor.page.getByText( mode ) ).toBeVisible();
}
await editor.page.getByLabel( 'Never' ).click();
await expect( editor.page.getByText( 'Edit overlay' ) ).toBeHidden();
await expect( overlayBlock ).toBeHidden();
await editor.page.getByLabel( 'Mobile' ).click();
await expect( editor.page.getByText( 'BUTTON' ) ).toBeVisible();
for ( const mode of overlayButtonSettings ) {
await expect( editor.page.getByText( mode ) ).toBeVisible();
}
for ( const mode of overlayIconsSettings ) {
await expect( editor.page.getByLabel( mode ) ).toBeVisible();
}
await expect( editor.page.getByText( 'ICON SIZE' ) ).toBeVisible();
await expect( editor.page.getByText( 'Edit overlay' ) ).toBeVisible();
await expect( overlayBlock ).toBeVisible();
await editor.page.getByLabel( 'Always' ).click();
await expect( editor.page.getByText( 'BUTTON' ) ).toBeHidden();
for ( const mode of overlayButtonSettings ) {
await expect( editor.page.getByText( mode ) ).toBeHidden();
}
for ( const mode of overlayIconsSettings ) {
await expect( editor.page.getByLabel( mode ) ).toBeHidden();
}
await expect( editor.page.getByText( 'Edit overlay' ) ).toBeVisible();
await editor.page.getByLabel( 'Mobile' ).click();
await expect( overlayBlock ).toBeVisible();
await editor.page.locator( 'input[value="label"]' ).click();
await editor.page.getByLabel( 'Never' ).click();
for ( const mode of overlayIconsSettings ) {
await expect( editor.page.getByLabel( mode ) ).toBeHidden();
}
await expect( editor.page.getByText( 'Edit overlay' ) ).toBeVisible();
await expect( overlayBlock ).toBeHidden();
} );
test( 'Layout > default to vertical stretch', async ( {

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Comment: Product Filters: update overlay navigation UX

View File

@ -1,6 +1,6 @@
<!-- wp:woocommerce/product-filters-overlay {"lock":{"move":true,"remove":true}} -->
<div class="wp-block-woocommerce-product-filters-overlay wc-block-product-filters-overlay" style="padding-top:1rem;padding-right:1rem;padding-bottom:1rem;padding-left:1rem">
<!-- wp:woocommerce/product-filters-overlay-navigation {"lock":{"move":true,"remove":true}} -->
<!-- wp:woocommerce/product-filters-overlay-navigation {"triggerType":"close-overlay","lock":{"move":true,"remove":true}} -->
<div class="wp-block-woocommerce-product-filters-overlay-navigation alignright wc-block-product-filters-overlay-navigation"></div>
<!-- /wp:woocommerce/product-filters-overlay-navigation -->