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
This commit is contained in:
Luigi Teschio 2023-08-31 18:15:31 +02:00 committed by GitHub
parent e9ff849e08
commit 22b9f6a952
13 changed files with 376 additions and 19 deletions

View File

@ -12,6 +12,10 @@ export const blockAttributes: BlockAttributes = {
type: 'boolean',
default: false,
},
isDescendentOfSingleProductTemplate: {
type: 'boolean',
default: false,
},
};
export default blockAttributes;

View File

@ -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;
}

View File

@ -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;

View File

@ -32,6 +32,7 @@ const blockConfig: BlockConfiguration = {
'woocommerce/single-product',
'core/post-template',
'woocommerce/product-template',
'woocommerce/product-gallery',
],
};

View File

@ -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;

View File

@ -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,

View File

@ -2,4 +2,5 @@ export interface BlockAttributes {
productId: number;
align: 'left' | 'center' | 'right';
isDescendentOfQueryLoop?: boolean | undefined;
isDescendentOfSingleProductTemplate?: boolean;
}

View File

@ -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',
},
},
],
],
],
],
],

View File

@ -37,7 +37,6 @@ export const Edit = ( {
const blockProps = useBlockProps( {
style: {
width: '100%',
height: '100%',
alignItems: getAlignmentStyle(
attributes.layout?.verticalAlignment
),

View File

@ -7,6 +7,8 @@
}
.wc-block-product-gallery-large-image__inner-blocks {
display: flex;
flex-direction: column;
position: absolute;
z-index: 1;
width: 100%;

View File

@ -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 = '<div class="wc-block-components-product-sale-badge '
. esc_attr( $classes_and_styles['classes'] ) . ' '
. esc_attr( $classname ) . '" '
. 'style="' . esc_attr( $classes_and_styles['styles'] ) . '"'
. '>';
$align = isset( $attributes['align'] ) ? $attributes['align'] : '';
$output = '<div class="wp-block-woocommerce-product-sale-badge ' . esc_attr( $classname ) . '">';
$output .= sprintf( '<div class="wc-block-components-product-sale-badge %1$s wc-block-components-product-sale-badge--align-%2$s" style="%3$s">', esc_attr( $classes_and_styles['classes'] ), $align, esc_attr( $classes_and_styles['styles'] ) );
$output .= '<span class="wc-block-components-product-sale-badge__text" aria-hidden="true">' . __( 'Sale', 'woo-gutenberg-products-block' ) . '</span>';
$output .= '<span class="screen-reader-text">'
. __( 'Product on sale', 'woo-gutenberg-products-block' )
. '</span>';
$output .= '</div>';
$output .= '</div></div>';
return $output;
}

View File

@ -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
);
} );
} );
} );

View File

@ -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();
}
}