Skip to content

[cdx-436]: add-product-swatches-for-product-card - #52

Open
niizom wants to merge 2 commits into
mainfrom
cdx-436-add-product-swatches-for-product-card
Open

[cdx-436]: add-product-swatches-for-product-card#52
niizom wants to merge 2 commits into
mainfrom
cdx-436-add-product-swatches-for-product-card

Conversation

@niizom

@niizom niizom commented Jul 10, 2026

Copy link
Copy Markdown

Pull Request Checklist

Before you submit a pull request, please make sure you have to following:

  • I have added or updated TypeScript types for my changes, ensuring they are compatible with the existing codebase.
  • I have added JSDoc comments to my TypeScript definitions for improved documentation.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have added any necessary documentation (if appropriate).
  • I have made sure my PR is up-to-date with the main branch.

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no API changes)
  • Documentation content changes
  • TypeScript type definitions update
  • Other... Please describe:

Copilot AI review requested due to automatic review settings July 10, 2026 12:38
@niizom
niizom requested a review from a team as a code owner July 10, 2026 12:38
@niizom
niizom force-pushed the cdx-436-add-product-swatches-for-product-card branch from 4950df9 to 514431a Compare July 10, 2026 12:39
@niizom niizom changed the title [cdx-436-add-product-swatches-for-product-card] [cdx-436]: add-product-swatches-for-product-card Jul 10, 2026
constructor-claude-bedrock[bot]

This comment was marked as outdated.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds product swatches (color/variant selectors) to the ProductCard component, including optional “View more” truncation/expansion behavior, with Storybook docs/examples and test coverage.

Changes:

  • Introduces swatch-related types and a new useProductSwatch hook to manage selection + truncation/expansion state.
  • Updates ProductCard to render a SwatchSection, apply selected-swatch overrides to displayed product data, and emit events/callbacks using the displayed variant.
  • Adds Storybook stories/docs and unit/component tests for swatch behavior.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/utils/styleHelpers.ts Adds isHexColor helper used to decide hex-vs-image swatch previews.
src/types/productCardTypes.ts Adds swatch types and new ProductCard props/override hooks for swatches.
src/hooks/useProductSwatch.ts New hook for swatch selection and “view more” expansion logic.
src/components/product-card.tsx Renders swatches, computes displayProduct, and wires “view more” behavior + overrides.
src/stories/components/ProductCard/UsagePatterns.mdx Documents new swatch usage patterns in Storybook.
src/stories/components/ProductCard/ProductCard.stories.tsx Adds swatch-focused stories (basic, image swatches, view-more).
src/stories/components/ProductCard/Code Examples - Swatches.mdx New swatch code example documentation page.
src/stories/components/ProductCard/Code Examples - Compound Components.mdx Documents new ProductCard.SwatchSection compound component.
spec/hooks/useProductSwatch.test.ts Adds unit tests for hook selection/truncation/expansion.
spec/components/product-card/product-card.test.tsx Adds component tests validating UI, data overrides, and view-more behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +23
import { useMemo, useState, useCallback } from 'react';
import type { Product, SwatchItem, ProductSwatchObject } from '@/types/productCardTypes';

export function useProductSwatch(
{ swatchList = [], variationId }: Product,
maxSwatches?: number,
): ProductSwatchObject {
const [selectedSwatch, setSelectedSwatch] = useState<SwatchItem | undefined>(() =>
swatchList.find((swatch) => swatch.variationId === variationId),
);
const [isExpanded, setIsExpanded] = useState(false);

const onSwatchClick = useCallback((swatch: SwatchItem) => {
setSelectedSwatch((selectedSwatch) => {
if (selectedSwatch?.variationId === swatch.variationId) return undefined;
else return swatch;
});
}, []);

const onViewMoreSwatchesClick = useCallback(() => {
setIsExpanded(true);
}, []);

description?: ComponentOverrideProps<ProductCardProps>;
rating?: ComponentOverrideProps<ProductCardProps>;
price?: ComponentOverrideProps<ProductCardProps>;
swatches?: ComponentOverrideProps<ProductCardProps>;
className?: string;
}

