[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:
parent
348455fb02
commit
ce618d6250
|
@ -1,114 +1,26 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import clsx from 'clsx';
|
||||
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';
|
||||
import {
|
||||
ProductRating,
|
||||
getAverageRating,
|
||||
getRatingCount,
|
||||
} from '@woocommerce/editor-components/product-rating';
|
||||
|
||||
/**
|
||||
* 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={ 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 {
|
||||
className?: string;
|
||||
textAlign?: string;
|
||||
isDescendentOfSingleProductBlock: boolean;
|
||||
isDescendentOfQueryLoop: boolean;
|
||||
postId: number;
|
||||
productId: number;
|
||||
|
@ -116,42 +28,29 @@ interface ProductRatingStarsProps {
|
|||
}
|
||||
|
||||
export const Block = ( props: ProductRatingStarsProps ): JSX.Element | null => {
|
||||
const { textAlign, shouldDisplayMockedReviewsWhenProductHasNoReviews } =
|
||||
props;
|
||||
const {
|
||||
textAlign = '',
|
||||
shouldDisplayMockedReviewsWhenProductHasNoReviews,
|
||||
} = props;
|
||||
const styleProps = useStyleProps( props );
|
||||
const { parentClassName } = useInnerBlockLayoutContext();
|
||||
const { product } = useProductDataContext();
|
||||
const rating = getAverageRating( product );
|
||||
const reviews = getRatingCount( product );
|
||||
|
||||
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
|
||||
);
|
||||
const className = 'wc-block-components-product-rating-stars';
|
||||
|
||||
return (
|
||||
<div className={ className } style={ styleProps.style }>
|
||||
<div className="wc-block-components-product-rating-stars__container">
|
||||
{ content }
|
||||
</div>
|
||||
</div>
|
||||
<ProductRating
|
||||
className={ className }
|
||||
showMockedReviews={
|
||||
shouldDisplayMockedReviewsWhenProductHasNoReviews
|
||||
}
|
||||
styleProps={ styleProps }
|
||||
parentClassName={ parentClassName }
|
||||
reviews={ reviews }
|
||||
rating={ rating }
|
||||
textAlign={ textAlign }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,129 +1,23 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import clsx from 'clsx';
|
||||
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';
|
||||
import {
|
||||
ProductRating,
|
||||
getAverageRating,
|
||||
getRatingCount,
|
||||
} from '@woocommerce/editor-components/product-rating';
|
||||
|
||||
/**
|
||||
* 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={ 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 = {
|
||||
className?: string;
|
||||
textAlign?: string;
|
||||
|
@ -136,7 +30,7 @@ type ProductRatingProps = {
|
|||
|
||||
export const Block = ( props: ProductRatingProps ): JSX.Element | undefined => {
|
||||
const {
|
||||
textAlign,
|
||||
textAlign = '',
|
||||
isDescendentOfSingleProductBlock,
|
||||
shouldDisplayMockedReviewsWhenProductHasNoReviews,
|
||||
} = props;
|
||||
|
@ -146,38 +40,22 @@ export const Block = ( props: ProductRatingProps ): JSX.Element | undefined => {
|
|||
const rating = getAverageRating( product );
|
||||
const reviews = getRatingCount( product );
|
||||
|
||||
const className = clsx(
|
||||
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
|
||||
);
|
||||
const className = 'wc-block-components-product-rating';
|
||||
|
||||
if ( reviews || shouldDisplayMockedReviewsWhenProductHasNoReviews ) {
|
||||
return (
|
||||
<div className={ className } style={ styleProps.style }>
|
||||
<div className="wc-block-components-product-rating__container">
|
||||
{ content }
|
||||
{ reviews && isDescendentOfSingleProductBlock ? (
|
||||
<ReviewsCount reviews={ reviews } />
|
||||
) : null }
|
||||
</div>
|
||||
</div>
|
||||
<ProductRating
|
||||
className={ className }
|
||||
showReviewCount={ isDescendentOfSingleProductBlock }
|
||||
showMockedReviews={
|
||||
shouldDisplayMockedReviewsWhenProductHasNoReviews
|
||||
}
|
||||
styleProps={ styleProps }
|
||||
parentClassName={ parentClassName }
|
||||
reviews={ reviews }
|
||||
rating={ rating }
|
||||
textAlign={ textAlign }
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
[Enhancement] Abstract rating block #50810
|
Loading…
Reference in New Issue