diff --git a/plugins/woocommerce-blocks/assets/css/style.scss b/plugins/woocommerce-blocks/assets/css/style.scss index b3003f7bdcf..5fdf23c1f0e 100644 --- a/plugins/woocommerce-blocks/assets/css/style.scss +++ b/plugins/woocommerce-blocks/assets/css/style.scss @@ -1,3 +1,7 @@ +body.wc-modal--open { + overflow: hidden; +} + body.wc-block-product-gallery-modal-open { overflow: hidden; } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/frontend.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/frontend.tsx index 0182c842c02..dab9ef6c6c9 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/frontend.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/frontend.tsx @@ -1,15 +1,40 @@ /** * External dependencies */ -import { store } from '@woocommerce/interactivity'; +import { getContext as getContextFn, store } from '@woocommerce/interactivity'; export interface ProductFiltersContext { - productId: string; + isDialogOpen: boolean; + hasPageWithWordPressAdminBar: boolean; } +const getContext = ( ns?: string ) => + getContextFn< ProductFiltersContext >( ns ); + const productFilters = { - state: {}, - actions: {}, + state: { + 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: {}, }; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/index.tsx index b0ef8827509..b003dabf907 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/index.tsx @@ -9,6 +9,7 @@ import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings'; */ import metadata from './block.json'; import { ProductFiltersBlockSettings } from './settings'; +import './style.scss'; if ( isExperimentalBlocksEnabled() ) { registerBlockType( metadata, ProductFiltersBlockSettings ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/block.json index a7330ff4e6d..f876968991c 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/block.json +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/block.json @@ -40,6 +40,7 @@ } }, "supports": { + "interactivity": true, "align": [ "left", "right", "center"], "inserter": false, "color": { diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/frontend.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/frontend.tsx new file mode 100644 index 00000000000..77411ae98ea --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/frontend.tsx @@ -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; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/inspector-controls.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/inspector-controls.tsx index 7c439cd1ac3..16c7352cfdf 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/inspector-controls.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/inspector-controls.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @wordpress/no-unsafe-wp-apis */ /** * External dependencies */ diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/style.scss index dfc9b703aab..da985d86584 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/style.scss +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/overlay-navigation/style.scss @@ -8,15 +8,18 @@ cursor: pointer; &.alignright { - margin-left: auto; + justify-content: right; } &.alignleft { - margin-left: unset; + justify-content: unset; } &.aligncenter { - margin-left: auto; - margin-right: auto; + justify-content: center; + } + + &.hidden { + display: none; } } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/style.scss new file mode 100644 index 00000000000..5b836c1e93c --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/style.scss @@ -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); + } + } +} diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts index 9601e004dea..a5e02ea94fa 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts @@ -3,6 +3,35 @@ */ 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.beforeEach( async ( { admin, requestUtils } ) => { await requestUtils.activatePlugin( @@ -34,10 +63,255 @@ test.describe( 'Filters Overlay Template Part', () => { .locator( '[data-type="core/template-part"]' ) .filter( { has: editor.canvas.getByLabel( - 'Block: Product Filters (Experimental)' + templatePartData.selectors.editor.blocks.productFilters + .blockLabel ), } ); 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(); + } ); + } ); } ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts index 8f4461180b6..aa019e69ed7 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts @@ -21,6 +21,7 @@ const blockData = { }, slug: 'archive-product', productPage: '/product/hoodie/', + shopPage: '/shop/', }; const test = base.extend< { pageObject: ProductFiltersPage } >( { diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.page.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.page.ts index e688c1b737d..bd7903fdb5c 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.page.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.page.ts @@ -43,4 +43,40 @@ export class ProductFiltersPage { } 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; + } + } } diff --git a/plugins/woocommerce/changelog/50505-feat-49977-add-fullscreen-filters-overlay-dialog b/plugins/woocommerce/changelog/50505-feat-49977-add-fullscreen-filters-overlay-dialog new file mode 100644 index 00000000000..efab38908ab --- /dev/null +++ b/plugins/woocommerce/changelog/50505-feat-49977-add-fullscreen-filters-overlay-dialog @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak +Comment: Add the Fullscreen view to the Product Filters + diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php index 024fb4f7960..9c149ef6b3c 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php @@ -1,6 +1,8 @@ render_template_part( $template_part ); + + $html = strtr( + '', + 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 = ''; + + $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 . + $pos = strrpos( $product_filters_html, '' ); + + 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. */ 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; } } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFiltersOverlayNavigation.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFiltersOverlayNavigation.php index 0eb6cfe11d6..ebfdaba12d5 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFiltersOverlayNavigation.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFiltersOverlayNavigation.php @@ -21,17 +21,6 @@ class ProductFiltersOverlayNavigation extends AbstractBlock { 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. * @@ -46,13 +35,13 @@ class ProductFiltersOverlayNavigation extends AbstractBlock { '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; } - $html_content = strtr( + $html = strtr( '
{{primary_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 ), ) ); - 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; } /**