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 &ldquo;%s&rdquo;', '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 .= '&nbsp;<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() ? '&nbsp;<span class="required">*</span>' : '' ) .
+					'</label><select name="rating" id="rating" required>
+					<option value="">' . esc_html__( 'Rate&hellip;', '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' ) . '&nbsp;<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';
 		}

 		/**