Implement `Fixed image` and `Repeated image` media controls for the `Featured Product` (https://github.com/woocommerce/woocommerce-blocks/pull/6344)

* Add `Fixed` and `Repeated` background controls to `Featured Product`

* Add `Repeated background` feature when the toggle is activated

* Extract `get_image_url` function

* Add the styles for rendering the repeated image on the frontend

* Add `hasParallax` and `isRepeated` to the `block.json` file

* Adjust styles

* Remove unused function, improve phpdoc

* Use alt and product name

Fix error rebasing master

* Hide alt if isRepeat is true

When isRepeated is true, the image is a background so it does not
make sense to have an alt attribute.

* Add `Fixed image` behaviour

* Remove unnecessary single quotes

* Remove duplicated const due to rebasing

* Fix focal point getting lost after enabling Fixed bg

* Fix duotone for fixed and repeated images

* Fix duotone for fixed and repeated images on the front end

* Don't allow alt if the image is a bg not an img element
This commit is contained in:
Alba Rincón 2022-05-18 16:08:32 +02:00 committed by GitHub
parent 89a553a8a7
commit 22e6394d47
8 changed files with 307 additions and 102 deletions

View File

@ -52,9 +52,9 @@ import {
dimRatioToClass,
getCategoryImageId,
getCategoryImageSrc,
calculateBackgroundImagePosition,
} from './utils';
import { withCategory } from '../../hocs';
import { calculateBackgroundImagePosition } from '../featured-product/utils';
import { ConstrainedResizable } from '../featured-product/block';
const DEFAULT_EDITOR_SIZE = {

View File

@ -60,3 +60,14 @@ export {
getBackgroundImageStyles,
dimRatioToClass,
};
export function calculateBackgroundImagePosition( coords ) {
if ( ! coords ) return {};
const x = Math.round( coords.x * 100 );
const y = Math.round( coords.y * 100 );
return {
objectPosition: `${ x }% ${ y }%`,
};
}

View File

@ -50,7 +50,11 @@ import { crop, Icon, starEmpty } from '@wordpress/icons';
/**
* Internal dependencies
*/
import { calculateBackgroundImagePosition, dimRatioToClass } from './utils';
import {
backgroundImageStyles,
calculateImagePosition,
dimRatioToClass,
} from './utils';
import {
getImageSrcFromProduct,
getImageIdFromProduct,
@ -265,11 +269,17 @@ const FeaturedProduct = ( {
const getInspectorControls = () => {
const url = attributes.mediaSrc || getImageSrcFromProduct( product );
const { focalPoint = { x: 0.5, y: 0.5 } } = attributes;
const {
focalPoint = { x: 0.5, y: 0.5 },
hasParallax,
isRepeated,
} = attributes;
// FocalPointPicker was introduced in Gutenberg 5.0 (WordPress 5.2),
// so we need to check if it exists before using it.
const focalPointPickerExists = typeof FocalPointPicker === 'function';
const isImgElement = ! isRepeated && ! hasParallax;
return (
<>
<InspectorControls key="inspector">
@ -313,50 +323,76 @@ const FeaturedProduct = ( {
'woo-gutenberg-products-block'
) }
>
<ToggleGroupControl
help={
<>
<p>
{ __(
'Choose “Cover” if you want the image to scale automatically to always fit its container.',
'woo-gutenberg-products-block'
) }
</p>
<p>
{ __(
'Note: by choosing “Cover” you will lose the ability to freely move the focal point precisely.',
'woo-gutenberg-products-block'
) }
</p>
</>
}
<ToggleControl
label={ __(
'Image fit',
'Fixed background',
'woo-gutenberg-products-block'
) }
value={ attributes.imageFit }
onChange={ ( value ) =>
checked={ hasParallax }
onChange={ () => {
setAttributes( {
imageFit: value,
} )
}
>
<ToggleGroupControlOption
hasParallax: ! hasParallax,
} );
} }
/>
<ToggleControl
label={ __(
'Repeated background',
'woo-gutenberg-products-block'
) }
checked={ isRepeated }
onChange={ () => {
setAttributes( {
isRepeated: ! isRepeated,
} );
} }
/>
{ ! isRepeated && (
<ToggleGroupControl
help={
<>
<p>
{ __(
'Choose “Cover” if you want the image to scale automatically to always fit its container.',
'woo-gutenberg-products-block'
) }
</p>
<p>
{ __(
'Note: by choosing “Cover” you will lose the ability to freely move the focal point precisely.',
'woo-gutenberg-products-block'
) }
</p>
</>
}
label={ __(
'None',
'Image fit',
'woo-gutenberg-products-block'
) }
value="none"
/>
<ToggleGroupControlOption
/* translators: "Cover" is a verb that indicates an image covering the entire container. */
label={ __(
'Cover',
'woo-gutenberg-products-block'
) }
value="cover"
/>
</ToggleGroupControl>
value={ attributes.imageFit }
onChange={ ( value ) =>
setAttributes( {
imageFit: value,
} )
}
>
<ToggleGroupControlOption
label={ __(
'None',
'woo-gutenberg-products-block'
) }
value="none"
/>
<ToggleGroupControlOption
/* translators: "Cover" is a verb that indicates an image covering the entire container. */
label={ __(
'Cover',
'woo-gutenberg-products-block'
) }
value="cover"
/>
</ToggleGroupControl>
) }
<FocalPointPicker
label={ __(
'Focal Point Picker',
@ -370,30 +406,32 @@ const FeaturedProduct = ( {
} )
}
/>
<TextareaControl
label={ __(
'Alt text (alternative text)',
'woo-gutenberg-products-block'
) }
value={ attributes.alt }
onChange={ ( alt ) => {
setAttributes( { alt } );
} }
help={
<>
<ExternalLink href="https://www.w3.org/WAI/tutorials/images/decision-tree">
{ isImgElement && (
<TextareaControl
label={ __(
'Alt text (alternative text)',
'woo-gutenberg-products-block'
) }
value={ attributes.alt }
onChange={ ( alt ) => {
setAttributes( { alt } );
} }
help={
<>
<ExternalLink href="https://www.w3.org/WAI/tutorials/images/decision-tree">
{ __(
'Describe the purpose of the image',
'woo-gutenberg-products-block'
) }
</ExternalLink>
{ __(
'Describe the purpose of the image',
'Leaving it empty will use the product name.',
'woo-gutenberg-products-block'
) }
</ExternalLink>
{ __(
'Leaving it empty will use the product name.',
'woo-gutenberg-products-block'
) }
</>
}
/>
</>
}
/>
) }
</PanelBody>
) }
<PanelColorGradientSettings
@ -454,6 +492,8 @@ const FeaturedProduct = ( {
contentAlign,
dimRatio,
focalPoint,
hasParallax,
isRepeated,
imageFit,
minHeight,
overlayColor,
@ -471,6 +511,7 @@ const FeaturedProduct = ( {
'is-loading': ! product && isLoading,
'is-not-found': ! product && ! isLoading,
'has-background-dim': dimRatio !== 0,
'is-repeated': isRepeated,
},
contentAlign !== 'center' && `has-${ contentAlign }-content`
);
@ -479,14 +520,31 @@ const FeaturedProduct = ( {
borderRadius: style?.border?.radius,
};
const backgroundImageStyle = {
objectPosition: calculateImagePosition( focalPoint ),
objectFit: imageFit,
};
const isImgElement = ! isRepeated && ! hasParallax;
const wrapperStyle = {
...getSpacingClassesAndStyles( attributes ).style,
minHeight,
};
const backgroundImageStyle = {
...calculateBackgroundImagePosition( focalPoint ),
objectFit: imageFit,
const backgroundDivStyle = {
...( ! isImgElement
? {
...backgroundImageStyles( backgroundImageSrc ),
backgroundPosition: calculateImagePosition(
focalPoint
),
}
: undefined ),
...( ! isRepeated && {
backgroundRepeat: 'no-repeat',
backgroundSize: imageFit === 'cover' ? imageFit : 'auto',
} ),
};
const overlayStyle = {
@ -504,25 +562,40 @@ const FeaturedProduct = ( {
/>
<div className={ classes } style={ containerStyle }>
<div
className="wc-block-featured-product__wrapper"
className={ classnames(
'wc-block-featured-product__wrapper'
) }
style={ wrapperStyle }
>
<div
className="wc-block-featured-product__overlay"
style={ overlayStyle }
/>
<img
alt={ product.short_description }
className="wc-block-featured-product__background-image"
src={ backgroundImageSrc }
style={ backgroundImageStyle }
onLoad={ ( e ) => {
setBackgroundImageSize( {
height: e.target?.naturalHeight,
width: e.target?.naturalWidth,
} );
} }
/>
{ isImgElement && (
<img
alt={ product.short_description }
className="wc-block-featured-product__background-image"
src={ backgroundImageSrc }
style={ backgroundImageStyle }
onLoad={ ( e ) => {
setBackgroundImageSize( {
height: e.target?.naturalHeight,
width: e.target?.naturalWidth,
} );
} }
/>
) }
{ ! isImgElement && (
<div
className={ classnames(
'wc-block-featured-product__background-image',
{
'has-parallax': hasParallax,
}
) }
style={ backgroundDivStyle }
/>
) }
<h2
className="wc-block-featured-product__title"
dangerouslySetInnerHTML={ {
@ -736,6 +809,7 @@ export default compose( [
state = {
doUrlUpdate: false,
};
componentDidUpdate() {
const {
attributes,
@ -757,9 +831,11 @@ export default compose( [
this.setState( { doUrlUpdate: false } );
}
}
triggerUrlUpdate = () => {
this.setState( { doUrlUpdate: true } );
};
render() {
return (
<ProductComponent
@ -769,6 +845,7 @@ export default compose( [
);
}
}
return WrappedComponent;
}, 'withUpdateButtonAttributes' ),
] )( FeaturedProduct );

View File

@ -56,6 +56,14 @@
"type": "string",
"default": "none"
},
"hasParallax": {
"type": "boolean",
"default": false
},
"isRepeated": {
"type": "boolean",
"default": false
},
"mediaId": {
"type": "number",
"default": 0

View File

@ -10,6 +10,8 @@ export const example = {
contentAlign: 'center',
dimRatio: 50,
editMode: false,
hasParallax: false,
isRepeated: false,
height: getSetting( 'default_height', 500 ),
mediaSrc: '',
overlayColor: '#000000',

View File

@ -17,6 +17,11 @@
}
}
&.is-repeated {
background-repeat: repeat;
background-size: auto;
}
// Applying image edits
.is-applying {
.components-spinner {
@ -53,6 +58,7 @@
display: flex;
flex-wrap: wrap;
height: 100%;
width: 100%;
justify-content: center;
overflow: hidden;
}
@ -133,6 +139,22 @@
.wc-block-featured-product__background-image {
@include absolute-stretch();
object-fit: none;
&.has-parallax {
background-attachment: fixed;
// Mobile Safari does not support fixed background attachment properly.
// See also https://stackoverflow.com/questions/24154666/background-size-cover-not-working-on-ios
// Chrome on Android does not appear to support the attachment at all: https://issuetracker.google.com/issues/36908439
@supports (-webkit-overflow-scrolling: touch) {
background-attachment: scroll;
}
// Remove the appearance of scrolling based on OS-level animation preferences.
@media (prefers-reduced-motion: reduce) {
background-attachment: scroll;
}
}
}
.wp-block-button.aligncenter {

View File

@ -1,12 +1,10 @@
export function calculateBackgroundImagePosition( coords ) {
export function calculateImagePosition( coords ) {
if ( ! coords ) return {};
const x = Math.round( coords.x * 100 );
const y = Math.round( coords.y * 100 );
return {
objectPosition: `${ x }% ${ y }%`,
};
return `${ x }% ${ y }%`;
}
/**
@ -20,3 +18,7 @@ export function dimRatioToClass( ratio ) {
? null
: `has-background-dim-${ 10 * Math.round( ratio / 10 ) }`;
}
export function backgroundImageStyles( url ) {
return { backgroundImage: `url(${ url })` };
}

View File

@ -68,7 +68,9 @@ class FeaturedProduct extends AbstractDynamicBlock {
}
$attributes = wp_parse_args( $attributes, $this->defaults );
$attributes['height'] = isset( $attributes['height'] ) ? $attributes['height'] : wc_get_theme_support( 'featured_block::default_height', 500 );
$default_height = wc_get_theme_support( 'featured_block::default_height', 500 );
$min_height = $attributes['minHeight'] ?? $default_height;
$attributes['height'] = $attributes['height'] ?? $default_height;
$title = sprintf(
'<h2 class="wc-block-featured-product__title">%s</h2>',
@ -92,13 +94,20 @@ class FeaturedProduct extends AbstractDynamicBlock {
wp_kses_post( $product->get_price_html() )
);
$styles = $this->get_styles( $attributes );
$image_url = esc_url( $this->get_image_url( $attributes, $product ) );
$classes = $this->get_classes( $attributes );
$output = sprintf( '<div class="%1$s wp-block-woocommerce-featured-product" style="%2$s">', esc_attr( trim( $classes ) ), esc_attr( $styles ) );
$output .= '<div class="wc-block-featured-product__wrapper">';
$output = sprintf( '<div class="%1$s wp-block-woocommerce-featured-product">', esc_attr( trim( $classes ) ) );
$output .= $this->render_wrapper( $attributes );
$output .= $this->render_overlay( $attributes );
$output .= $this->render_image( $attributes, $product );
if ( ! $attributes['isRepeated'] && ! $attributes['hasParallax'] ) {
$output .= $this->render_image( $attributes, $product, $image_url );
} else {
$output .= $this->render_bg_image( $attributes, $image_url );
}
$output .= $title;
if ( $attributes['showDesc'] ) {
$output .= $desc_str;
@ -114,29 +123,39 @@ class FeaturedProduct extends AbstractDynamicBlock {
}
/**
* Renders the featured image
* Returns the url of a product image
*
* @param array $attributes Block attributes. Default empty array.
* @param \WC_Product $product Product object.
*
* @return string
*/
private function render_image( $attributes, $product ) {
$style = '';
private function get_image_url( $attributes, $product ) {
$image_size = 'large';
if ( 'none' !== $attributes['align'] || $attributes['height'] > 800 ) {
$image_size = 'full';
}
$style .= sprintf( 'object-fit: %s;', $attributes['imageFit'] );
if ( $attributes['mediaId'] ) {
$image = wp_get_attachment_image_url( $attributes['mediaId'], $image_size );
} else {
$image = $this->get_image( $product, $image_size );
return wp_get_attachment_image_url( $attributes['mediaId'], $image_size );
}
if ( is_array( $attributes['focalPoint'] ) && 2 === count( $attributes['focalPoint'] ) ) {
return $this->get_image( $product, $image_size );
}
/**
* Renders the featured image
*
* @param array $attributes Block attributes. Default empty array.
* @param \WC_Product $product Product object.
* @param string $image_url Product image url.
*
* @return string
*/
private function render_image( $attributes, $product, $image_url ) {
$style = sprintf( 'object-fit: %s;', $attributes['imageFit'] );
if ( $this->hasFocalPoint( $attributes ) ) {
$style .= sprintf(
'object-position: %s%% %s%%;',
$attributes['focalPoint']['x'] * 100,
@ -144,11 +163,11 @@ class FeaturedProduct extends AbstractDynamicBlock {
);
}
if ( ! empty( $image ) ) {
if ( ! empty( $image_url ) ) {
return sprintf(
'<img alt="%1$s" class="wc-block-featured-product__background-image" src="%2$s" style="%3$s" />',
wp_kses_post( $attributes['alt'] ?: $product->get_name() ),
esc_url( $image ),
$image_url,
$style
);
}
@ -156,6 +175,43 @@ class FeaturedProduct extends AbstractDynamicBlock {
return '';
}
/**
* Renders the featured image as a div background.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $image_url Product image url.
*
* @return string
*/
private function render_bg_image( $attributes, $image_url ) {
$styles = $this->get_bg_styles( $attributes, $image_url );
$classes = [ 'wc-block-featured-product__background-image' ];
if ( $attributes['hasParallax'] ) {
$classes[] = ' has-parallax';
}
return sprintf( '<div class="%1$s" style="%2$s" /></div>', implode( ' ', $classes ), $styles );
}
/**
* Renders the image wrapper.
*
* @param array $attributes Block attributes. Default empty array.
*
* @return string
*/
private function render_wrapper( $attributes ) {
$min_height = $attributes['minHeight'] ?? wc_get_theme_support( 'featured_block::default_height', 500 );
if ( isset( $attributes['minHeight'] ) ) {
$style = sprintf( 'min-height:%dpx;', intval( $min_height ) );
}
return sprintf( '<div class="wc-block-featured-product__wrapper" style="%s">', esc_attr( $style ) );
}
/**
* Renders the block overlay
*
@ -180,17 +236,29 @@ class FeaturedProduct extends AbstractDynamicBlock {
/**
* Get the styles for the wrapper element (background image, color).
*
* @param array $attributes Block attributes. Default empty array.
* @param array $attributes Block attributes. Default empty array.
* @param string $image_url Product image url.
*
* @return string
*/
public function get_styles( $attributes ) {
public function get_bg_styles( $attributes, $image_url ) {
$style = '';
$min_height = isset( $attributes['minHeight'] ) ? $attributes['minHeight'] : wc_get_theme_support( 'featured_block::default_height', 500 );
if ( $attributes['isRepeated'] || $attributes['hasParallax'] ) {
$style .= "background-image: url($image_url);";
}
if ( isset( $attributes['minHeight'] ) ) {
$style .= sprintf( 'min-height:%dpx;', intval( $min_height ) );
if ( ! $attributes['isRepeated'] ) {
$style .= 'background-repeat: no-repeat;';
$style .= 'background-size: ' . ( 'cover' === $attributes['imageFit'] ? $attributes['imageFit'] : 'auto' ) . ';';
}
if ( $this->hasFocalPoint( $attributes ) ) {
$style .= sprintf(
'background-position: %s%% %s%%;',
$attributes['focalPoint']['x'] * 100,
$attributes['focalPoint']['y'] * 100
);
}
$global_style_style = StyleAttributesUtils::get_styles_by_attributes( $attributes, $this->global_style_wrapper );
@ -228,6 +296,10 @@ class FeaturedProduct extends AbstractDynamicBlock {
$classes[] = $attributes['className'];
}
if ( $attributes['isRepeated'] ) {
$classes[] = 'is-repeated';
}
$global_style_classes = StyleAttributesUtils::get_classes_by_attributes( $attributes, $this->global_style_wrapper );
$classes[] = $global_style_classes;
@ -255,6 +327,17 @@ class FeaturedProduct extends AbstractDynamicBlock {
return $image;
}
/**
* Returns whether the focal point is defined for the block.
*
* @param array $attributes Block attributes. Default empty array.
*
* @return bool
*/
private function hasFocalPoint( $attributes ): bool {
return is_array( $attributes['focalPoint'] ) && 2 === count( $attributes['focalPoint'] );
}
/**
* Extra data passed through from server to client for block.
*