Skip to content

Add composable cart parts - #1509

Merged
wcole1-godaddy merged 3 commits into
godaddy:mainfrom
sbolinger-godaddy:feat/composable-cart-parts
Oct 19, 2026
Merged

wcole1-godaddy merged 3 commits into
godaddy:mainfrom
sbolinger-godaddy:feat/composable-cart-parts

Conversation

@sbolinger-godaddy

@sbolinger-godaddy sbolinger-godaddy commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Why

This is PR 3 of the compound components work, after #1502 (ProductDetails) and #1506 (Catalog and ProductCard). It does the same for the cart, so the packaged components and their parts are complete.

AAB and other agents should keep using the packaged components, <CartDrawer /> via <CommerceStorefront />. The parts are an opt-in for layouts that need more customization.

The parts can't produce impossible states. Cart and item parts take no item, quantity or price props. Every change still goes through the shared cart queue in the provider, and sample items never reach the store.

What

  • Cart parts, for a drawer or a page: CartDrawerRoot is the dialog, and CartRoot is for a page such as /cart. Both use the same parts: CartTitle, CartDescription, CartClose (nothing outside the drawer), CartNotices, CartItems, CartEmpty, CartSummary, CartSubtotal, CartCheckoutNote and CartCheckout.
  • Item parts: whatever you put inside CartItems is the item, rendered once per line. It's built from CartItemRoot, CartItemImage, CartItemName, CartItemOptions, CartItemPrice, CartItemQuantity and CartItemRemove.
  • Hooks: useCart() works anywhere inside the provider, for example for a custom cart button. useCartItem() reads one line.
  • Cart pages: CommerceProvider (and CommerceStorefront) take an optional onItemAdded({ name, quantity, isSample }), so a site without a drawer can confirm adds. An add only opens the drawer while a CartDrawer or CartDrawerRoot is mounted; without one, open stays false.
  • CartDrawer is now composed from these parts. The README has a "Custom cart layouts" section with the drawer written out and a cart page example.
  • AddToCartButton says why it's disabled through its accessible description: over stock, a quantity that isn't a whole number of 1 or more, the cart loading, the store connecting, or no store. ProductDetailsQuantity shows the quantity message under the field and marks the field invalid.
  • Catalog: Catalog's title, description and showHeader props are deprecated but still work. The loading placeholders now render inside the CatalogProducts element, so they follow the host's grid.

Not repeating the #1502 and #1506 fixes

  • Host classes: every part puts className on a commerce-part wrapper the CSS reset skips. The panel's position is on an outer element, so host width and margin classes on CartDrawerRoot work, and mr-auto opens it from the left. The shipped-CSS conflict check covers every cart part.
  • The list is role="list", not a <ul>: a <ul> needs a package margin and padding reset, and the conflict check showed that reset would beat host classes on CartItems.
  • Naming: every part uses its root's prefix (Cart* or CartItem*).
  • README drift: the test app renders the README's written-out drawer and checks it's pixel-identical to <CartDrawer />.

Visual changes

  • Dividers between cart lines: the packaged drawer always declared divide-y between lines, but the CSS reset outranked it, so 0.4.0 never showed them. Lines are now part elements the reset skips, so the 1px divider appears. Easy to drop if we'd rather stay pixel-identical.
  • Catalog placeholders use the loaded grid's spacing from lg: 32px between cards and 40px above and below, instead of 24px and 32px.
  • Markup: each part has a wrapper <div>, the line list is role="list" with role="listitem" lines, and the drawer panel sits inside a positioning element. The commerce-cart-checkout-adjustments-note class moved from the note's paragraph to its outer element, so hiding it also removes its spacing.

Notes

  • Radix 1.1 and 1.3: the package allows @radix-ui/react-dialog ^1.1.6. The workspace resolves 1.1.15 and a fresh install resolves 1.3.1, and the two link the dialog description differently. CartDrawerRoot turns the link off only while no CartDescription is mounted, which works on both. The unit tests run on 1.1.15 and the test app on 1.3.1.

  • Host styles on plain elements inside a root lose: the reset sets color: inherit and removes borders on every non-part element, and because it's unlayered it beats a host's layered CSS. Host heading and paragraph styles lose their size and margin too. This applies to every root, not just the cart. The workaround is to put the class on a part, whose color reaches its children. The utility prefix follow-up could limit the reset to the package's own elements.

  • Review fixes (second commit): an independent review and a restyled "boutique" test site found:

    • Oversold SKUs (a negative available count) were enabled; they count as sold out again, as on main.
    • CartEmpty ignored empty children ({custom && …}); it now falls back like CartItems does.
    • Cart pages got no feedback on add; this is what onItemAdded is for.
    • An item template without CartItemRoot rendered an empty list silently; it now warns in development.
    • The checkout note class was on two elements.
    • Sample cart items were rebuilt every render, which defeated useCart's memo.

    A third commit fixes four more review points:

    • The README cart page example put its <h1> inside CartRoot, against the README's own advice.
    • A failed store connection said "Connecting to the store…"; it now says "The store could not be reached."
    • Mounting two drawers (CommerceStorefront plus a custom CartDrawerRoot) opened both silently; development builds now warn.
    • A CartProvider memo that never hit was removed, along with its misleading comment.

    The README now also covers parts that render nothing in a host grid, and the reset limit on host markup (next note).

  • Next: prefixing the package utilities (known issue 1), then the AAB components.md and skill.md updates. components.md still uses <Catalog showHeader={false} />, which this PR deprecates.

Testing

