Commit e3948a96cda for woocommerce
commit e3948a96cda11f89de2a7e9229d89b18fb125091
Author: Ann <annchichi@users.noreply.github.com>
Date: Mon Jul 27 13:57:52 2026 +0800
Fix shipping recommendation dismissal accessibility and fallbacks (#66689)
* Fix shipping recommendation dismissal and fallbacks
* Add changelog entry for shipping recommendation fixes
* Fix shipping fallback when suggestions are disabled
diff --git a/plugins/woocommerce/changelog/wooplug-7093-shipping-recommendations-dismiss b/plugins/woocommerce/changelog/wooplug-7093-shipping-recommendations-dismiss
new file mode 100644
index 00000000000..5592ef31ec9
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooplug-7093-shipping-recommendations-dismiss
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Preserve focus and screen reader feedback when hiding shipping recommendations, and restore marketplace links where the recommendations component is unavailable.
diff --git a/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations.tsx b/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations.tsx
index 770fffdf5a9..dd34dc9ffcd 100644
--- a/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations.tsx
+++ b/plugins/woocommerce/client/admin/client/shipping/shipping-recommendations.tsx
@@ -120,9 +120,7 @@ const ShippingRecommendations = () => {
! hasRecommendationEligibilityResolved ||
( ! hasRecommendationsDismissResolved && hasVisibleExtensions );
const shouldShowRecommendationsFallback =
- ( hasRecommendationsDismissResolved && isRecommendationsHidden ) ||
- ! hasVisibleExtensions ||
- isSellingDigitalProductsOnly;
+ ! hasVisibleExtensions || isSellingDigitalProductsOnly;
const shouldTrackRecommendationsImpression =
hasRecommendationEligibilityResolved &&
hasRecommendationsDismissResolved &&
@@ -182,7 +180,11 @@ const ShippingRecommendations = () => {
<ShippingTour
showShippingRecommendationsStep={ ! isRecommendationsHidden }
/>
- <div style={ { paddingBottom: 60 } }>
+ <div
+ style={ {
+ paddingBottom: isRecommendationsHidden ? 0 : 60,
+ } }
+ >
<ShippingRecommendationsList
dismissState={ recommendationsDismissState }
>
@@ -241,6 +243,7 @@ const ShippingRecommendations = () => {
} ) }
</ShippingRecommendationsList>
</div>
+ { isRecommendationsHidden && marketplaceFallbackLink }
</>
);
};
diff --git a/plugins/woocommerce/client/admin/client/shipping/test/shipping-recommendations.test.tsx b/plugins/woocommerce/client/admin/client/shipping/test/shipping-recommendations.test.tsx
index 6c06500bf95..2b635fce98c 100644
--- a/plugins/woocommerce/client/admin/client/shipping/test/shipping-recommendations.test.tsx
+++ b/plugins/woocommerce/client/admin/client/shipping/test/shipping-recommendations.test.tsx
@@ -2,6 +2,7 @@
* External dependencies
*/
import { render, screen } from '@testing-library/react';
+import { speak } from '@wordpress/a11y';
import { useSelect, useDispatch } from '@wordpress/data';
import { recordEvent } from '@woocommerce/tracks';
@@ -23,24 +24,35 @@ jest.mock( '~/components/tracked-link/tracked-link', () => ( {
</span>
),
} ) );
-jest.mock( '../../settings-recommendations/dismissable-list', () => ( {
- DismissableList: ( {
- children,
- isDismissed,
- }: {
- children: React.ReactNode;
- isDismissed?: boolean;
- } ) => (
- <div
- data-dismissed={ String( Boolean( isDismissed ) ) }
- data-testid="dismissable-list"
- >
- { ! isDismissed && children }
- </div>
- ),
- DismissableListHeading: ( { children }: { children: React.ReactNode } ) =>
- children,
-} ) );
+jest.mock( '../../settings-recommendations/dismissable-list', () => {
+ const { DismissableList } = jest.requireActual(
+ '../../settings-recommendations/dismissable-list'
+ );
+
+ return {
+ DismissableList: ( {
+ children,
+ isDismissed,
+ }: {
+ children: React.ReactNode;
+ isDismissed?: boolean;
+ } ) => (
+ <div
+ data-dismissed={ String( Boolean( isDismissed ) ) }
+ data-testid="dismissable-list"
+ >
+ <DismissableList isDismissed={ isDismissed }>
+ { children }
+ </DismissableList>
+ </div>
+ ),
+ DismissableListHeading: ( {
+ children,
+ }: {
+ children: React.ReactNode;
+ } ) => children,
+ };
+} );
jest.mock( '~/guided-tours/shipping-tour', () => ( {
ShippingTour: ( {
showShippingRecommendationsStep,
@@ -66,6 +78,9 @@ jest.mock( '../../lib/notices', () => ( {
jest.mock( '@woocommerce/tracks', () => ( {
recordEvent: jest.fn(),
} ) );
+jest.mock( '@wordpress/a11y', () => ( {
+ speak: jest.fn(),
+} ) );
const defaultSelectReturn = {
getActivePlugins: () => [],
@@ -97,6 +112,7 @@ describe( 'ShippingRecommendations', () => {
activatePlugins: () => Promise.resolve(),
} );
( recordEvent as jest.Mock ).mockClear();
+ ( speak as jest.Mock ).mockClear();
} );
it( 'renders recommendations and the shipping tour recommendations step', () => {
@@ -190,6 +206,46 @@ describe( 'ShippingRecommendations', () => {
);
} );
+ it( 'keeps the dismissal wrapper mounted to restore focus and announce the change', () => {
+ let dismissOption = 'no';
+ mockSelect( {
+ getOption: ( option: string ) =>
+ option === SHIPPING_RECOMMENDATIONS_DISMISS_OPTION
+ ? dismissOption
+ : undefined,
+ } );
+
+ const { rerender } = render( <ShippingRecommendations /> );
+ const dismissalWrapper = document.querySelector(
+ '.woocommerce-dismissable-list__wrapper'
+ );
+
+ expect( dismissalWrapper ).toBeInTheDocument();
+ expect( screen.getByTestId( 'dismissable-list' ) ).toHaveAttribute(
+ 'data-dismissed',
+ 'false'
+ );
+
+ dismissOption = 'yes';
+ rerender( <ShippingRecommendations /> );
+
+ expect(
+ document.querySelector( '.woocommerce-dismissable-list__wrapper' )
+ ).toBe( dismissalWrapper );
+ expect( screen.getByTestId( 'dismissable-list' ) ).toHaveAttribute(
+ 'data-dismissed',
+ 'true'
+ );
+ expect( speak ).toHaveBeenCalledWith(
+ 'Recommendation hidden.',
+ 'assertive'
+ );
+ expect( dismissalWrapper ).toHaveFocus();
+ expect(
+ screen.getByText( 'the WooCommerce Marketplace' )
+ ).toBeInTheDocument();
+ } );
+
it( 'does not render recommendations before the product profile resolves', () => {
mockSelect( {
hasFinishedResolution: ( selector: string ) =>
@@ -231,9 +287,11 @@ describe( 'ShippingRecommendations', () => {
expect(
screen.queryByText( 'the WooCommerce Marketplace' )
).toBeInTheDocument();
- expect(
- screen.queryByTestId( 'dismissable-list' )
- ).not.toBeInTheDocument();
+ expect( screen.getByTestId( 'dismissable-list' ) ).toHaveAttribute(
+ 'data-dismissed',
+ 'true'
+ );
+ expect( speak ).not.toHaveBeenCalled();
expect( screen.getByTestId( 'shipping-tour' ) ).toHaveAttribute(
'data-show-recommendations-step',
'false'
diff --git a/plugins/woocommerce/includes/admin/views/html-admin-settings.php b/plugins/woocommerce/includes/admin/views/html-admin-settings.php
index 8352756fdb8..a11799577f3 100644
--- a/plugins/woocommerce/includes/admin/views/html-admin-settings.php
+++ b/plugins/woocommerce/includes/admin/views/html-admin-settings.php
@@ -97,6 +97,12 @@ $marketplace_links = array(
/* translators: %1$s: opening link tag, %2$s: closing link tag */
'message' => __( '%1$sExplore solutions%2$s that help with tax calculations, compliance, and regional requirements.', 'woocommerce' ),
),
+ 'shipping' => array(
+ 'url' => $marketplace_base_url . 'shipping-delivery-and-fulfillment/',
+ 'is_external' => true,
+ /* translators: %1$s: opening link tag, %2$s: closing link tag */
+ 'message' => __( '%1$sExplore solutions%2$s that enhance shipping, delivery, and fulfillment workflows.', 'woocommerce' ),
+ ),
'account' => array(
'url' => $marketplace_base_url . 'store-content-and-customizations/cart-and-checkout-features/',
'is_external' => true,
@@ -117,6 +123,27 @@ $marketplace_links = array(
),
);
+// The React recommendations component owns the marketplace link on the main
+// Shipping screen, but it does not render on subsections, zone screens, or for
+// users who cannot install plugins.
+$shipping_zone_id = '';
+// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Used only to select non-mutating view output.
+if ( isset( $_GET['zone_id'] ) && is_string( $_GET['zone_id'] ) ) {
+ $shipping_zone_id = sanitize_text_field( wp_unslash( $_GET['zone_id'] ) );
+}
+// phpcs:enable WordPress.Security.NonceVerification.Recommended
+
+$is_shipping_zone_screen = '' !== $shipping_zone_id;
+if (
+ 'shipping' === $current_tab
+ && (
+ 'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' )
+ || ( '' === $current_section && ! $is_shipping_zone_screen && current_user_can( 'install_plugins' ) )
+ )
+) {
+ unset( $marketplace_links['shipping'] );
+}
+
?>
<div class="wrap woocommerce">
diff --git a/plugins/woocommerce/tests/php/includes/admin/views/class-wc-admin-settings-view-test.php b/plugins/woocommerce/tests/php/includes/admin/views/class-wc-admin-settings-view-test.php
index 30bf6b48394..4996d75c623 100644
--- a/plugins/woocommerce/tests/php/includes/admin/views/class-wc-admin-settings-view-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/views/class-wc-admin-settings-view-test.php
@@ -78,6 +78,82 @@ class WC_Admin_Settings_View_Test extends WC_Unit_Test_Case {
);
}
+ /**
+ * @testdox Should render the shipping marketplace link on Shipping settings subsections.
+ */
+ public function test_shipping_marketplace_link_is_rendered_on_shipping_settings_subsections(): void {
+ $output = $this->render_settings_view( 'shipping', 'options' );
+
+ $this->assertStringContainsString(
+ 'data-settings-tab="shipping"',
+ $output
+ );
+ $this->assertStringContainsString(
+ 'data-settings-section="options"',
+ $output
+ );
+ $this->assertStringContainsString(
+ 'shipping-delivery-and-fulfillment',
+ $output
+ );
+ }
+
+ /**
+ * @testdox Should not render the shipping marketplace link when marketplace suggestions are disabled.
+ */
+ public function test_shipping_marketplace_link_is_not_rendered_when_marketplace_suggestions_are_disabled(): void {
+ update_option( 'woocommerce_show_marketplace_suggestions', 'no' );
+
+ $output = $this->render_settings_view( 'shipping', 'options' );
+
+ $this->assertStringNotContainsString(
+ 'data-settings-tab="shipping"',
+ $output
+ );
+ $this->assertStringNotContainsString(
+ 'shipping-delivery-and-fulfillment',
+ $output
+ );
+ }
+
+ /**
+ * @testdox Should render the shipping marketplace link on Shipping zone screens.
+ */
+ public function test_shipping_marketplace_link_is_rendered_on_shipping_zone_screens(): void {
+ $_GET['zone_id'] = '1';
+
+ $output = $this->render_settings_view( 'shipping' );
+
+ $this->assertStringContainsString(
+ 'data-settings-tab="shipping"',
+ $output
+ );
+ $this->assertStringContainsString(
+ 'shipping-delivery-and-fulfillment',
+ $output
+ );
+ }
+
+ /**
+ * @testdox Should render the shipping marketplace link for users who cannot install plugins.
+ */
+ public function test_shipping_marketplace_link_is_rendered_for_users_without_plugin_install_permissions(): void {
+ $shop_manager_user_id = self::factory()->user->create( array( 'role' => 'shop_manager' ) );
+ wp_set_current_user( $shop_manager_user_id );
+ $this->assertFalse( current_user_can( 'install_plugins' ) );
+
+ $output = $this->render_settings_view( 'shipping' );
+
+ $this->assertStringContainsString(
+ 'data-settings-tab="shipping"',
+ $output
+ );
+ $this->assertStringContainsString(
+ 'shipping-delivery-and-fulfillment',
+ $output
+ );
+ }
+
/**
* @testdox Should keep non-shipping marketplace links when shipping smart defaults are enabled.
*/