Commit a22ff54792a for woocommerce
commit a22ff54792ae396a38d5a20526a5815e8a067318
Author: Luigi Teschio <gigitux@gmail.com>
Date: Mon Aug 10 14:31:14 2026 +0200
Add offset controls to Reviews by Product and Category blocks (#67163)
* Add offset controls to product review blocks
* Add changelog entry for review block offset controls
* fix types
* improve ui
* add custom error for offset
* fix re-rendering
* update UI
* improve label
* fix: reject invalid review offsets
* handle -0 case
* fix E2E test
* fix e2e test
---------
Co-authored-by: Tung Du <dinhtungdu@gmail.com>
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/components/reviews/review-list/index.tsx b/plugins/woocommerce/client/blocks/assets/js/base/components/reviews/review-list/index.tsx
index 49c5fe5652d..6e044604d49 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/components/reviews/review-list/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/base/components/reviews/review-list/index.tsx
@@ -40,9 +40,9 @@ const ReviewList = ( {
{ reviews.length === 0 ? (
<ReviewListItem attributes={ attrs } />
) : (
- reviews.map( ( review, i ) => (
+ reviews.map( ( review ) => (
<ReviewListItem
- key={ review.id || i }
+ key={ review.id }
attributes={ attrs }
review={ review }
/>
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/hocs/test/with-reviews.jsx b/plugins/woocommerce/client/blocks/assets/js/base/hocs/test/with-reviews.jsx
index 736918f6dd4..e931f3aa09e 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/hocs/test/with-reviews.jsx
+++ b/plugins/woocommerce/client/blocks/assets/js/base/hocs/test/with-reviews.jsx
@@ -121,6 +121,34 @@ describe( 'withReviews Component', () => {
} );
} );
+ it( 'replaces reviews when the offset changes as the display count increases', async () => {
+ const { getReviews } = mockUtils;
+ getReviews.mockResolvedValue( {
+ reviews: mockReviews,
+ totalReviews: 20,
+ } );
+ await renderComponent( { offset: 5, reviewsToDisplay: 5 } );
+
+ await settle( () =>
+ renderResult.rerender(
+ <TestComponent
+ attributes={ {} }
+ offset={ 0 }
+ order="desc"
+ orderby="date_gmt"
+ productId={ 1 }
+ reviewsToDisplay={ 10 }
+ />
+ )
+ );
+
+ expect( getReviews ).toHaveBeenNthCalledWith( 2, {
+ ...defaultArgs,
+ offset: 0,
+ per_page: 10,
+ } );
+ } );
+
describe( 'when the API returns product data', () => {
beforeEach( async () => {
mockUtils.getReviews.mockImplementation( () =>
@@ -134,12 +162,29 @@ describe( 'withReviews Component', () => {
it( 'sets reviews based on API response', () => {
expect( lastProps.error ).toBeNull();
+ expect( lastProps.hasReviewsHiddenByOffset ).toBe( false );
expect( lastProps.isLoading ).toBe( false );
expect( lastProps.reviews ).toEqual( mockReviews.slice( 0, 2 ) );
expect( lastProps.totalReviews ).toEqual( mockReviews.length );
} );
} );
+ describe( 'when the offset hides all available reviews', () => {
+ beforeEach( async () => {
+ mockUtils.getReviews.mockResolvedValue( {
+ reviews: [],
+ totalReviews: mockReviews.length,
+ } );
+ await renderComponent( { offset: mockReviews.length } );
+ } );
+
+ it( 'indicates that reviews are hidden by the offset', () => {
+ expect( lastProps.hasReviewsHiddenByOffset ).toBe( true );
+ expect( lastProps.reviews ).toEqual( [] );
+ expect( lastProps.totalReviews ).toEqual( 0 );
+ } );
+ } );
+
describe( 'when the API returns an error', () => {
const error = { message: 'There was an error.' };
const formattedError = { message: 'There was an error.', type: 'api' };
diff --git a/plugins/woocommerce/client/blocks/assets/js/base/hocs/with-reviews.tsx b/plugins/woocommerce/client/blocks/assets/js/base/hocs/with-reviews.tsx
index a01bf9f5e6c..39a09d952df 100644
--- a/plugins/woocommerce/client/blocks/assets/js/base/hocs/with-reviews.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/base/hocs/with-reviews.tsx
@@ -18,6 +18,7 @@ interface WithReviewsProps {
reviewsToDisplay: number;
categoryIds?: string | string[];
delayFunction?: ( f: () => void ) => DelayedFunction;
+ offset?: number;
onReviewsAppended?: () => void;
onReviewsLoadError?: ( error: ErrorObject ) => void;
onReviewsReplaced?: () => void;
@@ -29,6 +30,7 @@ interface WithReviewsProps {
interface WithReviewsState {
error: string | ErrorObject | null;
+ hasReviewsHiddenByOffset: boolean;
loading: boolean;
reviews: Review[];
totalReviews: number;
@@ -56,6 +58,7 @@ const withReviews = (
state: WithReviewsState = {
error: null,
+ hasReviewsHiddenByOffset: false,
loading: true,
reviews:
this.isPreview && this.props.attributes?.previewReviews
@@ -73,13 +76,15 @@ const withReviews = (
}
componentDidUpdate( prevProps: WithReviewsProps ) {
- if ( prevProps.reviewsToDisplay < this.props.reviewsToDisplay ) {
+ if ( this.shouldReplaceReviews( prevProps, this.props ) ) {
+ this.replaceReviews();
+ } else if (
+ prevProps.reviewsToDisplay < this.props.reviewsToDisplay
+ ) {
// Since this attribute might be controlled via something with
// short intervals between value changes, this allows for optionally
// delaying review fetches via the provided delay function.
this.delayedAppendReviews();
- } else if ( this.shouldReplaceReviews( prevProps, this.props ) ) {
- this.replaceReviews();
}
}
@@ -90,6 +95,7 @@ const withReviews = (
return (
prevProps.orderby !== nextProps.orderby ||
prevProps.order !== nextProps.order ||
+ this.getOffset( prevProps ) !== this.getOffset( nextProps ) ||
prevProps.productId !== nextProps.productId ||
! isShallowEqual(
prevProps.categoryIds as string[],
@@ -108,6 +114,14 @@ const withReviews = (
}
}
+ getOffset( props = this.props ) {
+ const parsedOffset = Number( props.offset ?? 0 );
+
+ return Number.isInteger( parsedOffset ) && parsedOffset >= 0
+ ? parsedOffset
+ : 0;
+ }
+
getArgs( reviewsToSkip: number ) {
const { categoryIds, order, orderby, productId, reviewsToDisplay } =
this.props;
@@ -115,7 +129,7 @@ const withReviews = (
order,
orderby,
per_page: reviewsToDisplay - reviewsToSkip,
- offset: reviewsToSkip,
+ offset: this.getOffset() + reviewsToSkip,
};
if ( categoryIds ) {
@@ -167,6 +181,7 @@ const withReviews = (
updateListOfReviews( oldReviews: Review[] = [] ) {
const { reviewsToDisplay } = this.props;
const { totalReviews } = this.state;
+ const configuredOffset = this.getOffset();
const reviewsToLoad =
Math.min( totalReviews, reviewsToDisplay ) - oldReviews.length;
@@ -181,15 +196,24 @@ const withReviews = (
reviews: newReviews,
totalReviews: newTotalReviews,
} ) => {
+ const availableReviews = Math.max(
+ newTotalReviews - configuredOffset,
+ 0
+ );
+
if ( this.isMounted ) {
this.setState( {
+ hasReviewsHiddenByOffset:
+ configuredOffset > 0 &&
+ newTotalReviews > 0 &&
+ availableReviews === 0,
reviews: oldReviews
.filter(
( review ) =>
Object.keys( review ).length
)
.concat( newReviews ),
- totalReviews: newTotalReviews,
+ totalReviews: availableReviews,
loading: false,
error: null,
} );
@@ -216,12 +240,19 @@ const withReviews = (
render() {
const { reviewsToDisplay } = this.props;
- const { error, loading, reviews, totalReviews } = this.state;
+ const {
+ error,
+ hasReviewsHiddenByOffset,
+ loading,
+ reviews,
+ totalReviews,
+ } = this.state;
return (
<OriginalComponent
{ ...this.props }
error={ error }
+ hasReviewsHiddenByOffset={ hasReviewsHiddenByOffset }
isLoading={ loading }
reviews={ reviews.slice( 0, reviewsToDisplay ) }
totalReviews={ totalReviews }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/attributes.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/attributes.ts
index a4a79bc4006..75a10053f80 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/attributes.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/attributes.ts
@@ -110,6 +110,7 @@ export default {
export type ReviewBlockAttributes = BlockAttributes & {
editMode: boolean;
imageType: string;
+ offset?: number;
orderby: 'most-recent' | 'highest-rating' | 'lowest-rating';
reviewsOnLoadMore: number;
reviewsOnPageLoad: number;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/edit-utils.js b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/edit-utils.js
index 3b9f7c80133..b4ce4c5f80d 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/edit-utils.js
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/edit-utils.js
@@ -9,9 +9,12 @@ import {
Notice,
ToggleControl,
ToolbarGroup,
- RangeControl,
SelectControl,
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
+ __experimentalInputControl as InputControl,
+ // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
+ __experimentalNumberControl as NumberControl,
+ // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalToggleGroupControl as ToggleGroupControl,
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
@@ -19,6 +22,20 @@ import {
__experimentalToolsPanelItem as ToolsPanelItem,
} from '@wordpress/components';
+const MIN_REVIEW_COUNT = 1;
+const MAX_REVIEW_COUNT = 20;
+const DEFAULT_REVIEW_COUNT = 10;
+
+const normalizeReviewCount = ( value ) => {
+ const parsedValue = Number.parseInt( value, 10 );
+ const reviewCount = Number.isNaN( parsedValue )
+ ? DEFAULT_REVIEW_COUNT
+ : parsedValue;
+ return Math.max(
+ MIN_REVIEW_COUNT,
+ Math.min( MAX_REVIEW_COUNT, reviewCount )
+ );
+};
export const getBlockControls = ( editMode, setAttributes, buttonTitle ) => (
<BlockControls>
<ToolbarGroup
@@ -100,6 +117,42 @@ export const getSharedReviewContentControls = ( attributes, setAttributes ) => {
}
/>
</ToolsPanelItem>
+ <ToolsPanelItem
+ hasValue={ () => ! attributes.showReviewDate }
+ label={ __( 'Review date', 'woocommerce' ) }
+ onDeselect={ () => setAttributes( { showReviewDate: true } ) }
+ isShownByDefault
+ >
+ <ToggleControl
+ __nextHasNoMarginBottom
+ label={ __( 'Review date', 'woocommerce' ) }
+ checked={ attributes.showReviewDate }
+ onChange={ () =>
+ setAttributes( {
+ showReviewDate: ! attributes.showReviewDate,
+ } )
+ }
+ />
+ </ToolsPanelItem>
+ <ToolsPanelItem
+ hasValue={ () => ! attributes.showReviewContent }
+ label={ __( 'Review content', 'woocommerce' ) }
+ onDeselect={ () =>
+ setAttributes( { showReviewContent: true } )
+ }
+ isShownByDefault
+ >
+ <ToggleControl
+ __nextHasNoMarginBottom
+ label={ __( 'Review content', 'woocommerce' ) }
+ checked={ attributes.showReviewContent }
+ onChange={ () =>
+ setAttributes( {
+ showReviewContent: ! attributes.showReviewContent,
+ } )
+ }
+ />
+ </ToolsPanelItem>
<ToolsPanelItem
hasValue={ () =>
! attributes.showReviewImage ||
@@ -177,77 +230,29 @@ export const getSharedReviewContentControls = ( attributes, setAttributes ) => {
) }
</div>
</ToolsPanelItem>
- <ToolsPanelItem
- hasValue={ () => ! attributes.showReviewDate }
- label={ __( 'Review date', 'woocommerce' ) }
- onDeselect={ () => setAttributes( { showReviewDate: true } ) }
- isShownByDefault
- >
- <ToggleControl
- __nextHasNoMarginBottom
- label={ __( 'Review date', 'woocommerce' ) }
- checked={ attributes.showReviewDate }
- onChange={ () =>
- setAttributes( {
- showReviewDate: ! attributes.showReviewDate,
- } )
- }
- />
- </ToolsPanelItem>
- <ToolsPanelItem
- hasValue={ () => ! attributes.showReviewContent }
- label={ __( 'Review content', 'woocommerce' ) }
- onDeselect={ () =>
- setAttributes( { showReviewContent: true } )
- }
- isShownByDefault
- >
- <ToggleControl
- __nextHasNoMarginBottom
- label={ __( 'Review content', 'woocommerce' ) }
- checked={ attributes.showReviewContent }
- onChange={ () =>
- setAttributes( {
- showReviewContent: ! attributes.showReviewContent,
- } )
- }
- />
- </ToolsPanelItem>
</>
);
};
-export const getSharedReviewListControls = ( attributes, setAttributes ) => {
- const minPerPage = 1;
- const maxPerPage = 20;
+export const getSharedReviewListControls = (
+ attributes,
+ setAttributes,
+ { showOffset = false } = {}
+) => {
+ const defaultOffset = 0;
return (
<>
- <ToolsPanelItem
- hasValue={ () => ! attributes.showOrderby }
- label={ __( 'Order by', 'woocommerce' ) }
- onDeselect={ () => setAttributes( { showOrderby: true } ) }
- isShownByDefault
- >
- <ToggleControl
- __nextHasNoMarginBottom
- label={ __( 'Order by', 'woocommerce' ) }
- checked={ attributes.showOrderby }
- onChange={ () =>
- setAttributes( {
- showOrderby: ! attributes.showOrderby,
- } )
- }
- />
- </ToolsPanelItem>
<ToolsPanelItem
hasValue={ () => attributes.orderby !== 'most-recent' }
- label={ __( 'Order Product Reviews by', 'woocommerce' ) }
+ label={ __( 'Default sort order', 'woocommerce' ) }
onDeselect={ () => setAttributes( { orderby: 'most-recent' } ) }
isShownByDefault
>
<SelectControl
- label={ __( 'Order Product Reviews by', 'woocommerce' ) }
+ __next40pxDefaultSize
+ __nextHasNoMarginBottom
+ label={ __( 'Default sort order', 'woocommerce' ) }
value={ attributes.orderby }
options={ [
{
@@ -267,31 +272,106 @@ export const getSharedReviewListControls = ( attributes, setAttributes ) => {
/>
</ToolsPanelItem>
<ToolsPanelItem
- hasValue={ () => attributes.reviewsOnPageLoad !== 10 }
- label={ __( 'Starting Number of Reviews', 'woocommerce' ) }
- onDeselect={ () => setAttributes( { reviewsOnPageLoad: 10 } ) }
+ hasValue={ () => ! attributes.showOrderby }
+ label={ __( 'Show "Order by" dropdown', 'woocommerce' ) }
+ onDeselect={ () => setAttributes( { showOrderby: true } ) }
isShownByDefault
>
- <RangeControl
- label={ __( 'Starting Number of Reviews', 'woocommerce' ) }
- value={ attributes.reviewsOnPageLoad }
- onChange={ ( reviewsOnPageLoad ) =>
- setAttributes( { reviewsOnPageLoad } )
+ <ToggleControl
+ __nextHasNoMarginBottom
+ label={ __( 'Show "Order by" dropdown', 'woocommerce' ) }
+ checked={ attributes.showOrderby }
+ onChange={ () =>
+ setAttributes( {
+ showOrderby: ! attributes.showOrderby,
+ } )
}
- max={ maxPerPage }
- min={ minPerPage }
/>
</ToolsPanelItem>
+ <ToolsPanelItem
+ hasValue={ () =>
+ attributes.reviewsOnPageLoad !== DEFAULT_REVIEW_COUNT
+ }
+ label={ __( 'Number of reviews', 'woocommerce' ) }
+ onDeselect={ () =>
+ setAttributes( {
+ reviewsOnPageLoad: DEFAULT_REVIEW_COUNT,
+ } )
+ }
+ isShownByDefault
+ >
+ <InputControl
+ __next40pxDefaultSize
+ label={ __( 'Number of reviews', 'woocommerce' ) }
+ value={ String( attributes.reviewsOnPageLoad ) }
+ onChange={ ( value ) => {
+ setAttributes( {
+ reviewsOnPageLoad: normalizeReviewCount( value ),
+ } );
+ } }
+ type="number"
+ max={ MAX_REVIEW_COUNT }
+ min={ MIN_REVIEW_COUNT }
+ step={ 1 }
+ />
+ </ToolsPanelItem>
+ { showOffset && (
+ <ToolsPanelItem
+ hasValue={ () =>
+ ( attributes.offset ?? defaultOffset ) !== defaultOffset
+ }
+ label={ __( 'Offset', 'woocommerce' ) }
+ onDeselect={ () =>
+ setAttributes( { offset: defaultOffset } )
+ }
+ isShownByDefault
+ >
+ <NumberControl
+ __next40pxDefaultSize
+ __unstableStateReducer={ ( state ) =>
+ Object.is( Number( state.value ), -0 )
+ ? {
+ ...state,
+ value: String( defaultOffset ),
+ }
+ : state
+ }
+ label={ __( 'Offset', 'woocommerce' ) }
+ help={ __(
+ 'Number of reviews to skip',
+ 'woocommerce'
+ ) }
+ value={ attributes.offset ?? defaultOffset }
+ onChange={ ( value ) => {
+ if ( value === '' ) {
+ return;
+ }
+
+ const offset = Number( value );
+ if (
+ ! Number.isInteger( offset ) ||
+ offset < defaultOffset
+ ) {
+ return;
+ }
+
+ setAttributes( { offset } );
+ } }
+ min={ defaultOffset }
+ step={ 1 }
+ />
+ </ToolsPanelItem>
+ ) }
<ToolsPanelItem
hasValue={ () =>
! attributes.showLoadMore ||
- attributes.reviewsOnLoadMore !== 10
+ attributes.reviewsOnLoadMore !== DEFAULT_REVIEW_COUNT
}
- label={ __( 'Load more', 'woocommerce' ) }
+ label={ __( 'Show "Load more" button', 'woocommerce' ) }
onDeselect={ () =>
setAttributes( {
showLoadMore: true,
- reviewsOnLoadMore: 10,
+ reviewsOnLoadMore: DEFAULT_REVIEW_COUNT,
} )
}
isShownByDefault
@@ -299,7 +379,7 @@ export const getSharedReviewListControls = ( attributes, setAttributes ) => {
<div className="wc-block-reviews__tools-panel-item-container">
<ToggleControl
__nextHasNoMarginBottom
- label={ __( 'Load more', 'woocommerce' ) }
+ label={ __( 'Show "Load more" button', 'woocommerce' ) }
checked={ attributes.showLoadMore }
onChange={ () =>
setAttributes( {
@@ -308,14 +388,20 @@ export const getSharedReviewListControls = ( attributes, setAttributes ) => {
}
/>
{ attributes.showLoadMore && (
- <RangeControl
- label={ __( 'Load More Reviews', 'woocommerce' ) }
- value={ attributes.reviewsOnLoadMore }
- onChange={ ( reviewsOnLoadMore ) =>
- setAttributes( { reviewsOnLoadMore } )
+ <InputControl
+ __next40pxDefaultSize
+ label={ __( 'Load more reviews', 'woocommerce' ) }
+ value={ String( attributes.reviewsOnLoadMore ) }
+ onChange={ ( value ) =>
+ setAttributes( {
+ reviewsOnLoadMore:
+ normalizeReviewCount( value ),
+ } )
}
- max={ maxPerPage }
- min={ minPerPage }
+ type="number"
+ max={ MAX_REVIEW_COUNT }
+ min={ MIN_REVIEW_COUNT }
+ step={ 1 }
/>
) }
</div>
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/editor-block.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/editor-block.tsx
index 382b184c9bf..3b1db1e76f8 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/editor-block.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/editor-block.tsx
@@ -26,9 +26,11 @@ interface EditorBlockProps {
reviews: Review[];
totalReviews: number;
error?: ErrorObject;
+ hasReviewsHiddenByOffset: boolean;
isLoading: boolean;
noReviewsPlaceholder: React.ComponentType< {
attributes: EditorBlockProps[ 'attributes' ];
+ reason: 'no-reviews' | 'offset';
} >;
}
@@ -40,6 +42,7 @@ class EditorBlock extends Component< EditorBlockProps > {
const {
attributes,
error,
+ hasReviewsHiddenByOffset,
isLoading,
noReviewsPlaceholder: NoReviewsPlaceholder,
reviews,
@@ -57,7 +60,14 @@ class EditorBlock extends Component< EditorBlockProps > {
}
if ( reviews.length === 0 && ! isLoading ) {
- return <NoReviewsPlaceholder attributes={ attributes } />;
+ return (
+ <NoReviewsPlaceholder
+ attributes={ attributes }
+ reason={
+ hasReviewsHiddenByOffset ? 'offset' : 'no-reviews'
+ }
+ />
+ );
}
const reviewRatingsEnabled = getSetting( 'reviewRatingsEnabled', true );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/editor-container-block.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/editor-container-block.tsx
index 384a9f52520..d1dd263da63 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/editor-container-block.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/editor-container-block.tsx
@@ -58,6 +58,7 @@ const EditorContainerBlock = ( {
debounce( callback, 400 )
}
noReviewsPlaceholder={ noReviewsPlaceholder }
+ offset={ attributes.offset ?? 0 }
orderby={ orderby }
order={ order }
productId={ productId }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/frontend-container-block.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/frontend-container-block.tsx
index 977e44f78f2..c36c74233fc 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/frontend-container-block.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/frontend-container-block.tsx
@@ -53,6 +53,13 @@ class FrontendContainerBlock extends Component<
: parseInt( attributes.reviewsOnLoadMore, 10 );
}
+ getOffset() {
+ const { attributes } = this.props;
+ const offset = Number( attributes.offset ?? 0 );
+
+ return Number.isInteger( offset ) && offset >= 0 ? offset : 0;
+ }
+
onAppendReviews() {
const { reviewsToDisplay } = this.state;
@@ -111,6 +118,7 @@ class FrontendContainerBlock extends Component<
onReviewsAppended={ this.onReviewsAppended }
onReviewsLoadError={ this.onReviewsLoadError }
onReviewsReplaced={ this.onReviewsReplaced }
+ offset={ this.getOffset() }
order={ order }
orderby={ orderby }
productId={ productId }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/frontend.ts
index 6c2535c748b..8f76999365d 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/frontend.ts
@@ -17,12 +17,19 @@ const selector = `
const getProps = ( el: HTMLElement ) => {
const showOrderby = el.dataset.showOrderby === 'true';
const showLoadMore = el.dataset.showLoadMore === 'true';
+ const parsedOffset = Number( el.dataset.offset ?? 0 );
+ const offset =
+ Number.isInteger( parsedOffset ) && parsedOffset >= 0
+ ? parsedOffset
+ : 0;
+
const isFilteredReviewsBlock =
el.classList.contains( 'wp-block-woocommerce-reviews-by-product' ) ||
el.classList.contains( 'wp-block-woocommerce-reviews-by-category' );
return {
attributes: {
+ offset,
isFilteredReviewsBlock,
showOrderby,
showLoadMore,
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-category/block.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-category/block.tsx
index db5c0879a9b..3c7b947985c 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-category/block.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-category/block.tsx
@@ -92,12 +92,15 @@ const ReviewsByCategoryEditor = ( {
showOrderby: true,
orderby: 'most-recent',
reviewsOnPageLoad: 10,
+ offset: 0,
showLoadMore: true,
reviewsOnLoadMore: 10,
} )
}
>
- { getSharedReviewListControls( attributes, setAttributes ) }
+ { getSharedReviewListControls( attributes, setAttributes, {
+ showOffset: true,
+ } ) }
</ToolsPanel>
<ToolsPanel
label={ __( 'Category', 'woocommerce' ) }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-category/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-category/index.tsx
index 730eb27c50d..965660765d8 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-category/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-category/index.tsx
@@ -34,6 +34,13 @@ registerBlockType( metadata, {
},
attributes: {
...sharedAttributes,
+ /**
+ * Number of reviews to skip.
+ */
+ offset: {
+ type: 'number',
+ default: 0,
+ },
/**
* The ids of the categories to load reviews for.
*/
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-category/no-reviews-placeholder.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-category/no-reviews-placeholder.tsx
index 963e51bac96..8e4777b5ba7 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-category/no-reviews-placeholder.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-category/no-reviews-placeholder.tsx
@@ -4,7 +4,14 @@
import { __ } from '@wordpress/i18n';
import { Placeholder } from '@wordpress/components';
import { Icon, commentContent } from '@wordpress/icons';
-const NoReviewsPlaceholder = (): JSX.Element => {
+
+interface NoReviewsPlaceholderProps {
+ reason: 'no-reviews' | 'offset';
+}
+
+const NoReviewsPlaceholder = ( {
+ reason,
+}: NoReviewsPlaceholderProps ): JSX.Element => {
return (
<Placeholder
className="wc-block-reviews-by-category"
@@ -16,10 +23,15 @@ const NoReviewsPlaceholder = (): JSX.Element => {
}
label={ __( 'Reviews by Category', 'woocommerce' ) }
>
- { __(
- 'This block lists reviews for products from selected categories. The selected categories do not have any reviews yet, but they will show up here when they do.',
- 'woocommerce'
- ) }
+ { reason === 'offset'
+ ? __(
+ 'No reviews are visible with the current offset. Reduce the offset to display reviews.',
+ 'woocommerce'
+ )
+ : __(
+ 'This block lists reviews for products from selected categories. The selected categories do not have any reviews yet, but they will show up here when they do.',
+ 'woocommerce'
+ ) }
</Placeholder>
);
};
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-category/types.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-category/types.ts
index aa48335d305..86719c6a015 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-category/types.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-category/types.ts
@@ -2,11 +2,13 @@ export interface ReviewsByCategoryEditorProps {
attributes: {
editMode: boolean;
categoryIds: number[];
+ offset: number;
showProductName: boolean;
};
setAttributes: ( attributes: {
editMode?: boolean;
categoryIds?: number[];
+ offset?: number;
showProductName?: boolean;
} ) => void;
debouncedSpeak: ( message: string ) => void;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-product/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-product/edit.tsx
index a1cbba691ec..43cff3894f4 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-product/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-product/edit.tsx
@@ -101,12 +101,15 @@ const ReviewsByProductEditor = ( {
showOrderby: true,
orderby: 'most-recent',
reviewsOnPageLoad: 10,
+ offset: 0,
showLoadMore: true,
reviewsOnLoadMore: 10,
} )
}
>
- { getSharedReviewListControls( attributes, setAttributes ) }
+ { getSharedReviewListControls( attributes, setAttributes, {
+ showOffset: true,
+ } ) }
</ToolsPanel>
<ToolsPanel
label={ __( 'Product', 'woocommerce' ) }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-product/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-product/index.tsx
index f52feac8f54..8d06be15ae9 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-product/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-product/index.tsx
@@ -33,6 +33,13 @@ registerBlockType( metadata, {
},
attributes: {
...sharedAttributes,
+ /**
+ * Number of reviews to skip.
+ */
+ offset: {
+ type: 'number',
+ default: 0,
+ },
/**
* The id of the product to load reviews for.
*/
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-product/no-reviews-placeholder.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-product/no-reviews-placeholder.tsx
index 103f5ee2406..7a3350dd790 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-product/no-reviews-placeholder.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-product/no-reviews-placeholder.tsx
@@ -20,6 +20,7 @@ interface NoReviewsPlaceholderProps {
getProduct: () => void;
isLoading: boolean;
product?: Product;
+ reason: 'no-reviews' | 'offset';
}
const NoReviewsPlaceholder = ( {
@@ -27,6 +28,7 @@ const NoReviewsPlaceholder = ( {
getProduct,
isLoading,
product,
+ reason,
}: NoReviewsPlaceholderProps ) => {
const renderApiError = () => (
<ErrorPlaceholder
@@ -41,19 +43,29 @@ const NoReviewsPlaceholder = ( {
return renderApiError();
}
- const content =
- ! product || isLoading ? (
- <Spinner />
- ) : (
- sprintf(
- /* translators: %s is the product name. */
- __(
- "This block lists reviews for a selected product. %s doesn't have any reviews yet, but they will show up here when it does.",
+ let content =
+ reason === 'offset'
+ ? __(
+ 'No reviews are visible with the current offset. Reduce the offset to display reviews.',
'woocommerce'
- ),
- decodeEntities( product.name )
- )
- );
+ )
+ : undefined;
+
+ if ( ! content ) {
+ content =
+ ! product || isLoading ? (
+ <Spinner />
+ ) : (
+ sprintf(
+ /* translators: %s is the product name. */
+ __(
+ "This block lists reviews for a selected product. %s doesn't have any reviews yet, but they will show up here when it does.",
+ 'woocommerce'
+ ),
+ decodeEntities( product.name )
+ )
+ );
+ }
return (
<Placeholder
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-product/types.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-product/types.ts
index 297d7f62fbe..cf57d287774 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-product/types.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/reviews-by-product/types.ts
@@ -5,6 +5,7 @@ import { BlockEditProps } from '@wordpress/blocks';
interface ReviewByProductAttributes {
editMode: boolean;
+ offset: number;
productId: number;
}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/types.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/types.ts
index 2bef274784c..264dafa7967 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/types.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/types.ts
@@ -17,6 +17,7 @@ export interface Attributes {
categoryIds?: number[];
editMode?: boolean;
imageType?: string;
+ offset?: number;
orderby?: string;
productId?: number;
reviewsOnLoadMore?: number;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/utils.js b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/utils.js
index f20df688d12..dad33ad3332 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/utils.js
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/reviews/utils.js
@@ -85,6 +85,7 @@ export const getDataAttrs = ( attributes ) => {
const {
categoryIds,
imageType,
+ offset,
orderby,
productId,
reviewsOnPageLoad,
@@ -102,6 +103,11 @@ export const getDataAttrs = ( attributes ) => {
'data-show-orderby': showOrderby,
};
+ const normalizedOffset = Number( offset );
+ if ( Number.isInteger( normalizedOffset ) && normalizedOffset > 0 ) {
+ data[ 'data-offset' ] = normalizedOffset;
+ }
+
if ( productId ) {
data[ 'data-product-id' ] = productId;
}
diff --git a/plugins/woocommerce/client/blocks/changelog/66286-add-review-block-offset b/plugins/woocommerce/client/blocks/changelog/66286-add-review-block-offset
new file mode 100644
index 00000000000..8e069f9a110
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/changelog/66286-add-review-block-offset
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add offset controls to the Reviews by Product and Reviews by Category blocks.
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/reviews-by-category/reviews-by-category.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/reviews-by-category/reviews-by-category.block_theme.spec.ts
index addfe575572..09e686b50bb 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/reviews-by-category/reviews-by-category.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/reviews-by-category/reviews-by-category.block_theme.spec.ts
@@ -6,7 +6,7 @@ import { expect, test } from '@woocommerce/e2e-utils';
/**
* Internal dependencies
*/
-import { hoodieReviews } from '../../../test-data/blocks/data/data';
+import { hoodieReviews, allReviews } from '../../../test-data/blocks/data/data';
const latestReview = hoodieReviews[ hoodieReviews.length - 1 ];
@@ -105,8 +105,72 @@ test.describe( `${ BLOCK_NAME } Block`, () => {
await editor.publishAndVisitPost();
const block = await frontendUtils.getBlockByName( BLOCK_NAME );
+
await expect(
block.locator( '.wc-block-components-review-list-item__text' )
).toHaveCount( 0 );
} );
+
+ test( 'can skip reviews with an offset and load subsequent reviews', async ( {
+ page,
+ admin,
+ frontendUtils,
+ editor,
+ } ) => {
+ await admin.createNewPost();
+ await editor.insertBlock( { name: BLOCK_NAME } );
+ const blockLocator = await editor.getBlockByName( BLOCK_NAME );
+ await blockLocator
+ .getByRole( 'checkbox', {
+ name: 'Clothing',
+ exact: true,
+ } )
+ .check();
+ await blockLocator
+ .getByRole( 'checkbox', { name: /Accessories/ } )
+ .check();
+ await blockLocator
+ .getByRole( 'button', {
+ name: 'Done',
+ } )
+ .click();
+
+ await editor.openDocumentSettingsSidebar();
+ const sidebarSettings = page.getByRole( 'region', {
+ name: 'Editor settings',
+ } );
+ await sidebarSettings
+ .getByRole( 'spinbutton', { name: 'Number of reviews' } )
+ .fill( '10' );
+ await sidebarSettings
+ .getByRole( 'spinbutton', { name: 'Offset' } )
+ .fill( '1' );
+
+ await expect(
+ editor.canvas.getByText( allReviews[ 0 ].review )
+ ).toBeVisible();
+ await expect(
+ editor.canvas.getByText( allReviews[ 1 ].review )
+ ).toBeVisible();
+ await expect(
+ editor.canvas.getByText( allReviews[ 2 ].review )
+ ).toBeVisible();
+ await expect(
+ editor.canvas.getByText( allReviews[ 3 ].review )
+ ).toBeVisible();
+
+ await expect(
+ editor.canvas.getByText( allReviews[ 4 ].review )
+ ).toBeHidden();
+
+ await editor.publishAndVisitPost();
+
+ const block = await frontendUtils.getBlockByName( BLOCK_NAME );
+
+ await expect( block.getByText( allReviews[ 0 ].review ) ).toBeVisible();
+ await expect( block.getByText( allReviews[ 1 ].review ) ).toBeVisible();
+ await expect( block.getByText( allReviews[ 2 ].review ) ).toBeVisible();
+ await expect( block.getByText( allReviews[ 3 ].review ) ).toBeVisible();
+ await expect( block.getByText( allReviews[ 4 ].review ) ).toBeHidden();
+ } );
} );
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/reviews-by-product/reviews-by-product.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/reviews-by-product/reviews-by-product.block_theme.spec.ts
index bf155726a36..3973bcda605 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/reviews-by-product/reviews-by-product.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/reviews-by-product/reviews-by-product.block_theme.spec.ts
@@ -101,8 +101,61 @@ test.describe( `${ BLOCK_NAME } Block`, () => {
await editor.publishAndVisitPost();
const block = await frontendUtils.getBlockByName( BLOCK_NAME );
+
await expect(
block.locator( '.wc-block-components-review-list-item__text' )
).toHaveCount( 0 );
} );
+
+ test( 'can skip reviews with an offset in the editor and frontend', async ( {
+ page,
+ admin,
+ frontendUtils,
+ editor,
+ } ) => {
+ await admin.createNewPost();
+ await editor.insertBlock( { name: BLOCK_NAME } );
+ const productCheckbox = editor.canvas.getByLabel(
+ 'Hoodie, has 3 reviews'
+ );
+ await productCheckbox.check();
+ await editor.canvas
+ .getByRole( 'button', {
+ name: 'Done',
+ } )
+ .click();
+
+ await editor.openDocumentSettingsSidebar();
+ const sidebarSettings = page.getByRole( 'region', {
+ name: 'Editor settings',
+ } );
+
+ await sidebarSettings
+ .getByRole( 'spinbutton', { name: 'Offset' } )
+ .fill( '1' );
+
+ await expect(
+ editor.canvas.getByText( hoodieReviews[ 0 ].review )
+ ).toBeVisible();
+ await expect(
+ editor.canvas.getByText( hoodieReviews[ 1 ].review )
+ ).toBeVisible();
+ await expect(
+ editor.canvas.getByText( hoodieReviews[ 2 ].review )
+ ).toBeHidden();
+
+ await editor.publishAndVisitPost();
+
+ const block = await frontendUtils.getBlockByName( BLOCK_NAME );
+
+ await expect(
+ block.getByText( hoodieReviews[ 0 ].review )
+ ).toBeVisible();
+ await expect(
+ block.getByText( hoodieReviews[ 1 ].review )
+ ).toBeVisible();
+ await expect(
+ block.getByText( hoodieReviews[ 2 ].review )
+ ).toBeHidden();
+ } );
} );