Package checks: typecheck, lint, build and all 86 storefront tests pass, 24 of them new. They cover:

  • a custom drawer that edits lines, removes them and checks out
  • the dialog description, which is linked only while CartDescription is rendered
  • a cart page with a custom empty state and no close button
  • the sample cart on a page, which never reaches the network
  • host classes kept while connecting
  • a storage warning and a refreshable error shown together
  • useCart counts for a custom button
  • errors that name a misplaced part
  • the shipped-CSS check on every cart part
  • dividers in the packaged drawer
  • the add button's reasons
  • the quantity message
  • placeholders in a host grid
  • oversold stock
  • the CartEmpty fallback
  • the missing-CartItemRoot warning
  • drawers opening only when mounted, and onItemAdded for live and sample adds

Test app (commerce-component-test-app, installs the pnpm pack tarball the way a host would, with Radix dialog 1.3.1):

  • Smoke run: all 15 checks pass on this PR and on main (0.4.0).
  • Parts page: all 13 checks pass. The 3 new ones cover:
    • layered host classes on every cart part, with a host panel 300px wide opening from the left
    • a cart page with no drawer
    • the README drawer matching <CartDrawer /> pixel for pixel with live items, sample items and an empty cart
  • New walkthrough: five shopper flows, with no console errors and no duplicate or broken element IDs:
    • an over-stock and an empty quantity on the product page, then adding
    • editing lines in the packaged drawer, with focus returning to the add button on close
    • a customized drawer (opens from the left, totals first, its own line layout and empty state, a useCart "Bag" button and useCartItem line prices)
    • a cart page
    • the sample cart in the custom drawer
  • Boutique restyle: a separate page restyles the storefront as a clothing boutique on a Tailwind-v4-style layered stylesheet, using only parts and hooks. It has image swatches and size pills from useProductDetailsOption, a cart page with no drawer, and a toast from onItemAdded. Its 4 screen checks pass at desktop and phone widths. The site's own headings and buttons inside a root needed the workarounds the README now describes; the reset fix is planned with the utility prefix PR.
  • Pixel comparison against main: 17 of 19 screenshots are identical. In the other two, the live and sample drawers, the only difference is the new 1px dividers, with each line below shifted down by them.

Screenshots

Packaged drawer with the new dividers, and a customized drawer built from the parts:

Packaged cart drawer with dividers between lines Customized cart drawer opening from the left with totals first

A cart page built from the same parts, and the quantity message on a product page:

Cart page built from CartRoot Product page showing Only 5 available under the quantity field

🤖 Generated with Claude Code

CartDrawerRoot builds a custom cart drawer and CartRoot a cart page from the
same parts: title, description, close, notices, items, empty state, summary,
subtotal, checkout note and checkout. CartItems renders its child as the item
for every line, built from CartItemRoot and the image, name, options, price,
quantity and remove parts. useCart reads the cart anywhere inside the
provider and useCartItem reads one line. CartDrawer is now composed from
these parts; the dividers it always declared between lines now show.

AddToCartButton says why it is disabled, and ProductDetailsQuantity shows
the quantity message under the field. Catalog's title, description and
showHeader props are deprecated, and catalog loading placeholders render
inside the host's grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 96c7380

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@godaddy/gd-commerce-storefront Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

sbolinger-godaddy and others added 2 commits October 16, 2026 13:13
- Oversold SKUs (negative available count) count as sold out again, as on
  main, in AddToCartButton, ProductDetailsAvailability and the quantity.
- CartEmpty falls back to its default content when its children render
  nothing, using the same check as CartItems and CatalogProducts.
- Adds open the drawer only while a CartDrawerRoot is mounted, and
  CommerceProvider takes onItemAdded so a site with a cart page can
  confirm adds. Without a drawer, open stays false.
- CartItems warns in development when its template has no CartItemRoot.
- The checkout note's stable class is on its outer element only.
- The cart context value and sample items are memoized.
- The conflict test now renders CartNotices and CartEmpty. The README
  covers onItemAdded, once-per-drawer title and description, parts that
  render nothing in a host grid, and the reset limit on host markup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Move the cart page example's h1 outside CartRoot, as the README advises.
- Give AddToCartButton its own reason when the store cannot be reached.
- Warn in development when more than one cart drawer is mounted.
- Drop the CartProvider memo, which never hit because the commerce value is new each render.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sbolinger-godaddy
sbolinger-godaddy marked this pull request as ready for review October 19, 2026 18:14
@sbolinger-godaddy
sbolinger-godaddy requested a review from a team as a code owner October 19, 2026 18:14
@wcole1-godaddy
wcole1-godaddy merged commit 938cc6d into godaddy:main Oct 19, 2026
3 checks passed
wcole1-godaddy pushed a commit that referenced this pull request Oct 19, 2026
- Oversold SKUs (negative available count) count as sold out again, as on
  main, in AddToCartButton, ProductDetailsAvailability and the quantity.
- CartEmpty falls back to its default content when its children render
  nothing, using the same check as CartItems and CatalogProducts.
- Adds open the drawer only while a CartDrawerRoot is mounted, and
  CommerceProvider takes onItemAdded so a site with a cart page can
  confirm adds. Without a drawer, open stays false.
- CartItems warns in development when its template has no CartItemRoot.
- The checkout note's stable class is on its outer element only.
- The cart context value and sample items are memoized.
- The conflict test now renders CartNotices and CartEmpty. The README
  covers onItemAdded, once-per-drawer title and description, parts that
  render nothing in a host grid, and the reset limit on host markup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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