From e7af435f088d1c2edbfa0c55458d987b19203c43 Mon Sep 17 00:00:00 2001 From: Raluca Stan Date: Thu, 28 Jan 2021 14:23:20 +0100 Subject: [PATCH] Fix image link in all products block. (https://github.com/woocommerce/woocommerce-blocks/pull/3722) * Fix image link in all products block. - add alt text on product image - prevent the appearance of both product and placeholder image at the same time - make anchor with placeholder image accessible * Add testing for product-elements/image block. --- .../blocks/product-elements/image/block.js | 90 +++++------- .../product-elements/image/test/block.test.js | 139 ++++++++++++++++++ 2 files changed, 179 insertions(+), 50 deletions(-) create mode 100644 plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/image/test/block.test.js 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 (
- { productLink ? ( - - { !! showSaleBadge && ( - - ) } - setImageLoaded( true ) } - loaded={ imageLoaded } - showFullSize={ imageSizing !== 'cropped' } + + { !! showSaleBadge && ( + - - ) : ( - <> - { !! showSaleBadge && ( - - ) } - setImageLoaded( true ) } - loaded={ imageLoaded } - showFullSize={ imageSizing !== 'cropped' } - /> - - ) } + ) } + setImageLoaded( true ) } + loaded={ imageLoaded } + showFullSize={ imageSizing !== 'cropped' } + /> +
); }; @@ -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 ); + } ); + } ); +} );