[Experimental] Product Filters Redesign > Overlay: Add Fullscreen view (#50505)

* Add variation to Product Filters Overlay Navigation

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

* Move Product Filters Overlay Navigation to correct position

* Hide block when it is outside the Product Filters template part

* Display Navigation block in the frontend

* Show the Product Filters Overlay Navigation on the frontend

* Add logic to hide Product Filters Overlay Navigation block on the frontend

* Hide block on the Overlay template part

* Fix eslint errors

* Update the block variation title

* Remove the `isActive` property from the block variations

* Use Product Filters block context

* Replace enum with const

* Remove unnecessary `StyleAttributesUtils`

* Rename context key

* Move BlockOverlayAttribute to the constants.ts file

* fix BlockOverlayAttribute import

* Fix import error

* Improve code for the shouldHideBlock method

* Remove unnecessary attributes property

* Fix error in ProductFiltersOverlay block

* Add dialog to the Product Filters block

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

* Fix interactivity api error

* Prevent block from being hidden on Product Filters template part

* Fix inspector controls when block is hidden

* Add clickable action to the Product Filters Overlay Navigation block

* Fix interactivity directives that were not working for the Overlay

* Fix issue with dialog styles not being correctly applied

* Add the `closeDialog` functionality

* Parse and render blocks for the Product Filters overlay

* Fix padding

* Fix style for Product Filters Overlay navigation block

* Add e2e test

* Add e2e test to Product Filters Overlay template part

* Fix e2e test

* Fix issue causing the trigger button to show even though the overlay mode is set to 'Never"

* Fix issue causing close button to not be displayed in the dialog

* Add e2e tests

* Fix issue that was preventing users from scrolling down the dialog content

* Remove text duplication in e2e tests

* Remove unnecessary imports

* Fix php cs errors

* Fix php cs error

* Revert changes on Product Gallery modal styles

* Fix lint errors

* fix php cs lint errors

* fix php cs error

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Alexandre Lara 2024-09-11 11:30:35 -03:00 committed by GitHub
parent 96497814e4
commit 0b16cfa06a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 521 additions and 31 deletions

View File

@ -1,3 +1,7 @@
body.wc-modal--open {
overflow: hidden;
}
body.wc-block-product-gallery-modal-open { body.wc-block-product-gallery-modal-open {
overflow: hidden; overflow: hidden;
} }

View File

@ -1,15 +1,40 @@
/** /**
* External dependencies * External dependencies
*/ */
import { store } from '@woocommerce/interactivity'; import { getContext as getContextFn, store } from '@woocommerce/interactivity';
export interface ProductFiltersContext { export interface ProductFiltersContext {
productId: string; isDialogOpen: boolean;
hasPageWithWordPressAdminBar: boolean;
} }
const getContext = ( ns?: string ) =>
getContextFn< ProductFiltersContext >( ns );
const productFilters = { const productFilters = {
state: {}, state: {
actions: {}, isDialogOpen: () => {
const context = getContext();
return context.isDialogOpen;
},
},
actions: {
openDialog: () => {
const context = getContext();
document.body.classList.add( 'wc-modal--open' );
context.hasPageWithWordPressAdminBar = Boolean(
document.getElementById( 'wpadminbar' )
);
context.isDialogOpen = true;
},
closeDialog: () => {
const context = getContext();
document.body.classList.remove( 'wc-modal--open' );
context.isDialogOpen = false;
},
},
callbacks: {}, callbacks: {},
}; };

View File

@ -9,6 +9,7 @@ import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
*/ */
import metadata from './block.json'; import metadata from './block.json';
import { ProductFiltersBlockSettings } from './settings'; import { ProductFiltersBlockSettings } from './settings';
import './style.scss';
if ( isExperimentalBlocksEnabled() ) { if ( isExperimentalBlocksEnabled() ) {
registerBlockType( metadata, ProductFiltersBlockSettings ); registerBlockType( metadata, ProductFiltersBlockSettings );

View File

@ -40,6 +40,7 @@
} }
}, },
"supports": { "supports": {
"interactivity": true,
"align": [ "left", "right", "center"], "align": [ "left", "right", "center"],
"inserter": false, "inserter": false,
"color": { "color": {

View File

@ -0,0 +1,19 @@
/**
* External dependencies
*/
import { store } from '@woocommerce/interactivity';
export interface ProductFiltersContext {
isDialogOpen: boolean;
}
const productFiltersOverlayNavigation = {
state: {},
actions: {},
callbacks: {},
};
store( 'woocommerce/product-filters', productFiltersOverlayNavigation );
export type ProductFiltersOverlayNavigation =
typeof productFiltersOverlayNavigation;

View File

@ -1,4 +1,3 @@
/* eslint-disable @wordpress/no-unsafe-wp-apis */
/** /**
* External dependencies * External dependencies
*/ */

View File

@ -8,15 +8,18 @@
cursor: pointer; cursor: pointer;
&.alignright { &.alignright {
margin-left: auto; justify-content: right;
} }
&.alignleft { &.alignleft {
margin-left: unset; justify-content: unset;
} }
&.aligncenter { &.aligncenter {
margin-left: auto; justify-content: center;
margin-right: auto; }
&.hidden {
display: none;
} }
} }

View File

@ -0,0 +1,22 @@
.wc-block-product-filters {
dialog {
flex-direction: column;
position: fixed;
border: none;
top: 0;
z-index: 9999;
height: 100vh;
width: 100vw;
&.wc-block-product-filters--dialog-open {
display: flex;
padding-left: 0;
overflow-y: auto;
}
&.wc-block-product-filters--with-admin-bar {
margin-top: $gap;
height: calc(100vh - 2 * $gap);
}
}
}

View File

@ -3,6 +3,35 @@
*/ */
import { test, expect } from '@woocommerce/e2e-utils'; import { test, expect } from '@woocommerce/e2e-utils';
const templatePartData = {
selectors: {
frontend: {},
editor: {
blocks: {
activeFilters: {
title: 'Active (Experimental)',
blockLabel: 'Block: Active (Experimental)',
},
productFilters: {
title: 'Product Filters (Experimental)',
blockLabel: 'Block: Product Filters (Experimental)',
},
filterOptions: {
title: 'Filter Options',
blockLabel: 'Block: Filter Options',
},
productFiltersOverlayNavigation: {
title: 'Overlay Navigation (Experimental)',
name: 'woocommerce/product-filters-overlay-navigation',
blockLabel: 'Block: Overlay Navigation (Experimental)',
},
},
},
},
slug: 'product-filters',
productPage: '/product/hoodie/',
};
test.describe( 'Filters Overlay Template Part', () => { test.describe( 'Filters Overlay Template Part', () => {
test.beforeEach( async ( { admin, requestUtils } ) => { test.beforeEach( async ( { admin, requestUtils } ) => {
await requestUtils.activatePlugin( await requestUtils.activatePlugin(
@ -34,10 +63,255 @@ test.describe( 'Filters Overlay Template Part', () => {
.locator( '[data-type="core/template-part"]' ) .locator( '[data-type="core/template-part"]' )
.filter( { .filter( {
has: editor.canvas.getByLabel( has: editor.canvas.getByLabel(
'Block: Product Filters (Experimental)' templatePartData.selectors.editor.blocks.productFilters
.blockLabel
), ),
} ); } );
await expect( productFiltersTemplatePart ).toBeVisible(); await expect( productFiltersTemplatePart ).toBeVisible();
} ); } );
test.describe( 'frontend', () => {
test.beforeEach( async ( { admin } ) => {
await admin.visitSiteEditor( {
postId: `woocommerce/woocommerce//archive-product`,
postType: 'wp_template',
canvas: 'edit',
} );
} );
test( 'should open and close the dialog when clicking on the Product Filters Overlay Navigation block', async ( {
editor,
page,
frontendUtils,
} ) => {
await editor.setContent( '' );
await editor.openGlobalBlockInserter();
await page
.getByText(
templatePartData.selectors.editor.blocks.productFilters
.title
)
.click();
const block = editor.canvas.getByLabel(
templatePartData.selectors.editor.blocks.productFilters
.blockLabel
);
await expect( block ).toBeVisible();
// This forces the list view to show the inner blocks of the Product Filters template part.
await editor.canvas
.getByLabel(
templatePartData.selectors.editor.blocks.activeFilters
.blockLabel
)
.getByLabel(
templatePartData.selectors.editor.blocks.filterOptions
.blockLabel
)
.click();
await editor.openDocumentSettingsSidebar();
await page.getByLabel( 'Document Overview' ).click();
await page
.getByRole( 'link', {
name: templatePartData.selectors.editor.blocks
.productFilters.title,
} )
.nth( 1 )
.click();
const layoutSettings = editor.page.getByText(
'OverlayNeverMobileAlways'
);
await layoutSettings.getByLabel( 'Always' ).click();
await editor.page
.getByRole( 'link', {
name: templatePartData.selectors.editor.blocks
.productFiltersOverlayNavigation.title,
} )
.click();
await editor.saveSiteEditorEntities( {
isOnlyCurrentEntityDirty: false,
} );
await page.goto( '/shop/' );
const productFiltersOverlayNavigation = (
await frontendUtils.getBlockByName(
templatePartData.selectors.editor.blocks
.productFiltersOverlayNavigation.name
)
).filter( {
has: page.locator( ':visible' ),
} );
await expect( productFiltersOverlayNavigation ).toBeVisible();
await page
.locator( '.wc-block-product-filters-overlay-navigation' )
.first()
.click();
const productFiltersDialog = page.locator(
'.wc-block-product-filters--dialog-open'
);
await expect( productFiltersDialog ).toBeVisible();
const productFiltersDialogCloseButton = (
await frontendUtils.getBlockByName(
templatePartData.selectors.editor.blocks
.productFiltersOverlayNavigation.name
)
).filter( { hasText: 'Close' } );
await expect( productFiltersDialogCloseButton ).toBeVisible();
await productFiltersDialogCloseButton.click();
await expect( productFiltersDialog ).toBeHidden();
} );
test( 'should hide Product Filters Overlay Navigation block when the Overlay mode is set to `Never`', async ( {
editor,
page,
frontendUtils,
} ) => {
await editor.setContent( '' );
await editor.openGlobalBlockInserter();
await page
.getByText(
templatePartData.selectors.editor.blocks.productFilters
.title
)
.click();
const block = editor.canvas.getByLabel(
templatePartData.selectors.editor.blocks.productFilters
.blockLabel
);
await expect( block ).toBeVisible();
// This forces the list view to show the inner blocks of the Product Filters template part.
await editor.canvas
.getByLabel(
templatePartData.selectors.editor.blocks.activeFilters
.blockLabel
)
.getByLabel(
templatePartData.selectors.editor.blocks.filterOptions
.blockLabel
)
.click();
await editor.openDocumentSettingsSidebar();
await page.getByLabel( 'Document Overview' ).click();
await page
.getByRole( 'link', {
name: templatePartData.selectors.editor.blocks
.productFilters.title,
} )
.nth( 1 )
.click();
const layoutSettings = editor.page.getByText(
'OverlayNeverMobileAlways'
);
await layoutSettings.getByLabel( 'Never' ).click();
await editor.page
.getByRole( 'link', {
name: templatePartData.selectors.editor.blocks
.productFiltersOverlayNavigation.title,
} )
.click();
await editor.saveSiteEditorEntities( {
isOnlyCurrentEntityDirty: true,
} );
await page.goto( '/shop/' );
const productFiltersOverlayNavigation = (
await frontendUtils.getBlockByName(
templatePartData.selectors.editor.blocks
.productFiltersOverlayNavigation.name
)
).filter( {
has: page.locator( ':visible' ),
} );
await expect( productFiltersOverlayNavigation ).toBeHidden();
} );
test( 'should hide Product Filters Overlay Navigation block when the Overlay mode is set to `Mobile` and user is on desktop', async ( {
editor,
page,
frontendUtils,
} ) => {
await editor.setContent( '' );
await editor.openGlobalBlockInserter();
await page
.getByText(
templatePartData.selectors.editor.blocks.productFilters
.title
)
.click();
const block = editor.canvas.getByLabel(
templatePartData.selectors.editor.blocks.productFilters
.blockLabel
);
await expect( block ).toBeVisible();
// This forces the list view to show the inner blocks of the Product Filters template part.
await editor.canvas
.getByLabel(
templatePartData.selectors.editor.blocks.activeFilters
.blockLabel
)
.getByLabel(
templatePartData.selectors.editor.blocks.filterOptions
.blockLabel
)
.click();
await editor.openDocumentSettingsSidebar();
await page.getByLabel( 'Document Overview' ).click();
await page
.getByRole( 'link', {
name: templatePartData.selectors.editor.blocks
.productFilters.title,
} )
.nth( 1 )
.click();
const layoutSettings = editor.page.getByText(
'OverlayNeverMobileAlways'
);
await layoutSettings.getByLabel( 'Mobile' ).click();
await editor.page
.getByRole( 'link', {
name: templatePartData.selectors.editor.blocks
.productFiltersOverlayNavigation.title,
} )
.click();
await editor.saveSiteEditorEntities( {
isOnlyCurrentEntityDirty: false,
} );
await page.goto( '/shop/' );
const productFiltersOverlayNavigation = (
await frontendUtils.getBlockByName(
templatePartData.selectors.editor.blocks
.productFiltersOverlayNavigation.name
)
).filter( {
has: page.locator( ':visible' ),
} );
await expect( productFiltersOverlayNavigation ).toBeHidden();
} );
} );
} ); } );

View File

@ -21,6 +21,7 @@ const blockData = {
}, },
slug: 'archive-product', slug: 'archive-product',
productPage: '/product/hoodie/', productPage: '/product/hoodie/',
shopPage: '/shop/',
}; };
const test = base.extend< { pageObject: ProductFiltersPage } >( { const test = base.extend< { pageObject: ProductFiltersPage } >( {

View File

@ -43,4 +43,40 @@ export class ProductFiltersPage {
} }
return this.editor.getBlockByName( blockName ); return this.editor.getBlockByName( blockName );
} }
async getProductFiltersOverlayNavigationBlock( {
page,
}: {
page: 'frontend' | 'editor';
} ) {
const blockName = 'woocommerce/product-filters-overlay-navigation';
if ( page === 'frontend' ) {
return (
await this.frontendUtils.getBlockByName( blockName )
).filter( {
has: this.page.locator( ':visible' ),
} );
}
return this.editor.canvas.getByLabel(
'Block: Overlay Navigation (Experimental)'
);
}
async selectOverlayMode( {
mode,
}: {
mode: 'mobile' | 'always' | 'never';
} ) {
switch ( mode ) {
case 'always':
await this.page.getByLabel( 'Always' ).click();
break;
case 'mobile':
await this.page.getByLabel( 'Mobile' ).click();
break;
case 'never':
await this.page.getByLabel( 'Never' ).click();
break;
}
}
} }

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Comment: Add the Fullscreen view to the Product Filters

View File

@ -1,6 +1,8 @@
<?php <?php
namespace Automattic\WooCommerce\Blocks\BlockTypes; namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/** /**
* ProductFilters class. * ProductFilters class.
*/ */
@ -18,16 +20,91 @@ class ProductFilters extends AbstractBlock {
* @return string[] * @return string[]
*/ */
protected function get_block_type_uses_context() { protected function get_block_type_uses_context() {
return [ 'postId' ]; return array( 'postId' );
} }
/** /**
* Get the frontend style handle for this block type. * Return the dialog content.
* *
* @return null * @return string
*/ */
protected function get_block_type_style() { protected function render_dialog() {
return null; $template_part = BlockTemplateUtils::get_template_part( 'product-filters-overlay' );
$html = $this->render_template_part( $template_part );
$html = strtr(
'<dialog hidden role="dialog" aria-modal="true">
{{html}}
</dialog>',
array(
'{{html}}' => $html,
)
);
$p = new \WP_HTML_Tag_Processor( $html );
if ( $p->next_tag() ) {
$p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-filters' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
$p->set_attribute( 'data-wc-bind--hidden', '!state.isDialogOpen' );
$p->set_attribute( 'data-wc-class--wc-block-product-filters--dialog-open', 'state.isDialogOpen' );
$p->set_attribute( 'data-wc-class--wc-block-product-filters--with-admin-bar', 'context.hasPageWithWordPressAdminBar' );
$html = $p->get_updated_html();
}
return $html;
}
/**
* This method is used to render the template part. For each template part, we parse the blocks and render them.
*
* @param string $template_part The template part to render.
* @return string The rendered template part.
*/
protected function render_template_part( $template_part ) {
$parsed_blocks = parse_blocks( $template_part );
$wrapper_template_part_block = $parsed_blocks[0];
$html = $wrapper_template_part_block['innerHTML'];
$target_div = '</div>';
$template_part_content_html = array_reduce(
$wrapper_template_part_block['innerBlocks'],
function ( $carry, $item ) {
if ( 'core/template-part' === $item['blockName'] ) {
$inner_template_part = BlockTemplateUtils::get_template_part( $item['attrs']['slug'] );
$inner_template_part_content_html = $this->render_template_part( $inner_template_part );
return $carry . $inner_template_part_content_html;
}
return $carry . render_block( $item );
},
''
);
$html = str_replace( $target_div, $template_part_content_html . $target_div, $html );
return $html;
}
/**
* Inject dialog into the product filters HTML.
*
* @param string $product_filters_html The Product Filters HTML.
* @param string $dialog_html The dialog HTML.
*
* @return string
*/
protected function inject_dialog( $product_filters_html, $dialog_html ) {
// Find the position of the last </div>.
$pos = strrpos( $product_filters_html, '</div>' );
if ( $pos ) {
// Inject the dialog_html at the correct position.
$html = substr_replace( $product_filters_html, $dialog_html, $pos, 0 );
return $html;
}
return $product_filters_html;
} }
/** /**
@ -39,6 +116,28 @@ class ProductFilters extends AbstractBlock {
* @return string Rendered block type output. * @return string Rendered block type output.
*/ */
protected function render( $attributes, $content, $block ) { protected function render( $attributes, $content, $block ) {
return $content; $html = $content;
$p = new \WP_HTML_Tag_Processor( $html );
if ( $p->next_tag() ) {
$p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-filters' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
$p->set_attribute(
'data-wc-context',
wp_json_encode(
array(
'isDialogOpen' => false,
'hasPageWithWordPressAdminBar' => false,
),
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
)
);
$html = $p->get_updated_html();
}
$dialog_html = $this->render_dialog();
$html = $this->inject_dialog( $html, $dialog_html );
return $html;
} }
} }

View File

@ -21,17 +21,6 @@ class ProductFiltersOverlayNavigation extends AbstractBlock {
return [ 'woocommerce/product-filters/overlay' ]; return [ 'woocommerce/product-filters/overlay' ];
} }
/**
* Get the frontend script handle for this block type.
*
* @see $this->register_block_type()
* @param string $key Data to get, or default to everything.
* @return array|string|null
*/
protected function get_block_type_script( $key = null ) {
return null;
}
/** /**
* Include and render the block. * Include and render the block.
* *
@ -46,13 +35,13 @@ class ProductFiltersOverlayNavigation extends AbstractBlock {
'class' => 'wc-block-product-filters-overlay-navigation', 'class' => 'wc-block-product-filters-overlay-navigation',
) )
); );
$overlay_mode = $block->context['woocommerce/product-filters/overlay']; $overlay_mode = isset( $block->context['woocommerce/product-filters/overlay'] ) ? $block->context['woocommerce/product-filters/overlay'] : 'never';
if ( 'never' === $overlay_mode || ( ! wp_is_mobile() && 'mobile' === $overlay_mode ) ) { if ( 'open-overlay' === $attributes['triggerType'] && ( 'never' === $overlay_mode || ( ! wp_is_mobile() && 'mobile' === $overlay_mode ) ) ) {
return null; return null;
} }
$html_content = strtr( $html = strtr(
'<div {{wrapper_attributes}}> '<div {{wrapper_attributes}}>
{{primary_content}} {{primary_content}}
{{secondary_content}} {{secondary_content}}
@ -63,7 +52,20 @@ class ProductFiltersOverlayNavigation extends AbstractBlock {
'{{secondary_content}}' => 'open-overlay' === $attributes['triggerType'] ? $this->render_label( $attributes ) : $this->render_icon( $attributes ), '{{secondary_content}}' => 'open-overlay' === $attributes['triggerType'] ? $this->render_label( $attributes ) : $this->render_icon( $attributes ),
) )
); );
return $html_content;
$p = new \WP_HTML_Tag_Processor( $html );
if ( $p->next_tag() ) {
$p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-filters' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
$p->set_attribute(
'data-wc-on--click',
'open-overlay' === $attributes['triggerType'] ? 'actions.openDialog' : 'actions.closeDialog'
);
$p->set_attribute( 'data-wc-class--hidden', 'open-overlay' === $attributes['triggerType'] ? 'state.isDialogOpen' : '!state.isDialogOpen' );
$html = $p->get_updated_html();
}
return $html;
} }
/** /**