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.
This commit is contained in:
parent
f2012158f3
commit
e7af435f08
|
@ -2,7 +2,8 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import PropTypes from 'prop-types';
|
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 classnames from 'classnames';
|
||||||
import { PLACEHOLDER_IMG_SRC } from '@woocommerce/block-settings';
|
import { PLACEHOLDER_IMG_SRC } from '@woocommerce/block-settings';
|
||||||
import {
|
import {
|
||||||
|
@ -10,7 +11,6 @@ import {
|
||||||
useProductDataContext,
|
useProductDataContext,
|
||||||
} from '@woocommerce/shared-context';
|
} from '@woocommerce/shared-context';
|
||||||
import { withProductDataContext } from '@woocommerce/shared-hocs';
|
import { withProductDataContext } from '@woocommerce/shared-hocs';
|
||||||
import { isEmpty } from 'lodash';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -29,10 +29,10 @@ import './style.scss';
|
||||||
* @param {string} [props.saleBadgeAlign] How should the sale badge be aligned if displayed.
|
* @param {string} [props.saleBadgeAlign] How should the sale badge be aligned if displayed.
|
||||||
* @return {*} The component.
|
* @return {*} The component.
|
||||||
*/
|
*/
|
||||||
const Block = ( {
|
export const Block = ( {
|
||||||
className,
|
className,
|
||||||
imageSizing = 'full-size',
|
imageSizing = 'full-size',
|
||||||
productLink = true,
|
productLink: showProductLink = true,
|
||||||
showSaleBadge,
|
showSaleBadge,
|
||||||
saleBadgeAlign = 'right',
|
saleBadgeAlign = 'right',
|
||||||
} ) => {
|
} ) => {
|
||||||
|
@ -56,8 +56,19 @@ const Block = ( {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const hasProductImages = !! product.images.length;
|
||||||
const image = ! isEmpty( product.images ) ? product.images[ 0 ] : null;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -69,37 +80,21 @@ const Block = ( {
|
||||||
}
|
}
|
||||||
) }
|
) }
|
||||||
>
|
>
|
||||||
{ productLink ? (
|
<ParentComponent { ...( showProductLink && anchorProps ) }>
|
||||||
<a href={ product.permalink } rel="nofollow">
|
{ !! showSaleBadge && (
|
||||||
{ !! showSaleBadge && (
|
<ProductSaleBadge
|
||||||
<ProductSaleBadge
|
align={ saleBadgeAlign }
|
||||||
align={ saleBadgeAlign }
|
product={ product }
|
||||||
product={ product }
|
|
||||||
/>
|
|
||||||
) }
|
|
||||||
<Image
|
|
||||||
image={ image }
|
|
||||||
onLoad={ () => setImageLoaded( true ) }
|
|
||||||
loaded={ imageLoaded }
|
|
||||||
showFullSize={ imageSizing !== 'cropped' }
|
|
||||||
/>
|
/>
|
||||||
</a>
|
) }
|
||||||
) : (
|
<Image
|
||||||
<>
|
fallbackAlt={ product.name }
|
||||||
{ !! showSaleBadge && (
|
image={ image }
|
||||||
<ProductSaleBadge
|
onLoad={ () => setImageLoaded( true ) }
|
||||||
align={ saleBadgeAlign }
|
loaded={ imageLoaded }
|
||||||
product={ product }
|
showFullSize={ imageSizing !== 'cropped' }
|
||||||
/>
|
/>
|
||||||
) }
|
</ParentComponent>
|
||||||
<Image
|
|
||||||
image={ image }
|
|
||||||
onLoad={ () => setImageLoaded( true ) }
|
|
||||||
loaded={ imageLoaded }
|
|
||||||
showFullSize={ imageSizing !== 'cropped' }
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) }
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 || {};
|
const { thumbnail, src, srcset, sizes, alt } = image || {};
|
||||||
|
const imageProps = {
|
||||||
let imageProps = {
|
alt: alt || fallbackAlt,
|
||||||
alt,
|
|
||||||
onLoad,
|
onLoad,
|
||||||
hidden: ! loaded,
|
hidden: ! loaded,
|
||||||
src: thumbnail,
|
src: thumbnail,
|
||||||
|
...( showFullSize && { src, srcSet: srcset, sizes } ),
|
||||||
};
|
};
|
||||||
if ( showFullSize ) {
|
|
||||||
imageProps = {
|
|
||||||
...imageProps,
|
|
||||||
src,
|
|
||||||
srcSet: srcset,
|
|
||||||
sizes,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ /* eslint-disable-next-line jsx-a11y/alt-text */ }
|
{ imageProps.src && (
|
||||||
<img { ...imageProps } />
|
/* eslint-disable-next-line jsx-a11y/alt-text */
|
||||||
|
<img data-testid="product-image" { ...imageProps } />
|
||||||
|
) }
|
||||||
{ ! loaded && <ImagePlaceholder /> }
|
{ ! loaded && <ImagePlaceholder /> }
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -139,6 +128,7 @@ const Image = ( { image, onLoad, loaded, showFullSize } ) => {
|
||||||
|
|
||||||
Block.propTypes = {
|
Block.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
fallbackAlt: PropTypes.string,
|
||||||
productLink: PropTypes.bool,
|
productLink: PropTypes.bool,
|
||||||
showSaleBadge: PropTypes.bool,
|
showSaleBadge: PropTypes.bool,
|
||||||
saleBadgeAlign: PropTypes.string,
|
saleBadgeAlign: PropTypes.string,
|
||||||
|
|
|
@ -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(
|
||||||
|
<ProductDataContextProvider product={ productWithImages }>
|
||||||
|
<Block productLink />
|
||||||
|
</ProductDataContextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
<ProductDataContextProvider product={ productWithoutImages }>
|
||||||
|
<Block productLink />
|
||||||
|
</ProductDataContextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<ProductDataContextProvider product={ productWithImages }>
|
||||||
|
<Block productLink={ false } />
|
||||||
|
</ProductDataContextProvider>
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
<ProductDataContextProvider product={ productWithoutImages }>
|
||||||
|
<Block productLink={ false } />
|
||||||
|
</ProductDataContextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const placeholderImage = component.getByAltText( '' );
|
||||||
|
expect( placeholderImage.getAttribute( 'src' ) ).toBe(
|
||||||
|
'placeholder.jpg'
|
||||||
|
);
|
||||||
|
|
||||||
|
const anchor = placeholderImage.closest( 'a' );
|
||||||
|
expect( anchor ).toBe( null );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
} );
|
Loading…
Reference in New Issue