[Enhancement] Abstract rating block (#50810)

* Create rating component

* Refactor rating block

* Fix ProductRating component

* Refactor rating blocks

* Add changelog

* Review count maybe visible for rating stars

* Rename props

* Remove 0 after No reviews message

* Remove review count from rating-stars
This commit is contained in:
Fernando Marichal 2024-09-13 09:35:56 -03:00 committed by GitHub
parent 348455fb02
commit ce618d6250
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 228 additions and 263 deletions

View File

@ -1,114 +1,26 @@
/** /**
* External dependencies * External dependencies
*/ */
import { __, _n, sprintf } from '@wordpress/i18n';
import clsx from 'clsx';
import { import {
useInnerBlockLayoutContext, useInnerBlockLayoutContext,
useProductDataContext, useProductDataContext,
} from '@woocommerce/shared-context'; } from '@woocommerce/shared-context';
import { useStyleProps } from '@woocommerce/base-hooks'; import { useStyleProps } from '@woocommerce/base-hooks';
import { withProductDataContext } from '@woocommerce/shared-hocs'; import { withProductDataContext } from '@woocommerce/shared-hocs';
import { isNumber, ProductResponseItem } from '@woocommerce/types'; import {
ProductRating,
getAverageRating,
getRatingCount,
} from '@woocommerce/editor-components/product-rating';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import './style.scss'; 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={ clsx(
'wc-block-components-product-rating-stars__norating-container',
`${ parentClassName }-product-rating-stars__norating-container`
) }
>
<div
className={
'wc-block-components-product-rating-stars__norating'
}
role="img"
>
<span style={ starStyle } />
</div>
<span>{ __( 'No Reviews', 'woocommerce' ) }</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', 'woocommerce' ),
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,
'woocommerce'
),
sprintf( '<strong class="rating">%f</strong>', rating ),
sprintf( '<span class="rating">%d</span>', reviews )
),
};
return (
<div
className={ clsx(
'wc-block-components-product-rating-stars__stars',
`${ parentClassName }__product-rating-stars__stars`
) }
role="img"
aria-label={ ratingText }
>
<span style={ starStyle } dangerouslySetInnerHTML={ ratingHTML } />
</div>
);
};
interface ProductRatingStarsProps { interface ProductRatingStarsProps {
className?: string; className?: string;
textAlign?: string; textAlign?: string;
isDescendentOfSingleProductBlock: boolean;
isDescendentOfQueryLoop: boolean; isDescendentOfQueryLoop: boolean;
postId: number; postId: number;
productId: number; productId: number;
@ -116,42 +28,29 @@ interface ProductRatingStarsProps {
} }
export const Block = ( props: ProductRatingStarsProps ): JSX.Element | null => { export const Block = ( props: ProductRatingStarsProps ): JSX.Element | null => {
const { textAlign, shouldDisplayMockedReviewsWhenProductHasNoReviews } = const {
props; textAlign = '',
shouldDisplayMockedReviewsWhenProductHasNoReviews,
} = props;
const styleProps = useStyleProps( props ); const styleProps = useStyleProps( props );
const { parentClassName } = useInnerBlockLayoutContext(); const { parentClassName } = useInnerBlockLayoutContext();
const { product } = useProductDataContext(); const { product } = useProductDataContext();
const rating = getAverageRating( product ); const rating = getAverageRating( product );
const reviews = getRatingCount( product ); const reviews = getRatingCount( product );
const className = 'wc-block-components-product-rating-stars';
const className = clsx(
styleProps.className,
'wc-block-components-product-rating-stars',
{
[ `${ 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 ( return (
<div className={ className } style={ styleProps.style }> <ProductRating
<div className="wc-block-components-product-rating-stars__container"> className={ className }
{ content } showMockedReviews={
</div> shouldDisplayMockedReviewsWhenProductHasNoReviews
</div> }
styleProps={ styleProps }
parentClassName={ parentClassName }
reviews={ reviews }
rating={ rating }
textAlign={ textAlign }
/>
); );
}; };

View File

@ -1,129 +1,23 @@
/** /**
* External dependencies * External dependencies
*/ */
import { __, _n, sprintf } from '@wordpress/i18n';
import clsx from 'clsx';
import { import {
useInnerBlockLayoutContext, useInnerBlockLayoutContext,
useProductDataContext, useProductDataContext,
} from '@woocommerce/shared-context'; } from '@woocommerce/shared-context';
import { useStyleProps } from '@woocommerce/base-hooks'; import { useStyleProps } from '@woocommerce/base-hooks';
import { withProductDataContext } from '@woocommerce/shared-hocs'; import { withProductDataContext } from '@woocommerce/shared-hocs';
import { isNumber, ProductResponseItem } from '@woocommerce/types'; import {
ProductRating,
getAverageRating,
getRatingCount,
} from '@woocommerce/editor-components/product-rating';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import './style.scss'; 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={ clsx(
'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', 'woocommerce' ) }</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', 'woocommerce' ),
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,
'woocommerce'
),
sprintf( '<strong class="rating">%f</strong>', rating ),
sprintf( '<span class="rating">%d</span>', reviews )
),
};
return (
<div
className={ clsx(
'wc-block-components-product-rating__stars',
`${ parentClassName }__product-rating__stars`
) }
role="img"
aria-label={ ratingText }
>
<span style={ starStyle } dangerouslySetInnerHTML={ ratingHTML } />
</div>
);
};
const ReviewsCount = ( props: { reviews: number } ): JSX.Element => {
const { reviews } = props;
const reviewsCount = sprintf(
/* translators: %s is referring to the total of reviews for a product */
_n(
'(%s customer review)',
'(%s customer reviews)',
reviews,
'woocommerce'
),
reviews
);
return (
<span className="wc-block-components-product-rating__reviews_count">
{ reviewsCount }
</span>
);
};
type ProductRatingProps = { type ProductRatingProps = {
className?: string; className?: string;
textAlign?: string; textAlign?: string;
@ -136,7 +30,7 @@ type ProductRatingProps = {
export const Block = ( props: ProductRatingProps ): JSX.Element | undefined => { export const Block = ( props: ProductRatingProps ): JSX.Element | undefined => {
const { const {
textAlign, textAlign = '',
isDescendentOfSingleProductBlock, isDescendentOfSingleProductBlock,
shouldDisplayMockedReviewsWhenProductHasNoReviews, shouldDisplayMockedReviewsWhenProductHasNoReviews,
} = props; } = props;
@ -146,38 +40,22 @@ export const Block = ( props: ProductRatingProps ): JSX.Element | undefined => {
const rating = getAverageRating( product ); const rating = getAverageRating( product );
const reviews = getRatingCount( product ); const reviews = getRatingCount( product );
const className = clsx( const className = 'wc-block-components-product-rating';
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
);
if ( reviews || shouldDisplayMockedReviewsWhenProductHasNoReviews ) { if ( reviews || shouldDisplayMockedReviewsWhenProductHasNoReviews ) {
return ( return (
<div className={ className } style={ styleProps.style }> <ProductRating
<div className="wc-block-components-product-rating__container"> className={ className }
{ content } showReviewCount={ isDescendentOfSingleProductBlock }
{ reviews && isDescendentOfSingleProductBlock ? ( showMockedReviews={
<ReviewsCount reviews={ reviews } /> shouldDisplayMockedReviewsWhenProductHasNoReviews
) : null } }
</div> styleProps={ styleProps }
</div> parentClassName={ parentClassName }
reviews={ reviews }
rating={ rating }
textAlign={ textAlign }
/>
); );
} }
}; };

View File

@ -0,0 +1,184 @@
/**
* External dependencies
*/
import { __, _n, sprintf } from '@wordpress/i18n';
import clsx from 'clsx';
import type { CSSProperties } from 'react';
import { isNumber, ProductResponseItem } from '@woocommerce/types';
type RatingProps = {
className: string;
reviews: number;
rating: number;
parentClassName?: string;
};
export const getAverageRating = (
product: Omit< ProductResponseItem, 'average_rating' > & {
average_rating: string;
}
) => {
const rating = parseFloat( product.average_rating );
return Number.isFinite( rating ) && rating > 0 ? rating : 0;
};
export 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 = ( {
className,
parentClassName,
}: {
className: string;
parentClassName: string;
} ) => {
const starStyle = getStarStyle( 0 );
return (
<div
className={ clsx(
`${ className }__norating-container`,
`${ parentClassName }-product-rating__norating-container`
) }
>
<div className={ `${ className }__norating` } role="img">
<span style={ starStyle } />
</div>
<span>{ __( 'No Reviews', 'woocommerce' ) }</span>
</div>
);
};
const Rating = ( props: RatingProps ): JSX.Element => {
const { className, 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', 'woocommerce' ),
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,
'woocommerce'
),
sprintf( '<strong class="rating">%f</strong>', rating ),
sprintf( '<span class="rating">%d</span>', reviews )
),
};
return (
<div
className={ clsx(
`${ className }__stars`,
`${ parentClassName }__product-rating__stars`
) }
role="img"
aria-label={ ratingText }
>
<span style={ starStyle } dangerouslySetInnerHTML={ ratingHTML } />
</div>
);
};
const ReviewsCount = ( props: {
className: string;
reviews: number;
} ): JSX.Element => {
const { className, reviews } = props;
const reviewsCount = sprintf(
/* translators: %s is referring to the total of reviews for a product */
_n(
'(%s customer review)',
'(%s customer reviews)',
reviews,
'woocommerce'
),
reviews
);
return (
<span className={ `${ className }__reviews_count` }>
{ reviewsCount }
</span>
);
};
type ProductRatingProps = {
className: string;
showReviewCount?: boolean;
showMockedReviews?: boolean;
parentClassName?: string;
rating: number;
reviews: number;
styleProps: {
className: string;
style: CSSProperties;
};
textAlign?: string;
};
export const ProductRating = (
props: ProductRatingProps
): JSX.Element | null => {
const {
className = 'wc-block-components-product-rating',
showReviewCount,
showMockedReviews,
parentClassName = '',
rating,
reviews,
styleProps,
textAlign,
} = props;
const wrapperClassName = clsx( styleProps.className, className, {
[ `${ parentClassName }__product-rating` ]: parentClassName,
[ `has-text-align-${ textAlign }` ]: textAlign,
} );
const mockedRatings = showMockedReviews && (
<NoRating className={ className } parentClassName={ parentClassName } />
);
const content = reviews ? (
<Rating
className={ className }
rating={ rating }
reviews={ reviews }
parentClassName={ parentClassName }
/>
) : (
mockedRatings
);
const isReviewCountVisible = reviews && showReviewCount;
return (
<div className={ wrapperClassName } style={ styleProps.style }>
<div className={ `${ className }__container` }>
{ content }
{ isReviewCountVisible ? (
<ReviewsCount className={ className } reviews={ reviews } />
) : null }
</div>
</div>
);
};

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
[Enhancement] Abstract rating block #50810