Commit d0196346674 for woocommerce

commit d0196346674b7c0daa248f5869596838a97c91d5
Author: Tom Cafferkey <tjcafferkey@gmail.com>
Date:   Fri Oct 23 15:38:59 2026 +0100

    Paginate order notes (#70012)

    * Add opt-in pagination for order notes APIs

    * Add changelog entry for order notes API pagination

    * UI pagination

    * Remove stale PHPStan baseline entries for order notes

    * Remove second changelog

    * Update plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js

    Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

    * move test

    * Limit AJAX order note responses to the first page

    * margin right

    * Make order item AJAX notes pagination opt-in

    ---------

    Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/add-order-notes-pagination-ui b/plugins/woocommerce/changelog/add-order-notes-pagination-ui
new file mode 100644
index 00000000000..c19c5b272fc
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-order-notes-pagination-ui
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Browse order notes ten at a time with page navigation in the order editor.
diff --git a/plugins/woocommerce/client/legacy/css/admin.scss b/plugins/woocommerce/client/legacy/css/admin.scss
index 5a60cd3b9b4..5d287d21f69 100644
--- a/plugins/woocommerce/client/legacy/css/admin.scss
+++ b/plugins/woocommerce/client/legacy/css/admin.scss
@@ -2315,7 +2315,7 @@ body.woocommerce_page_wc-customer-stock-notifications {
 	margin: 0;
 	padding: 0;

-	button {
+	button:not([data-page-action]) {
 		margin: 1px;
 		vertical-align: top;
 	}
@@ -3082,6 +3082,82 @@ ul.order_notes {
 	}
 }

+.order-notes-pagination {
+	border-top: 1px solid $gray-200;
+	padding: 12px;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	flex-wrap: wrap;
+	gap: 4px;
+	color: $subtext;
+	font-size: 11px;
+
+	&[hidden] {
+		display: none;
+	}
+
+	.order-notes-page-controls {
+		display: flex;
+		align-items: center;
+		gap: 2px;
+	}
+
+	button {
+		background: none;
+		border: 0;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		padding: 0;
+		margin: 0;
+		width: 24px;
+		height: 28px;
+		color: $gray-900;
+		cursor: pointer;
+
+		&:disabled {
+			color: $gray-400;
+			cursor: default;
+		}
+
+		&:focus-visible {
+			outline: 2px solid var(--wp-admin-theme-color, #2271b1);
+			outline-offset: 1px;
+		}
+	}
+
+	#order-notes-page {
+		width: 30px;
+		min-height: 28px;
+		padding: 0 2px;
+		margin: 0;
+		text-align: center;
+		appearance: textfield;
+		margin-right: 2px;
+
+		&::-webkit-inner-spin-button,
+		&::-webkit-outer-spin-button {
+			appearance: none;
+			margin: 0;
+		}
+	}
+
+	.order-notes-total-pages {
+		white-space: nowrap;
+	}
+
+	&:dir(rtl) .dashicons,
+	&:dir(rtl) svg {
+		transform: scaleX(-1);
+	}
+}
+
+.order-notes-error {
+	padding: 0 12px 12px;
+	color: $red;
+}
+
 .add_note {
 	border-bottom: 1px solid $gray-200;
 	padding: 16px 12px;
@@ -8260,7 +8336,7 @@ table.bar_chart {
 		clear: both;
 	}

-	#poststuff #woocommerce-order-notes .inside button {
+	#poststuff #woocommerce-order-notes .inside button:not([data-page-action]) {
 		margin: 0 1px 1px;
 	}

@@ -8282,7 +8358,7 @@ table.bar_chart {
 			}
 		}

-		#poststuff #woocommerce-order-notes .inside button {
+		#poststuff #woocommerce-order-notes .inside button:not([data-page-action]) {
 			line-height: 40px;
 		}
 	}
diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js
index 0d6d7daa515..197b087b2ce 100644
--- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js
+++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-order.js
@@ -593,10 +593,7 @@ jQuery( function ( $ ) {
 							$( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html );

 							// Update notes.
-							const notesEl = document.querySelector( '#woocommerce-order-notes ul.order_notes' );
-							if ( notesEl && response.data && typeof response.data.notes_html === 'string' ) {
-								notesEl.outerHTML = response.data.notes_html;
-							}
+							wc_meta_boxes_order_notes.load_notes( 1 );

 							wc_meta_boxes_order_items.reloaded_items();
 							wc_meta_boxes_order_items.unblock();
@@ -622,6 +619,7 @@ jQuery( function ( $ ) {

 			var data = $.extend( {}, wc_meta_boxes_order_items.get_taxable_address(), {
 				action : 'woocommerce_remove_order_coupon',
+				paginate_notes: true,
 				dataType : 'json',
 				order_id : woocommerce_admin_meta_boxes.post_id,
 				security : woocommerce_admin_meta_boxes.order_item_nonce,
@@ -637,8 +635,7 @@ jQuery( function ( $ ) {

 					// Update notes.
 					if ( response.data.notes_html ) {
-						$( 'ul.order_notes' ).empty();
-						$( 'ul.order_notes' ).append( $( response.data.notes_html ).find( 'li' ) );
+						wc_meta_boxes_order_notes.load_notes( 1 );
 					}

 					wc_meta_boxes_order_items.reloaded_items();
@@ -855,6 +852,7 @@ jQuery( function ( $ ) {
 					order_id      : woocommerce_admin_meta_boxes.post_id,
 					order_item_ids: order_item_id,
 					action        : 'woocommerce_remove_order_item',
+					paginate_notes: true,
 					security      : woocommerce_admin_meta_boxes.order_item_nonce
 				} );

@@ -875,10 +873,7 @@ jQuery( function ( $ ) {
 							$( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html );

 							// Update notes.
-							const notesEl = document.querySelector( '#woocommerce-order-notes ul.order_notes' );
-							if ( notesEl && response.data && typeof response.data.notes_html === 'string' ) {
-								notesEl.outerHTML = response.data.notes_html;
-							}
+							wc_meta_boxes_order_notes.load_notes( 1 );

 							wc_meta_boxes_order_items.reloaded_items();
 							wc_meta_boxes_order_items.unblock();
@@ -1027,6 +1022,7 @@ jQuery( function ( $ ) {
 				order_id: woocommerce_admin_meta_boxes.post_id,
 				items:    $( 'table.woocommerce_order_items :input[name], .wc-order-totals-items :input[name]' ).serialize(),
 				action:   'woocommerce_save_order_items',
+				paginate_notes: true,
 				security: woocommerce_admin_meta_boxes.order_item_nonce
 			};

@@ -1045,8 +1041,7 @@ jQuery( function ( $ ) {

 						// Update notes.
 						if ( response.data.notes_html ) {
-							$( 'ul.order_notes' ).empty();
-							$( 'ul.order_notes' ).append( $( response.data.notes_html ).find( 'li' ) );
+							wc_meta_boxes_order_notes.load_notes( 1 );
 						}

 						wc_meta_boxes_order_items.reloaded_items();
@@ -1776,6 +1771,7 @@ jQuery( function ( $ ) {

 				var data = $.extend( {}, wc_meta_boxes_order_items.get_taxable_address(), {
 					action   : 'woocommerce_add_order_item',
+					paginate_notes: true,
 					order_id : woocommerce_admin_meta_boxes.post_id,
 					security : woocommerce_admin_meta_boxes.order_item_nonce,
 					data     : add_items
@@ -1798,10 +1794,7 @@ jQuery( function ( $ ) {
 							$( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html );

 							// Update notes.
-							const notesEl = document.querySelector( '#woocommerce-order-notes ul.order_notes' );
-							if ( notesEl && response.data && typeof response.data.notes_html === 'string' ) {
-								notesEl.outerHTML = response.data.notes_html;
-							}
+							wc_meta_boxes_order_notes.load_notes( 1 );

 							wc_meta_boxes_order_items.reloaded_items();
 							wc_meta_boxes_order_items.unblock();
@@ -1894,6 +1887,9 @@ jQuery( function ( $ ) {
 	 * Order Notes Panel
 	 */
 	var wc_meta_boxes_order_notes = {
+		loading: false,
+		pending_page: null,
+
 		init: function() {
 			$( '#woocommerce-order-notes' )
 				.on( 'click', 'button.add_note', this.add_order_note )
@@ -1902,6 +1898,128 @@ jQuery( function ( $ ) {

 			// Sync the CTA + helper link to reflect the current select state on load.
 			$( 'select#order_note_type' ).trigger( 'change' );
+
+			var panel = document.getElementById( 'woocommerce-order-notes' );
+			if ( ! panel || panel.dataset.paginationInitialized ) {
+				return;
+			}
+			panel.dataset.paginationInitialized = 'true';
+
+			panel.addEventListener( 'click', function( event ) {
+				if ( event.target.closest( '.order-notes-retry' ) ) {
+					wc_meta_boxes_order_notes.load_notes( Number( panel.querySelector( '.order-notes-error' ).dataset.page ) );
+					return;
+				}
+				var button = event.target.closest( '[data-page-action]' );
+				if ( ! button || button.disabled ) {
+					return;
+				}
+				var pagination = button.closest( '.order-notes-pagination' );
+				var currentPage = Number( pagination.dataset.page );
+				var pages = {
+					first: 1,
+					previous: currentPage - 1,
+					next: currentPage + 1,
+					last: Number( pagination.dataset.totalPages )
+				};
+				wc_meta_boxes_order_notes.load_notes( pages[ button.dataset.pageAction ] );
+			} );
+
+			panel.addEventListener( 'change', function( event ) {
+				if ( event.target.id === 'order-notes-page' ) {
+					wc_meta_boxes_order_notes.change_page( event.target );
+				}
+			} );
+			panel.addEventListener( 'keydown', function( event ) {
+				if ( event.target.id === 'order-notes-page' && event.key === 'Enter' ) {
+					event.preventDefault();
+					wc_meta_boxes_order_notes.change_page( event.target );
+				}
+			} );
+		},
+
+		change_page: function( input ) {
+			var pagination = input.closest( '.order-notes-pagination' );
+			var currentPage = Number( pagination.dataset.page );
+			var requestedPage = Number( input.value );
+			var page = Number.isFinite( requestedPage )
+				? Math.max( 1, Math.min( Math.trunc( requestedPage ), Number( input.max ) ) )
+				: currentPage;
+			input.value = page;
+			if ( page !== currentPage ) {
+				wc_meta_boxes_order_notes.load_notes( page );
+			}
+		},
+
+		load_notes: function( page ) {
+			var panel = document.getElementById( 'woocommerce-order-notes' );
+			var list = panel && panel.querySelector( '.order-notes-list' );
+			if ( ! list ) {
+				return;
+			}
+			if ( wc_meta_boxes_order_notes.loading ) {
+				wc_meta_boxes_order_notes.pending_page = page || Number( panel.querySelector( '.order-notes-pagination' ).dataset.page );
+				return;
+			}
+
+			var pagination = panel.querySelector( '.order-notes-pagination' );
+			var activeElement = document.activeElement;
+			var restoreFocus = !! activeElement && pagination.contains( activeElement );
+			var pageAction = activeElement && activeElement.dataset ? activeElement.dataset.pageAction : undefined;
+			var error = panel.querySelector( '.order-notes-error' );
+			var requestedPage = page || Number( pagination.dataset.page );
+			var show_error = function() {
+				error.dataset.page = requestedPage;
+				error.hidden = false;
+				panel.querySelector( '#order-notes-page' ).value = pagination.dataset.page;
+			};
+			error.hidden = true;
+			list.setAttribute( 'aria-busy', 'true' );
+			wc_meta_boxes_order_notes.loading = true;
+			$( panel ).block( {
+				message: null,
+				overlayCSS: { background: '#fff', opacity: 0.6 }
+			} );
+
+			$.ajax( {
+				url: woocommerce_admin_meta_boxes.ajax_url,
+				type: 'POST',
+				dataType: 'json',
+				data: {
+					action: 'woocommerce_get_order_notes',
+					order_id: woocommerce_admin_meta_boxes.post_id,
+					page: requestedPage,
+					security: woocommerce_admin_meta_boxes.get_order_notes_nonce
+				},
+				success: function( response ) {
+					if ( ! response || ! response.success || ! response.data || typeof response.data.html !== 'string' ) {
+						show_error();
+						return;
+					}
+					list.innerHTML = response.data.html;
+					if ( window.wp && window.wp.a11y && typeof window.wp.a11y.speak === 'function' ) {
+						var announcement = list.querySelector( '.order-notes-page-announcement' ).textContent.trim();
+						var count = list.querySelector( '.order-notes-count' ).textContent.trim();
+						window.wp.a11y.speak( announcement + ' ' + count, 'polite' );
+					}
+				},
+				error: show_error,
+				complete: function() {
+					wc_meta_boxes_order_notes.loading = false;
+					list.removeAttribute( 'aria-busy' );
+					$( panel ).unblock();
+					if ( restoreFocus && ( document.activeElement === document.body || document.activeElement === activeElement ) ) {
+						var button = pageAction && list.querySelector( '[data-page-action="' + pageAction + '"]' );
+						var target = button && ! button.disabled ? button : list.querySelector( '#order-notes-page' );
+						target.focus();
+					}
+					if ( wc_meta_boxes_order_notes.pending_page !== null ) {
+						var nextPage = wc_meta_boxes_order_notes.pending_page;
+						wc_meta_boxes_order_notes.pending_page = null;
+						wc_meta_boxes_order_notes.load_notes( nextPage );
+					}
+				}
+			} );
 		},

 		update_note_type_ui: function() {
@@ -1939,9 +2057,11 @@ jQuery( function ( $ ) {
 			};

 			$.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) {
-				$( 'ul.order_notes .no-items' ).remove();
-				$( 'ul.order_notes' ).prepend( response );
-				$( '#woocommerce-order-notes' ).unblock();
+				if ( ! response || '-1' === response.trim() ) {
+					$( '#woocommerce-order-notes' ).unblock();
+					return;
+				}
+				wc_meta_boxes_order_notes.load_notes( 1 );
 				$( '#add_order_note' ).val( '' );

 				// Announce the result to screen readers (WCAG 4.1.3).
@@ -1959,7 +2079,9 @@ jQuery( function ( $ ) {
 					note_type: data.note_type || 'private',
 					status: $( '#order_status' ).val()
 				} );
-			});
+			}).fail( function() {
+				$( '#woocommerce-order-notes' ).unblock();
+			} );

 			return false;
 		},
@@ -1986,7 +2108,11 @@ jQuery( function ( $ ) {
 					security: woocommerce_admin_meta_boxes.delete_order_note_nonce
 				};

-				$.post( woocommerce_admin_meta_boxes.ajax_url, data, function() {
+				$.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) {
+					$( note ).unblock();
+					if ( '-1' === String( response ).trim() ) {
+						return;
+					}
 					if ( window.wcTracks && window.wcTracks.recordEvent ) {
 						var noteType = note.hasClass( 'customer-note' ) ? 'customer' : 'private';
 						var dateStr  = note.find( '.exact-date' ).attr( 'title' );
@@ -2001,12 +2127,10 @@ jQuery( function ( $ ) {
 							seconds_since_added: seconds
 						} );
 					}
-					$( note ).remove();
-					var $list = $( 'ul.order_notes' );
-					if ( 0 === $list.find( 'li.note' ).length ) {
-						$list.append( $( '<li class="no-items"></li>' ).text( woocommerce_admin_meta_boxes.i18n_no_notes_yet ) );
-					}
-				});
+					wc_meta_boxes_order_notes.load_notes();
+				}).fail( function() {
+					$( note ).unblock();
+				} );
 			}

 			return false;
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
index c9ac1862db5..39accd8a333 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
@@ -700,6 +700,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
 					'grant_access_nonce'                              => wp_create_nonce( 'grant-access' ),
 					'revoke_access_nonce'                             => wp_create_nonce( 'revoke-access' ),
 					'add_order_note_nonce'                            => wp_create_nonce( 'add-order-note' ),
+					'get_order_notes_nonce'                           => wp_create_nonce( 'get-order-notes' ),
 					'delete_order_note_nonce'                         => wp_create_nonce( 'delete-order-note' ),
 					'calendar_image'                                  => WC()->plugin_url() . '/assets/images/calendar.png',
 					'post_id'                                         => $this->is_order_meta_box_screen( $screen_id ) && isset( $order_or_post_object ) ? \Automattic\WooCommerce\Utilities\OrderUtil::get_post_or_order_id( $order_or_post_object ) : $post_id,
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-notes.php b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-notes.php
index e4ba9f52e00..a28d19688dd 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-notes.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-order-notes.php
@@ -26,13 +26,8 @@ class WC_Meta_Box_Order_Notes {
 			$order_id = $post->ID;
 		}

-		$args = array( 'order_id' => $order_id );
-
-		if ( 0 !== $order_id ) {
-			$notes = wc_get_order_notes( $args );
-		} else {
-			$notes = array();
-		}
+		$pagination = self::get_page( (int) $order_id );
+		$notes      = $pagination['notes'];

 		$private_button_label  = __( 'Add private note', 'woocommerce' );
 		$customer_button_label = __( 'Send note to customer →', 'woocommerce' );
@@ -69,7 +64,81 @@ class WC_Meta_Box_Order_Notes {
 				<button type="button" class="add_note button"><?php echo esc_html( $private_button_label ); ?></button>
 			</p>
 		</div>
+		<div class="order-notes-list">
+			<?php
+			include __DIR__ . '/views/html-order-notes.php';
+			include __DIR__ . '/views/html-order-notes-pagination.php';
+			?>
+		</div>
+		<p class="order-notes-error" role="alert" hidden>
+			<?php esc_html_e( 'Could not load order notes.', 'woocommerce' ); ?>
+			<button type="button" class="button-link order-notes-retry"><?php esc_html_e( 'Try again', 'woocommerce' ); ?></button>
+		</p>
 		<?php
+	}
+
+	/**
+	 * Get a page of notes and its pagination details.
+	 *
+	 * @since 11.3.0
+	 * @param int $order_id Order ID.
+	 * @param int $page     Requested page.
+	 * @return array
+	 */
+	public static function get_page( int $order_id, int $page = 1 ): array {
+		$total = 0;
+		if ( $order_id > 0 ) {
+			remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10 );
+			$total = (int) get_comments(
+				array(
+					'post_id' => $order_id,
+					'type'    => 'order_note',
+					'status'  => 'approve',
+					'count'   => true,
+				)
+			);
+
+			add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );
+		}
+
+		$total_pages = max( 1, (int) ceil( $total / 10 ) );
+		$page        = max( 1, min( $page, $total_pages ) );
+		$notes       = $total > 0 ? wc_get_order_notes(
+			array(
+				'order_id' => $order_id,
+				'limit'    => 10,
+				'page'     => $page,
+			)
+		) : array();
+
+		return compact( 'notes', 'total', 'page', 'total_pages' );
+	}
+
+	/**
+	 * Render a page of notes for an authenticated AJAX request.
+	 *
+	 * @since 11.3.0
+	 */
+	public static function get_order_notes(): void {
+		check_ajax_referer( 'get-order-notes', 'security' );
+
+		$order_id = isset( $_POST['order_id'] ) && is_scalar( $_POST['order_id'] ) ? absint( wp_unslash( (string) $_POST['order_id'] ) ) : 0;
+		// phpcs:ignore WordPress.WP.Capabilities.Unknown -- WooCommerce maps this per-order meta capability.
+		if ( ! $order_id || ! current_user_can( 'edit_shop_order', $order_id ) ) {
+			wp_send_json_error( null, 403 );
+		}
+
+		if ( ! wc_get_order( $order_id ) ) {
+			wp_send_json_error( null, 404 );
+		}
+
+		$page       = isset( $_POST['page'] ) && is_scalar( $_POST['page'] ) ? absint( wp_unslash( (string) $_POST['page'] ) ) : 1;
+		$pagination = self::get_page( $order_id, $page );
+		$notes      = $pagination['notes'];
+
+		ob_start();
 		include __DIR__ . '/views/html-order-notes.php';
+		include __DIR__ . '/views/html-order-notes-pagination.php';
+		wp_send_json_success( array( 'html' => ob_get_clean() ) );
 	}
 }
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-notes-pagination.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-notes-pagination.php
new file mode 100644
index 00000000000..db7312cec28
--- /dev/null
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-order-notes-pagination.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * Order notes pagination for the meta box.
+ *
+ * @package WooCommerce\Admin
+ * @var array $pagination Pagination details.
+ * @var array $notes      Notes on this page.
+ */
+
+declare( strict_types = 1 );
+
+defined( 'ABSPATH' ) || exit;
+
+?>
+<nav class="order-notes-pagination" aria-label="<?php esc_attr_e( 'Order notes pagination', 'woocommerce' ); ?>" data-page="<?php echo esc_attr( $pagination['page'] ); ?>" data-total-pages="<?php echo esc_attr( $pagination['total_pages'] ); ?>" <?php echo $pagination['total'] > 10 ? '' : 'hidden'; ?>>
+	<span class="screen-reader-text order-notes-page-announcement">
+		<?php
+		printf(
+			/* translators: %1$s: current page of notes, %2$s: total number of pages of notes. */
+			esc_html__( 'Page %1$s of %2$s.', 'woocommerce' ),
+			esc_html( number_format_i18n( $pagination['page'] ) ),
+			esc_html( number_format_i18n( $pagination['total_pages'] ) )
+		);
+		?>
+	</span>
+	<span class="order-notes-count" aria-live="polite" aria-atomic="true">
+		<?php
+		printf(
+			/* translators: %1$s: number of notes on this page, %2$s: total number of notes. */
+			esc_html__( '%1$s of %2$s notes', 'woocommerce' ),
+			esc_html( number_format_i18n( count( $notes ) ) ),
+			esc_html( number_format_i18n( $pagination['total'] ) )
+		);
+		?>
+	</span>
+	<div class="order-notes-page-controls">
+		<button type="button" data-page-action="first" aria-label="<?php esc_attr_e( 'First page of notes', 'woocommerce' ); ?>" <?php disabled( 1, $pagination['page'] ); ?>>
+			<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m11 6-6 6 6 6m7-12-6 6 6 6" fill="none" stroke="currentColor" stroke-width="1.8" /></svg>
+		</button>
+		<button type="button" data-page-action="previous" aria-label="<?php esc_attr_e( 'Previous page of notes', 'woocommerce' ); ?>" <?php disabled( 1, $pagination['page'] ); ?>>
+			<span class="dashicons dashicons-arrow-left-alt2" aria-hidden="true"></span>
+		</button>
+		<label class="screen-reader-text" for="order-notes-page"><?php esc_html_e( 'Current page of notes', 'woocommerce' ); ?></label>
+		<input type="number" id="order-notes-page" min="1" max="<?php echo esc_attr( $pagination['total_pages'] ); ?>" value="<?php echo esc_attr( $pagination['page'] ); ?>" inputmode="numeric" />
+		<span class="order-notes-total-pages">
+			<?php
+			/* translators: %s: total number of pages of notes. */
+			printf( esc_html__( 'of %s', 'woocommerce' ), esc_html( number_format_i18n( $pagination['total_pages'] ) ) );
+			?>
+		</span>
+		<button type="button" data-page-action="next" aria-label="<?php esc_attr_e( 'Next page of notes', 'woocommerce' ); ?>" <?php disabled( $pagination['total_pages'], $pagination['page'] ); ?>>
+			<span class="dashicons dashicons-arrow-right-alt2" aria-hidden="true"></span>
+		</button>
+		<button type="button" data-page-action="last" aria-label="<?php esc_attr_e( 'Last page of notes', 'woocommerce' ); ?>" <?php disabled( $pagination['total_pages'], $pagination['page'] ); ?>>
+			<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 6 6 6-6 6m7-12 6 6-6 6" fill="none" stroke="currentColor" stroke-width="1.8" /></svg>
+		</button>
+	</div>
+</nav>
diff --git a/plugins/woocommerce/includes/class-wc-ajax.php b/plugins/woocommerce/includes/class-wc-ajax.php
index d761f4f31f3..17db983ccaf 100644
--- a/plugins/woocommerce/includes/class-wc-ajax.php
+++ b/plugins/woocommerce/includes/class-wc-ajax.php
@@ -187,6 +187,7 @@ class WC_AJAX {
 			'save_order_items',
 			'load_order_items',
 			'add_order_note',
+			'get_order_notes',
 			'delete_order_note',
 			'json_search_order_metakeys',
 			'json_search_tax_rates',
@@ -1324,7 +1325,12 @@ class WC_AJAX {
 			$items_html = ob_get_clean();

 			ob_start();
-			$notes = wc_get_order_notes( array( 'order_id' => $order_id ) );
+			if ( isset( $_POST['paginate_notes'] ) ) {
+				$pagination = WC_Meta_Box_Order_Notes::get_page( (int) $order_id );
+				$notes      = $pagination['notes'];
+			} else {
+				$notes = wc_get_order_notes( array( 'order_id' => $order_id ) );
+			}
 			include __DIR__ . '/admin/meta-boxes/views/html-order-notes.php';
 			$notes_html = ob_get_clean();

@@ -1559,7 +1565,12 @@ class WC_AJAX {
 			$response['html'] = ob_get_clean();

 			ob_start();
-			$notes = wc_get_order_notes( array( 'order_id' => $order_id ) );
+			if ( isset( $_POST['paginate_notes'] ) ) {
+				$pagination = WC_Meta_Box_Order_Notes::get_page( (int) $order_id );
+				$notes      = $pagination['notes'];
+			} else {
+				$notes = wc_get_order_notes( array( 'order_id' => $order_id ) );
+			}
 			include __DIR__ . '/admin/meta-boxes/views/html-order-notes.php';
 			$response['notes_html'] = ob_get_clean();

@@ -1669,7 +1680,12 @@ class WC_AJAX {
 			$items_html = ob_get_clean();

 			ob_start();
-			$notes = wc_get_order_notes( array( 'order_id' => $order_id ) );
+			if ( isset( $_POST['paginate_notes'] ) ) {
+				$pagination = WC_Meta_Box_Order_Notes::get_page( (int) $order_id );
+				$notes      = $pagination['notes'];
+			} else {
+				$notes = wc_get_order_notes( array( 'order_id' => $order_id ) );
+			}
 			include __DIR__ . '/admin/meta-boxes/views/html-order-notes.php';
 			$notes_html = ob_get_clean();

@@ -1779,7 +1795,12 @@ class WC_AJAX {
 			$items_html = ob_get_clean();

 			ob_start();
-			$notes = wc_get_order_notes( array( 'order_id' => $order_id ) );
+			if ( isset( $_POST['paginate_notes'] ) ) {
+				$pagination = WC_Meta_Box_Order_Notes::get_page( (int) $order_id );
+				$notes      = $pagination['notes'];
+			} else {
+				$notes = wc_get_order_notes( array( 'order_id' => $order_id ) );
+			}
 			include __DIR__ . '/admin/meta-boxes/views/html-order-notes.php';
 			$notes_html = ob_get_clean();

@@ -1884,6 +1905,16 @@ class WC_AJAX {
 		wp_die();
 	}

+	/**
+	 * Get a rendered page of order notes via AJAX.
+	 *
+	 * @since 11.3.0
+	 * @return void
+	 */
+	public static function get_order_notes() {
+		WC_Meta_Box_Order_Notes::get_order_notes();
+	}
+
 	/**
 	 * Delete order note via ajax.
 	 *
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php
index 54ed6e36fc5..81f3d035ea1 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php
@@ -63,9 +63,31 @@ class WC_REST_Order_Notes_V2_Controller extends WC_REST_Order_Notes_V1_Controlle
 			);
 		}

+		$paginate = $request->has_param( 'page' ) || $request->has_param( 'per_page' );
+		$per_page = $request['per_page'] ?? 10;
+		$total    = 0;
+		if ( $paginate ) {
+			$args['number']        = $per_page;
+			$args['offset']        = ( ( $request['page'] ?? 1 ) - 1 ) * $args['number'];
+			$args['orderby']       = 'comment_ID';
+			$args['no_found_rows'] = false;
+		}
+
 		remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );

-		$notes = get_comments( $args );
+		if ( $paginate ) {
+			$query = new WP_Comment_Query();
+			$notes = (array) $query->query( $args );
+			$total = (int) $query->found_comments;
+
+			if ( 0 === $total ) {
+				unset( $args['number'], $args['offset'] );
+				$args['count'] = true;
+				$total         = (int) ( new WP_Comment_Query() )->query( $args );
+			}
+		} else {
+			$notes = (array) get_comments( $args );
+		}

 		add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 );

@@ -76,7 +98,23 @@ class WC_REST_Order_Notes_V2_Controller extends WC_REST_Order_Notes_V1_Controlle
 			$data[]     = $order_note;
 		}

-		return rest_ensure_response( $data );
+		$response = rest_ensure_response( $data );
+		if ( $paginate ) {
+			$page      = $request['page'] ?? 1;
+			$max_pages = (int) ceil( $total / $per_page );
+			$response->header( 'X-WP-Total', (string) $total );
+			$response->header( 'X-WP-TotalPages', (string) $max_pages );
+
+			$base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '%s/orders/%d/notes', $this->namespace, $order->get_id() ) ) );
+			if ( $page > 1 && $max_pages > 0 ) {
+				$response->link_header( 'prev', add_query_arg( 'page', min( $page - 1, $max_pages ), $base ) );
+			}
+			if ( $page < $max_pages ) {
+				$response->link_header( 'next', add_query_arg( 'page', $page + 1, $base ) );
+			}
+		}
+
+		return $response;
 	}

 	/**
@@ -166,9 +204,9 @@ class WC_REST_Order_Notes_V2_Controller extends WC_REST_Order_Notes_V1_Controlle
 	 * @return array
 	 */
 	public function get_collection_params() {
-		$params            = array();
-		$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
-		$params['type']    = array(
+		$params             = array();
+		$params['context']  = $this->get_context_param( array( 'default' => 'view' ) );
+		$params['type']     = array(
 			'default'           => 'any',
 			'description'       => __( 'Limit result to customers or internal notes.', 'woocommerce' ),
 			'type'              => 'string',
@@ -176,6 +214,19 @@ class WC_REST_Order_Notes_V2_Controller extends WC_REST_Order_Notes_V1_Controlle
 			'sanitize_callback' => 'sanitize_key',
 			'validate_callback' => 'rest_validate_request_arg',
 		);
+		$params['page']     = array(
+			'description'       => __( 'Page of order notes to retrieve. Pagination is opt-in.', 'woocommerce' ),
+			'type'              => 'integer',
+			'minimum'           => 1,
+			'validate_callback' => 'rest_validate_request_arg',
+		);
+		$params['per_page'] = array(
+			'description'       => __( 'Maximum number of order notes per page.', 'woocommerce' ),
+			'type'              => 'integer',
+			'minimum'           => 1,
+			'maximum'           => 100,
+			'validate_callback' => 'rest_validate_request_arg',
+		);

 		return $params;
 	}
diff --git a/plugins/woocommerce/includes/wc-order-functions.php b/plugins/woocommerce/includes/wc-order-functions.php
index 8b0cb81aea0..111ddb10d75 100644
--- a/plugins/woocommerce/includes/wc-order-functions.php
+++ b/plugins/woocommerce/includes/wc-order-functions.php
@@ -1225,6 +1225,8 @@ function wc_get_order_note( $data ) {
  *
  *     @type string $limit         Maximum number of notes to retrieve.
  *                                 Default empty (no limit).
+ *     @type int    $page          Page of notes to retrieve (starting at 1). Use with limit.
+ *                                 Default 1.
  *     @type int    $order_id      Limit results to those affiliated with a given order ID.
  *                                 Default 0.
  *     @type array  $order__in     Array of order IDs to include affiliated notes for.
@@ -1246,6 +1248,7 @@ function wc_get_order_note( $data ) {
 function wc_get_order_notes( $args ) {
 	$key_mapping = array(
 		'limit'         => 'number',
+		'page'          => 'paged',
 		'order_id'      => 'post_id',
 		'order__in'     => 'post__in',
 		'order__not_in' => 'post__not_in',
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index c3b055fb149..56a1883b23b 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -25200,12 +25200,6 @@ parameters:
 			count: 1
 			path: includes/rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php

-		-
-			message: '#^Argument of an invalid type array\<int\|WP_Comment\>\|int supplied for foreach, only iterables are supported\.$#'
-			identifier: foreach.nonIterable
-			count: 1
-			path: includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php
-
 		-
 			message: '#^Function remove_filter invoked with 4 parameters, 2\-3 required\.$#'
 			identifier: arguments.count
@@ -25242,12 +25236,6 @@ parameters:
 			count: 1
 			path: includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php

-		-
-			message: '#^Parameter \#1 \$note of method WC_REST_Order_Notes_V2_Controller\:\:prepare_item_for_response\(\) expects WP_Comment, int\|WP_Comment given\.$#'
-			identifier: argument.type
-			count: 1
-			path: includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php
-
 		-
 			message: '#^Call to an undefined method WC_Data\:\:get_parent_id\(\)\.$#'
 			identifier: method.notFound
diff --git a/plugins/woocommerce/tests/php/includes/admin/meta-boxes/class-wc-meta-box-order-notes-test.php b/plugins/woocommerce/tests/php/includes/admin/meta-boxes/class-wc-meta-box-order-notes-test.php
new file mode 100644
index 00000000000..cfc1fb4bbfe
--- /dev/null
+++ b/plugins/woocommerce/tests/php/includes/admin/meta-boxes/class-wc-meta-box-order-notes-test.php
@@ -0,0 +1,83 @@
+<?php
+/**
+ * Tests for paginated notes in the order editor.
+ *
+ * @package WooCommerce\Tests\Admin
+ */
+
+declare( strict_types = 1 );
+
+/**
+ * Tests for WC_Meta_Box_Order_Notes.
+ */
+class WC_Meta_Box_Order_Notes_Test extends WC_Unit_Test_Case {
+
+	/**
+	 * @testdox Should return ten notes per page in newest-first order, scoped to the order.
+	 */
+	public function test_get_page(): void {
+		$order = wc_create_order();
+		$ids   = array();
+		for ( $i = 1; $i <= 21; $i++ ) {
+			$ids[] = $order->add_order_note( 'Note ' . $i );
+		}
+		$other_order = wc_create_order();
+		$other_order->add_order_note( 'Another order' );
+
+		$first  = WC_Meta_Box_Order_Notes::get_page( $order->get_id() );
+		$second = WC_Meta_Box_Order_Notes::get_page( $order->get_id(), 2 );
+		$last   = WC_Meta_Box_Order_Notes::get_page( $order->get_id(), 3 );
+
+		$this->assertCount( 10, $first['notes'], 'The first page should contain ten notes.' );
+		$this->assertSame( 21, $first['total'], 'Only this order should contribute to the count.' );
+		$this->assertSame( 3, $first['total_pages'], 'Twenty-one notes should span three pages.' );
+		$this->assertSame( array_reverse( array_slice( $ids, 11 ) ), array_column( $first['notes'], 'id' ), 'The first page should show the newest notes.' );
+		$this->assertSame( array_reverse( array_slice( $ids, 1, 10 ) ), array_column( $second['notes'], 'id' ), 'The next page should not overlap the first.' );
+		$this->assertSame( array( $ids[0] ), array_column( $last['notes'], 'id' ), 'The final page should contain the remaining note.' );
+	}
+
+	/**
+	 * @testdox Should move back to the last available page after its final note is deleted.
+	 */
+	public function test_deleted_last_page(): void {
+		$order = wc_create_order();
+		for ( $i = 0; $i < 11; $i++ ) {
+			$order->add_order_note( 'Note ' . $i );
+		}
+		$last = WC_Meta_Box_Order_Notes::get_page( $order->get_id(), 2 );
+		wc_delete_order_note( reset( $last['notes'] )->id );
+
+		$result = WC_Meta_Box_Order_Notes::get_page( $order->get_id(), 2 );
+
+		$this->assertSame( 1, $result['page'], 'An empty final page should fall back to the preceding page.' );
+		$this->assertCount( 10, $result['notes'], 'The remaining notes should fill the first page.' );
+		$this->assertSame( 10, $result['total'], 'The total should reflect the deletion.' );
+	}
+
+	/**
+	 * @testdox Should return an empty first page for an order with no notes or an unsaved order.
+	 */
+	public function test_empty_order(): void {
+		$order = wc_create_order();
+		foreach ( array( 0, $order->get_id() ) as $order_id ) {
+			$result = WC_Meta_Box_Order_Notes::get_page( $order_id, 50 );
+			$this->assertCount( 0, $result['notes'], 'An empty order should not load notes from another order.' );
+			$this->assertSame( 0, $result['total'], 'An empty order should have no notes.' );
+			$this->assertSame( 1, $result['page'], 'An empty order should remain on page one.' );
+			$this->assertSame( 1, $result['total_pages'], 'The page input should have a valid minimum and maximum.' );
+		}
+	}
+
+	/**
+	 * @testdox Should clamp a page below one to the first page.
+	 */
+	public function test_negative_page(): void {
+		$order = wc_create_order();
+		$order->add_order_note( 'A note' );
+
+		$result = WC_Meta_Box_Order_Notes::get_page( $order->get_id(), -1 );
+
+		$this->assertSame( 1, $result['page'], 'Negative pages should load the first page.' );
+		$this->assertCount( 1, $result['notes'], 'The first page should contain the note.' );
+	}
+}
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php b/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
index 5aa7d1da368..b818b345a22 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
@@ -36,6 +36,143 @@ class WC_AJAX_Test extends \WP_Ajax_UnitTestCase {
 		remove_action( 'admin_init', '_maybe_update_themes' );
 	}

+	/**
+	 * @testdox Should return a rendered page of notes with pagination for an order editor.
+	 */
+	public function test_get_order_notes_page(): void {
+		$this->_setRole( 'administrator' );
+		$order = wc_create_order();
+		for ( $i = 1; $i <= 11; $i++ ) {
+			$order->add_order_note( 'Pagination note ' . $i );
+		}
+
+		$_POST['security'] = wp_create_nonce( 'get-order-notes' );
+		$_POST['order_id'] = $order->get_id();
+		$_POST['page']     = 2;
+
+		$response = $this->do_ajax( 'woocommerce_get_order_notes' );
+
+		$this->assertTrue( $response['success'], 'An order editor should be able to load notes.' );
+		$this->assertStringContainsString( 'Pagination note 1</p>', $response['data']['html'], 'The second page should contain the oldest note.' );
+		$this->assertStringNotContainsString( 'Pagination note 2</p>', $response['data']['html'], 'The response should not contain notes from the first page.' );
+		$this->assertStringContainsString( '1 of 11 notes', $response['data']['html'], 'The count should reflect the current page.' );
+		$this->assertStringContainsString( 'data-page="2"', $response['data']['html'], 'The response should identify the current page.' );
+	}
+
+	/**
+	 * @testdox Should deny access to order notes to a customer with a valid nonce.
+	 */
+	public function test_get_order_notes_denies_customer(): void {
+		$this->_setRole( 'customer' );
+		$order = wc_create_order();
+		$order->add_order_note( 'Private order note' );
+		$_POST['security'] = wp_create_nonce( 'get-order-notes' );
+		$_POST['order_id'] = $order->get_id();
+
+		$response = $this->do_ajax( 'woocommerce_get_order_notes' );
+
+		$this->assertFalse( $response['success'], 'Customers must not be able to read private notes.' );
+		$this->assertEmpty( $response['data'], 'An unauthorized response must not disclose note content.' );
+	}
+
+	/**
+	 * @testdox Should reject a request for order notes without a valid nonce.
+	 */
+	public function test_get_order_notes_requires_nonce(): void {
+		$this->_setRole( 'administrator' );
+		$_POST['order_id'] = wc_create_order()->get_id();
+		$_POST['security'] = 'invalid';
+
+		$response = $this->do_ajax( 'woocommerce_get_order_notes' );
+
+		$this->assertNotTrue( $response['success'] ?? false, 'A request with an invalid nonce must not succeed.' );
+	}
+
+	/**
+	 * @testdox Should paginate item mutation notes only when the request opts in.
+	 * @param string $action AJAX action without the WooCommerce prefix.
+	 * @param bool   $paginate Whether to request paginated notes.
+	 * @testWith ["add_order_item", false]
+	 *           ["add_order_item", true]
+	 *           ["remove_order_coupon", false]
+	 *           ["remove_order_coupon", true]
+	 *           ["remove_order_item", false]
+	 *           ["remove_order_item", true]
+	 *           ["save_order_items", false]
+	 *           ["save_order_items", true]
+	 */
+	public function test_order_item_notes_pagination_is_opt_in( string $action, bool $paginate ): void {
+		$this->_setRole( 'administrator' );
+		$order   = wc_create_order();
+		$product = WC_Helper_Product::create_simple_product();
+		$item_id = $order->add_product( $product );
+		for ( $i = 1; $i <= 11; $i++ ) {
+			$order->add_order_note( 'Compatibility note ' . $i );
+		}
+
+		$_POST['order_id'] = $order->get_id();
+		$_POST['security'] = wp_create_nonce( 'order-item' );
+		unset( $_POST['paginate_notes'] );
+		if ( $paginate ) {
+			$_POST['paginate_notes'] = true;
+		}
+
+		switch ( $action ) {
+			case 'add_order_item':
+				$_POST['data'] = array(
+					array(
+						'id'  => $product->get_id(),
+						'qty' => 1,
+					),
+				);
+				break;
+			case 'remove_order_coupon':
+				$coupon = new WC_Coupon();
+				$coupon->set_code( 'notes-pagination' );
+				$coupon->save();
+				$order->apply_coupon( $coupon );
+				$_POST['coupon'] = $coupon->get_code();
+				break;
+			case 'remove_order_item':
+				$_POST['order_item_ids'] = array( $item_id );
+				break;
+			case 'save_order_items':
+				$_POST['items'] = '';
+				break;
+		}
+
+		// An Error bypasses endpoint catch blocks, matching wp_die() exiting a real request.
+		$stop_request = new Error( 'AJAX response sent.' );
+		$die_handler  = function () use ( $stop_request ) {
+			return function () use ( $stop_request ) {
+				$this->_last_response .= ob_get_clean();
+				throw $stop_request;
+			};
+		};
+		add_filter( 'wp_die_ajax_handler', $die_handler, PHP_INT_MAX );
+		try {
+			$this->do_ajax( 'woocommerce_' . $action );
+		} catch ( Error $error ) {
+			if ( $error !== $stop_request ) {
+				throw $error;
+			}
+		} finally {
+			remove_filter( 'wp_die_ajax_handler', $die_handler, PHP_INT_MAX );
+		}
+		$response             = json_decode( $this->_last_response, true );
+		$this->_last_response = false;
+
+		$this->assertTrue( $response['success'], 'The order item mutation should succeed.' );
+		$html = $response['data']['notes_html'];
+		$this->assertStringContainsString( 'Compatibility note 11</p>', $html, 'The newest fixture note should be included.' );
+		if ( $paginate ) {
+			$this->assertSame( 10, substr_count( $html, '<li rel=' ), 'An opted-in response should contain one page of notes.' );
+			$this->assertStringNotContainsString( 'Compatibility note 1</p>', $html, 'An opted-in response should omit the oldest note.' );
+		} else {
+			$this->assertStringContainsString( 'Compatibility note 1</p>', $html, 'Legacy requests should receive the oldest note too.' );
+		}
+	}
+
 	/**
 	 * Stock should not be reduced from AJAX when an item is added to an order.
 	 */
diff --git a/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-order-notes-controller-tests.php b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-order-notes-controller-tests.php
new file mode 100644
index 00000000000..7336606bb11
--- /dev/null
+++ b/plugins/woocommerce/tests/php/includes/rest-api/Controllers/Version3/class-wc-rest-order-notes-controller-tests.php
@@ -0,0 +1,68 @@
+<?php
+declare( strict_types=1 );
+
+/**
+ * Tests for paginating order notes in the v2 and v3 REST APIs.
+ */
+class WC_REST_Order_Notes_Controller_Tests extends WC_REST_Unit_Test_Case {
+
+	/**
+	 * @testdox Should return paginated notes with totals and navigation while preserving unpaginated responses.
+	 */
+	public function test_get_items_pagination(): void {
+		$order = WC_Helper_Order::create_order();
+		$order->add_order_note( 'First internal note' );
+		add_filter( 'woocommerce_email_log_add_order_note', '__return_false' );
+		try {
+			$order->add_order_note( 'Customer note', 1 );
+		} finally {
+			remove_filter( 'woocommerce_email_log_add_order_note', '__return_false' );
+		}
+		$order->add_order_note( 'Last internal note' );
+
+		foreach ( array( 'wc/v2', 'wc/v3' ) as $namespace ) {
+			$controller = 'wc/v2' === $namespace ? new WC_REST_Order_Notes_V2_Controller() : new WC_REST_Order_Notes_Controller();
+			$path       = "/{$namespace}/orders/{$order->get_id()}/notes";
+			$request    = new WP_REST_Request( 'GET', $path );
+			$request->set_param( 'order_id', $order->get_id() );
+			$request->set_param( 'page', 2 );
+			$request->set_param( 'per_page', 1 );
+
+			$response = $controller->get_items( $request );
+			$this->assertSame( 'Customer note', $response->get_data()[0]['note'] );
+			$this->assertSame( '3', $response->get_headers()['X-WP-Total'] );
+			$this->assertSame( '3', $response->get_headers()['X-WP-TotalPages'] );
+			$this->assertStringContainsString( 'page=1', $response->get_headers()['Link'] );
+			$this->assertStringContainsString( 'page=3', $response->get_headers()['Link'] );
+
+			$request->set_param( 'type', 'internal' );
+			$response = $controller->get_items( $request );
+			$this->assertSame( 'First internal note', $response->get_data()[0]['note'] );
+			$this->assertSame( '2', $response->get_headers()['X-WP-Total'] );
+
+			$request->set_param( 'type', 'customer' );
+			$response = $controller->get_items( $request );
+			$this->assertSame( array(), $response->get_data() );
+			$this->assertSame( '1', $response->get_headers()['X-WP-Total'] );
+
+			$request->set_param( 'type', 'internal' );
+			$request->set_param( 'page', 5 );
+			$response = $controller->get_items( $request );
+			$this->assertSame( array(), $response->get_data() );
+			$this->assertSame( '2', $response->get_headers()['X-WP-Total'] );
+
+			$request = new WP_REST_Request( 'GET', $path );
+			$request->set_param( 'order_id', $order->get_id() );
+			$request->set_param( 'page', 1 );
+			$response = $controller->get_items( $request );
+			$this->assertCount( 3, $response->get_data() );
+			$this->assertSame( '1', $response->get_headers()['X-WP-TotalPages'] );
+
+			$request = new WP_REST_Request( 'GET', $path );
+			$request->set_param( 'order_id', $order->get_id() );
+			$response = $controller->get_items( $request );
+			$this->assertCount( 3, $response->get_data() );
+			$this->assertArrayNotHasKey( 'X-WP-Total', $response->get_headers() );
+		}
+	}
+}
diff --git a/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php
index 2ef95e266c7..60ff29f43f6 100644
--- a/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php
@@ -793,6 +793,40 @@ class WC_Order_Functions_Test extends \WC_Unit_Test_Case {
 		);
 	}

+	/**
+	 * @testdox Should page order notes without changing unpaginated results or type filtering.
+	 */
+	public function test_wc_get_order_notes_pagination(): void {
+		$order = WC_Helper_Order::create_order();
+		$order->add_order_note( 'First internal note' );
+		add_filter( 'woocommerce_email_log_add_order_note', '__return_false' );
+		try {
+			$order->add_order_note( 'Customer note', 1 );
+		} finally {
+			remove_filter( 'woocommerce_email_log_add_order_note', '__return_false' );
+		}
+		$order->add_order_note( 'Last internal note' );
+
+		$args = array(
+			'order_id' => $order->get_id(),
+			'limit'    => 1,
+		);
+		$this->assertSame( 'Last internal note', wc_get_order_notes( $args + array( 'page' => 1 ) )[0]->content );
+		$this->assertSame( 'Customer note', wc_get_order_notes( $args + array( 'page' => 2 ) )[0]->content );
+		$this->assertSame( 'First internal note', wc_get_order_notes( $args + array( 'page' => 3 ) )[0]->content );
+		$this->assertEmpty( wc_get_order_notes( $args + array( 'page' => 4 ) ) );
+		$this->assertCount( 3, wc_get_order_notes( array( 'order_id' => $order->get_id() ) ) );
+		$this->assertSame(
+			'First internal note',
+			wc_get_order_notes(
+				$args + array(
+					'page' => 2,
+					'type' => 'internal',
+				)
+			)[0]->content
+		);
+	}
+
 	/**
 	 * @testdox Should show an order note's time in the current site timezone after the timezone changes.
 	 *