export interface SwatchSectionProps extends IncludeRenderProps<ProductCardProps> {

@Alexey-Pavlov Alexey-Pavlov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@niizom Thanks for working on this! Left a few comments there and there. Could you please check when you get a chance?

Comment thread src/components/product-card.tsx Outdated
Comment thread src/components/product-card.tsx Outdated
Comment thread src/components/product-card.tsx Outdated
const isSelected = swatchItem.variationId === selectedSwatch?.variationId;
const bgValue = isHexColor(swatchItem.swatchPreview)
? swatchItem.swatchPreview
: `url(${swatchItem.swatchPreview}) center/cover`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The raw url(${swatchPreview}) broke the background shorthand when the URL contained spaces or parens. Quote-wrapping should fix that — inside a CSS string, spaces/parens are already literal — so we only need to escape the two chars that could break out of the quotes (" and \). Does this look right?

Suggested change
: `url(${swatchItem.swatchPreview}) center/cover`;
: `url("${swatchItem.swatchPreview.replace(/["\\]/g, '\\$&')}") center/cover`;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Comment thread src/components/product-card.tsx Outdated
data-testid={`cio-swatch-${swatchItem.variationId}`}
data-cnstrc-item-variation-id={swatchItem.variationId}
className={cn(
'cio-swatch-item cio:size-[25px] cio:rounded-full cio:border cio:border-black cio:cursor-pointer p-0',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
'cio-swatch-item cio:size-[25px] cio:rounded-full cio:border cio:border-black cio:cursor-pointer p-0',
'cio-swatch-item cio:size-[25px] cio:rounded-full cio:border cio:border-black cio:cursor-pointer cio:p-0',

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Comment thread src/components/product-card.tsx Outdated
type='button'
data-testid='cio-swatch-show-more'
className='cio-swatch-show-more cio:bg-transparent cio:border-0 cio:p-0 cio:text-xs cio:underline cio:cursor-pointer cio:text-[var(--cio-swatch-more-color,#333)] cio:hover:text-[var(--cio-swatch-more-hover-color,#000)]'
onClick={(e) => onViewMoreSwatchesClick?.(e, selectedSwatch)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
onClick={(e) => onViewMoreSwatchesClick?.(e, selectedSwatch)}
onClick={(e) => {
e.stopPropagation();
onViewMoreSwatchesClick?.(e, selectedSwatch);
}}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Comment thread src/utils/styleHelpers.ts
Comment thread src/hooks/useProductSwatch.ts

@constructor-claude-bedrock constructor-claude-bedrock Bot 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.

Code Review

This PR adds product swatch support to the ProductCard component — a solid feature addition with good test coverage, a clean new hook, and useful Storybook documentation.

Inline comments: 7 discussions added

Overall Assessment: ⚠️ Needs Work

);
const [isExpanded, setIsExpanded] = useState(false);

const productKey = JSON.stringify([id, variationId ?? null]);

This comment was marked as low quality.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The suggestion is valid but minor, JSON.stringify on a tiny array works fine and is correct

props.showViewMoreSwatches ? props.maxSwatches : undefined,
);
const displayProduct = useMemo(() => {
const filtered = Object.fromEntries(

This comment was marked as low quality.

@niizom niizom Aug 19, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The Object.fromEntries pattern is slightly verbose but functionally correct and safe. The "bug" described (a swatch intentionally having undefined to clear a field) contradicts how the TypeScript types are designed. SwatchItem fields are optional (absent when not applicable), not nullable

componentOverrides,
}),
[props, componentOverrides],
[props, displayProduct, swatch, componentOverrides],

This comment was marked as low quality.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is correct that the object envelope is a new reference every render. However, props in the dependency array has the exact same problem. It's the component's props object, which is also a new reference every render. So the useMemo already recomputes every render due to props.

onViewMoreSwatchesClick?.(e, selected);
},
[product, onProductClick],
[expandInline, swatch.onViewMoreSwatchesClick, onViewMoreSwatchesClick],

This comment was marked as low quality.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The callback is already stable via useCallback([], []) inside the hook - destructuring it out wouldn't change behavior, just adds noise

}) => {
const { componentOverrides, renderProps } = useProductCardContext();

if (!swatchList?.length) return null;

This comment was marked as low quality.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

the ?. is redundant

@Alexey-Pavlov Alexey-Pavlov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM! Thanks for working on this!

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.

3 participants