Skip to content

Add support for Option Type 'Buttons' kind and fix Variant Image count regression#20

Open
umeshravani wants to merge 4 commits into
spree:mainfrom
umeshravani:main
Open

Add support for Option Type 'Buttons' kind and fix Variant Image count regression#20
umeshravani wants to merge 4 commits into
spree:mainfrom
umeshravani:main

Conversation

@umeshravani
Copy link
Copy Markdown
Contributor

@umeshravani umeshravani commented Apr 13, 2026

  1. Summary of Changes
    This PR updates the Spree Storefront theme to fully support the new Option Type Enhancement features introduced in Spree 5.4. It ensures that the storefront honors the kind rendering hint (Dropdown, Color Swatch, or Buttons) provided by the backend.

  2. Problems Solved
    Fixed 5.4 Core Regression: Resolved a NoMethodError in _featured_image.html.erb caused by the removal of the legacy image_count method from the Spree::Variant model. Switched to object.images.count for compatibility.

Native Theme Integration: Replaced hardcoded Tailwind utility classes in the variant picker with native Spree classes (btn-primary, rounded-input, border-default). This allows variant selectors to automatically inherit styles (colors, border-radius) set via the Spree Page Builder.

  1. New Features
    Buttons/Chips Support: Implemented a non-dropdown "Buttons" layout for option types.

Hugging Layout: The buttons are designed to "hug" the text content with consistent padding (py-2 px-4), preventing layout shifts when selecting options.

UI Stability: Applied specific CSS overrides (!font-medium, !normal-case) to ensure that selected buttons do not change size or text transformation, maintaining a stable UX.

  1. Technical Implementation Notes
    A11y: Maintained accessibility standards using role="radiogroup" for buttons and role="listbox" for swatches.

Backwards Compatibility: Maintained the default "Dropdown" behavior for any option types where a specific kind is not defined.

Summary by CodeRabbit

  • New Features

    • Added support for button-style variant options alongside existing color swatches and dropdowns.
  • Style

    • Improved spacing and visual hierarchy in the variant picker interface.
  • Refactor

    • Enhanced variant option rendering with improved semantic markup and accessibility.
    • Refined selection and disabled-state handling for all variant controls.

Review Change Stack

Updated the condition to check for images using 'object.images.count' instead of 'object.image_count'.

The Problem: The legacy image_count method was removed from the Spree::Variant model in 5.4 to optimize performance in favor of the new primary_media association.

The Fix: Switched to object.images.count. This is a standard ActiveRecord call that resolves the "undefined method" crash while maintaining the logic that decides whether to show a secondary hover image.
Updated the variant picker layout to improve spacing and semantics. Changed class names and adjusted the structure for better accessibility.

The Problem: The original file was binary—it only knew "Color Swatch" or "Dropdown." It ignored the new buttons kind.

The Fix: Integrated a three-way switch based on option_type.kind.

Native Integration: Replaced hardcoded Tailwind colors with Spree theme variables (text-default, bg-background, rounded-input). This ensures that if a user changes their theme settings in the Page Builder, the variant picker actually updates to match.
The Problem: High-UX friction. When a button was selected, it inherited global btn-primary styles which made it bold and uppercase, causing the button to "jump" in size and look misaligned.

The Fix: Used Utility Overrides (!py-2 !px-4 !font-medium !normal-case).

Result: The buttons now "hug" the text and maintain a perfectly consistent size regardless of whether they are selected or unselected.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fdcb1ac8-67a5-4b28-8055-6c031d57fa65

📥 Commits

Reviewing files that changed from the base of the PR and between a9831d0 and f7bbb9e.

📒 Files selected for processing (1)
  • storefront/app/views/themes/default/spree/products/_featured_image.html.erb
✅ Files skipped from review due to trivial changes (1)
  • storefront/app/views/themes/default/spree/products/_featured_image.html.erb

Walkthrough

