Skip to content

Conversation

@ritoban23
Copy link

@ritoban23 ritoban23 commented Oct 25, 2025

Summary

This PR enhances the storefront UI with improved focus states, keyboard navigation, animated loading skeletons, and empty state feedback for product grids. It significantly improves accessibility and user experience.

Related issues

Closes #31

Changes

  • Adds focus-visible ring states to product cards, buttons, and quantity controls
  • Implements arrow key navigation in ProductQuantityControl (↑↓←→)
  • Adds hover effects with scale and shadow to ProductCard
  • Creates ProductCardSkeleton for animated loading states
  • Displays styled empty state message when no products are available
  • Improves button focus behavior (blur on mouse click, show ring on keyboard focus)
  • Adds role and aria attributes for better screen reader support

Screenshots / recordings (if UI)

Screenshot (10) Screenshot (9) Screenshot (8) Screenshot (7)
  • Focus rings on cards and buttons
  • Keyboard navigation in quantity control
  • Hover effects on cards
  • Loading skeletons and empty state UI

How to test

  1. Start the dev server
  2. Tab through product cards and buttons to see focus rings
  3. Click "Purchase" and use arrow keys in quantity control
  4. Hover over product cards to see scale and shadow effects
  5. Check loading and empty states in the product grid

Checklist

  • Follows project code style (TypeScript, Next.js App Router, shadcn/ui, Tailwind)
  • Includes tests or reasoning why not needed
  • Updated docs/README where appropriate

Summary by CodeRabbit

  • New Features

    • Loading skeleton screens display while products fetch
    • Empty state messaging when no products are available
    • Keyboard navigation support (arrow keys) for quantity adjustments
  • Style

    • Enhanced transitions and hover effects on interactive elements
    • Improved focus indicators and visual feedback for better accessibility

@coderabbitai
Copy link

coderabbitai bot commented Oct 25, 2025

Walkthrough

Product components receive accessibility and visual enhancements: focus-visible states on buttons, keyboard navigation (arrow keys) for quantity controls, new animated skeleton loaders for loading states, and empty state messaging. No API changes; additions are purely presentational and interactive.

Changes

Cohort / File(s) Summary
UI Enhancement & Accessibility
src/components/product/ProductCard.tsx, src/components/product/ProductQuantityControl.tsx
ProductCard adds CSS transitions, focus-visible rings, hover/focus-within interactions (border, shadow, scale) to container and buttons. ProductQuantityControl introduces keyboard navigation (arrow keys for increment/decrement), button refs for programmatic focus, mouse handlers to blur after clicks, and enhanced aria attributes (role="group", aria-label, aria-live status).
Loading & Empty States
src/components/product/ProductCardSkeleton.tsx, src/components/product/ProductGrid.tsx
New ProductCardSkeleton and ProductGridSkeleton components render animated pulse placeholders. ProductGrid now accepts isLoading and skeletonCount props, conditionally rendering skeleton grid during load, empty state message when no products, or product grid otherwise. Updates product key from name to product_id.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant QtyControl as ProductQuantityControl
    participant Ref as Button Ref

    User->>QtyControl: Press ArrowUp/ArrowRight
    QtyControl->>QtyControl: handleKeyDown triggered
    QtyControl->>QtyControl: Increment quantity
    QtyControl->>Ref: Focus increment button
    Ref->>User: Visual focus ring appears

    User->>QtyControl: Press ArrowDown/ArrowLeft
    QtyControl->>QtyControl: handleKeyDown triggered
    QtyControl->>QtyControl: Decrement quantity
    QtyControl->>Ref: Focus decrement button
    Ref->>User: Visual focus ring appears

    User->>QtyControl: Mouse click button
    QtyControl->>QtyControl: handleMouseDown (blur)
    QtyControl->>User: Focus ring removed
