Commit b638729fadd for woocommerce

commit b638729fadddfef936a32a05829d1268b4cae1a3
Author: Jana Hernandez <71137829+JanaMW27@users.noreply.github.com>
Date:   Wed Jul 29 15:58:33 2026 +0200

    Fix keyboard flow when adding products to an order (rogue search box) + clear/chevron overlap (#66381)

    * docs(DSGWOO-1422): add design spec for order add-products keyboard fix

    Design spec for fixing the legacy admin Add item(s) -> Add product(s)
    modal: keep the select2 search dropdown inside the popup (dropdownParent),
    stop the modal Enter handler from hijacking select2, and fix the
    clear/chevron icon overlap. Includes grep-verified blast radius and a
    keyboard e2e regression test plan.

    Ref: https://github.com/woocommerce/woocommerce/issues/33517

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * docs(DSGWOO-1422): add implementation plan for add-products keyboard fix

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * test(DSGWOO-1422): add keyboard-flow regression for add-products modal

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * fix(DSGWOO-1422): render add-products search dropdown inside the modal

    Set selectWoo dropdownParent to the enclosing .wc-backbone-modal-content so
    the product-search dropdown no longer detaches to <body> and strands a rogue
    search box in the page corner. Only applied when the field is inside a modal.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * fix(DSGWOO-1422): don't submit backbone modal on Enter inside a select

    keyboardActions no longer fires the OK/Next button when focus is on a
    selectWoo/select2 control or a dropdown is open within the modal, so Enter
    operates the dropdown instead of submitting and closing the modal. Shared by
    all admin backbone modals; the guard only backs off for enhanced-select.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * fix(DSGWOO-1422): stop clear/chevron overlap in add-products modal

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * fix(DSGWOO-1422): add changelog entry for add-products keyboard fix

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * fix(DSGWOO-1422): remove position:relative that broke modal centering

    The earlier position:relative safeguard on .wc-backbone-modal-content
    overrode the modal's own position:fixed centering, dropping the Add
    products modal into normal flow (rendered low/off-centre) and
    mispositioning the search dropdown. position:fixed already provides the
    containing block the dropdown needs, so the safeguard was unnecessary.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * docs(DSGWOO-1422): record position:relative regression in plan

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * fix(DSGWOO-1422): drop dropdownParent; let search dropdown attach to body

    selectWoo's _positionDropdown adjusts the dropdown's left for the parent
    offset but never its top, so pointing dropdownParent at the position:fixed
    modal placed the results panel ~parent-top px down the page (bottom of the
    viewport). The default <body> attachment positions correctly under the
    field, exactly like every other .wc-product-search in admin. The ticket's
    actual keyboard bug is fixed by the Enter guard in backbone-modal.js alone,
    so dropdownParent is both broken here and unnecessary. Widen the guard's
    open-dropdown check to a document-wide lookup since the dropdown now lives
    on body, and drop the test's now-invalid 'no dropdown on body' assertions.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * docs(DSGWOO-1422): mark dropdownParent approach abandoned (selectWoo positioning)

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * chore(DSGWOO-1422): remove internal planning docs from branch

    These brainstorming/plan artifacts are not WooCommerce core content and
    should not ship in the PR; kept locally in the workspace .context.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * test(DSGWOO-1422): locate select2 search field on body, not in modal

    The keyboard e2e located the search field with modal.locator(), but selectWoo
    attaches its search dropdown to <body>, so the locator never resolved and the
    test timed out in CI. Use the same page-level 'span > .select2-search__field'
    locator the passing mouse-driven helper uses.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * test(DSGWOO-1422): assert on rendered selection, not ambiguous getByText

    The keyboard flow works in CI (product is selected), but the intermediate
    assertion modal.getByText(name) matched both the visible selection and the
    hidden <option>, tripping Playwright strict mode. Assert on the specific
    .select2-selection__rendered element instead.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    * Address review: remove dead open-dropdown check, trim Enter guard comment (cc @jorgeatorres on PR #66381)

    * Address review: bump modal select2 padding specificity above global rule (cc coderabbit on PR #66381)

    ---------

    Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/33517-fix-add-products-modal-keyboard b/plugins/woocommerce/changelog/33517-fix-add-products-modal-keyboard
new file mode 100644
index 00000000000..c94e7c8da42
--- /dev/null
+++ b/plugins/woocommerce/changelog/33517-fix-add-products-modal-keyboard
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix keyboard flow in the order Add products modal so products can be added without a rogue search box, and fix the clear/chevron icon overlap.
diff --git a/plugins/woocommerce/client/legacy/css/admin.scss b/plugins/woocommerce/client/legacy/css/admin.scss
index fccd54424f6..0d252ec412f 100644
--- a/plugins/woocommerce/client/legacy/css/admin.scss
+++ b/plugins/woocommerce/client/legacy/css/admin.scss
@@ -8050,6 +8050,17 @@ table.bar_chart {
 	.select2-container {
 		width: 100% !important;
 	}
+
+	// Reserve room for the clear (x) and chevron icons; .select2-container keeps this rule above the later global 24px padding.
+	.select2-container .select2-selection--single {
+		.select2-selection__rendered {
+			padding-right: 40px;
+		}
+
+		.select2-selection__clear {
+			margin-right: 18px;
+		}
+	}
 }

 .wc-backbone-modal-backdrop {
diff --git a/plugins/woocommerce/client/legacy/js/admin/backbone-modal.js b/plugins/woocommerce/client/legacy/js/admin/backbone-modal.js
index afce07540b0..85aa0d3f336 100644
--- a/plugins/woocommerce/client/legacy/js/admin/backbone-modal.js
+++ b/plugins/woocommerce/client/legacy/js/admin/backbone-modal.js
@@ -149,14 +149,22 @@
 			var button = e.keyCode || e.which;

 			// Enter key
-			if (
-				13 === button &&
-				! ( e.target.tagName && ( e.target.tagName.toLowerCase() === 'input' || e.target.tagName.toLowerCase() === 'textarea' ) )
-			) {
-				if ( $( '#btn-ok' ).length ) {
-					this.addButton( e );
-				}	else if ( $( '#btn-next' ).length ) {
-					this.nextButton( e );
+			if ( 13 === button ) {
+				var isFormField = e.target.tagName &&
+					( e.target.tagName.toLowerCase() === 'input' ||
+						e.target.tagName.toLowerCase() === 'textarea' );
+
+				// Let selectWoo handle Enter on an enhanced-select control instead of submitting the modal.
+				var inEnhancedSelect = $( e.target ).closest(
+					'.select2-container, .select2-selection, .select2-search__field, [role="combobox"]'
+				).length > 0;
+
+				if ( ! isFormField && ! inEnhancedSelect ) {
+					if ( $( '#btn-ok' ).length ) {
+						this.addButton( e );
+					}	else if ( $( '#btn-next' ).length ) {
+						this.nextButton( e );
+					}
 				}
 			}

diff --git a/plugins/woocommerce/tests/e2e/tests/order/create-order.spec.ts b/plugins/woocommerce/tests/e2e/tests/order/create-order.spec.ts
index 130fbf22dca..7a5f868c666 100644
--- a/plugins/woocommerce/tests/e2e/tests/order/create-order.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/order/create-order.spec.ts
@@ -421,6 +421,65 @@ test.describe(
 			).toBeVisible();
 		} );

+		test( 'can add a product using the keyboard without a rogue search box', async ( {
+			page,
+			simpleProduct,
+		} ) => {
+			await page.goto( 'wp-admin/admin.php?page=wc-orders&action=new' );
+
+			// Open the Add products modal.
+			await page.getByRole( 'button', { name: 'Add item(s)' } ).click();
+			await page
+				.getByRole( 'button', { name: 'Add product(s)' } )
+				.click();
+
+			const modal = page.locator( '.wc-backbone-modal-content' );
+			await expect( modal ).toBeVisible();
+
+			// Focus the (closed) product-search control and press Enter.
+			// Before the fix this submitted the modal, closing it and
+			// stranding a detached selectWoo dropdown at the top-left of the
+			// page.
+			await modal.locator( '.select2-selection' ).first().focus();
+			await page.keyboard.press( 'Enter' );
+
+			// The modal must still be open: Enter on the search control must
+			// not submit/close the modal.
+			await expect( modal ).toBeVisible();
+
+			// selectWoo attaches its search dropdown to <body> (not inside the
+			// modal), so target the search field at the page level — the same
+			// locator the mouse-driven helper uses. Enter (above) opened the
+			// dropdown; type the query, wait for the result, then press Enter to
+			// choose it with the keyboard (results are loaded first, so this is
+			// not the premature-Enter path).
+			await page
+				.locator( 'span > .select2-search__field' )
+				.fill( simpleProduct.name );
+			await page
+				.getByRole( 'option', { name: simpleProduct.name } )
+				.first()
+				.waitFor();
+			await page.keyboard.press( 'Enter' );
+
+			// The product is selected in the modal's search control; commit.
+			// Assert on the rendered selection specifically — a bare getByText
+			// also matches the hidden <option>, which trips strict mode.
+			await expect(
+				modal
+					.locator( '.select2-selection__rendered' )
+					.filter( { hasText: simpleProduct.name } )
+			).toBeVisible();
+			await page.locator( '#btn-ok' ).click();
+
+			// Line item lands on the order.
+			await expect(
+				page
+					.locator( 'td.name > a' )
+					.filter( { hasText: simpleProduct.name } )
+			).toBeVisible();
+		} );
+
 		test( 'can create an order for an existing customer', async ( {
 			page,
 			simpleProduct,