Commit 714bf08ef7 for woocommerce
commit 714bf08ef77c5e858157e2698fa01dbd3ad540da
Author: louwie17 <lourensschep@gmail.com>
Date: Fri Apr 18 08:11:26 2025 +0200
Add product review form block (#56477)
* Add initial review form block
* Fix star rating issue
* Add review wrapper
* Add review disable/enable logic
* Update styling of the comment reply title
* Fix font weigth and size styling of form
* Fix lint errors
* Add verified owner warning
* Make sure reviews are not rendered when not enabled
* Add changelog
* Remove experimental usage
* Remove unneeded styles and remove use of frontend script
* Revert "Fix star rating issue"
This reverts commit 9a7d66141f727a9498cb5ac832cb54dd6700e0ab.
---------
Co-authored-by: Tung Du <dinhtungdu@gmail.com>
diff --git a/plugins/woocommerce/changelog/add-55729-product-review-form-block b/plugins/woocommerce/changelog/add-55729-product-review-form-block
new file mode 100644
index 0000000000..2a7fe91daf
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-55729-product-review-form-block
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add new experimental product review form block for use within the product reviews block.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-form/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-form/block.json
new file mode 100644
index 0000000000..2861913efe
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-form/block.json
@@ -0,0 +1,59 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "woocommerce/product-review-form",
+ "title": "Product Reviews Form",
+ "category": "woocommerce",
+ "description": "Display a product's reviews form.",
+ "textdomain": "woocommerce",
+ "attributes": {
+ "textAlign": {
+ "type": "string"
+ }
+ },
+ "usesContext": [ "postId", "postType" ],
+ "supports": {
+ "html": false,
+ "color": {
+ "gradients": true,
+ "heading": true,
+ "link": true,
+ "__experimentalDefaultControls": {
+ "background": true,
+ "text": true
+ }
+ },
+ "spacing": {
+ "margin": true,
+ "padding": true
+ },
+ "typography": {
+ "fontSize": true,
+ "lineHeight": true,
+ "__experimentalFontStyle": true,
+ "__experimentalFontWeight": true,
+ "__experimentalLetterSpacing": true,
+ "__experimentalTextTransform": true,
+ "__experimentalDefaultControls": {
+ "fontSize": true
+ }
+ },
+ "__experimentalBorder": {
+ "radius": true,
+ "color": true,
+ "width": true,
+ "style": true,
+ "__experimentalDefaultControls": {
+ "radius": true,
+ "color": true,
+ "width": true,
+ "style": true
+ }
+ }
+ },
+ "example": {
+ "attributes": {
+ "textAlign": "center"
+ }
+ }
+}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-form/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-form/edit.tsx
new file mode 100644
index 0000000000..fb1ee4805d
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-form/edit.tsx
@@ -0,0 +1,61 @@
+/**
+ * External dependencies
+ */
+import clsx from 'clsx';
+import {
+ AlignmentControl,
+ BlockControls,
+ useBlockProps,
+} from '@wordpress/block-editor';
+import { VisuallyHidden } from '@wordpress/components';
+import { useInstanceId } from '@wordpress/compose';
+import { __, sprintf } from '@wordpress/i18n';
+import type { BlockEditProps } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import CommentsForm from './form';
+import './editor.scss';
+
+export default function PostCommentsFormEdit( {
+ attributes,
+ context,
+ setAttributes,
+}: BlockEditProps< {
+ textAlign: string;
+} > & {
+ context: { postId: string; postType: string };
+} ) {
+ const { textAlign } = attributes;
+ const { postId, postType } = context;
+
+ const instanceId = useInstanceId( PostCommentsFormEdit );
+ const instanceIdDesc = sprintf( 'comments-form-edit-%d-desc', instanceId );
+
+ const blockProps = useBlockProps( {
+ className: clsx( 'comment-respond', {
+ [ `has-text-align-${ textAlign }` ]: textAlign,
+ } ),
+ 'aria-describedby': instanceIdDesc,
+ } );
+
+ return (
+ <>
+ <BlockControls group="block">
+ <AlignmentControl
+ value={ textAlign }
+ onChange={ ( nextAlign: string ) => {
+ setAttributes( { textAlign: nextAlign } );
+ } }
+ />
+ </BlockControls>
+ <div { ...blockProps }>
+ <CommentsForm postId={ postId } postType={ postType } />
+ <VisuallyHidden id={ instanceIdDesc }>
+ { __( 'Reviews form disabled in editor.', 'woocommerce' ) }
+ </VisuallyHidden>
+ </div>
+ </>
+ );
+}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-form/editor.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-form/editor.scss
new file mode 100644
index 0000000000..6bca7d3584
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-form/editor.scss
@@ -0,0 +1,8 @@
+.wp-block-woocommerce-product-review-form {
+ .comment-form-rating {
+ .wp-block-woocommerce-product-reviews__editor__stars {
+ display: block;
+ margin: 0 0 10px;
+ }
+ }
+}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-form/form.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-form/form.tsx
new file mode 100644
index 0000000000..d26b89f7bb
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-form/form.tsx
@@ -0,0 +1,140 @@
+/**
+ * External dependencies
+ */
+import clsx from 'clsx';
+import { __, _x } from '@wordpress/i18n';
+import {
+ Warning,
+ __experimentalGetElementClassName,
+} from '@wordpress/block-editor';
+import { Button, Disabled } from '@wordpress/components';
+import { useInstanceId } from '@wordpress/compose';
+import { useSelect, useDispatch } from '@wordpress/data';
+import { store as coreStore } from '@wordpress/core-data';
+import { productsStore } from '@woocommerce/data';
+
+const CommentsFormPlaceholder = () => {
+ const instanceId = useInstanceId( CommentsFormPlaceholder );
+
+ return (
+ <div>
+ <span
+ id="reply-title"
+ className="comment-reply-title"
+ role="heading"
+ aria-level={ 3 }
+ >
+ { __( 'Add a review', 'woocommerce' ) }
+ </span>
+ <form
+ noValidate
+ className="review-form"
+ onSubmit={ ( event ) => event.preventDefault() }
+ >
+ <div className="comment-form-rating">
+ <span>{ __( 'Your rating *', 'woocommerce' ) }</span>
+ <p className="wp-block-woocommerce-product-reviews__editor__stars"></p>
+ </div>
+ <p>
+ <label htmlFor={ `review-${ instanceId }` }>
+ { __( 'Your review *', 'woocommerce' ) }
+ </label>
+ <textarea
+ id={ `review-${ instanceId }` }
+ name="review"
+ cols={ 45 }
+ rows={ 8 }
+ readOnly
+ />
+ </p>
+ <p className="form-submit wp-block-button">
+ <input
+ name="submit"
+ type="submit"
+ className={ clsx(
+ 'wp-block-button__link',
+ __experimentalGetElementClassName( 'button' )
+ ) }
+ value={ __( 'Submit', 'woocommerce' ) }
+ aria-disabled="true"
+ />
+ </p>
+ </form>
+ </div>
+ );
+};
+
+const CommentsForm = ( {
+ postId,
+ postType,
+}: {
+ postId: string;
+ postType: string;
+} ) => {
+ const { updateProduct } = useDispatch( productsStore );
+ const product = useSelect(
+ ( select ) => {
+ return select( productsStore ).getProduct( Number( postId ) );
+ },
+ [ postId ]
+ );
+
+ const setReviewsAllowed = ( allowed: boolean ) => {
+ updateProduct( Number( postId ), {
+ reviews_allowed: allowed,
+ } );
+ };
+
+ const isSiteEditor = postType === undefined || postId === undefined;
+
+ const postTypeSupportsComments = useSelect(
+ ( select ) =>
+ postType
+ ? !! select( coreStore ).getPostType( postType )?.supports
+ .comments
+ : false,
+ [ postType ]
+ );
+
+ if ( ! isSiteEditor && product && ! product?.reviews_allowed ) {
+ const actions = [
+ <Button
+ __next40pxDefaultSize
+ key="enableReviews"
+ onClick={ () => setReviewsAllowed( true ) }
+ variant="primary"
+ >
+ { _x(
+ 'Enable reviews',
+ 'action that affects the current product',
+ 'woocommerce'
+ ) }
+ </Button>,
+ ];
+ return (
+ <Warning actions={ actions }>
+ { __(
+ 'Product Reviews Form block: Reviews are not enabled for this product.',
+ 'woocommerce'
+ ) }
+ </Warning>
+ );
+ } else if ( ! postTypeSupportsComments ) {
+ return (
+ <Warning>
+ { __(
+ 'Product Reviews Form block: Reviews are not enabled.',
+ 'woocommerce'
+ ) }
+ </Warning>
+ );
+ }
+
+ return (
+ <Disabled>
+ <CommentsFormPlaceholder />
+ </Disabled>
+ );
+};
+
+export default CommentsForm;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-form/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-form/index.tsx
new file mode 100644
index 0000000000..a22432e570
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-form/index.tsx
@@ -0,0 +1,18 @@
+/**
+ * External dependencies
+ */
+import { registerBlockType } from '@wordpress/blocks';
+import { postCommentsForm as icon } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import metadata from './block.json';
+import edit from './edit';
+import './style.scss';
+
+// @ts-expect-error metadata is not typed.
+registerBlockType( metadata, {
+ icon,
+ edit,
+} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-form/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-form/style.scss
new file mode 100644
index 0000000000..e511acfdd9
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/inner-blocks/review-form/style.scss
@@ -0,0 +1,83 @@
+// Allow these default styles to be overridden by global styles.
+:where(.wp-block-woocommerce-product-review-form) {
+ textarea,
+ input:not([type="submit"]) {
+ border: 1px solid $gray-600;
+ font-size: 1em;
+ font-family: inherit;
+ }
+
+ textarea,
+ input:where(:not([type="submit"]):not([type="checkbox"])) {
+ padding: calc(0.667em + 2px); // The extra 2px is added to match outline buttons.
+ }
+}
+
+.wp-block-woocommerce-product-review-form {
+ // This block has customizable padding, border-box makes that more predictable.
+ box-sizing: border-box;
+
+ &[style*="font-weight"].comment-respond :where(.comment-reply-title) {
+ font-weight: inherit;
+ }
+ &[style*="font-family"].comment-respond :where(.comment-reply-title) {
+ font-family: inherit;
+ }
+ &[class*="-font-size"].comment-respond :where(.comment-reply-title),
+ &[style*="font-size"].comment-respond :where(.comment-reply-title) {
+ font-size: inherit;
+ }
+ &[style*="line-height"].comment-respond :where(.comment-reply-title) {
+ line-height: inherit;
+ }
+ &[style*="font-style"].comment-respond :where(.comment-reply-title) {
+ font-style: inherit;
+ }
+ &[style*="letter-spacing"].comment-respond :where(.comment-reply-title) {
+ letter-spacing: inherit;
+ }
+
+ // Styles copied from button block styles.
+ :where(input[type="submit"]) {
+ box-shadow: none;
+ cursor: pointer;
+ display: inline-block;
+ text-align: center;
+ overflow-wrap: break-word;
+ }
+
+ .review-form {
+ textarea,
+ // Make sure to not set display block on hidden input fields, to prevent
+ // the Safari bug experienced in https://github.com/WordPress/gutenberg/issues/50830
+ input:not([type="submit"]):not([type="checkbox"]):not([type="hidden"]) {
+ display: block;
+ box-sizing: border-box;
+ width: 100%;
+ }
+ }
+
+ .comment-form-author,
+ .comment-form-email,
+ .comment-form-url {
+ label {
+ display: block;
+ margin-bottom: 0.25em;
+ }
+ }
+
+ .comment-form-cookies-consent {
+ display: flex;
+ gap: 0.25em;
+
+ #wp-comment-cookies-consent {
+ margin-top: 0.35em;
+ }
+ }
+
+ .comment-reply-title {
+ font-size: var(--wp--preset--font-size--medium);
+ font-weight: 700;
+ margin-bottom: 0;
+ }
+}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/template.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/template.ts
index c80c641c7b..476f9aef7a 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/template.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-reviews/template.ts
@@ -88,7 +88,7 @@ const TEMPLATE: InnerBlockTemplate[] = [
],
],
[ 'core/comments-pagination' ],
- [ 'core/post-comments-form' ],
+ [ 'woocommerce/product-review-form' ],
];
export default TEMPLATE;
diff --git a/plugins/woocommerce/client/blocks/bin/webpack-entries.js b/plugins/woocommerce/client/blocks/bin/webpack-entries.js
index 102f8c9f46..e770a87a8c 100644
--- a/plugins/woocommerce/client/blocks/bin/webpack-entries.js
+++ b/plugins/woocommerce/client/blocks/bin/webpack-entries.js
@@ -200,6 +200,9 @@ const blocks = {
'product-reviews-title': {
customDir: 'product-reviews/inner-blocks/reviews-title',
},
+ 'product-review-form': {
+ customDir: 'product-reviews/inner-blocks/review-form',
+ },
};
/**
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Reviews/ProductReviewForm.php b/plugins/woocommerce/src/Blocks/BlockTypes/Reviews/ProductReviewForm.php
new file mode 100644
index 0000000000..7341e485b8
--- /dev/null
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/Reviews/ProductReviewForm.php
@@ -0,0 +1,156 @@
+<?php declare( strict_types = 1 );
+namespace Automattic\WooCommerce\Blocks\BlockTypes\Reviews;
+
+use Automattic\WooCommerce\Blocks\BlockTypes\AbstractBlock;
+
+/**
+ * ProductReviewForm class.
+ */
+class ProductReviewForm extends AbstractBlock {
+ /**
+ * Block name.
+ *
+ * @var string
+ */
+ protected $block_name = 'product-review-form';
+
+ /**
+ * Get the frontend script handle for this block type.
+ *
+ * @see $this->register_block_type()
+ * @param string $key Data to get, or default to everything.
+ * @return array|string|null
+ */
+ protected function get_block_type_script( $key = null ) {
+ return null;
+ }
+
+ /**
+ * Render the block.
+ *
+ * @param array $attributes Block attributes.
+ * @param string $content Block content.
+ * @param WP_Block $block Block instance.
+ * @return string Rendered block content.
+ */
+ protected function render( $attributes, $content, $block ) {
+ if ( ! isset( $block->context['postId'] ) ) {
+ return '';
+ }
+
+ if ( post_password_required( $block->context['postId'] ) ) {
+ return;
+ }
+
+ $product = wc_get_product( $block->context['postId'] );
+
+ if ( ! $product ) {
+ return '';
+ }
+
+ if ( get_option( 'woocommerce_review_rating_verification_required' ) !== 'no' && ! wc_customer_bought_product( '', get_current_user_id(), $product->get_id() ) ) {
+ return '<p class="woocommerce-verification-required">' . esc_html__( 'Only logged in customers who have purchased this product may leave a review.', 'woocommerce' ) . '</p>';
+ }
+
+ $classes = array( 'comment-respond' );
+ if ( isset( $attributes['textAlign'] ) ) {
+ $classes[] = 'has-text-align-' . $attributes['textAlign'];
+ }
+ if ( isset( $attributes['style']['elements']['link']['color']['text'] ) ) {
+ $classes[] = 'has-link-color';
+ }
+ $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => implode( ' ', $classes ) ) );
+
+ $commenter = wp_get_current_commenter();
+ $comment_form = array(
+ /* translators: %s is product title */
+ 'title_reply' => have_comments() ? esc_html__( 'Add a review', 'woocommerce' ) : sprintf( esc_html__( 'Be the first to review “%s”', 'woocommerce' ), get_the_title() ),
+ /* translators: %s is product title */
+ 'title_reply_to' => esc_html__( 'Leave a Reply to %s', 'woocommerce' ),
+ 'title_reply_before' => '<span id="reply-title" class="comment-reply-title" role="heading" aria-level="3">',
+ 'title_reply_after' => '</span>',
+ 'comment_notes_after' => '',
+ 'label_submit' => esc_html__( 'Submit', 'woocommerce' ),
+ 'logged_in_as' => '',
+ 'comment_field' => '',
+ );
+
+ $name_email_required = (bool) get_option( 'require_name_email', 1 );
+ $fields = array(
+ 'author' => array(
+ 'label' => __( 'Name', 'woocommerce' ),
+ 'type' => 'text',
+ 'value' => $commenter['comment_author'],
+ 'required' => $name_email_required,
+ 'autocomplete' => 'name',
+ ),
+ 'email' => array(
+ 'label' => __( 'Email', 'woocommerce' ),
+ 'type' => 'email',
+ 'value' => $commenter['comment_author_email'],
+ 'required' => $name_email_required,
+ 'autocomplete' => 'email',
+ ),
+ );
+
+ $comment_form['fields'] = array();
+
+ foreach ( $fields as $key => $field ) {
+ $field_html = '<p class="comment-form-' . esc_attr( $key ) . '">';
+ $field_html .= '<label for="' . esc_attr( $key ) . '">' . esc_html( $field['label'] );
+
+ if ( $field['required'] ) {
+ $field_html .= ' <span class="required">*</span>';
+ }
+
+ $field_html .= '</label><input id="' . esc_attr( $key ) . '" name="' . esc_attr( $key ) . '" type="' . esc_attr( $field['type'] ) . '" autocomplete="' . esc_attr( $field['autocomplete'] ) . '" value="' . esc_attr( $field['value'] ) . '" size="30" ' . ( $field['required'] ? 'required' : '' ) . ' /></p>';
+
+ $comment_form['fields'][ $key ] = $field_html;
+ }
+
+ $account_page_url = wc_get_page_permalink( 'myaccount' );
+ if ( $account_page_url ) {
+ /* translators: %s opening and closing link tags respectively */
+ $comment_form['must_log_in'] = '<p class="must-log-in">' . sprintf( esc_html__( 'You must be %1$slogged in%2$s to post a review.', 'woocommerce' ), '<a href="' . esc_url( $account_page_url ) . '">', '</a>' ) . '</p>';
+ }
+
+ if ( wc_review_ratings_enabled() ) {
+ $comment_form['comment_field'] = '<div class="comment-form-rating"><label for="rating" id="comment-form-rating-label">' .
+ esc_html__( 'Your rating', 'woocommerce' ) .
+ ( wc_review_ratings_required() ? ' <span class="required">*</span>' : '' ) .
+ '</label><select name="rating" id="rating" required>
+ <option value="">' . esc_html__( 'Rate…', 'woocommerce' ) . '</option>
+ <option value="5">' . esc_html__( 'Perfect', 'woocommerce' ) . '</option>
+ <option value="4">' . esc_html__( 'Good', 'woocommerce' ) . '</option>
+ <option value="3">' . esc_html__( 'Average', 'woocommerce' ) . '</option>
+ <option value="2">' . esc_html__( 'Not that bad', 'woocommerce' ) . '</option>
+ <option value="1">' . esc_html__( 'Very poor', 'woocommerce' ) . '</option>
+ </select></div>';
+ }
+
+ $comment_form['comment_field'] .= '<p class="comment-form-comment"><label for="comment">' . esc_html__( 'Your review', 'woocommerce' ) . ' <span class="required">*</span></label><textarea id="comment" name="comment" cols="45" rows="8" required></textarea></p>';
+
+ add_filter( 'comment_form_defaults', 'post_comments_form_block_form_defaults' );
+
+ ob_start();
+ echo '<div id="review_form_wrapper"><div id="review_form">';
+ /**
+ * Filters the comment form arguments.
+ *
+ * @since 9.9.0
+ * @param array $comment_form The comment form arguments.
+ * @param int $post_id The post ID.
+ */
+ comment_form( apply_filters( 'woocommerce_product_review_comment_form_args', $comment_form ), $block->context['postId'] );
+ echo '</div></div>';
+ $form = ob_get_clean();
+
+ remove_filter( 'comment_form_defaults', 'post_comments_form_block_form_defaults' );
+
+ $form = str_replace( 'class="comment-respond"', $wrapper_attributes, $form );
+
+ wp_enqueue_script( 'comment-reply' );
+
+ return $form;
+ }
+}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Reviews/ProductReviews.php b/plugins/woocommerce/src/Blocks/BlockTypes/Reviews/ProductReviews.php
index 7533d9f346..588b81a6ba 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/Reviews/ProductReviews.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/Reviews/ProductReviews.php
@@ -34,4 +34,20 @@ class ProductReviews extends AbstractBlock {
protected function get_block_type_script( $key = null ) {
return null;
}
+
+ /**
+ * Render the block.
+ *
+ * @param array $attributes Block attributes.
+ * @param string $content Block content.
+ * @param WP_Block $block Block instance.
+ *
+ * @return string Rendered block output.
+ */
+ protected function render( $attributes, $content, $block ) {
+ if ( ! comments_open() ) {
+ return '';
+ }
+ return $content;
+ }
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypesController.php b/plugins/woocommerce/src/Blocks/BlockTypesController.php
index ed881b0596..11aee505fa 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypesController.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypesController.php
@@ -532,6 +532,7 @@ final class BlockTypesController {
$block_types[] = 'Reviews\ProductReviews';
$block_types[] = 'Reviews\ProductReviewRating';
$block_types[] = 'Reviews\ProductReviewsTitle';
+ $block_types[] = 'Reviews\ProductReviewForm';
}
/**