Loading
sequenceDiagram
    participant App
    participant ProductGrid as ProductGrid
    participant Skeleton as ProductGridSkeleton
    participant Card as ProductCard

    App->>ProductGrid: Render with isLoading=true
    ProductGrid->>Skeleton: Render skeleton grid
    Skeleton->>User: Display animated pulse placeholders

    Note over App: Data fetches...

    App->>ProductGrid: Render with isLoading=false, products=[]
    ProductGrid->>ProductGrid: No products, render empty state
    ProductGrid->>User: Display empty state message

    App->>ProductGrid: Render with isLoading=false, products=[...]
    ProductGrid->>Card: Map products to ProductCard components
    Card->>User: Display interactive product cards with focus states
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

  • ProductCard.tsx & ProductQuantityControl.tsx: Styling and keyboard handler additions are straightforward; verify transition classes, focus-visible ring styling, and keyboard event logic (arrow key handling, ref usage).
  • ProductCardSkeleton.tsx: New presentational components with standard Tailwind utility classes; verify grid responsive breakpoints and skeleton animation.
  • ProductGrid.tsx: Conditional rendering logic straightforward; verify empty state display, skeleton grid integration, and product_id key replacement.

Poem

🐰 Focus rings gleam on buttons bright,
Arrow keys dance left and right,
Skeletons pulse while data loads,
Empty states guide users on their roads,
Accessibility blooms—the storefront's delight!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "feat: enhance UI polish with focus states, keyboard navigation, and loading fallbacks" directly aligns with the main changes in the changeset. According to the raw summary, the PR includes CSS transitions and accessibility enhancements for focus states across ProductCard and ProductQuantityControl components, implements arrow key navigation in ProductQuantityControl, and adds ProductCardSkeleton components for loading states with empty state handling in ProductGrid. The title accurately captures all three primary aspects of the work (focus states, keyboard navigation, and loading fallbacks) in clear, concise language that would help a teammate quickly understand the changeset's purpose.
Linked Issues Check ✅ Passed All primary coding requirements from linked issue #31 are met by the code changes. The PR implements clear focus-visible ring states for product cards and buttons, adds arrow key navigation (↑↓←→) in ProductQuantityControl with keyboard handlers, creates animated ProductCardSkeleton components for loading states, adds empty state feedback in ProductGrid, improves button focus behavior to show rings only on keyboard focus using onMouseDown blur handlers, and includes appropriate ARIA attributes (role="group", aria-label, aria-live, role="status") for screen reader support. The implementation aligns with the objectives specified in issue #31 without apparent gaps.
Out of Scope Changes Check ✅ Passed All changes in the PR are directly aligned with the objectives specified in linked issue #31. ProductCard.tsx modifications add focus-visible and hover enhancements as required; ProductCardSkeleton.tsx introduces the skeleton loaders explicitly requested; ProductGrid.tsx adds the loading state and empty state handling required; and ProductQuantityControl.tsx implements keyboard navigation and focus behavior improvements. No changes appear to address unrelated functionality, business logic modifications outside the UI/accessibility scope, or features not mentioned in issue #31.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/components/product/ProductQuantityControl.tsx (1)

47-47: Consider using a theme-based focus ring color.

The hard-coded ring-blue-500 might not align with your design system's theme colors. Consider using a CSS variable or Tailwind theme color for the focus ring.

Example using a custom color:

-        className="bg-button-secondary-bg hover:bg-button-secondary-bg-hover rounded-md aspect-square h-[24px] flex items-center justify-center transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed active:scale-95"
+        className="bg-button-secondary-bg hover:bg-button-secondary-bg-hover rounded-md aspect-square h-[24px] flex items-center justify-center transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed active:scale-95"

The same applies to line 66.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 930ebde and a00b2bc.

📒 Files selected for processing (4)
  • src/components/product/ProductCard.tsx (4 hunks)
  • src/components/product/ProductCardSkeleton.tsx (1 hunks)
  • src/components/product/ProductGrid.tsx (1 hunks)
  • src/components/product/ProductQuantityControl.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/product/ProductGrid.tsx (2)
src/components/product/ProductCard.tsx (2)
  • ProductCardProps (16-28)
  • ProductCard (107-255)
src/components/product/ProductCardSkeleton.tsx (1)
  • ProductGridSkeleton (27-35)
🔇 Additional comments (13)
src/components/product/ProductQuantityControl.tsx (4)

16-17: LGTM! Clean ref setup for focus management.

