diff --git a/src/components/shop/ProductDrawer.tsx b/src/components/shop/ProductDrawer.tsx
index e20e5467e..8e64bce23 100644
--- a/src/components/shop/ProductDrawer.tsx
+++ b/src/components/shop/ProductDrawer.tsx
@@ -2,9 +2,10 @@ import * as React from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { twMerge } from 'tailwind-merge'
import { getProduct } from '~/utils/shop.functions'
-import type {
- ProductDetail,
- ProductDetailVariant,
+import {
+ hasAvailableVariant,
+ type ProductDetail,
+ type ProductDetailVariant,
} from '~/utils/shopify-queries'
import { formatMoney } from '~/utils/shopify-format'
import { resolveShopProductColor, shopColorContrast } from '~/utils/shop-color'
@@ -474,15 +475,16 @@ function DrawerContent({
Select {option.name}
{option.values.map((value) => {
- const match = findMatchingVariant(
- variants,
- getCandidate(value),
- )
return (
@@ -505,11 +507,10 @@ function DrawerContent({
{option.values.map((value) => {
const isSelected = selected[option.name] === value
- const match = findMatchingVariant(
+ const isUnavailable = !hasAvailableVariant(
variants,
getCandidate(value),
)
- const isUnavailable = !match?.availableForSale
return (
{option.values.map((value) => {
const isSelected = selected[option.name] === value
- const match = findMatchingVariant(
+ const isUnavailable = !hasAvailableVariant(
variants,
getCandidate(value),
)
- const isUnavailable = !match?.availableForSale
const hex = resolveShopProductColor(value)
return (
{option.values.map((value) => {
- const match = findAvailableVariant(
- variants,
- getCandidate(value),
- )
return (
@@ -361,11 +360,10 @@ function VariantSelector({
>
{option.values.map((value) => {
const isSelected = selected[option.name] === value
- const match = findAvailableVariant(
+ const isUnavailable = !hasAvailableVariant(
variants,
getCandidate(value),
)
- const isUnavailable = !match?.availableForSale
const handleClick = () => handleChange(value)
if (isSizeOption) {
return (
@@ -519,18 +517,6 @@ function findMatchingVariant(
)
}
-function findAvailableVariant(
- variants: Array,
- selected: Record,
-): ProductDetailVariant | undefined {
- return variants.find((variant) =>
- variant.selectedOptions.every(
- (option) =>
- !selected[option.name] || selected[option.name] === option.value,
- ),
- )
-}
-
function ProductJsonLd({
product,
selectedVariant,
diff --git a/src/utils/shopify-queries.ts b/src/utils/shopify-queries.ts
index 844227da5..b7a4b4359 100644
--- a/src/utils/shopify-queries.ts
+++ b/src/utils/shopify-queries.ts
@@ -255,6 +255,22 @@ export type ProductDetailVariant = Pick<
image: Pick | null
}
+export function hasAvailableVariant(
+ variants: Array<
+ Pick
+ >,
+ selected: Record,
+): boolean {
+ return variants.some(
+ (variant) =>
+ variant.availableForSale &&
+ variant.selectedOptions.every(
+ (option) =>
+ !selected[option.name] || selected[option.name] === option.value,
+ ),
+ )
+}
+
export type ProductDetail = Pick<
Product,
'id' | 'handle' | 'title' | 'descriptionHtml'
diff --git a/tests/shopify-variant.test.ts b/tests/shopify-variant.test.ts
new file mode 100644
index 000000000..a94bae49a
--- /dev/null
+++ b/tests/shopify-variant.test.ts
@@ -0,0 +1,46 @@
+import assert from 'node:assert/strict'
+import { test } from 'node:test'
+import { hasAvailableVariant } from '../src/utils/shopify-queries'
+
+const variants = [
+ {
+ availableForSale: false,
+ selectedOptions: [
+ { name: 'Color', value: 'Black' },
+ { name: 'Size', value: 'Small' },
+ ],
+ },
+ {
+ availableForSale: true,
+ selectedOptions: [
+ { name: 'Color', value: 'Black' },
+ { name: 'Size', value: 'Large' },
+ ],
+ },
+ {
+ availableForSale: false,
+ selectedOptions: [
+ { name: 'Color', value: 'Blue' },
+ { name: 'Size', value: 'Large' },
+ ],
+ },
+]
+
+test('partial selections stay available when any matching variant is in stock', () => {
+ assert.equal(hasAvailableVariant(variants, { Color: 'Black' }), true)
+})
+
+test('complete selections only match the selected variant', () => {
+ assert.equal(
+ hasAvailableVariant(variants, { Color: 'Black', Size: 'Small' }),
+ false,
+ )
+ assert.equal(
+ hasAvailableVariant(variants, { Color: 'Black', Size: 'Large' }),
+ true,
+ )
+})
+
+test('partial selections are unavailable when all matches are sold out', () => {
+ assert.equal(hasAvailableVariant(variants, { Color: 'Blue' }), false)
+})