Commit f9f311f5b9d for woocommerce

commit f9f311f5b9d7856bb0006f65caa829ef4e0b5c94
Author: Taha Paksu <3295+tpaksu@users.noreply.github.com>
Date:   Thu May 7 17:11:36 2026 +0300

    Add per-item review form row and submit gate (#64526)

    * Add accessible star-rating control for Review Order page

    Native `<input type="radio">` ✕ 5 wrapped in a `role="radiogroup"` container,
    visually replaced by SVG stars. Progressive enhancement adds:

    - Keyboard nav: ArrowLeft/Up, ArrowRight/Down, Home, End. Selecting also
      moves focus and dispatches `change`, so any future submission listener
      reacts naturally.
    - Dynamic caption underneath the stars, populated from the focused/checked
      radio's `data-label`. Defaults match the existing WC product-review
      labels (Very poor / Not that bad / Average / Good / Perfect) and are
      filterable via `woocommerce_review_order_rating_labels`.
    - High-contrast focus ring on the wrapping label whenever its input is
      `:focus-visible`.
    - Hover preview lights stars left of the cursor; selection persists when
      the cursor leaves.

    Without JavaScript the underlying radio group still works as a plain
    form field, so the page stays functional.

    Files:

    - `src/Internal/OrderReviews/StarRating.php` — server-side renderer.
      Builds the markup and exposes `get_labels()` (filter-aware, with safe
      fallbacks for buggy filters that drop slot keys).
    - `templates/order/star-rating.php` — theme-overridable partial.
    - `client/legacy/js/frontend/order-review.js` — vanilla JS, no jQuery.
    - `client/legacy/css/order-review.scss` — visual states.
    - `Internal/OrderReviews/Endpoint::enqueue_assets()` — script + style
      enqueued only when the review-order endpoint is actually rendering.
    - `templates/order/customer-review-order.php` — temporary preview render
      per item so reviewers can interact with the control. M4 (WOOPLUG-6595)
      swaps this preview for the real per-item form row.

    Tests:

    - `Internal\OrderReviews\StarRatingTest` — markup, defaults, filter
      override, filter fallback for missing keys.

    Closes #64308 (WOOPLUG-6594).

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Remove duplicate changelog file (workflow generates the canonical one)

    * Address Copilot feedback on star-rating control PR

    - Route enqueue URLs through woocommerce_get_asset_url filter so
      CDN/asset-rewrite setups continue to work.
    - Add screen-reader-only 'Required' text alongside the visual asterisk
      on the rating label.
    - Correct the JS file header comment: only Arrow keys + Home/End are
      handled, not Space/Enter.

    * Fix StarRating docblock to use flat @param syntax

    The PHPDoc shape syntax (array{...}) trips the WC PHPCS sniff, which
    expects a single @param line per parameter. Reverted to plain
    @param array with the key descriptions in the body.

    * Drop the planning comment + minor tidy on star-rating styles

    * Load the RTL stylesheet variant on RTL sites

    wp_style_add_data( 'wc-order-review', 'rtl', 'replace' ) tells the
    classic-assets pipeline to swap order-review.css for order-review-rtl.css
    on right-to-left locales.

    * Trim defensive fallbacks and dedupe asset enqueue helpers

    - StarRating::get_labels(): replace the manual 1-5 fallback loop and
      reorder block with array_replace + array_intersect_key.
    - StarRating::render(): drop the unused $selected pre-cast; read it
      inline only when it's used.
    - Endpoint::enqueue_assets(): factor a local closure for the
      woocommerce_get_asset_url filter so we don't repeat the same call
      for the style and the script.
    - order/star-rating.php template: drop the data-caption-id attribute
      that duplicated aria-describedby.
    - order-review.js: read the caption id from aria-describedby and drop
      the empty-input early-out (init() already filters by class).

    No behaviour change. All six StarRating tests still pass.

    * Address review feedback on the star-rating control

    - Add @since 10.8.0 annotations on StarRating::render() and
      StarRating::get_labels() docblocks (per ayushpahwa).
    - Bound the `selected` argument: values outside 0-5 fall back to
      no-selection rather than passing an unrenderable value to the
      template (per ayushpahwa).
    - Cover the bound with a regression test.

    * Address remaining review feedback on the star-rating control

    - CSS: drop every `:has()` selector. The template now renders the
      inputs in reverse DOM order (5..1) and uses `flex-direction:
      row-reverse`; selected/hover/focus states are driven by the `~`
      sibling combinator and `:focus-visible + label`. Caption gets
      `order: -1` so it stays on the visual right.
    - JS: keyboard navigation flips so ArrowRight/Down still moves
      visually right (DOM-previous) and Home/End map to the visual
      leftmost/rightmost stars.
    - Template: pre-compute `$initial_caption` so the caption element no
      longer mixes selection logic, escaping, and output in a single
      expression.
    - Endpoint: enqueue the JS with `strategy => defer` (in addition to
      `in_footer => true`) since it only attaches DOM-ready listeners.

    * Add accessible star-rating control for Review Order page

    Native `<input type="radio">` ✕ 5 wrapped in a `role="radiogroup"` container,
    visually replaced by SVG stars. Progressive enhancement adds:

    - Keyboard nav: ArrowLeft/Up, ArrowRight/Down, Home, End. Selecting also
      moves focus and dispatches `change`, so any future submission listener
      reacts naturally.
    - Dynamic caption underneath the stars, populated from the focused/checked
      radio's `data-label`. Defaults match the existing WC product-review
      labels (Very poor / Not that bad / Average / Good / Perfect) and are
      filterable via `woocommerce_review_order_rating_labels`.
    - High-contrast focus ring on the wrapping label whenever its input is
      `:focus-visible`.
    - Hover preview lights stars left of the cursor; selection persists when
      the cursor leaves.

    Without JavaScript the underlying radio group still works as a plain
    form field, so the page stays functional.

    Files:

    - `src/Internal/OrderReviews/StarRating.php` — server-side renderer.
      Builds the markup and exposes `get_labels()` (filter-aware, with safe
      fallbacks for buggy filters that drop slot keys).
    - `templates/order/star-rating.php` — theme-overridable partial.
    - `client/legacy/js/frontend/order-review.js` — vanilla JS, no jQuery.
    - `client/legacy/css/order-review.scss` — visual states.
    - `Internal/OrderReviews/Endpoint::enqueue_assets()` — script + style
      enqueued only when the review-order endpoint is actually rendering.
    - `templates/order/customer-review-order.php` — temporary preview render
      per item so reviewers can interact with the control. M4 (WOOPLUG-6595)
      swaps this preview for the real per-item form row.

    Tests:

    - `Internal\OrderReviews\StarRatingTest` — markup, defaults, filter
      override, filter fallback for missing keys.

    Closes #64308 (WOOPLUG-6594).

    * Add per-item review form row and submit gate

    Replaces the temporary preview render from #64525 with the real per-item
    form that the Customer Review Request page submits to. Submission handler
    itself lands in M4 (WOOPLUG-6596).

    `templates/order/customer-review-order-row.php` (new, theme-overridable):

    - Linked product title that opens the product page in a new tab.
    - 120×120 thumbnail.
    - Hidden `product_id` and `order_item_id` so the row is identifiable
      on submit.
    - The accessible star-rating control from #64525, indexed by `row_index`
      for stable POST data.
    - Textarea labelled "Your review" with the i18n placeholder
      "Share your experience with this product...".
    - `do_action( 'woocommerce_review_order_form_fields', $item, $product,
      $order, $row_index )` after the textarea so extensions can inject
      fields without overriding the whole template.

    `templates/order/customer-review-order.php`:

    - Wraps the item list in `<form class="woocommerce-review-order__form"
      method="post" action="">` with a hidden `order_id`, hidden `key`
      (echoed back from the URL the Endpoint already validated), and the
      `woocommerce_submit_order_reviews` nonce.
    - "Submit reviews" button rendered disabled by default. The JS module
      enables it as soon as any row has a rating selected.

    `client/legacy/js/frontend/order-review.js` gains `initSubmitGate(form)`,
    which listens for `change` events on `.woocommerce-star-rating__input`
    inside the form and toggles the button's `disabled` state.

    - StarRating's docblock simplified to plain `@param array` so the WC
      PHPCS sniffs don't misparse the structured array shorthand.

    Closes #64309 (WOOPLUG-6595).

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Drop M-numbered milestone references from per-item row templates

    * Strip non-layout styling from per-item row; let theme drive typography

    * Stack the per-item row vertically on viewports under 600px

    Folded in from the (now-closed) design-tokens-responsive PR; the only
    contribution that survived the simplification was the 12-line media
    query, which fits naturally with the per-item layout introduced here.

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Wire customer-review-order.php to the per-item row partial

    The 6595 changes had been dropped during cascading rebases: the form
    wrapper, hidden inputs, foreach loop calling
    wc_get_template('order/customer-review-order-row.php', ...), and the
    submit button were all missing. Restored so each item actually renders
    the row partial and the submit gate's selectors exist in the DOM.

    * Strengthen the new-tab product link

    - rel='noopener noreferrer' (was just 'noopener') to also prevent
      referrer leakage in some browsers/configurations.
    - Add a .screen-reader-text 'opens in a new tab' suffix so assistive
      tech users are warned the link spawns a new window.

    * Address per-item-row review feedback

    - Collapse the .woocommerce-review-order__item top border to a single
      declaration; the previous form layered border-top-color over a
      currentColor border, which was redundant.
    - Log a console.warn from initSubmitGate when the submit button is
      missing so theme overrides that drop it surface the cause.

    * Wrap the long console.warn message to satisfy eslint max-len

    * Address CodeRabbit feedback on the per-item form PR

    - order-review.js: expose syncSubmit on the form element as
      form.syncReviewOrderSubmitGate so external code (the AJAX submission
      handler in #64527) can re-evaluate the disabled state after async
      state changes. Matches the PR description.
    - customer-review-order.php: pre-filter $items to renderable rows
      (line items with a still-existing product) and gate the <form>
      render on the filtered list, so an order with all-deleted products
      doesn't show a form whose submit button can't do anything useful.

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/64526-wooplug-6595-per-item-review-form-row b/plugins/woocommerce/changelog/64526-wooplug-6595-per-item-review-form-row
new file mode 100644
index 00000000000..903aacf48dc
--- /dev/null
+++ b/plugins/woocommerce/changelog/64526-wooplug-6595-per-item-review-form-row
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add the per-item review form row on the Review Order page: linked product title, thumbnail, accessible star rating, review textarea, and an extensions hook (`woocommerce_review_order_form_fields`). Submit button is gated on at least one rating being selected. Narrow-viewport layout stacks the row vertically below 600px.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/wooplug-6594-star-rating-control b/plugins/woocommerce/changelog/wooplug-6594-star-rating-control
new file mode 100644
index 00000000000..7477460ff57
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooplug-6594-star-rating-control
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add the accessible 5-star rating control used by the Review Order page. Native radio inputs with keyboard navigation (arrows, Home, End), dynamic caption, and visible focus ring; filterable labels via `woocommerce_review_order_rating_labels`.
diff --git a/plugins/woocommerce/client/legacy/css/order-review.scss b/plugins/woocommerce/client/legacy/css/order-review.scss
index 06ea797fe44..da3954e638d 100644
--- a/plugins/woocommerce/client/legacy/css/order-review.scss
+++ b/plugins/woocommerce/client/legacy/css/order-review.scss
@@ -2,14 +2,69 @@
  * Review Order page styles.
  *
  * Intentionally minimal. The active theme drives typography, color, and
- * button styling; only the interactive star-rating control needs bespoke
- * CSS to map radio inputs to SVG stars.
+ * button styling; only the per-item row layout and the interactive
+ * star-rating control need bespoke CSS.
  *
  * The star control renders with `flex-direction: row-reverse`: the DOM
  * order is 5..1 so that sibling combinators (`~`) can fill all stars up
  * to the selected/hovered one without relying on `:has()`.
  */

+.woocommerce-review-order {
+	&__items {
+		list-style: none;
+		padding: 0;
+		margin: 0 0 1.5em;
+	}
+
+	&__item {
+		padding: 1.5em 0;
+		border-top: 1px solid rgba(0, 0, 0, 0.1);
+
+		&:first-child {
+			border-top: 0;
+			padding-top: 0;
+		}
+	}
+
+	&__item-row {
+		display: flex;
+		gap: 1.5em;
+		align-items: flex-start;
+	}
+
+	&__item-image {
+		flex: 0 0 120px;
+		max-width: 120px;
+	}
+
+	&__item-fields {
+		flex: 1 1 auto;
+		min-width: 0;
+		display: flex;
+		flex-direction: column;
+		gap: 0.75em;
+	}
+
+	&__item-review-textarea {
+		width: 100%;
+		min-height: 6em;
+		resize: vertical;
+	}
+
+	@media (max-width: 600px) {
+		&__item-row {
+			flex-direction: column;
+		}
+
+		&__item-image {
+			flex: 0 0 auto;
+			max-width: 240px;
+			width: 100%;
+		}
+	}
+}
+
 .woocommerce-star-rating {
 	display: inline-flex;
 	flex-direction: row-reverse;
diff --git a/plugins/woocommerce/client/legacy/js/frontend/order-review.js b/plugins/woocommerce/client/legacy/js/frontend/order-review.js
index 9106800c736..9a6810368d0 100644
--- a/plugins/woocommerce/client/legacy/js/frontend/order-review.js
+++ b/plugins/woocommerce/client/legacy/js/frontend/order-review.js
@@ -73,9 +73,59 @@
 		syncCaption();
 	}

+	/**
+	 * Enable / disable the review-order submit button based on whether at
+	 * least one row has a rating selected.
+	 *
+	 * @param {HTMLFormElement} form `.woocommerce-review-order__form`
+	 */
+	function initSubmitGate( form ) {
+		var submit = form.querySelector( '.woocommerce-review-order__submit' );
+		if ( ! submit ) {
+			if ( window.console && window.console.warn ) {
+				window.console.warn(
+					'Review Order form is missing its submit button ' +
+						'(.woocommerce-review-order__submit); ' +
+						'the rating-based gate will not run.'
+				);
+			}
+			return;
+		}
+
+		function syncSubmit() {
+			var anyChecked = !! form.querySelector(
+				'.woocommerce-star-rating__input:checked'
+			);
+			submit.disabled = ! anyChecked;
+		}
+
+		// Expose so external code (e.g. the AJAX submission handler in #64527)
+		// can re-evaluate the gate after async state changes.
+		form.syncReviewOrderSubmitGate = syncSubmit;
+
+		form.addEventListener( 'change', function ( event ) {
+			if (
+				event.target &&
+				event.target.classList &&
+				event.target.classList.contains(
+					'woocommerce-star-rating__input'
+				)
+			) {
+				syncSubmit();
+			}
+		} );
+
+		syncSubmit();
+	}
+
 	function init() {
 		var groups = document.querySelectorAll( '.woocommerce-star-rating' );
 		Array.prototype.forEach.call( groups, initGroup );
+
+		var forms = document.querySelectorAll(
+			'.woocommerce-review-order__form'
+		);
+		Array.prototype.forEach.call( forms, initSubmitGate );
 	}

 	if ( document.readyState === 'loading' ) {
diff --git a/plugins/woocommerce/templates/order/customer-review-order-row.php b/plugins/woocommerce/templates/order/customer-review-order-row.php
new file mode 100644
index 00000000000..31128db5ccf
--- /dev/null
+++ b/plugins/woocommerce/templates/order/customer-review-order-row.php
@@ -0,0 +1,110 @@
+<?php
+/**
+ * Customer Review Order — per-item form row.
+ *
+ * Theme-overridable. Copy to `yourtheme/woocommerce/order/customer-review-order-row.php`.
+ *
+ * Renders one product per row: linked title, thumbnail, hidden inputs that
+ * tie the submission back to the order item, the accessible star-rating
+ * control, and the review textarea.
+ *
+ * @see https://woocommerce.com/document/template-structure/
+ * @package WooCommerce\Templates
+ * @version 10.8.0
+ *
+ * @var WC_Order_Item_Product $item       Order line item being rendered.
+ * @var WC_Product            $product    Product attached to the line item.
+ * @var WC_Order              $order      Order being reviewed.
+ * @var int                   $row_index  Zero-based row index, used in input names.
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+if ( ! $item instanceof WC_Order_Item_Product || ! $product instanceof WC_Product || ! $order instanceof WC_Order ) {
+	return;
+}
+
+$item_id         = $item->get_id();
+$product_id      = $product->get_id();
+$product_link    = $product->is_visible() ? get_permalink( $product_id ) : '';
+$product_name    = $item->get_name();
+$image_html      = $product->get_image( 'woocommerce_thumbnail' );
+$rating_label_id = 'woocommerce-review-rating-label-' . $item_id;
+$review_label_id = 'woocommerce-review-text-label-' . $item_id;
+$review_input_id = 'woocommerce-review-text-' . $item_id;
+
+$rating_control = \Automattic\WooCommerce\Internal\OrderReviews\StarRating::render(
+	array(
+		'name'      => 'reviews[' . $row_index . '][rating]',
+		'id_prefix' => 'woocommerce-review-rating-' . $item_id,
+		'label_id'  => $rating_label_id,
+	)
+);
+?>
+<li class="woocommerce-review-order__item" data-row-index="<?php echo esc_attr( (string) $row_index ); ?>">
+	<p class="woocommerce-review-order__item-title">
+		<?php if ( $product_link ) : ?>
+			<a href="<?php echo esc_url( $product_link ); ?>" target="_blank" rel="noopener noreferrer">
+				<?php echo esc_html( $product_name ); ?>
+				<span class="screen-reader-text"><?php esc_html_e( '(opens in a new tab)', 'woocommerce' ); ?></span>
+			</a>
+		<?php else : ?>
+			<?php echo esc_html( $product_name ); ?>
+		<?php endif; ?>
+	</p>
+
+	<div class="woocommerce-review-order__item-row">
+		<div class="woocommerce-review-order__item-image">
+			<?php echo $image_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_image() returns escaped HTML. ?>
+		</div>
+
+		<div class="woocommerce-review-order__item-fields">
+			<input type="hidden" name="reviews[<?php echo esc_attr( (string) $row_index ); ?>][product_id]" value="<?php echo esc_attr( (string) $product_id ); ?>" />
+			<input type="hidden" name="reviews[<?php echo esc_attr( (string) $row_index ); ?>][order_item_id]" value="<?php echo esc_attr( (string) $item_id ); ?>" />
+
+			<div class="woocommerce-review-order__item-rating">
+				<p id="<?php echo esc_attr( $rating_label_id ); ?>" class="woocommerce-review-order__item-rating-label">
+					<?php
+					printf(
+						'%1$s <span class="required" aria-hidden="true">*</span><span class="screen-reader-text"> %2$s</span>',
+						esc_html__( 'Your rating', 'woocommerce' ),
+						esc_html__( 'Required', 'woocommerce' )
+					);
+					?>
+				</p>
+				<?php echo $rating_control; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- StarRating::render() returns escaped HTML. ?>
+			</div>
+
+			<div class="woocommerce-review-order__item-review">
+				<label id="<?php echo esc_attr( $review_label_id ); ?>" for="<?php echo esc_attr( $review_input_id ); ?>" class="woocommerce-review-order__item-review-label">
+					<?php esc_html_e( 'Your review', 'woocommerce' ); ?>
+				</label>
+				<textarea
+					id="<?php echo esc_attr( $review_input_id ); ?>"
+					class="woocommerce-review-order__item-review-textarea"
+					name="reviews[<?php echo esc_attr( (string) $row_index ); ?>][text]"
+					rows="3"
+					placeholder="<?php esc_attr_e( 'Share your experience with this product...', 'woocommerce' ); ?>"
+				></textarea>
+			</div>
+
+			<?php
+			/**
+			 * Fires after the rating + textarea inside a Review Order form row.
+			 *
+			 * Lets extensions inject extra fields (e.g. an "I recommend this"
+			 * checkbox) without overriding the whole template. Echo HTML directly
+			 * — the surrounding container expects no return value.
+			 *
+			 * @since 10.8.0
+			 *
+			 * @param WC_Order_Item_Product $item       The line item being reviewed.
+			 * @param WC_Product            $product    The associated product.
+			 * @param WC_Order              $order      The order.
+			 * @param int                   $row_index  Zero-based row index for input names.
+			 */
+			do_action( 'woocommerce_review_order_form_fields', $item, $product, $order, $row_index );
+			?>
+		</div>
+	</div>
+</li>
diff --git a/plugins/woocommerce/templates/order/customer-review-order.php b/plugins/woocommerce/templates/order/customer-review-order.php
index aecd676209b..a634755b026 100644
--- a/plugins/woocommerce/templates/order/customer-review-order.php
+++ b/plugins/woocommerce/templates/order/customer-review-order.php
@@ -68,6 +68,27 @@ $meta_parts = array_filter(
  * @param WC_Order        $order The order being reviewed.
  */
 $items = (array) apply_filters( 'woocommerce_review_order_eligible_items', $order->get_items(), $order );
+
+// Pre-filter to the rows we can actually render so the <form> doesn't open
+// when every item is non-product or has a deleted product.
+$renderable_rows = array();
+foreach ( $items as $item ) {
+	if ( ! $item instanceof WC_Order_Item_Product ) {
+		continue;
+	}
+	$product = $item->get_product();
+	if ( ! $product instanceof WC_Product ) {
+		continue;
+	}
+	$renderable_rows[] = array(
+		'item'    => $item,
+		'product' => $product,
+	);
+}
+
+// The Endpoint has already validated the URL key against the order key, so the
+// canonical value on the order is the right thing to echo into the form post.
+$order_key = (string) $order->get_order_key();
 ?>
 <div class="woocommerce-review-order">
 	<p class="woocommerce-review-order__meta">
@@ -86,37 +107,40 @@ $items = (array) apply_filters( 'woocommerce_review_order_eligible_items', $orde
 		<?php esc_html_e( '* Mandatory fields', 'woocommerce' ); ?>
 	</p>

-	<?php if ( ! empty( $items ) ) : ?>
-		<ul class="woocommerce-review-order__items">
-			<?php foreach ( $items as $item ) : ?>
+	<?php if ( ! empty( $renderable_rows ) ) : ?>
+		<form
+			class="woocommerce-review-order__form"
+			method="post"
+			novalidate
+		>
+			<input type="hidden" name="order_id" value="<?php echo esc_attr( (string) $order->get_id() ); ?>" />
+			<input type="hidden" name="key" value="<?php echo esc_attr( $order_key ); ?>" />
+			<?php wp_nonce_field( 'woocommerce_submit_order_reviews', '_wcnonce' ); ?>
+
+			<ul class="woocommerce-review-order__items">
 				<?php
-				if ( ! $item instanceof WC_Order_Item_Product ) {
-					continue;
-				}
-				$product = $item->get_product();
-				if ( ! $product instanceof WC_Product ) {
-					continue;
+				foreach ( $renderable_rows as $row_index => $row ) {
+					wc_get_template(
+						'order/customer-review-order-row.php',
+						array(
+							'item'      => $row['item'],
+							'product'   => $row['product'],
+							'order'     => $order,
+							'row_index' => $row_index,
+						)
+					);
 				}
-				$product_link = $product->is_visible() ? get_permalink( $product->get_id() ) : '';
-				$product_name = $item->get_name();
-				$image_html   = $product->get_image( 'woocommerce_thumbnail' );
 				?>
-				<li class="woocommerce-review-order__item">
-					<p class="woocommerce-review-order__item-title">
-						<?php if ( $product_link ) : ?>
-							<a href="<?php echo esc_url( $product_link ); ?>"><?php echo esc_html( $product_name ); ?></a>
-						<?php else : ?>
-							<?php echo esc_html( $product_name ); ?>
-						<?php endif; ?>
-					</p>
-					<div class="woocommerce-review-order__item-row">
-						<div class="woocommerce-review-order__item-image">
-							<?php echo $image_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_image() returns escaped HTML. ?>
-						</div>
-						<div class="woocommerce-review-order__item-form-placeholder"></div>
-					</div>
-				</li>
-			<?php endforeach; ?>
-		</ul>
+			</ul>
+
+			<div class="woocommerce-review-order__actions">
+				<button
+					type="submit"
+					class="woocommerce-review-order__submit button"
+				>
+					<?php esc_html_e( 'Submit reviews', 'woocommerce' ); ?>
+				</button>
+			</div>
+		</form>
 	<?php endif; ?>
 </div>