diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/image/block.js b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/image/block.js
index c9a59e8bf7e..ec5835722fa 100644
--- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/image/block.js
+++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/image/block.js
@@ -2,7 +2,8 @@
* External dependencies
*/
import PropTypes from 'prop-types';
-import { useState } from '@wordpress/element';
+import { useState, Fragment } from '@wordpress/element';
+import { __, sprintf } from '@wordpress/i18n';
import classnames from 'classnames';
import { PLACEHOLDER_IMG_SRC } from '@woocommerce/block-settings';
import {
@@ -10,7 +11,6 @@ import {
useProductDataContext,
} from '@woocommerce/shared-context';
import { withProductDataContext } from '@woocommerce/shared-hocs';
-import { isEmpty } from 'lodash';
/**
* Internal dependencies
@@ -29,10 +29,10 @@ import './style.scss';
* @param {string} [props.saleBadgeAlign] How should the sale badge be aligned if displayed.
* @return {*} The component.
*/
-const Block = ( {
+export const Block = ( {
className,
imageSizing = 'full-size',
- productLink = true,
+ productLink: showProductLink = true,
showSaleBadge,
saleBadgeAlign = 'right',
} ) => {
@@ -56,8 +56,19 @@ const Block = ( {
);
}
-
- const image = ! isEmpty( product.images ) ? product.images[ 0 ] : null;
+ const hasProductImages = !! product.images.length;
+ const image = hasProductImages ? product.images[ 0 ] : null;
+ const ParentComponent = showProductLink ? 'a' : Fragment;
+ const anchorLabel = sprintf(
+ /* Translators: %s is referring to the product name */
+ __( 'Link to %s', 'woo-gutenberg-products-block' ),
+ product.name
+ );
+ const anchorProps = {
+ href: product.permalink,
+ rel: 'nofollow',
+ ...( ! hasProductImages && { 'aria-label': anchorLabel } ),
+ };
return (
);
};
@@ -110,28 +105,22 @@ const ImagePlaceholder = () => {
);
};
-const Image = ( { image, onLoad, loaded, showFullSize } ) => {
+const Image = ( { image, onLoad, loaded, showFullSize, fallbackAlt } ) => {
const { thumbnail, src, srcset, sizes, alt } = image || {};
-
- let imageProps = {
- alt,
+ const imageProps = {
+ alt: alt || fallbackAlt,
onLoad,
hidden: ! loaded,
src: thumbnail,
+ ...( showFullSize && { src, srcSet: srcset, sizes } ),
};
- if ( showFullSize ) {
- imageProps = {
- ...imageProps,
- src,
- srcSet: srcset,
- sizes,
- };
- }
return (
<>
- { /* eslint-disable-next-line jsx-a11y/alt-text */ }
-
+ { imageProps.src && (
+ /* eslint-disable-next-line jsx-a11y/alt-text */
+
+ ) }
{ ! loaded && }
>
);
@@ -139,6 +128,7 @@ const Image = ( { image, onLoad, loaded, showFullSize } ) => {
Block.propTypes = {
className: PropTypes.string,
+ fallbackAlt: PropTypes.string,
productLink: PropTypes.bool,
showSaleBadge: PropTypes.bool,
saleBadgeAlign: PropTypes.string,
diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/image/test/block.test.js b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/image/test/block.test.js
new file mode 100644
index 00000000000..5c2415b8e0a
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/image/test/block.test.js
@@ -0,0 +1,139 @@
+/**
+ * External dependencies
+ */
+import { render, fireEvent } from '@testing-library/react';
+import { ProductDataContextProvider } from '@woocommerce/shared-context';
+
+/**
+ * Internal dependencies
+ */
+import { Block } from '../block';
+
+jest.mock( '@woocommerce/block-settings', () => ( {
+ __esModule: true,
+ PLACEHOLDER_IMG_SRC: 'placeholder.jpg',
+} ) );
+
+const productWithoutImages = {
+ name: 'Test product',
+ id: 1,
+ fallbackAlt: 'Test product',
+ permalink: 'http://test.com/product/test-product/',
+ images: [],
+};
+
+const productWithImages = {
+ name: 'Test product',
+ id: 1,
+ fallbackAlt: 'Test product',
+ permalink: 'http://test.com/product/test-product/',
+ images: [
+ {
+ id: 56,
+ src: 'logo-1.jpg',
+ thumbnail: 'logo-1-324x324.jpg',
+ srcset:
+ 'logo-1.jpg 800w, logo-1-300x300.jpg 300w, logo-1-150x150.jpg 150w, logo-1-768x767.jpg 768w, logo-1-324x324.jpg 324w, logo-1-416x415.jpg 416w, logo-1-100x100.jpg 100w',
+ sizes: '(max-width: 800px) 100vw, 800px',
+ name: 'logo-1.jpg',
+ alt: '',
+ },
+ {
+ id: 55,
+ src: 'beanie-with-logo-1.jpg',
+ thumbnail: 'beanie-with-logo-1-324x324.jpg',
+ srcset:
+ 'beanie-with-logo-1.jpg 800w, beanie-with-logo-1-300x300.jpg 300w, beanie-with-logo-1-150x150.jpg 150w, beanie-with-logo-1-768x768.jpg 768w, beanie-with-logo-1-324x324.jpg 324w, beanie-with-logo-1-416x416.jpg 416w, beanie-with-logo-1-100x100.jpg 100w',
+ sizes: '(max-width: 800px) 100vw, 800px',
+ name: 'beanie-with-logo-1.jpg',
+ alt: '',
+ },
+ ],
+};
+
+describe( 'Product Image Block', () => {
+ describe( 'with product link', () => {
+ test( 'should render an anchor with the product image', () => {
+ const component = render(
+
+
+
+ );
+
+ // use testId as alt is added after image is loaded
+ const image = component.getByTestId( 'product-image' );
+ fireEvent.load( image );
+
+ const productImage = component.getByAltText(
+ productWithImages.name
+ );
+ expect( productImage.getAttribute( 'src' ) ).toBe(
+ productWithImages.images[ 0 ].src
+ );
+
+ const anchor = productImage.closest( 'a' );
+ expect( anchor.getAttribute( 'href' ) ).toBe(
+ productWithImages.permalink
+ );
+ } );
+
+ test( 'should render an anchor with the placeholder image', () => {
+ const component = render(
+
+
+
+ );
+
+ const placeholderImage = component.getByAltText( '' );
+ expect( placeholderImage.getAttribute( 'src' ) ).toBe(
+ 'placeholder.jpg'
+ );
+
+ const anchor = placeholderImage.closest( 'a' );
+ expect( anchor.getAttribute( 'href' ) ).toBe(
+ productWithoutImages.permalink
+ );
+ expect( anchor.getAttribute( 'aria-label' ) ).toBe(
+ `Link to ${ productWithoutImages.name }`
+ );
+ } );
+ } );
+
+ describe( 'without product link', () => {
+ test( 'should render the product image without an anchor wrapper', () => {
+ const component = render(
+
+
+
+ );
+ const image = component.getByTestId( 'product-image' );
+ fireEvent.load( image );
+
+ const productImage = component.getByAltText(
+ productWithImages.name
+ );
+ expect( productImage.getAttribute( 'src' ) ).toBe(
+ productWithImages.images[ 0 ].src
+ );
+
+ const anchor = productImage.closest( 'a' );
+ expect( anchor ).toBe( null );
+ } );
+
+ test( 'should render the placeholder image without an anchor wrapper', () => {
+ const component = render(
+
+
+
+ );
+
+ const placeholderImage = component.getByAltText( '' );
+ expect( placeholderImage.getAttribute( 'src' ) ).toBe(
+ 'placeholder.jpg'
+ );
+
+ const anchor = placeholderImage.closest( 'a' );
+ expect( anchor ).toBe( null );
+ } );
+ } );
+} );