* Add new `Product Rating Stars` block

* Make block experimental

* Fix dep
This commit is contained in:
Alba Rincón 2023-07-03 11:48:50 +02:00 committed by GitHub
parent fd86fd2e57
commit b1bc51e3f0
13 changed files with 647 additions and 0 deletions

View File

@ -45,6 +45,15 @@ registerBlockComponent( {
),
} );
registerBlockComponent( {
blockName: 'woocommerce/product-rating-stars',
component: lazy( () =>
import(
/* webpackChunkName: "product-rating-stars" */ './product-elements/rating-stars/block'
)
),
} );
registerBlockComponent( {
blockName: 'woocommerce/product-button',
component: lazy( () =>

View File

@ -5,6 +5,7 @@ import './product-elements/title';
import './product-elements/price';
import './product-elements/image';
import './product-elements/rating';
import './product-elements/rating-stars';
import './product-elements/button';
import './product-elements/summary';
import './product-elements/sale-badge';

View File

@ -0,0 +1,38 @@
{
"name": "woocommerce/product-rating-stars",
"version": "1.0.0",
"icon": "info",
"title": "Product Rating Stars",
"description": "Display the average rating of a product with stars",
"attributes": {
"productId": {
"type": "number",
"default": 0
},
"isDescendentOfQueryLoop": {
"type": "boolean",
"default": false
},
"textAlign": {
"type": "string",
"default": ""
},
"isDescendentOfSingleProductBlock": {
"type": "boolean",
"default": false
},
"isDescendentOfSingleProductTemplate": {
"type": "boolean",
"default": false
}
},
"usesContext": [ "query", "queryId", "postId" ],
"category": "woocommerce",
"keywords": [ "WooCommerce" ],
"supports": {
"align": true
},
"textdomain": "woo-gutenberg-products-block",
"apiVersion": 2,
"$schema": "https://schemas.wp.org/trunk/block.json"
}

View File

@ -0,0 +1,156 @@
/**
* External dependencies
*/
import { __, _n, sprintf } from '@wordpress/i18n';
import classnames from 'classnames';
import {
useInnerBlockLayoutContext,
useProductDataContext,
} from '@woocommerce/shared-context';
import { useStyleProps } from '@woocommerce/base-hooks';
import { withProductDataContext } from '@woocommerce/shared-hocs';
import { isNumber, ProductResponseItem } from '@woocommerce/types';
/**
* Internal dependencies
*/
import './style.scss';
type RatingProps = {
reviews: number;
rating: number;
parentClassName?: string;
};
const getAverageRating = (
product: Omit< ProductResponseItem, 'average_rating' > & {
average_rating: string;
}
) => {
const rating = parseFloat( product.average_rating );
return Number.isFinite( rating ) && rating > 0 ? rating : 0;
};
const getRatingCount = ( product: ProductResponseItem ) => {
const count = isNumber( product.review_count )
? product.review_count
: parseInt( product.review_count, 10 );
return Number.isFinite( count ) && count > 0 ? count : 0;
};
const getStarStyle = ( rating: number ) => ( {
width: ( rating / 5 ) * 100 + '%',
} );
const NoRating = ( { parentClassName }: { parentClassName: string } ) => {
const starStyle = getStarStyle( 0 );
return (
<div
className={ classnames(
'wc-block-components-product-rating__norating-container',
`${ parentClassName }-product-rating__norating-container`
) }
>
<div
className={ 'wc-block-components-product-rating__norating' }
role="img"
>
<span style={ starStyle } />
</div>
<span>{ __( 'No Reviews', 'woo-gutenberg-products-block' ) }</span>
</div>
);
};
const Rating = ( props: RatingProps ): JSX.Element => {
const { rating, reviews, parentClassName } = props;
const starStyle = getStarStyle( rating );
const ratingText = sprintf(
/* translators: %f is referring to the average rating value */
__( 'Rated %f out of 5', 'woo-gutenberg-products-block' ),
rating
);
const ratingHTML = {
__html: sprintf(
/* translators: %1$s is referring to the average rating value, %2$s is referring to the number of ratings */
_n(
'Rated %1$s out of 5 based on %2$s customer rating',
'Rated %1$s out of 5 based on %2$s customer ratings',
reviews,
'woo-gutenberg-products-block'
),
sprintf( '<strong class="rating">%f</strong>', rating ),
sprintf( '<span class="rating">%d</span>', reviews )
),
};
return (
<div
className={ classnames(
'wc-block-components-product-rating__stars',
`${ parentClassName }__product-rating__stars`
) }
role="img"
aria-label={ ratingText }
>
<span style={ starStyle } dangerouslySetInnerHTML={ ratingHTML } />
</div>
);
};
interface ProductRatingStarsProps {
className?: string;
textAlign?: string;
isDescendentOfSingleProductBlock: boolean;
isDescendentOfQueryLoop: boolean;
postId: number;
productId: number;
shouldDisplayMockedReviewsWhenProductHasNoReviews: boolean;
}
export const Block = ( props: ProductRatingStarsProps ): JSX.Element | null => {
const { textAlign, shouldDisplayMockedReviewsWhenProductHasNoReviews } =
props;
const styleProps = useStyleProps( props );
const { parentClassName } = useInnerBlockLayoutContext();
const { product } = useProductDataContext();
const rating = getAverageRating( product );
const reviews = getRatingCount( product );
const className = classnames(
styleProps.className,
'wc-block-components-product-rating',
{
[ `${ parentClassName }__product-rating` ]: parentClassName,
[ `has-text-align-${ textAlign }` ]: textAlign,
}
);
const mockedRatings = shouldDisplayMockedReviewsWhenProductHasNoReviews ? (
<NoRating parentClassName={ parentClassName } />
) : null;
const content = reviews ? (
<Rating
rating={ rating }
reviews={ reviews }
parentClassName={ parentClassName }
/>
) : (
mockedRatings
);
return (
<div className={ className } style={ styleProps.style }>
<div className="wc-block-components-product-rating-stars__container">
{ content }
</div>
</div>
);
};
export default withProductDataContext( Block );

View File

@ -0,0 +1,20 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Icon, starFilled } from '@wordpress/icons';
export const BLOCK_TITLE: string = __(
'Product Rating Stars',
'woo-gutenberg-products-block'
);
export const BLOCK_ICON: JSX.Element = (
<Icon
icon={ starFilled }
className="wc-block-editor-components-block-icon"
/>
);
export const BLOCK_DESCRIPTION: string = __(
'Display the average rating of a product with stars',
'woo-gutenberg-products-block'
);

View File

@ -0,0 +1,75 @@
/**
* External dependencies
*/
import {
AlignmentToolbar,
BlockControls,
useBlockProps,
} from '@wordpress/block-editor';
import type { BlockEditProps } from '@wordpress/blocks';
import { useEffect } from '@wordpress/element';
import { ProductQueryContext as Context } from '@woocommerce/blocks/product-query/types';
/**
* Internal dependencies
*/
import Block from './block';
import { BlockAttributes } from './types';
import { useIsDescendentOfSingleProductBlock } from '../shared/use-is-descendent-of-single-product-block';
import { useIsDescendentOfSingleProductTemplate } from '../shared/use-is-descendent-of-single-product-template';
const Edit = (
props: BlockEditProps< BlockAttributes > & { context: Context }
): JSX.Element => {
const { attributes, setAttributes, context } = props;
const blockProps = useBlockProps( {
className: 'wp-block-woocommerce-product-rating',
} );
const blockAttrs = {
...attributes,
...context,
shouldDisplayMockedReviewsWhenProductHasNoReviews: true,
};
const isDescendentOfQueryLoop = Number.isFinite( context.queryId );
const { isDescendentOfSingleProductBlock } =
useIsDescendentOfSingleProductBlock( {
blockClientId: blockProps?.id,
} );
let { isDescendentOfSingleProductTemplate } =
useIsDescendentOfSingleProductTemplate();
if ( isDescendentOfQueryLoop || isDescendentOfSingleProductBlock ) {
isDescendentOfSingleProductTemplate = false;
}
useEffect( () => {
setAttributes( {
isDescendentOfQueryLoop,
isDescendentOfSingleProductBlock,
isDescendentOfSingleProductTemplate,
} );
}, [
setAttributes,
isDescendentOfQueryLoop,
isDescendentOfSingleProductBlock,
isDescendentOfSingleProductTemplate,
] );
return (
<>
<BlockControls>
<AlignmentToolbar
value={ attributes.textAlign }
onChange={ ( newAlign ) => {
setAttributes( { textAlign: newAlign || '' } );
} }
/>
</BlockControls>
<div { ...blockProps }>
<Block { ...blockAttrs } />
</div>
</>
);
};
export default Edit;

View File

@ -0,0 +1,36 @@
/**
* External dependencies
*/
import { BlockConfiguration } from '@wordpress/blocks';
import { registerBlockSingleProductTemplate } from '@woocommerce/atomic-utils';
import { isExperimentalBuild } from '@woocommerce/block-settings';
/**
* Internal dependencies
*/
import metadata from './block.json';
import edit from './edit';
import sharedConfig from '../shared/config';
import { supports } from './support';
import { BLOCK_ICON } from './constants';
const blockConfig: BlockConfiguration = {
...sharedConfig,
ancestor: [
'woocommerce/all-products',
'woocommerce/single-product',
'core/post-template',
'woocommerce/product-template',
],
icon: { src: BLOCK_ICON },
supports,
edit,
};
if ( isExperimentalBuild() ) {
registerBlockSingleProductTemplate( {
blockName: 'woocommerce/product-rating-stars',
blockMetadata: metadata,
blockSettings: blockConfig,
} );
}

View File

@ -0,0 +1,106 @@
.wc-block-components-product-rating {
display: block;
line-height: 1;
&__stars {
display: inline-block;
overflow: hidden;
position: relative;
width: 5.3em;
height: 1.618em;
line-height: 1.618;
font-size: 1em;
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
font-family: star;
font-weight: 400;
&::before {
content: "\53\53\53\53\53";
top: 0;
left: 0;
right: 0;
position: absolute;
opacity: 0.5;
color: inherit;
white-space: nowrap;
}
span {
overflow: hidden;
top: 0;
left: 0;
right: 0;
position: absolute;
color: inherit;
padding-top: 1.5em;
}
span::before {
content: "\53\53\53\53\53";
top: 0;
left: 0;
right: 0;
position: absolute;
color: inherit;
white-space: nowrap;
}
}
&__link {
display: inline-block;
height: 1.618em;
width: 100%;
text-align: inherit;
@include font-size(small);
}
.wc-block-all-products & {
margin-top: 0;
margin-bottom: $gap-small;
}
&__norating-container {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: $gap-smaller;
}
&__norating {
display: inline-block;
overflow: hidden;
position: relative;
width: 1.5em;
height: 1.618em;
line-height: 1.618;
font-size: 1em;
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
font-family: star;
font-weight: 400;
-webkit-text-stroke: 2px var(--wp--preset--color--black, #000);
&::before {
content: "\53";
top: 0;
left: 0;
right: 0;
position: absolute;
color: transparent;
white-space: nowrap;
text-align: center;
}
}
}
.wp-block-woocommerce-single-product {
.wc-block-components-product-rating__stars {
margin: 0;
}
}
.wc-block-all-products,
.wp-block-query {
.is-loading {
.wc-block-components-product-rating {
@include placeholder();
width: 7em;
}
}
}

View File

@ -0,0 +1,32 @@
/* eslint-disable @wordpress/no-unsafe-wp-apis */
/**
* External dependencies
*/
import { isFeaturePluginBuild } from '@woocommerce/block-settings';
import { __experimentalGetSpacingClassesAndStyles } from '@wordpress/block-editor';
export const supports = {
...( isFeaturePluginBuild() && {
color: {
text: true,
background: false,
link: false,
__experimentalSkipSerialization: true,
},
spacing: {
margin: true,
padding: true,
},
typography: {
fontSize: true,
__experimentalSkipSerialization: true,
},
__experimentalSelector: '.wc-block-components-product-rating',
} ),
...( ! isFeaturePluginBuild() &&
typeof __experimentalGetSpacingClassesAndStyles === 'function' && {
spacing: {
margin: true,
},
} ),
};

View File

@ -0,0 +1,7 @@
export interface BlockAttributes {
productId: number;
isDescendentOfQueryLoop: boolean;
isDescendentOfSingleProductBlock: boolean;
isDescendentOfSingleProductTemplate: boolean;
textAlign: string;
}

View File

@ -58,6 +58,9 @@ const blocks = {
'product-top-rated': {},
'products-by-attribute': {},
'rating-filter': {},
'product-rating-stars': {
isExperimental: true,
},
'reviews-by-category': {
customDir: 'reviews/reviews-by-category',
},

View File

@ -0,0 +1,163 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* ProductRating class.
*/
class ProductRatingStars extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-rating-stars';
/**
* API version name.
*
* @var string
*/
protected $api_version = '2';
/**
* Get block supports. Shared with the frontend.
* IMPORTANT: If you change anything here, make sure to update the JS file too.
*
* @return array
*/
protected function get_block_type_supports() {
return array(
'color' =>
array(
'text' => true,
'background' => false,
'link' => false,
'__experimentalSkipSerialization' => true,
),
'typography' =>
array(
'fontSize' => true,
'__experimentalSkipSerialization' => true,
),
'spacing' =>
array(
'margin' => true,
'padding' => true,
'__experimentalSkipSerialization' => true,
),
'__experimentalSelector' => '.wc-block-components-product-rating',
);
}
/**
* Overwrite parent method to prevent script registration.
*
* It is necessary to register and enqueues assets during the render
* phase because we want to load assets only if the block has the content.
*/
protected function register_block_type_assets() {
return null;
}
/**
* Register the context.
*/
protected function get_block_type_uses_context() {
return [ 'query', 'queryId', 'postId' ];
}
/**
* Include and render the block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
if ( ! empty( $content ) ) {
parent::register_block_type_assets();
$this->register_chunk_translations( [ $this->block_name ] );
return $content;
}
$post_id = $block->context['postId'];
$product = wc_get_product( $post_id );
if ( $product ) {
$product_reviews_count = $product->get_review_count();
$product_rating = $product->get_average_rating();
$styles_and_classes = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$text_align_styles_and_classes = StyleAttributesUtils::get_text_align_class_and_style( $attributes );
/**
* Filter the output from wc_get_rating_html.
*
* @param string $html Star rating markup. Default empty string.
* @param float $rating Rating being shown.
* @param int $count Total number of ratings.
* @return string
*/
$filter_rating_html = function( $html, $rating, $count ) use ( $product_rating, $product_reviews_count ) {
$product_permalink = get_permalink();
$reviews_count = $count;
$average_rating = $rating;
if ( $product_rating ) {
$average_rating = $product_rating;
}
if ( $product_reviews_count ) {
$reviews_count = $product_reviews_count;
}
if ( 0 < $average_rating || false === $product_permalink ) {
/* translators: %s: rating */
$label = sprintf( __( 'Rated %s out of 5', 'woo-gutenberg-products-block' ), $average_rating );
$html = sprintf(
'<div class="wc-block-components-product-rating-stars__container">
<div class="wc-block-components-product-rating__stars wc-block-grid__product-rating__stars" role="img" aria-label="%1$s">
%2$s
</div>
</div>
',
esc_attr( $label ),
wc_get_star_rating_html( $average_rating, $reviews_count )
);
} else {
$html = '';
}
return $html;
};
add_filter(
'woocommerce_product_get_rating_html',
$filter_rating_html,
10,
3
);
$rating_html = wc_get_rating_html( $product->get_average_rating() );
remove_filter(
'woocommerce_product_get_rating_html',
$filter_rating_html,
10
);
return sprintf(
'<div class="wc-block-components-product-rating wc-block-grid__product-rating %1$s %2$s" style="%3$s">
%4$s
</div>',
esc_attr( $text_align_styles_and_classes['class'] ?? '' ),
esc_attr( $styles_and_classes['classes'] ),
esc_attr( $styles_and_classes['styles'] ?? '' ),
$rating_html
);
}
}
}

View File

@ -226,6 +226,7 @@ final class BlockTypesController {
if ( Package::feature()->is_experimental_build() ) {
$block_types[] = 'ProductCollection';
$block_types[] = 'ProductRatingStars';
$block_types[] = 'ProductTemplate';
}