Commit ca2697cafb6 for woocommerce
commit ca2697cafb6763f276959aafdfc8a3878a8665b4
Author: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com>
Date: Thu Sep 3 12:26:52 2026 +0200
Move the quality badge above the price on marketplace product cards (#68259)
* Move the quality badge above the price on marketplace product cards
* Add changelog entry for the quality badge placement change
* Align the price row across cards with and without the quality badge
---------
Co-authored-by: Herman <KokkieH@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/wccom-2892-quality-badge-compact-card-placement b/plugins/woocommerce/changelog/wccom-2892-quality-badge-compact-card-placement
new file mode 100644
index 00000000000..a9a2b4ce540
--- /dev/null
+++ b/plugins/woocommerce/changelog/wccom-2892-quality-badge-compact-card-placement
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+Move the quality badge above the price on in-app marketplace cards (between the name and the price on compact cards), centre compact card content vertically, and pin the price row to the bottom of regular cards.
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.scss b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.scss
index a771ef1b69a..c09c95d2a7d 100644
--- a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.scss
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.scss
@@ -92,10 +92,13 @@
&__footer {
display: flex;
- /* The quality badge, when present, wraps onto its own row below the price: */
+ /* The quality badge, when present, takes its own row above the price: */
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
+ /* Pack rows to the bottom so the price row sits at the same height
+ whether or not the badge row is present above it. */
+ align-content: flex-end;
row-gap: $grid-unit-10;
min-height: 36px;
}
@@ -104,6 +107,12 @@
outline: 1.5px solid var(--wp-admin-theme-color);
}
+ /* Fill the card so the content grid below can pin the footer to the
+ bottom (and compact cards can centre their content). */
+ &-wrapper {
+ height: 100%;
+ }
+
&__content {
display: grid;
gap: $medium-gap;
@@ -420,16 +429,27 @@
min-height: 0;
}
+ /* Centre the icon and text in the card so that, in a grid mixing
+ cards with and without the badge, the extra height of a badged
+ card is split evenly instead of leaving a gap at the bottom. */
+ .woocommerce-marketplace__product-card__content {
+ align-content: center;
+ }
+
.woocommerce-marketplace__product-card__details {
- /* Top-align the icon with the title, like regular cards, so it
- sits identically whether or not the badge row is present. */
- align-items: flex-start;
+ align-items: center;
}
.woocommerce-marketplace__product-card__meta {
/* Optical compensation: the title's negative focus-ring margin
- would otherwise pin the text to the icon's top edge. */
+ would otherwise shift the text up relative to the icon. */
margin-top: $grid-unit-05;
+
+ /* Title, badge and price stack with 4px between them; without
+ the badge the title and price keep the tighter 2px gap. */
+ &:has(.woocommerce-marketplace__quality-badge) {
+ gap: $grid-unit-05;
+ }
}
.woocommerce-marketplace__product-card__link {
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.tsx b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.tsx
index 557200f83a4..106b9049b47 100644
--- a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.tsx
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/product-card.tsx
@@ -294,16 +294,21 @@ function ProductCard( props: ProductCardProps ): React.JSX.Element {
);
};
+ const qualityBadge =
+ ! isLoading && props.product ? (
+ <QualityBadge product={ props.product } />
+ ) : null;
+
const footer = ! isBusinessService ? (
<footer className="woocommerce-marketplace__product-card__footer">
{ isLoading && (
<div className="woocommerce-marketplace__product-card__price" />
) }
+ { /* Regular cards show the badge on its own row above the price;
+ compact cards show it between the title and the price. */ }
+ { ! isCompact && qualityBadge }
{ ! isLoading && props.product && (
- <>
- <ProductCardFooter product={ props.product } />
- <QualityBadge product={ props.product } />
- </>
+ <ProductCardFooter product={ props.product } />
) }
</footer>
) : null;
@@ -371,6 +376,7 @@ function ProductCard( props: ProductCardProps ): React.JSX.Element {
) }
</p>
) }
+ { isCompact && qualityBadge }
{ isCompact && footer }
</div>
</div>
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/product-card/test/product-card.test.tsx b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/test/product-card.test.tsx
new file mode 100644
index 00000000000..ce6bb4876b3
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/product-card/test/product-card.test.tsx
@@ -0,0 +1,138 @@
+/**
+ * External dependencies
+ */
+import { render } from '@testing-library/react';
+import React from 'react';
+
+jest.mock( '@woocommerce/navigation', () => ( {
+ getNewPath: jest.fn( () => '/new-path' ),
+ navigateTo: jest.fn(),
+ useQuery: jest.fn( () => ( {} ) ),
+} ) );
+
+jest.mock( '@woocommerce/tracks', () => ( {
+ recordEvent: jest.fn(),
+ queueRecordEvent: jest.fn(),
+} ) );
+
+jest.mock( '@woocommerce/data', () => ( {
+ useUser: jest.fn( () => ( {
+ user: null,
+ currentUserCan: jest.fn( () => false ),
+ } ) ),
+} ) );
+
+/**
+ * Internal dependencies
+ */
+import ProductCard from '../product-card';
+import { MarketplaceContext } from '../../../contexts/marketplace-context';
+import { MarketplaceContextType } from '../../../contexts/types';
+import {
+ Product,
+ ProductCardType,
+ ProductType,
+} from '../../product-list/types';
+
+const context = {
+ selectedTab: 'discover',
+ isProductInstalled: () => false,
+ iamSettings: {
+ quality_badge: {
+ enabled: true,
+ label: 'Excellence Verified',
+ tooltip: 'Verified against WooCommerce standards.',
+ },
+ },
+} as unknown as MarketplaceContextType;
+
+const product: Product = {
+ id: 1,
+ title: 'Test extension',
+ image: '',
+ type: ProductType.extension,
+ description: '',
+ vendorName: '',
+ vendorUrl: '',
+ icon: '',
+ url: '',
+ price: 0,
+ isInstallable: false,
+ currency: 'USD',
+ isOnSale: false,
+ regularPrice: 0,
+ averageRating: 4.5,
+ reviewsCount: 10,
+ hasQualityBadge: true,
+};
+
+function renderCard( cardType: ProductCardType ) {
+ return render(
+ <MarketplaceContext.Provider value={ context }>
+ <ProductCard
+ type={ ProductType.extension }
+ product={ product }
+ cardType={ cardType }
+ tracksData={ { position: 1 } }
+ />
+ </MarketplaceContext.Provider>
+ );
+}
+
+function getBadge( container: HTMLElement ) {
+ return container.querySelector( '.woocommerce-marketplace__quality-badge' );
+}
+
+describe( 'ProductCard quality badge placement', () => {
+ it( 'renders the badge between the title and the price on compact cards', () => {
+ const { container } = renderCard( ProductCardType.compact );
+ const badge = getBadge( container );
+ const meta = container.querySelector(
+ '.woocommerce-marketplace__product-card__meta'
+ );
+ const footer = container.querySelector(
+ '.woocommerce-marketplace__product-card__footer'
+ );
+
+ expect( badge ).not.toBeNull();
+ expect( badge?.parentElement ).toBe( meta );
+ expect( footer?.contains( badge ) ).toBe( false );
+
+ const title = container.querySelector(
+ '.woocommerce-marketplace__product-card__title'
+ );
+ expect( title?.nextElementSibling ).toBe( badge );
+ expect( badge?.nextElementSibling ).toBe( footer );
+ } );
+
+ it( 'renders the badge in the footer above the price on regular cards', () => {
+ const { container } = renderCard( ProductCardType.regular );
+ const badge = getBadge( container );
+ const footer = container.querySelector(
+ '.woocommerce-marketplace__product-card__footer'
+ );
+ const price = container.querySelector(
+ '.woocommerce-marketplace__product-card__price'
+ );
+
+ expect( badge ).not.toBeNull();
+ expect( badge?.parentElement ).toBe( footer );
+ expect( footer?.firstElementChild ).toBe( badge );
+ expect( badge?.nextElementSibling ).toBe( price );
+ } );
+
+ it( 'renders no badge when the product does not have one', () => {
+ const { container } = render(
+ <MarketplaceContext.Provider value={ context }>
+ <ProductCard
+ type={ ProductType.extension }
+ product={ { ...product, hasQualityBadge: false } }
+ cardType={ ProductCardType.compact }
+ tracksData={ {} }
+ />
+ </MarketplaceContext.Provider>
+ );
+
+ expect( getBadge( container ) ).toBeNull();
+ } );
+} );
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/quality-badge.scss b/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/quality-badge.scss
index 3d0b0d68af8..c64021b9a54 100644
--- a/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/quality-badge.scss
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/quality-badge.scss
@@ -1,8 +1,9 @@
@import "../../stylesheets/_variables.scss";
.woocommerce-marketplace {
- // Chip on product cards. Sits on its own row at the bottom of the card
- // footer, below the price, to avoid crowding the title on narrow cards.
+ // Chip on product cards. Regular cards show it on its own row in the
+ // footer, above the price; compact cards show it between the title and
+ // the price.
&__quality-badge {
width: 100%;
@@ -10,8 +11,8 @@
align-items: center;
background: $studio-gray-0;
border: 0;
- border-radius: $grid-unit-05;
- color: $gray-900;
+ border-radius: $grid-unit-10;
+ color: $studio-gray-70;
cursor: pointer;
display: inline-flex;
font-family: inherit;
@@ -19,7 +20,7 @@
font-weight: 500;
gap: $grid-unit-05;
line-height: 20px;
- padding: 0 $grid-unit-10 0 $grid-unit-05;
+ padding: 2px $grid-unit-10;
/* Punch through the "whole card clickable" trick so the popover opens: */
position: relative;
}