Adds a single ERB comment with the template path to the featured image partial; no functional or interface changes are present.

Changes

Featured Image Template

Layer / File(s) Summary
Add template path comment
storefront/app/views/themes/default/spree/products/_featured_image.html.erb
Inserted an ERB comment at the top of the featured image partial documenting its template path. No logic or exported/public declarations changed.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~2 minutes

Possibly related PRs

Poem

🐰 A tiny note atop the frame,
The partial knows its rightful name.
No bugs to chase, no specs to tweak,
A quiet comment, mild and meek—
I nibble code and hop away.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: adding Option Type 'Buttons' kind support and fixing the Variant Image count regression.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


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.

@umeshravani
Copy link
Copy Markdown
Contributor Author

Default First Load Preview:
Screenshot 2026-04-13 at 9 02 07 PM

Selected Variant Preview:
Screenshot 2026-04-13 at 9 02 33 PM

Copy link
Copy Markdown
Contributor

@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: 1

🧹 Nitpick comments (4)
storefront/app/views/themes/default/spree/products/_featured_image.html.erb (2)

19-19: Remove commit-style inline note from the template.

Line 19 looks like a temporary migration/debug note and should be removed to keep view code clean.

Suggested cleanup
-    <%# FIXED LINE BELOW: Changed image_count to images.count %>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@storefront/app/views/themes/default/spree/products/_featured_image.html.erb`
at line 19, Remove the commit-style inline note from the ERB partial
_featured_image.html.erb (the comment line "<%# FIXED LINE BELOW: Changed
image_count to images.count %>") — simply delete that comment so the view
contains only the actual template markup and helpers (e.g., the code using
images.count or related helpers) without temporary migration/debug notes.

20-20: Simplify the image guard to avoid redundant association checks.

At Line 20, object.images.any? && object.images.count > 1 is redundant. Checking if any images exist followed by checking if more than one exists is unnecessary. Use object.images.size > 1 instead for clarity and to avoid potential extra database queries.

Proposed refactor
-    <% if object.images.any? && object.images.count > 1 && object.secondary_image.present? && object.secondary_image.attached? %>
+    <% if object.images.size > 1 && object.secondary_image.present? && object.secondary_image.attached? %>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@storefront/app/views/themes/default/spree/products/_featured_image.html.erb`
at line 20, The conditional currently redundantly checks object.images.any? and
object.images.count > 1; replace that compound check with a single size-based
check to avoid extra queries—use object.images.size > 1 and keep the existing
secondary_image check (object.secondary_image.present? &&
object.secondary_image.attached?) so the full guard becomes object.images.size >
1 && object.secondary_image.present? && object.secondary_image.attached?.
storefront/app/views/themes/default/spree/products/_variant_options.html.erb (1)

41-52: Minor inconsistency in disabled opacity values.

The buttons branch uses opacity-40 for disabled state (line 45), while the default dropdown branch uses opacity-50 (line 63). Consider aligning these for visual consistency across option types.

♻️ Optional: Align opacity values
-                  <%= 'opacity-40 cursor-not-allowed' if option_disabled %>">
+                  <%= 'opacity-50 cursor-not-allowed' if option_disabled %>">

Alternatively, extract the disabled classes to a shared helper/constant.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@storefront/app/views/themes/default/spree/products/_variant_options.html.erb`
around lines 41 - 52, The disabled opacity is inconsistent between the buttons
branch (option_type.kind == 'buttons') which uses "opacity-40" and the dropdown
branch which uses "opacity-50"; update the conditional for the label rendering
that references option_disabled (and any corresponding class logic near
radio_button_tag/selected_option) so both branches use the same opacity value
(pick either 40 or 50) or extract the disabled classes into a shared
helper/constant used by both branches to ensure visual consistency.
storefront/app/views/themes/default/spree/products/_variant_picker.html.erb (1)

46-76: Accessibility: aria-haspopup="listbox" doesn't match dropdown content structure.

The button declares aria-haspopup="listbox" (line 52), but the dropdown menu (line 66) lacks role="listbox", and the rendered options in _variant_options.html.erb default branch are radio inputs without role="option" wrappers. This creates a semantic mismatch for assistive technologies.

Consider either:

  1. Adding role="listbox" to the menu container and wrapping options with role="option", or
  2. Removing aria-haspopup="listbox" since the implementation uses radio inputs rather than a true listbox pattern
♻️ Option 1: Add listbox role to dropdown menu
             <div data-dropdown-target="menu"
-                 class="hidden absolute top-12 left-0 z-50 w-full shadow-lg border border-default bg-background overflow-hidden rounded-input">
-              <div class="max-h-60 overflow-y-auto">
+                 class="hidden absolute top-12 left-0 z-50 w-full shadow-lg border border-default bg-background overflow-hidden rounded-input"
+                 role="listbox"
+                 aria-label="<%= option_type.presentation %>">
+              <div class="max-h-60 overflow-y-auto" role="presentation">

Note: This would also require updating _variant_options.html.erb default branch to wrap each option with role="option".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@storefront/app/views/themes/default/spree/products/_variant_picker.html.erb`
around lines 46 - 76, The button currently declares aria-haspopup="listbox" but
the dropdown (data-dropdown-target="menu") and the options (rendered by
_variant_options.html.erb) don't follow listbox semantics; update the menu
container (the element with data-dropdown-target="menu") to include
role="listbox" and update _variant_options.html.erb default branch to wrap each
option/radio with role="option" (or add role="option" to the radio's wrapper
element) so ARIA roles match, keeping the button's aria-haspopup as-is; ensure
each option has a unique id and, if you manage active selection, wire
aria-activedescendant on the button/menu to that id for proper focus handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@storefront/app/views/themes/default/spree/products/_variant_picker.html.erb`:
- Line 9: Replace direct comparisons of option_type.kind == 'color_swatch' with
the predicate method option_type.color_swatch? to follow the codebase
convention; update the usages in the variant picker and variant options
templates (replace the occurrences of option_type.kind == 'color_swatch' with
option_type.color_swatch?), ensuring both conditional branches and any dependent
logic use the predicate method so behavior remains identical but consistent with
helpers like product_selected_option_for_option and
option_type_colors_preview_styles.

---

Nitpick comments:
In `@storefront/app/views/themes/default/spree/products/_featured_image.html.erb`:
- Line 19: Remove the commit-style inline note from the ERB partial
_featured_image.html.erb (the comment line "<%# FIXED LINE BELOW: Changed
image_count to images.count %>") — simply delete that comment so the view
contains only the actual template markup and helpers (e.g., the code using
images.count or related helpers) without temporary migration/debug notes.
- Line 20: The conditional currently redundantly checks object.images.any? and
object.images.count > 1; replace that compound check with a single size-based
check to avoid extra queries—use object.images.size > 1 and keep the existing
secondary_image check (object.secondary_image.present? &&
object.secondary_image.attached?) so the full guard becomes object.images.size >
1 && object.secondary_image.present? && object.secondary_image.attached?.

In
`@storefront/app/views/themes/default/spree/products/_variant_options.html.erb`:
- Around line 41-52: The disabled opacity is inconsistent between the buttons
branch (option_type.kind == 'buttons') which uses "opacity-40" and the dropdown
branch which uses "opacity-50"; update the conditional for the label rendering
that references option_disabled (and any corresponding class logic near
radio_button_tag/selected_option) so both branches use the same opacity value
(pick either 40 or 50) or extract the disabled classes into a shared
helper/constant used by both branches to ensure visual consistency.

In `@storefront/app/views/themes/default/spree/products/_variant_picker.html.erb`:
- Around line 46-76: The button currently declares aria-haspopup="listbox" but
the dropdown (data-dropdown-target="menu") and the options (rendered by
_variant_options.html.erb) don't follow listbox semantics; update the menu
container (the element with data-dropdown-target="menu") to include
role="listbox" and update _variant_options.html.erb default branch to wrap each
option/radio with role="option" (or add role="option" to the radio's wrapper
element) so ARIA roles match, keeping the button's aria-haspopup as-is; ensure
each option has a unique id and, if you manage active selection, wire
aria-activedescendant on the button/menu to that id for proper focus handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dad3de58-f793-4b6f-a44b-7f5ba3e5e46f

📥 Commits

Reviewing files that changed from the base of the PR and between 751b806 and a9831d0.

📒 Files selected for processing (3)
  • storefront/app/views/themes/default/spree/products/_featured_image.html.erb
  • storefront/app/views/themes/default/spree/products/_variant_options.html.erb
  • storefront/app/views/themes/default/spree/products/_variant_picker.html.erb

<% selected_option = product_selected_option_for_option(option_type, product: product, selected_variant: selected_variant, options_param_name: options_param_name) %>
<% if option_type.color_swatch? %>

<% if option_type.kind == 'color_swatch' %>
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that color_swatch? method is equivalent to kind == 'color_swatch'
# and check for any other usages of color_swatch? in the codebase

echo "=== Searching for color_swatch? method definition ==="
ast-grep --pattern 'def color_swatch?
  $$$
end'

echo ""
echo "=== Searching for all color_swatch? usages ==="
rg -n 'color_swatch\?' --type erb --type ruby

echo ""
echo "=== Searching for kind comparisons ==="
rg -n "kind\s*==\s*['\"]color_swatch['\"]" --type erb --type ruby

Repository: spree/spree-rails-storefront

Length of output: 1842


🏁 Script executed:

# Search for method definitions or where color_swatch? might be defined
echo "=== Searching for color_swatch in app/models ==="
find storefront/app/models -type f -name "*.rb" -exec rg -l "color_swatch" {} \;

echo ""
echo "=== Checking if there's a Gemfile with Spree dependencies ==="
head -20 Gemfile 2>/dev/null || head -20 storefront/Gemfile 2>/dev/null

echo ""
echo "=== Searching for any OptionType references ==="
rg -n "class.*OptionType\|def.*option_type" --type ruby -A 3 -B 1 | head -50

echo ""
echo "=== Looking for delegate or method_missing patterns ==="
rg -n "delegate\|alias_method\|define_method.*color" --type ruby

Repository: spree/spree-rails-storefront

Length of output: 780


🏁 Script executed:

# Search in the Spree dependency to understand the OptionType model
echo "=== Checking if Spree is included as a gem/path ==="
cat storefront/Gemfile | grep -A 5 -B 5 "spree_opts\|'spree"

echo ""
echo "=== Look for any Spree version specifications ==="
rg -n "spree" storefront/Gemfile.lock -A 2 -B 1 | head -30

echo ""
echo "=== Check if there's any documentation or comments about the kind field ==="
rg -n "kind.*color_swatch\|color_swatch.*kind" --type erb --type ruby -B 2 -A 2

echo ""
echo "=== Search for schema or migration files that show the kind field ==="
find . -name "*.rb" -path "*/migrate/*" -o -name "schema.rb" | xargs rg -l "option_type\|option_kinds" 2>/dev/null | head -5

Repository: spree/spree-rails-storefront

Length of output: 973


🏁 Script executed:

# Search for the git history of the variant_picker file to understand the change context
echo "=== Check git log for recent changes to _variant_picker.html.erb ==="
git log --oneline -10 -- storefront/app/views/themes/default/spree/products/_variant_picker.html.erb 2>/dev/null || echo "Git history not available"

echo ""
echo "=== Check if there's any PR or commit message explaining the change ==="
git log -p -1 -- storefront/app/views/themes/default/spree/products/_variant_picker.html.erb 2>/dev/null | head -50 || echo "Detailed log not available"

echo ""
echo "=== Verify the actual uses of color_swatch? vs kind == in context ==="
echo "File: _filters.html.erb"
rg -n -B 2 -A 2 "color_swatch\?" storefront/app/views/themes/default/spree/products/_filters.html.erb

echo ""
echo "File: _variant_options.html.erb (line 28)"
rg -n -B 2 -A 2 "kind.*color_swatch" storefront/app/views/themes/default/spree/products/_variant_options.html.erb

echo ""
echo "=== Check if option_type is the same object type in both cases ==="
echo "Looking at contexts where both methods might be called..."
rg -n "option_type\.kind\|option_type\.color_swatch" storefront/app/views/themes/default/spree/products/ -A 1 -B 1

Repository: spree/spree-rails-storefront

Length of output: 997


🏁 Script executed:

# Since Spree is loaded from github, let's check if there's any inline documentation or comments
# about what color_swatch? does in the context of this repository

echo "=== Check all files that reference both kind and color_swatch in same context ==="
rg -n "option_type" storefront/app/views/themes/default/spree/products/_variant_picker.html.erb -B 5 -A 10 | head -40

echo ""
echo "=== Look at the full _variant_options file to see how option_type is used ==="
cat -n storefront/app/views/themes/default/spree/products/_variant_options.html.erb | head -50

echo ""
echo "=== Check if there are any comments or documentation about the kind field ==="
rg -n "kind\|color.swatch" storefront/app/views/themes/default/spree/products/_variant_picker.html.erb -B 3 -A 3

Repository: spree/spree-rails-storefront

Length of output: 6319


🏁 Script executed:

# Let's check the variant_picker commit to understand the intent
echo "=== Full git diff for the variant_picker change ==="
git show a9831d0 -- storefront/app/views/themes/default/spree/products/_variant_picker.html.erb 2>/dev/null | head -100

echo ""
echo "=== Check if color_swatch? is consistently used in non-modified files ==="
rg -n "color_swatch\?" storefront/app/helpers/spree/products_helper.rb -B 1 -A 1

echo ""
echo "=== Summary: Files using direct kind comparison ==="
rg -l "kind.*==.*color_swatch\|kind.*==.*buttons" storefront/app/views --type erb

echo ""
echo "=== Summary: Files using color_swatch? method ==="
rg -l "color_swatch\?" storefront/app --type erb --type ruby

Repository: spree/spree-rails-storefront

Length of output: 6584


Use color_swatch? method for consistency with rest of codebase.

The template uses direct option_type.kind == 'color_swatch' comparison (lines 9, 26), but this breaks the established pattern used throughout the codebase. Related helpers and finders consistently use the option_type.color_swatch? method instead:

  • product_selected_option_for_option helper (products_helper.rb:85-87)
  • option_type_colors_preview_styles helper (products_helper.rb:229)
  • VariantFinder class (variant_finder.rb:51, 67)
  • Other views: _filters.html.erb, _color_swatches.html.erb, _line_item_options.html.erb

Update lines 9 and 26 to use option_type.color_swatch? instead of option_type.kind == 'color_swatch' to maintain consistency with the Spree API pattern used throughout the application. Similarly, update _variant_options.html.erb lines 28 and 41.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@storefront/app/views/themes/default/spree/products/_variant_picker.html.erb`
at line 9, Replace direct comparisons of option_type.kind == 'color_swatch' with
the predicate method option_type.color_swatch? to follow the codebase
convention; update the usages in the variant picker and variant options
templates (replace the occurrences of option_type.kind == 'color_swatch' with
option_type.color_swatch?), ensuring both conditional branches and any dependent
logic use the predicate method so behavior remains identical but consistent with
helpers like product_selected_option_for_option and
option_type_colors_preview_styles.

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