The refs are properly typed and enable the keyboard navigation focus flow.


31-34: LGTM! Appropriate focus ring management.

Blurring on mouse click ensures focus rings only appear for keyboard navigation, which aligns with modern accessibility best practices.


37-41: LGTM! Proper semantic grouping.

The role="group" with a descriptive aria-label correctly groups the quantity controls for assistive technologies.


53-60: LGTM! Excellent accessibility implementation.

The quantity display uses aria-live="polite", role="status", and a descriptive aria-label to ensure screen readers announce quantity changes appropriately. The font-medium also provides visual emphasis.

src/components/product/ProductCard.tsx (4)

79-79: LGTM! Good focus and hover states.

The transition, hover background, and focus-visible ring improve both polish and accessibility. Note: the ring-blue-500 is hard-coded here as well (see similar comment in ProductQuantityControl).


221-221: LGTM! Consistent focus styling.

The focus-visible ring and transition enhance accessibility. Same note applies regarding the hard-coded ring-blue-500 color (see ProductQuantityControl comments).


244-244: LGTM! Consistent focus styling.

Good consistency across all interactive elements. Same note about hard-coded focus ring color applies here as well.


167-167: Verify focus ring stacking through user testing; layout shift risk is minimal.

Verification found:

  1. Layout shift: The grid's gap-6 (24px) spacing adequately accommodates the scale-105 hover effect on cards up to 260px wide. This is normal hover behavior and unlikely to cause disruptive shifts.

  2. Focus indicator stacking: Confirmed—when buttons inside the card receive focus, both the button's individual focus-visible:ring and the card's focus-within:ring display simultaneously, creating nested rings. This is a valid design choice but would benefit from UX testing to ensure it's not visually confusing to users.

src/components/product/ProductCardSkeleton.tsx (2)

3-25: LGTM! Clean skeleton implementation.

The skeleton structure appropriately mirrors the actual ProductCard layout with clear semantic sections. The animate-pulse provides a good loading indicator, and sizing matches the real component.


27-35: LGTM!

Grid classes match ProductGrid perfectly. The skeleton implementation is clean with sensible defaults and maintains visual consistency across all responsive breakpoints.

src/components/product/ProductGrid.tsx (3)

8-9: LGTM! Sensible loading state props.

The optional isLoading and skeletonCount props with reasonable defaults enable flexible loading state management.


19-34: LGTM! Well-designed empty state.

The empty state provides clear, helpful messaging with appropriate visual hierarchy. The dashed border and muted text colors effectively communicate the "empty" status.


41-53: LGTM! Clean loading state implementation.

The conditional rendering clearly separates loading and loaded states, using the skeleton grid during loading and the actual product grid otherwise.

