Commit 4fd8a76244e for woocommerce

commit 4fd8a76244eb108b092e625c5d64fa5290889304
Author: Poli Gilad <83961704+poligilad-auto@users.noreply.github.com>
Date:   Fri Jun 26 09:57:31 2026 +0200

    Fix express payment button focus outline (#65901)

    * Fix express payment focus outline

    * Add changelog entry for express payment focus outline

    * Fix express payment iframe focus outline

    * Fix express payment iframe focus state

    * Handle express iframe focus via native events

    * Clear stale express iframe focus state

    * Sync express iframe focus while active

    * Address express payment focus test feedback

    * Extract express payment focus handling

    * Improve express payment focus contrast

    * Align express focus color with buttons

diff --git a/plugins/woocommerce/changelog/fix-express-payment-focus-outline b/plugins/woocommerce/changelog/fix-express-payment-focus-outline
new file mode 100644
index 00000000000..cbe83d0d6a7
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-express-payment-focus-outline
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix express payment buttons missing a visible focus outline in Cart and Checkout.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.tsx
index 256923aefb9..15f3b3ebcd3 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment-methods.tsx
@@ -15,6 +15,7 @@ import {
 import { useEditorContext } from '@woocommerce/base-context';
 import deprecated from '@wordpress/deprecated';
 import { useDispatch, useSelect } from '@wordpress/data';
+import clsx from 'clsx';
 import {
 	ActionCreatorsOf,
 	ConfigOf,
@@ -28,6 +29,7 @@ import PaymentMethodErrorBoundary from './payment-method-error-boundary';
 import { STORE_KEY as PAYMENT_STORE_KEY } from '../../../data/payment/constants';
 import { useExpressPaymentContext } from '../../cart-checkout-shared/payment-methods/express-payment/express-payment-context';
 import type { PaymentStoreDescriptor } from '../../../data/payment';
+import { useExpressPaymentFocus } from './use-express-payment-focus';

 const ExpressPaymentMethods = () => {
 	const { isEditor } = useEditorContext();
@@ -69,6 +71,9 @@ const ExpressPaymentMethods = () => {
 	const paymentMethodInterface = usePaymentMethodInterface();
 	const previousActivePaymentMethod = useRef( activePaymentMethod );
 	const previousPaymentMethodData = useRef( paymentMethodData );
+	const entries = Object.entries( paymentMethods );
+	const { expressPaymentWrapperRef, focusedExpressPaymentMethod } =
+		useExpressPaymentFocus( ! isEditor && entries.length > 0 );

 	/**
 	 * onExpressPaymentClick should be triggered when the express payment button is clicked.
@@ -156,7 +161,6 @@ const ExpressPaymentMethods = () => {
 	 * Currently re-renders excessively but is not easy to useMemo because paymentMethodInterface could become stale.
 	 * paymentMethodInterface itself also updates on most renders.
 	 */
-	const entries = Object.entries( paymentMethods );
 	/*
 	 * Define the elements used for the Express Payments markup.
 	 *
@@ -181,6 +185,10 @@ const ExpressPaymentMethods = () => {
 					<ExpressPayItem
 						key={ id }
 						id={ `express-payment-method-${ id }` }
+						className={ clsx( {
+							'wc-block-components-express-payment__event-button--focused':
+								focusedExpressPaymentMethod === id,
+						} ) }
 					>
 						{ cloneElement( expressPaymentMethod, {
 							...paymentMethodInterface,
@@ -202,7 +210,10 @@ const ExpressPaymentMethods = () => {

 	return (
 		<PaymentMethodErrorBoundary isEditor={ isEditor }>
-			<ExpressPayWrapper className="wc-block-components-express-payment__event-buttons">
+			<ExpressPayWrapper
+				className="wc-block-components-express-payment__event-buttons"
+				ref={ expressPaymentWrapperRef }
+			>
 				{ content }
 			</ExpressPayWrapper>
 		</PaymentMethodErrorBoundary>
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss
index 496d2b5eea6..b0cdb3ce7a8 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/style.scss
@@ -16,12 +16,27 @@ $border-width: 1px;
 			margin: 0;
 			width: 100%;
 			overflow: hidden;
+			border-radius: $universal-border-radius;
+			position: relative;
 			// ensure the buttons don't create a horizontal layout shift
 			min-height: 48px;
 			// ensure the elements take space before the button is loaded
 			// 150 is the minimum width of the button; allows for
 			min-width: 150px;

+			&:focus-within::after,
+			&.wc-block-components-express-payment__event-button--focused::after {
+				border: 2px solid $studio-blue;
+				border-radius: inherit;
+				box-shadow: inset 0 0 0 1px $white;
+				content: "";
+				inset: 0;
+				outline: 3px solid transparent;
+				pointer-events: none;
+				position: absolute;
+				z-index: 1;
+			}
+
 			> img {
 				width: 100%;
 				height: 48px;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/express-payment-methods.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/express-payment-methods.tsx
index 8f628b45149..4fec31c85fd 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/express-payment-methods.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/test/express-payment-methods.tsx
@@ -1,7 +1,13 @@
 /**
  * External dependencies
  */
-import { render, screen } from '@testing-library/react';
+import {
+	act,
+	fireEvent,
+	render,
+	screen,
+	waitFor,
+} from '@testing-library/react';
 import { paymentStore } from '@woocommerce/block-data';
 import {
 	registerExpressPaymentMethod,
@@ -34,7 +40,7 @@ jest.mock( '../express-payment/express-payment-context', () => {
 const mockExpressPaymentMethodNames = [ 'paypal', 'google-pay', 'apple-pay' ];

 const MockExpressButton = jest.fn( ( { name } ) => (
-	<div className="boo">{ `${ name } button` }</div>
+	<button className="boo">{ `${ name } button` }</button>
 ) );

 const MockEditorExpressButton = jest.fn( ( { name } ) => (
@@ -253,6 +259,79 @@ describe( 'Express payment methods', () => {
 					).toHaveProperty( 'tagName', 'LI' );
 				} );
 			} );
+
+			it( 'should add a focused class to the active express payment method item', async () => {
+				render( <ExpressPaymentMethods /> );
+
+				const button = screen.getByText( 'paypal button' );
+				const paymentMethodItem = document.querySelector(
+					'#express-payment-method-paypal'
+				);
+
+				await act( async () => {
+					button.focus();
+					fireEvent.focusIn( button );
+				} );
+
+				await waitFor( () =>
+					expect( paymentMethodItem ).toHaveClass(
+						'wc-block-components-express-payment__event-button--focused'
+					)
+				);
+
+				await act( async () => {
+					button.blur();
+					fireEvent.focusOut( button );
+					await new Promise( ( resolve ) =>
+						setTimeout( resolve, 0 )
+					);
+				} );
+
+				await waitFor( () =>
+					expect( paymentMethodItem ).not.toHaveClass(
+						'wc-block-components-express-payment__event-button--focused'
+					)
+				);
+			} );
+
+			it( 'should keep a focused express payment iframe when the window loses focus', async () => {
+				render( <ExpressPaymentMethods /> );
+
+				const paymentMethodItem = document.querySelector(
+					'#express-payment-method-paypal'
+				);
+				expect( paymentMethodItem ).not.toBeNull();
+
+				if ( ! paymentMethodItem ) {
+					throw new Error(
+						'Missing #express-payment-method-paypal in test DOM'
+					);
+				}
+
+				const iframe = document.createElement( 'iframe' );
+				paymentMethodItem.appendChild( iframe );
+
+				await act( async () => {
+					iframe.focus();
+					fireEvent.focusIn( iframe );
+				} );
+
+				await waitFor( () =>
+					expect( paymentMethodItem ).toHaveClass(
+						'wc-block-components-express-payment__event-button--focused'
+					)
+				);
+
+				await act( async () => {
+					window.dispatchEvent( new Event( 'blur' ) );
+				} );
+
+				await waitFor( () =>
+					expect( paymentMethodItem ).toHaveClass(
+						'wc-block-components-express-payment__event-button--focused'
+					)
+				);
+			} );
 		} );
 		describe( 'In an editor context', () => {
 			beforeEach( () => {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/use-express-payment-focus.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/use-express-payment-focus.ts
new file mode 100644
index 00000000000..01f78d2968b
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/payment-methods/use-express-payment-focus.ts
@@ -0,0 +1,127 @@
+/**
+ * External dependencies
+ */
+import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
+
+type ExpressPaymentFocus = {
+	expressPaymentWrapperRef: React.MutableRefObject< HTMLElement | null >;
+	focusedExpressPaymentMethod: string | null;
+};
+
+export const useExpressPaymentFocus = (
+	enabled: boolean
+): ExpressPaymentFocus => {
+	const expressPaymentWrapperRef = useRef< HTMLElement | null >( null );
+	const focusSyncIntervalRef = useRef< ReturnType<
+		typeof setInterval
+	> | null >( null );
+	const [ focusedExpressPaymentMethod, setFocusedExpressPaymentMethod ] =
+		useState< string | null >( null );
+
+	const stopFocusSyncInterval = useCallback( () => {
+		if ( focusSyncIntervalRef.current ) {
+			clearInterval( focusSyncIntervalRef.current );
+			focusSyncIntervalRef.current = null;
+		}
+	}, [] );
+
+	const getFocusedExpressPaymentMethod = useCallback( () => {
+		const wrapper = expressPaymentWrapperRef.current;
+		const activeElement = wrapper?.ownerDocument.activeElement;
+
+		if ( ! wrapper || ! ( activeElement instanceof Element ) ) {
+			return {
+				focusedPaymentMethod: null,
+				isExpressPaymentIframe: false,
+			};
+		}
+
+		const item = activeElement.closest( '[id^="express-payment-method-"]' );
+		const isExpressPaymentIframe =
+			activeElement instanceof HTMLIFrameElement &&
+			wrapper.contains( activeElement );
+
+		if ( item && wrapper.contains( item ) ) {
+			return {
+				focusedPaymentMethod: item.id.replace(
+					'express-payment-method-',
+					''
+				),
+				isExpressPaymentIframe,
+			};
+		}
+
+		return {
+			focusedPaymentMethod: null,
+			isExpressPaymentIframe: false,
+		};
+	}, [] );
+
+	const syncFocusedExpressPaymentMethod = useCallback( () => {
+		const { focusedPaymentMethod, isExpressPaymentIframe } =
+			getFocusedExpressPaymentMethod();
+
+		setFocusedExpressPaymentMethod( focusedPaymentMethod );
+
+		if ( ! isExpressPaymentIframe ) {
+			stopFocusSyncInterval();
+			return;
+		}
+
+		if ( ! focusSyncIntervalRef.current ) {
+			focusSyncIntervalRef.current = setInterval( () => {
+				const {
+					focusedPaymentMethod: nextFocusedPaymentMethod,
+					isExpressPaymentIframe: nextIsExpressPaymentIframe,
+				} = getFocusedExpressPaymentMethod();
+
+				setFocusedExpressPaymentMethod( nextFocusedPaymentMethod );
+
+				if ( ! nextIsExpressPaymentIframe ) {
+					stopFocusSyncInterval();
+				}
+			}, 100 );
+		}
+	}, [ getFocusedExpressPaymentMethod, stopFocusSyncInterval ] );
+
+	useEffect( () => {
+		if ( ! enabled ) {
+			setFocusedExpressPaymentMethod( null );
+			stopFocusSyncInterval();
+			return;
+		}
+
+		const wrapper = expressPaymentWrapperRef.current;
+
+		if ( ! wrapper ) {
+			return;
+		}
+
+		const doc = wrapper.ownerDocument;
+		const win = doc.defaultView;
+		const syncSoon = () => {
+			setTimeout( syncFocusedExpressPaymentMethod, 0 );
+		};
+
+		doc.addEventListener( 'focusin', syncFocusedExpressPaymentMethod );
+		doc.addEventListener( 'focusout', syncSoon );
+		win?.addEventListener( 'blur', syncFocusedExpressPaymentMethod );
+		win?.addEventListener( 'focus', syncSoon );
+
+		return () => {
+			stopFocusSyncInterval();
+			doc.removeEventListener(
+				'focusin',
+				syncFocusedExpressPaymentMethod
+			);
+			doc.removeEventListener( 'focusout', syncSoon );
+			win?.removeEventListener( 'blur', syncFocusedExpressPaymentMethod );
+			win?.removeEventListener( 'focus', syncSoon );
+		};
+	}, [ enabled, stopFocusSyncInterval, syncFocusedExpressPaymentMethod ] );
+
+	return {
+		expressPaymentWrapperRef,
+		focusedExpressPaymentMethod,
+	};
+};