feat: add price slider filter to Shop page - #11
Conversation
Adds a dual-handle price range slider that auto-detects the min/max prices from the catalog and filters products in real-time with 300ms debounce. Pure React, no extra dependencies. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a dual-handle price range slider to the Shop page, allowing users to filter products by minimum and maximum prices. The changes include the new PriceSlider component, updated product API methods to handle price filtering and calculate the catalog's price range, and integration with the Shop page and ProductList component. Feedback on these changes highlights several critical improvements: resolving a potential crash in getPriceRange by using optional chaining, addressing a performance bottleneck caused by sequential pagination requests, implementing a 'push' behavior to prevent overlapping slider handles from getting stuck, and adding visible focus styles to ensure keyboard accessibility.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const unit = allPriceData[0].prices.currency_minor_unit ?? 2; | ||
| const symbol = allPriceData[0].prices.currency_symbol ?? '$'; |
There was a problem hiding this comment.
Using direct property access on allPriceData[0].prices is unsafe here. If the first product in the fetched batch has missing or incomplete price data, this will throw a TypeError and crash the page. Use optional chaining to safely access these properties, matching the pattern used in the demo mode fallback.
| const unit = allPriceData[0].prices.currency_minor_unit ?? 2; | |
| const symbol = allPriceData[0].prices.currency_symbol ?? '$'; | |
| const unit = allPriceData[0].prices?.currency_minor_unit ?? 2; | |
| const symbol = allPriceData[0].prices?.currency_symbol ?? '$'; |
| while (hasMore) { | ||
| const batch = await storeApiRequest('products', { | ||
| query: { _fields: 'prices', per_page: 100, page }, | ||
| }); | ||
| if (!Array.isArray(batch) || batch.length === 0) break; | ||
| allPriceData = allPriceData.concat(batch); | ||
| hasMore = batch.length === 100; | ||
| page += 1; | ||
| } |
There was a problem hiding this comment.
Paginating through all products sequentially in a while loop to find the min/max prices is a major performance bottleneck. For stores with many products, this will result in numerous sequential API requests on every mount of the Shop page, leading to slow load times and high server load.
Consider using the WooCommerce Store API's dedicated /products/collection-data endpoint, which returns the min/max prices of the collection in a single lightweight request, or at least caching the result.
| function handleMinChange(e) { | ||
| const value = Number(e.target.value); | ||
| onChange(Math.min(value, currentMax), currentMax); | ||
| } | ||
|
|
||
| function handleMaxChange(e) { | ||
| const value = Number(e.target.value); | ||
| onChange(currentMin, Math.max(value, currentMin)); | ||
| } |
There was a problem hiding this comment.
When the minimum and maximum thumbs meet or overlap, the thumb with the lower z-index becomes completely unreachable and cannot be dragged. This causes the slider to get stuck, preventing the user from expanding the range again.
To resolve this, we can implement a 'push' behavior where dragging the minimum handle past the maximum handle (or vice versa) automatically updates the other handle, allowing them to cross smoothly without getting stuck.
| function handleMinChange(e) { | |
| const value = Number(e.target.value); | |
| onChange(Math.min(value, currentMax), currentMax); | |
| } | |
| function handleMaxChange(e) { | |
| const value = Number(e.target.value); | |
| onChange(currentMin, Math.max(value, currentMin)); | |
| } | |
| function handleMinChange(e) { | |
| const value = Number(e.target.value); | |
| if (value > currentMax) { | |
| onChange(currentMax, value); | |
| } else { | |
| onChange(value, currentMax); | |
| } | |
| } | |
| function handleMaxChange(e) { | |
| const value = Number(e.target.value); | |
| if (value < currentMin) { | |
| onChange(value, currentMin); | |
| } else { | |
| onChange(currentMin, value); | |
| } | |
| } |
| outline: none; | ||
| } |
There was a problem hiding this comment.
The price slider inputs have outline: none without any alternative focus styles. This makes the slider completely inaccessible to keyboard users, as there is no visual indication of which handle is currently focused when tabbing through the page.
Please add visible focus styles (e.g., using :focus-visible) for the slider thumbs to ensure compliance with accessibility standards.
outline: none;
}
.price-slider__input:focus-visible::-webkit-slider-thumb {
box-shadow: 0 0 0 3px var(--color-accent-subtle);
border-color: var(--color-accent);
}
.price-slider__input:focus-visible::-moz-range-thumb {
box-shadow: 0 0 0 3px var(--color-accent-subtle);
border-color: var(--color-accent);
}
Summary
min_price/max_priceparams with 300ms debounceChanges
PriceSlider.jsx— dual-handle range slider component with overlapping native<input type="range">elementsPriceSlider.css— themed styles using design tokens (hairline track, circular thumbs)products.js— addedgetPriceRange()(paginates all products),minPrice/maxPricesupport ingetProducts()ProductList.jsx— accepts price filter props, contextual empty messageShop.jsx— price range state, debounce viauseRef, conditional slider renderingTest plan
/shopbut NOT on/(Home)npm run devwithout WordPress)🤖 Generated with Claude Code