Skip to content

[MAR-3033] Anchor extended product card CTAs to the bottom of the card - #175

Merged
rymorale merged 2 commits into
adobe:devfrom
rymorale:mar-3033-bottom-align-product-card-cta
Sep 28, 2026
Merged

rymorale merged 2 commits into
adobe:devfrom
rymorale:mar-3033-bottom-align-product-card-cta

Conversation

@rymorale

Copy link
Copy Markdown
Contributor

Description

Anchors the CTA buttons of the extended (productDetail) product card to the bottom of the card, so CTAs line up across cards with different amounts of content in the carousel.

  • ExtendedProductCard.kt
    • Replaced the outer Card with a Surface, which passes the card's height through to its content. Shape, background, border, shadow and click handling are unchanged.
    • Moved the CTA row out of the text column and added a weighted Spacer above it, so the CTAs sit at the bottom of the card. When content is taller than the card, the spacer shrinks to nothing and the card scrolls as before.
    • Moved the bottom content padding below the CTA row, so spacing is the same.
  • ExtendedProductCardTest.kt
    • Added extendedProductCard_ctaButtons_anchorToCardBottom_whenContentIsShort, which checks that both CTAs end at the card's bottom padding when content is short.

Behavior notes:

  • A single card shorter than --product-card-min-height now shows its blank space above the CTAs instead of below them.
  • The basic ProductCard (actionButton style) is unchanged.
  • Carousel height matching is unchanged: all cards still match the tallest card.

Related Issue

MAR-3033

Motivation and Context

In the carousel, every extended card is set to the tallest card's height. The outer Material3 Card wrapped its content in a way that dropped that height, so on shorter cards the CTA row sat right after the content, partway down the card. As a result, CTAs didn't line up across cards.

How Has This Been Tested?

  • Ran the product card instrumented tests (ExtendedProductCardTest, ProductCarouselTest, RecommendationCardsTest, ProductCardTest) on a Pixel 8 API 35 emulator: all 48 tests passed, including the new one.

Screenshots (if appropriate):

01-carousel-page1

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I have signed the Adobe Open Source CLA.
  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

Carousel cards are sized to the tallest card, but Card's built-in
content wrapper dropped that height, so CTAs on shorter cards sat
partway down the card. Replace the outer Card with a Surface, which
passes the card height through to its content, and add a weighted
spacer so the CTA row sits at the bottom. Content taller than the card
still scrolls.

Add a UI test checking that CTAs line up with the card's bottom
padding when content is short.
@rymorale
rymorale requested a review from cdhoffmann September 28, 2026 18:35
@codecov

codecov Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.15686% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...oncierge/ui/components/card/ExtendedProductCard.kt 92.15% 0 Missing and 4 partials ⚠️

📢 Thoughts on this report? Let us know!

@cdhoffmann cdhoffmann left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Spacer(weight(1f)) at L281 is placed after the text Column closes, but the price block is nested inside that column at L246 — so only the CTAs anchor. Prices still misalign across cards with different description lengths.

Figma treats pricing as a bottom element: "Content elements reside in a standard top stack, pushing functional pricing and CTAs to the bottom."

Suggest hoisting the price Column out as a sibling after the spacer:

Column(verticalScroll)
  imageBox
  Column { title; subtitle }
  Spacer(weight(1f))
  priceColumn                    // moved out
  Row { ctas }
  Spacer(contentPaddingBottom)

That also covers cards with a price but no CTA, which get no anchoring today since the Row at L288 is skipped.

iOS anchors price + CTA together as one block — worth keeping the two structurally identical.

Move the price column out of the text column so it sits after the
weighted spacer, keeping the price and CTAs together at the bottom of
the card.

Add tests for:
- the price anchoring to the bottom when there are no CTAs
- the price anchoring to the bottom when there is no "was" price
- prices and CTAs lining up across cards with different subtitles
- CTAs lining up when only one card has a "was" price

@cdhoffmann cdhoffmann left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Price and CTAs anchor together now — this matches iOS, where the spacer sits between the title/subtitle block and the price block for the same reason.

Nice catch re-applying start/end = style.contentPadding on the hoisted price Column; that padding was previously inherited from the text column and would have been easy to drop.

Tests cover the no-CTA and one-card-has-was-price cases. 👍

@rymorale
rymorale merged commit 0bf772a into adobe:dev Sep 28, 2026
18 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants