@@ -2,10 +2,11 @@ import type {CartLineUpdateInput} from '@shopify/hydrogen/storefront-api-types';
22import type { CartLayout } from '~/components/CartMain' ;
33import { CartForm , Image , type OptimisticCartLine } from '@shopify/hydrogen' ;
44import { useVariantUrl } from '~/lib/variants' ;
5- import { Link } from 'react-router' ;
5+ import { href , Link , useFetcher , type FetcherWithComponents } from 'react-router' ;
66import { ProductPrice } from './ProductPrice' ;
77import { useAside } from './Aside' ;
88import type { CartApiQueryFragment } from 'storefrontapi.generated' ;
9+ import { useId } from 'react' ;
910
1011type CartLine = OptimisticCartLine < CartApiQueryFragment > ;
1112
@@ -75,13 +76,13 @@ export function CartLineItem({
7576 */
7677function CartLineQuantity ( { line} : { line : CartLine } ) {
7778 if ( ! line || typeof line ?. quantity === 'undefined' ) return null ;
79+
7880 const { id : lineId , quantity, isOptimistic} = line ;
7981 const prevQuantity = Number ( Math . max ( 0 , quantity - 1 ) . toFixed ( 0 ) ) ;
8082 const nextQuantity = Number ( ( quantity + 1 ) . toFixed ( 0 ) ) ;
81-
8283 return (
8384 < div className = "cart-line-quantity" >
84- < small > Quantity: { quantity } </ small >
85+ < span > Quantity: </ span >
8586 < CartLineUpdateButton lines = { [ { id : lineId , quantity : prevQuantity } ] } >
8687 < button
8788 aria-label = "Decrease quantity"
@@ -93,6 +94,8 @@ function CartLineQuantity({line}: {line: CartLine}) {
9394 </ button >
9495 </ CartLineUpdateButton >
9596
97+ < CartLineQuantityInput disabled = { ! ! isOptimistic } line = { line } />
98+
9699 < CartLineUpdateButton lines = { [ { id : lineId , quantity : nextQuantity } ] } >
97100 < button
98101 aria-label = "Increase quantity"
@@ -135,6 +138,67 @@ function CartLineRemoveButton({
135138 ) ;
136139}
137140
141+ function isKeyboardEvent (
142+ e : React . ChangeEvent < HTMLInputElement > ,
143+ ) : e is typeof e & { nativeEvent : InputEvent } {
144+ if ( e . nativeEvent instanceof InputEvent ) {
145+ if (
146+ e . nativeEvent . inputType === 'insertText' ||
147+ e . nativeEvent . inputType === 'deleteContentBackward' ||
148+ e . nativeEvent . inputType === 'deleteContentForward'
149+ ) {
150+ return true ;
151+ }
152+ }
153+ return false ;
154+ }
155+
156+ async function submitQuantity (
157+ e : React . ChangeEvent < HTMLInputElement > ,
158+ fetcher : FetcherWithComponents < any > ,
159+ line : CartLine ,
160+ ) {
161+ const value = e . target . valueAsNumber ;
162+ const formData = new FormData ( ) ;
163+ formData . set (
164+ CartForm . INPUT_NAME ,
165+ JSON . stringify ( {
166+ action : CartForm . ACTIONS . LinesUpdate ,
167+ inputs : { lines : [ { id : line . id , quantity : value } ] } ,
168+ } ) ,
169+ ) ;
170+ await fetcher . submit ( formData , { method : 'post' , action : href ( '/cart' ) } ) ;
171+ }
172+
173+ function CartLineQuantityInput ( {
174+ line,
175+ disabled,
176+ } : {
177+ line : CartLine ;
178+ disabled : boolean ;
179+ } ) {
180+ const fetcher = useFetcher ( { key : getUpdateKey ( [ line . id ] ) } ) ;
181+
182+ return (
183+ < input
184+ aria-label = "Quantity"
185+ min = { 1 }
186+ className = "cart-line-quantity-input"
187+ disabled = { disabled }
188+ key = { line . quantity }
189+ type = "number"
190+ defaultValue = { line . quantity }
191+ onChange = { ( e ) => {
192+ if ( isKeyboardEvent ( e ) ) return ;
193+ void submitQuantity ( e , fetcher , line ) ;
194+ } }
195+ onBlur = { ( e ) => {
196+ void submitQuantity ( e , fetcher , line ) ;
197+ } }
198+ />
199+ ) ;
200+ }
201+
138202function CartLineUpdateButton ( {
139203 children,
140204 lines,
0 commit comments