From 22b9f6a952a47db0b51c5669c428c9dde3be860a Mon Sep 17 00:00:00 2001 From: Luigi Teschio Date: Thu, 31 Aug 2023 18:15:31 +0200 Subject: [PATCH] Product Gallery Block: add support for the on Sale Badge block (https://github.com/woocommerce/woocommerce-blocks/pull/10764) * Product Gallery: add support for On Sale Badge Block * add align support * Add E2E tests * set margin via Block Styles * disable experimental flag * add next previous block * restore support file * fix TS error * fix layout * change product --- .../product-elements/sale-badge/attributes.ts | 4 + .../product-elements/sale-badge/block.tsx | 5 +- .../product-elements/sale-badge/edit.tsx | 24 +- .../product-elements/sale-badge/index.ts | 1 + .../product-elements/sale-badge/style.scss | 18 +- .../product-elements/sale-badge/support.ts | 2 + .../product-elements/sale-badge/types.ts | 1 + .../assets/js/blocks/product-gallery/edit.tsx | 27 ++ .../edit.tsx | 1 - .../product-gallery-large-image/style.scss | 2 + .../src/BlockTypes/ProductSaleBadge.php | 13 +- ...-template.block_theme.side_effects.spec.ts | 287 ++++++++++++++++++ .../e2e/utils/editor/editor-utils.page.ts | 10 + 13 files changed, 376 insertions(+), 19 deletions(-) create mode 100644 plugins/woocommerce-blocks/tests/e2e/tests/on-sale-badge/on-sale-badge-single-product-template.block_theme.side_effects.spec.ts diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/attributes.ts b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/attributes.ts index d7194c6d347..46b5750999a 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/attributes.ts +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/attributes.ts @@ -12,6 +12,10 @@ export const blockAttributes: BlockAttributes = { type: 'boolean', default: false, }, + isDescendentOfSingleProductTemplate: { + type: 'boolean', + default: false, + }, }; export default blockAttributes; diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/block.tsx b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/block.tsx index 6fbc4107f44..5cd31f51f14 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/block.tsx @@ -26,7 +26,10 @@ export const Block = ( props: Props ): JSX.Element | null => { const { parentClassName } = useInnerBlockLayoutContext(); const { product } = useProductDataContext(); - if ( ! product.id || ! product.on_sale ) { + if ( + ( ! product.id || ! product.on_sale ) && + ! props.isDescendentOfSingleProductTemplate + ) { return null; } diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/edit.tsx b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/edit.tsx index ecfc84200e8..c0947af320e 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/edit.tsx @@ -10,13 +10,8 @@ import { useEffect } from '@wordpress/element'; * Internal dependencies */ import Block from './block'; -import withProductSelector from '../shared/with-product-selector'; -import { - BLOCK_TITLE as label, - BLOCK_ICON as icon, - BLOCK_DESCRIPTION as description, -} from './constants'; import type { BlockAttributes } from './types'; +import { useIsDescendentOfSingleProductTemplate } from '../shared/use-is-descendent-of-single-product-template'; const Edit = ( { attributes, @@ -31,9 +26,20 @@ const Edit = ( { }; const isDescendentOfQueryLoop = Number.isFinite( context.queryId ); + const { isDescendentOfSingleProductTemplate } = + useIsDescendentOfSingleProductTemplate(); + useEffect( - () => setAttributes( { isDescendentOfQueryLoop } ), - [ setAttributes, isDescendentOfQueryLoop ] + () => + setAttributes( { + isDescendentOfQueryLoop, + isDescendentOfSingleProductTemplate, + } ), + [ + setAttributes, + isDescendentOfQueryLoop, + isDescendentOfSingleProductTemplate, + ] ); return ( @@ -43,4 +49,4 @@ const Edit = ( { ); }; -export default withProductSelector( { icon, label, description } )( Edit ); +export default Edit; diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/index.ts b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/index.ts index 8769f7f3c76..63e573f4be8 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/index.ts +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/index.ts @@ -32,6 +32,7 @@ const blockConfig: BlockConfiguration = { 'woocommerce/single-product', 'core/post-template', 'woocommerce/product-template', + 'woocommerce/product-gallery', ], }; diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/style.scss b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/style.scss index 8a8c0bbac0e..092397b3cab 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/style.scss +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/style.scss @@ -1,9 +1,13 @@ +.wp-block-woocommerce-product-sale-badge { + display: flex; + flex-direction: column; +} + .wc-block-components-product-sale-badge { - margin: 0 auto $gap-small; @include font-size(small); padding: em($gap-smallest) em($gap-small); display: inline-block; - width: auto; + width: fit-content; border: 1px solid #43454b; border-radius: 3px; box-sizing: border-box; @@ -15,6 +19,16 @@ z-index: 9; position: static; + &--align-left { + align-self: auto; + } + &--align-center { + align-self: center; + } + &--align-right { + align-self: flex-end; + } + span { color: inherit; background-color: inherit; diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/support.ts b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/support.ts index 8ac34fd7e41..707a76bc8d5 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/support.ts +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/support.ts @@ -7,6 +7,7 @@ import { __experimentalGetSpacingClassesAndStyles } from '@wordpress/block-edito export const supports = { html: false, + align: true, ...( isFeaturePluginBuild() && { color: { gradients: true, @@ -31,6 +32,7 @@ export const supports = { width: true, __experimentalSkipSerialization: true, }, + // @todo: Improve styles support when WordPress 6.4 is released. https://make.wordpress.org/core/2023/07/17/introducing-the-block-selectors-api/ ...( typeof __experimentalGetSpacingClassesAndStyles === 'function' && { spacing: { margin: true, diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/types.ts b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/types.ts index b9b0ecc8afc..3395dfacbfa 100644 --- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/types.ts +++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/sale-badge/types.ts @@ -2,4 +2,5 @@ export interface BlockAttributes { productId: number; align: 'left' | 'center' | 'right'; isDescendentOfQueryLoop?: boolean | undefined; + isDescendentOfSingleProductTemplate?: boolean; } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/edit.tsx index 7577a3113cc..ad8b208726b 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/edit.tsx @@ -36,6 +36,33 @@ const TEMPLATE: InnerBlockTemplate[] = [ [ 'woocommerce/product-gallery-large-image', getInnerBlocksLockAttributes( 'lock' ), + [ + [ + 'woocommerce/product-sale-badge', + { + align: 'right', + style: { + spacing: { + margin: { + top: '4px', + right: '4px', + bottom: '4px', + left: '4px', + }, + }, + }, + }, + ], + [ + 'woocommerce/product-gallery-large-image-next-previous', + { + layout: { + type: 'flex', + verticalAlignment: 'bottom', + }, + }, + ], + ], ], ], ], diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/inner-blocks/product-gallery-large-image-next-previous/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/inner-blocks/product-gallery-large-image-next-previous/edit.tsx index 67fbd976976..b2ab03c08cc 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/inner-blocks/product-gallery-large-image-next-previous/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/inner-blocks/product-gallery-large-image-next-previous/edit.tsx @@ -37,7 +37,6 @@ export const Edit = ( { const blockProps = useBlockProps( { style: { width: '100%', - height: '100%', alignItems: getAlignmentStyle( attributes.layout?.verticalAlignment ), diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/inner-blocks/product-gallery-large-image/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/inner-blocks/product-gallery-large-image/style.scss index 6b6b42cf93a..95bbba97647 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/inner-blocks/product-gallery-large-image/style.scss +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-gallery/inner-blocks/product-gallery-large-image/style.scss @@ -7,6 +7,8 @@ } .wc-block-product-gallery-large-image__inner-blocks { + display: flex; + flex-direction: column; position: absolute; z-index: 1; width: 100%; diff --git a/plugins/woocommerce-blocks/src/BlockTypes/ProductSaleBadge.php b/plugins/woocommerce-blocks/src/BlockTypes/ProductSaleBadge.php index a11cb1bb278..abac56a2094 100644 --- a/plugins/woocommerce-blocks/src/BlockTypes/ProductSaleBadge.php +++ b/plugins/woocommerce-blocks/src/BlockTypes/ProductSaleBadge.php @@ -58,8 +58,10 @@ class ProductSaleBadge extends AbstractBlock { 'margin' => true, 'padding' => true, '__experimentalSkipSerialization' => true, + ), '__experimentalSelector' => '.wc-block-components-product-sale-badge', + ); } @@ -106,16 +108,15 @@ class ProductSaleBadge extends AbstractBlock { $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes ); $classname = isset( $attributes['className'] ) ? $attributes['className'] : ''; - $output = '
'; + $align = isset( $attributes['align'] ) ? $attributes['align'] : ''; + + $output = '
'; + $output .= sprintf( '
', esc_attr( $classes_and_styles['classes'] ), $align, esc_attr( $classes_and_styles['styles'] ) ); $output .= ''; $output .= '' . __( 'Product on sale', 'woo-gutenberg-products-block' ) . ''; - $output .= '
'; + $output .= '
'; return $output; } diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/on-sale-badge/on-sale-badge-single-product-template.block_theme.side_effects.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/on-sale-badge/on-sale-badge-single-product-template.block_theme.side_effects.spec.ts new file mode 100644 index 00000000000..dec336018b8 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e/tests/on-sale-badge/on-sale-badge-single-product-template.block_theme.side_effects.spec.ts @@ -0,0 +1,287 @@ +/** + * External dependencies + */ +import { test, expect } from '@woocommerce/e2e-playwright-utils'; +import { EditorUtils, FrontendUtils } from '@woocommerce/e2e-utils'; + +/** + * Internal dependencies + */ + +const blockData = { + name: 'woocommerce/product-sale-badge', + mainClass: '.wp-block-woocommerce-product-sale-badge', + selectors: { + frontend: { + productSaleBadge: '.wc-block-components-product-sale-badge', + productSaleBadgeContainer: + '.wp-block-woocommerce-product-sale-badge', + }, + editor: { + productSaleBadge: '.wc-block-components-product-sale-badge', + productSaleBadgeContainer: + '.wp-block-woocommerce-product-sale-badge', + }, + }, + // This margin is applied via Block Styles to the product sale badge. It's necessary to take it into account when calculating the position of the badge. https://github.com/woocommerce/woocommerce-blocks/blob/445b9431ccba460f9badd41d52ed991958524e33/assets/js/blocks/product-gallery/edit.tsx/#L44-L53 + margin: 4, + slug: 'single-product', + productPage: '/product/hoodie/', + productPageNotOnSale: '/product/album/', +}; + +const getBoundingClientRect = async ( { + frontendUtils, + editorUtils, + isFrontend, +}: { + frontendUtils: FrontendUtils; + editorUtils: EditorUtils; + isFrontend: boolean; +} ) => { + const page = isFrontend ? frontendUtils.page : editorUtils.editor.canvas; + return { + productSaleBadge: await page + .locator( + blockData.selectors[ isFrontend ? 'frontend' : 'editor' ] + .productSaleBadge + ) + .evaluate( ( el ) => el.getBoundingClientRect() ), + productSaleBadgeContainer: await page + .locator( + blockData.selectors[ isFrontend ? 'frontend' : 'editor' ] + .productSaleBadgeContainer + ) + .evaluate( ( el ) => el.getBoundingClientRect() ), + }; +}; +test.describe( `${ blockData.name }`, () => { + test.describe( `On the Single Product Template`, () => { + test.beforeEach( + async ( { requestUtils, admin, editorUtils, editor } ) => { + await requestUtils.deleteAllTemplates( 'wp_template' ); + await requestUtils.deleteAllTemplates( 'wp_template_part' ); + await admin.visitSiteEditor( { + postId: `woocommerce/woocommerce//${ blockData.slug }`, + postType: 'wp_template', + } ); + await editorUtils.enterEditMode(); + await editor.setContent( '' ); + } + ); + + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllTemplates( 'wp_template' ); + await requestUtils.deleteAllTemplates( 'wp_template_part' ); + } ); + + test( 'should be rendered on the editor side', async ( { + editorUtils, + editor, + } ) => { + await editor.insertBlock( { + name: 'woocommerce/product-gallery', + } ); + + const block = await editorUtils.getBlockByName( blockData.name ); + + await expect( block ).toBeVisible(); + } ); + + test( 'should be rendered on the frontend side', async ( { + frontendUtils, + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'woocommerce/product-gallery', + } ); + await Promise.all( [ + editor.saveSiteEditorEntities(), + page.waitForResponse( ( response ) => + response.url().includes( 'wp-json/wp/v2/templates/' ) + ), + ] ); + + await page.goto( blockData.productPage, { + waitUntil: 'commit', + } ); + + const block = await frontendUtils.getBlockByName( blockData.name ); + + await expect( block ).toBeVisible(); + } ); + + test( `should be not rendered when the product isn't on sale the frontend side`, async ( { + frontendUtils, + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'woocommerce/product-gallery', + } ); + await Promise.all( [ + editor.saveSiteEditorEntities(), + page.waitForResponse( ( response ) => + response.url().includes( 'wp-json/wp/v2/templates/' ) + ), + ] ); + + await page.goto( blockData.productPageNotOnSale, { + waitUntil: 'commit', + } ); + + const block = await frontendUtils.getBlockByName( blockData.name ); + + await expect( block ).toBeHidden(); + } ); + + test( 'should be aligned on the left', async ( { + frontendUtils, + editorUtils, + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'woocommerce/product-gallery', + } ); + + const block = await editorUtils.getBlockByName( blockData.name ); + + await block.click(); + + await editorUtils.setAlignOption( 'Align Left' ); + + const editorBoundingClientRect = await getBoundingClientRect( { + frontendUtils, + editorUtils, + isFrontend: false, + } ); + + await expect( + editorBoundingClientRect.productSaleBadge.x - blockData.margin + ).toEqual( editorBoundingClientRect.productSaleBadgeContainer.x ); + + await Promise.all( [ + editor.saveSiteEditorEntities(), + page.waitForResponse( ( response ) => + response.url().includes( 'wp-json/wp/v2/templates/' ) + ), + ] ); + + await page.goto( blockData.productPage, { + waitUntil: 'commit', + } ); + + const clientBoundingClientRect = await getBoundingClientRect( { + frontendUtils, + editorUtils, + isFrontend: true, + } ); + + await expect( + clientBoundingClientRect.productSaleBadge.x - blockData.margin + ).toEqual( clientBoundingClientRect.productSaleBadgeContainer.x ); + } ); + + test( 'should be aligned on the center', async ( { + frontendUtils, + editorUtils, + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'woocommerce/product-gallery', + } ); + + const block = await editorUtils.getBlockByName( blockData.name ); + + await block.click(); + + await editorUtils.setAlignOption( 'Align Center' ); + + const editorBoundingClientRect = await getBoundingClientRect( { + frontendUtils, + editorUtils, + isFrontend: false, + } ); + + await expect( + editorBoundingClientRect.productSaleBadge.right + ).toBeLessThan( + editorBoundingClientRect.productSaleBadgeContainer.right + ); + + await Promise.all( [ + editor.saveSiteEditorEntities(), + page.waitForResponse( ( response ) => + response.url().includes( 'wp-json/wp/v2/templates/' ) + ), + ] ); + + await page.goto( blockData.productPage, { + waitUntil: 'commit', + } ); + + const clientBoundingClientRect = await getBoundingClientRect( { + frontendUtils, + editorUtils, + isFrontend: true, + } ); + + await expect( + clientBoundingClientRect.productSaleBadge.right + ).toBeLessThan( + clientBoundingClientRect.productSaleBadgeContainer.right + ); + } ); + + test( 'should be aligned on the right by default', async ( { + frontendUtils, + editorUtils, + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'woocommerce/product-gallery', + } ); + + const editorBoundingClientRect = await getBoundingClientRect( { + frontendUtils, + editorUtils, + isFrontend: false, + } ); + + await expect( + editorBoundingClientRect.productSaleBadge.right + + blockData.margin + ).toEqual( + editorBoundingClientRect.productSaleBadgeContainer.right + ); + + await Promise.all( [ + editor.saveSiteEditorEntities(), + page.waitForResponse( ( response ) => + response.url().includes( 'wp-json/wp/v2/templates/' ) + ), + ] ); + + await page.goto( blockData.productPage, { + waitUntil: 'commit', + } ); + + const clientBoundingClientRect = await getBoundingClientRect( { + frontendUtils, + editorUtils, + isFrontend: true, + } ); + + await expect( + clientBoundingClientRect.productSaleBadge.right + + blockData.margin + ).toEqual( + clientBoundingClientRect.productSaleBadgeContainer.right + ); + } ); + } ); +} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/utils/editor/editor-utils.page.ts b/plugins/woocommerce-blocks/tests/e2e/utils/editor/editor-utils.page.ts index 30cc61ffd50..55b907bffca 100644 --- a/plugins/woocommerce-blocks/tests/e2e/utils/editor/editor-utils.page.ts +++ b/plugins/woocommerce-blocks/tests/e2e/utils/editor/editor-utils.page.ts @@ -239,4 +239,14 @@ export class EditorUtils { await this.page.getByText( option ).click(); } + + async setAlignOption( + option: 'Align Left' | 'Align Center' | 'Align Right' | 'None' + ) { + const button = this.page.locator( "button[aria-label='Align']" ); + + await button.click(); + + await this.page.getByText( option ).click(); + } }