<div className="grid w-full grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 items-start gap-6">
{products.map((product, index) => (
<ProductCard
key={`${product.product_id}-${index}`}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Consider removing index from the key.

The key combines product_id with index. If product_id is guaranteed to be unique (which it should be as a product identifier), the index is unnecessary and should be removed. Using index in keys can mask issues and cause subtle React reconciliation problems when the list changes.

If product_id is unique, apply this diff:

-              key={`${product.product_id}-${index}`}
+              key={product.product_id}

If there's a possibility of duplicate product_id values, that would be a data integrity issue that should be addressed at the source rather than worked around in the UI.

Verify that product_id is unique across all products:


🏁 Script executed:

#!/bin/bash
# Search for where products are fetched/defined to understand uniqueness guarantees
echo "=== Finding product data sources ==="
rg -n "product_id" --type ts --type tsx -g '!*.test.*' -C 2

echo -e "\n=== Looking for product type definitions ==="
ast-grep --pattern 'interface $_ {
  $$$
  product_id: $_ 
  $$$
}'

Length of output: 1596



---

🏁 Script executed:

```shell
#!/bin/bash
# Corrected search without tsx file type
echo "=== Finding product_id references in TypeScript files ==="
rg -n "product_id" --type ts -g '!*.test.*' -C 2 | head -50

echo -e "\n=== Viewing ProductGrid component ==="
cat src/components/product/ProductGrid.tsx

echo -e "\n=== Searching for product data fetching/API calls ==="
rg -n "getProduct\|fetchProduct\|products:" --type ts -C 3

Length of output: 2987



---

🏁 Script executed:

```shell
#!/bin/bash
# Use correct path prefix shown in rg output
echo "=== Viewing ProductGrid component with correct path ==="
cat git/src/components/product/ProductGrid.tsx

echo -e "\n=== Checking storefront-client for API response structure ==="
cat git/src/lib/server/storefront-client.ts | grep -A 20 "type.*ProductApiResponse"

echo -e "\n=== Looking for fetch/API call logic ==="
rg -n "getProduct|fetchProduct|storefront-client" git/src/app/\\[slug\\]/page.tsx -A 5

Length of output: 377



---

🏁 Script executed:

```shell
#!/bin/bash
# Find the actual file structure
echo "=== Listing repository structure ==="
ls -la | head -20

echo -e "\n=== Finding ProductGrid.tsx ==="
fd ProductGrid

echo -e "\n=== Finding all TypeScript files in components/product ==="
fd . --path "*/components/product/*" --type f

echo -e "\n=== Showing directory structure ==="
find . -type d -name product | head -5

Length of output: 1673



---

🏁 Script executed:

```shell
#!/bin/bash
echo "=== Viewing ProductGrid component ==="
cat src/components/product/ProductGrid.tsx

echo -e "\n=== Viewing product type definitions ==="
cat src/type/product.ts

echo -e "\n=== Viewing page.tsx for data fetching ==="
cat src/app/\[slug\]/page.tsx

Length of output: 7204


Remove index from key—use product_id alone as the key.

The key should be product.product_id instead of ${product.product_id}-${index}. The product_id is the unique identifier returned by the API and designed to be unique. Including the array index is a React anti-pattern that can cause subtle reconciliation bugs when the list reorders; if items move positions, React loses track of component state because the key changes.

-              key={`${product.product_id}-${index}`}
+              key={product.product_id}

If product_id values were non-unique, that would indicate a data integrity problem that should be fixed at the API level, not worked around in the UI.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
key={`${product.product_id}-${index}`}
key={product.product_id}
🤖 Prompt for AI Agents
In src/components/product/ProductGrid.tsx around line 47, the list item key
currently concatenates the index with product.product_id; remove the index and
use product.product_id alone as the key. Edit the JSX to set
key={product.product_id} (ensure the prop exists and is a stable primitive),
then run the component to verify no TypeScript or runtime errors and that list
reordering preserves component state.

Comment on lines +19 to +29
const handleKeyDown = (e: React.KeyboardEvent, type: 'increment' | 'decrement') => {
if (e.key === "ArrowRight" || e.key === "ArrowUp") {
e.preventDefault();
onIncrement();
incrementRef.current?.focus();
} else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
e.preventDefault();
onDecrement();
decrementRef.current?.focus();
}
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused type parameter.

The type parameter is defined but never referenced in the function body. The keyboard navigation logic works correctly without it.

Apply this diff:

-  const handleKeyDown = (e: React.KeyboardEvent, type: 'increment' | 'decrement') => {
+  const handleKeyDown = (e: React.KeyboardEvent) => {
     if (e.key === "ArrowRight" || e.key === "ArrowUp") {
       e.preventDefault();
       onIncrement();
       incrementRef.current?.focus();
     } else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
       e.preventDefault();
       onDecrement();
       decrementRef.current?.focus();
     }
   };

Then update the onKeyDown handlers:

-        onKeyDown={(e) => handleKeyDown(e, 'decrement')}
+        onKeyDown={handleKeyDown}
-        onKeyDown={(e) => handleKeyDown(e, 'increment')}
+        onKeyDown={handleKeyDown}

Committable suggestion skipped: line range outside the PR's diff.

@aagarwal1012
Copy link
Member

@Amanfromearth can you review - this is a good PR

@Amanfromearth
Copy link
Member

@ritoban23 Can you change outline/focus colors to match brand accent

@Amanfromearth
Copy link
Member

And rebase the pr too

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.

Enhance Storefront UI: Focus States, Keyboard Navigation, Loading Skeletons, and Empty State Feedback

3 participants