@akinon/pz-theme 2.0.37-beta.0 → 2.0.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -4
- package/package.json +4 -4
- package/src/__tests__/__snapshots__/facet-filters-block.test.tsx.snap +11 -0
- package/src/__tests__/custom-css.test.ts +181 -0
- package/src/__tests__/designer-chrome-clipping.test.tsx +497 -0
- package/src/__tests__/facet-filters-block.test.tsx +707 -0
- package/src/__tests__/item-template.test.tsx +184 -0
- package/src/__tests__/listing-controls.test.tsx +961 -0
- package/src/__tests__/part-style-scope.test.tsx +229 -0
- package/src/__tests__/part-styles.test.ts +311 -0
- package/src/__tests__/registry-parity.test.ts +42 -0
- package/src/__tests__/row-visibility.test.ts +672 -0
- package/src/__tests__/sort-select-page-cursor.test.tsx +336 -0
- package/src/__tests__/theme-block-html-id.test.tsx +167 -0
- package/src/blocks/__tests__/__snapshots__/product-gallery-block.test.tsx.snap +15 -0
- package/src/blocks/__tests__/accordion-block-state.test.tsx +201 -0
- package/src/blocks/__tests__/facet-filters-block.test.tsx +190 -0
- package/src/blocks/__tests__/listing-context-card-model.test.tsx +230 -0
- package/src/blocks/__tests__/listing-lookup-hardening.test.ts +80 -0
- package/src/blocks/__tests__/product-gallery-block.test.tsx +974 -0
- package/src/blocks/__tests__/product-grid-composition.test.tsx +570 -0
- package/src/blocks/__tests__/product-grid-lookups.test.tsx +230 -0
- package/src/blocks/__tests__/product-price-block.test.tsx +248 -0
- package/src/blocks/__tests__/product-recommendations-block.test.tsx +702 -0
- package/src/blocks/__tests__/share-block.test.tsx +392 -0
- package/src/blocks/__tests__/store-locator-block.test.tsx +691 -0
- package/src/blocks/__tests__/variant-selector-block.test.tsx +817 -0
- package/src/blocks/__tests__/variant-selector-golden.json +20 -0
- package/src/blocks/account-menu-block.tsx +264 -0
- package/src/blocks/add-to-cart-block.tsx +417 -0
- package/src/blocks/address-list-block.tsx +348 -0
- package/src/blocks/applied-filters-block.tsx +288 -0
- package/src/blocks/auth-form-block.tsx +295 -0
- package/src/blocks/basket-items-block.tsx +457 -0
- package/src/blocks/basket-summary-block.tsx +404 -0
- package/src/blocks/block-renderer-registry.tsx +94 -0
- package/src/blocks/change-password-form-block.tsx +240 -0
- package/src/blocks/checkout-steps-block.tsx +288 -0
- package/src/blocks/checkout-summary-block.tsx +449 -0
- package/src/blocks/countdown-block.tsx +182 -0
- package/src/blocks/coupon-list-block.tsx +292 -0
- package/src/blocks/facet-filters-block.tsx +1686 -0
- package/src/blocks/faq-accordion-block.tsx +358 -0
- package/src/blocks/favorite-button-block.tsx +252 -0
- package/src/blocks/favourites-grid-block.tsx +510 -0
- package/src/blocks/forgot-password-block.tsx +155 -0
- package/src/blocks/item-template-utils.ts +131 -0
- package/src/blocks/item-template.tsx +164 -0
- package/src/blocks/listing-breadcrumb-block.tsx +175 -0
- package/src/blocks/listing-context.ts +850 -0
- package/src/blocks/listing-pagination-block.tsx +923 -0
- package/src/blocks/listing-result-count-block.tsx +126 -0
- package/src/blocks/listing-title-block.tsx +115 -0
- package/src/blocks/oauth-buttons-block.tsx +205 -0
- package/src/blocks/order-detail-block.tsx +474 -0
- package/src/blocks/order-list-block.tsx +274 -0
- package/src/blocks/order-success-block.tsx +641 -0
- package/src/blocks/order-tracking-block.tsx +533 -0
- package/src/blocks/product-attributes-block.tsx +307 -0
- package/src/blocks/product-block-utils.tsx +639 -0
- package/src/blocks/product-breadcrumb-block.tsx +215 -0
- package/src/blocks/product-description-block.tsx +169 -0
- package/src/blocks/product-gallery-block.tsx +1523 -0
- package/src/blocks/product-grid-block.tsx +966 -0
- package/src/blocks/product-price-block.tsx +373 -0
- package/src/blocks/product-recommendations-block.tsx +1433 -0
- package/src/blocks/product-title-block.tsx +129 -0
- package/src/blocks/profile-form-block.tsx +297 -0
- package/src/blocks/recently-viewed-store.ts +201 -0
- package/src/blocks/register-form-block.tsx +362 -0
- package/src/blocks/share-block.tsx +667 -0
- package/src/blocks/size-guide-block.tsx +313 -0
- package/src/blocks/sort-select-block.tsx +636 -0
- package/src/blocks/stock-status-block.tsx +168 -0
- package/src/blocks/store-locator-block.tsx +1184 -0
- package/src/blocks/variant-selector-block.tsx +1110 -0
- package/src/sections/__tests__/tablet-raw-bucket-readers.test.tsx +186 -0
- package/src/utils/__tests__/part-ownership-guard.test.tsx +29 -4
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import NextLink from 'next/link';
|
|
5
|
+
import {
|
|
6
|
+
useGetBasketQuery,
|
|
7
|
+
useUpdateQuantityMutation
|
|
8
|
+
} from '@akinon/next/data/client/basket';
|
|
9
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
10
|
+
import { getResponsiveValue, partAttrs } from '../utils';
|
|
11
|
+
import { useThemePageContext } from '../theme-page-context';
|
|
12
|
+
import { formatMoney } from './commerce-format';
|
|
13
|
+
import { hasItemTemplate, ItemTemplateList } from './item-template';
|
|
14
|
+
import { readToggle } from './product-block-utils';
|
|
15
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
16
|
+
|
|
17
|
+
interface BasketLineLike {
|
|
18
|
+
id?: number;
|
|
19
|
+
quantity?: number;
|
|
20
|
+
image?: string;
|
|
21
|
+
price?: string;
|
|
22
|
+
unit_price?: string;
|
|
23
|
+
total_amount?: string;
|
|
24
|
+
currency_type?: string;
|
|
25
|
+
attributes?: unknown;
|
|
26
|
+
product?: {
|
|
27
|
+
pk?: number;
|
|
28
|
+
name?: string;
|
|
29
|
+
absolute_url?: string;
|
|
30
|
+
image?: string;
|
|
31
|
+
} | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The basket line's variant choices as one line ("M / Red"). Commerce sends
|
|
36
|
+
* them as a plain object, so the values are joined in payload order — the keys
|
|
37
|
+
* are attribute slugs and are not shopper-facing copy.
|
|
38
|
+
*/
|
|
39
|
+
const formatVariant = (attributes: unknown, separator: string): string => {
|
|
40
|
+
if (!attributes || typeof attributes !== 'object') return '';
|
|
41
|
+
|
|
42
|
+
return Object.values(attributes as Record<string, unknown>)
|
|
43
|
+
.map((value) =>
|
|
44
|
+
value === null || value === undefined ? '' : String(value).trim()
|
|
45
|
+
)
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.join(separator);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const readCount = (value: unknown, fallback: number): number => {
|
|
51
|
+
const numeric = Math.round(Number(value));
|
|
52
|
+
|
|
53
|
+
return Number.isFinite(numeric) && numeric >= 0 ? numeric : fallback;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const RemoveGlyph = ({ icon }: { icon: string }) =>
|
|
57
|
+
icon === 'trash' ? (
|
|
58
|
+
<svg
|
|
59
|
+
width="18"
|
|
60
|
+
height="18"
|
|
61
|
+
viewBox="0 0 24 24"
|
|
62
|
+
fill="none"
|
|
63
|
+
stroke="currentColor"
|
|
64
|
+
strokeWidth="1.6"
|
|
65
|
+
strokeLinecap="round"
|
|
66
|
+
>
|
|
67
|
+
<path d="M4 7h16M9 7V5h6v2M7 7l1 13h8l1-13" />
|
|
68
|
+
</svg>
|
|
69
|
+
) : (
|
|
70
|
+
<svg
|
|
71
|
+
width="18"
|
|
72
|
+
height="18"
|
|
73
|
+
viewBox="0 0 24 24"
|
|
74
|
+
fill="none"
|
|
75
|
+
stroke="currentColor"
|
|
76
|
+
strokeWidth="1.6"
|
|
77
|
+
strokeLinecap="round"
|
|
78
|
+
>
|
|
79
|
+
<path d="M6 6l12 12M18 6L6 18" />
|
|
80
|
+
</svg>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Functional commerce block: editable basket lines. Basket CONTENT stays
|
|
85
|
+
* commerce-driven (basket service); the editor owns which affordances are
|
|
86
|
+
* exposed, every shopper-facing string and the styling.
|
|
87
|
+
*
|
|
88
|
+
* Parts: root, item, image, content, name, variant, price, quantity,
|
|
89
|
+
* quantity-button, quantity-input, actions, line-total, remove, empty.
|
|
90
|
+
* A child marked as the item template replaces the built-in row.
|
|
91
|
+
*/
|
|
92
|
+
const BasketItemsBlock = ({
|
|
93
|
+
block,
|
|
94
|
+
placeholderId,
|
|
95
|
+
sectionId,
|
|
96
|
+
selectedBlockId = null,
|
|
97
|
+
currentBreakpoint = 'desktop',
|
|
98
|
+
isDesigner = false
|
|
99
|
+
}: BlockRendererProps) => {
|
|
100
|
+
const prop = (key: string, fallback: string) =>
|
|
101
|
+
String(
|
|
102
|
+
getResponsiveValue(block.properties?.[key], currentBreakpoint, fallback)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const boolProp = (key: string, fallback: boolean) =>
|
|
106
|
+
readToggle(block.properties?.[key], currentBreakpoint, fallback);
|
|
107
|
+
|
|
108
|
+
const showImage = boolProp('showImage', true);
|
|
109
|
+
const allowQuantityChange = boolProp('allowQuantityChange', true);
|
|
110
|
+
const allowRemove = boolProp('allowRemove', true);
|
|
111
|
+
const showPrice = boolProp('showPrice', true);
|
|
112
|
+
const showTotal = boolProp('showTotal', true);
|
|
113
|
+
const showVariant = boolProp('showVariant', false);
|
|
114
|
+
const showDivider = boolProp('showDivider', true);
|
|
115
|
+
|
|
116
|
+
const variantSeparator = prop('variantSeparator', ' / ');
|
|
117
|
+
const decreaseLabel = prop('decreaseLabel', '–');
|
|
118
|
+
const increaseLabel = prop('increaseLabel', '+');
|
|
119
|
+
const decreaseAriaLabel = prop('decreaseAriaLabel', 'Decrease quantity');
|
|
120
|
+
const increaseAriaLabel = prop('increaseAriaLabel', 'Increase quantity');
|
|
121
|
+
const removeAriaLabel = prop('removeAriaLabel', 'Remove item');
|
|
122
|
+
const removeIcon = prop('removeIcon', 'cross');
|
|
123
|
+
const removeLabel = prop('removeLabel', 'Remove');
|
|
124
|
+
const emptyLabel = prop('emptyLabel', '').trim();
|
|
125
|
+
const loadingLabel = prop('loadingLabel', '').trim();
|
|
126
|
+
|
|
127
|
+
const imageWidth = readCount(
|
|
128
|
+
getResponsiveValue(block.properties?.imageWidth, currentBreakpoint, 72),
|
|
129
|
+
72
|
|
130
|
+
);
|
|
131
|
+
const imageHeight = readCount(
|
|
132
|
+
getResponsiveValue(block.properties?.imageHeight, currentBreakpoint, 96),
|
|
133
|
+
96
|
|
134
|
+
);
|
|
135
|
+
const nameLines = readCount(
|
|
136
|
+
getResponsiveValue(block.properties?.nameLines, currentBreakpoint, 0),
|
|
137
|
+
0
|
|
138
|
+
);
|
|
139
|
+
const maxItems = readCount(
|
|
140
|
+
getResponsiveValue(block.properties?.maxItems, currentBreakpoint, 0),
|
|
141
|
+
0
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const pageContext = useThemePageContext();
|
|
145
|
+
const { locale, currency: localeCurrency } = useLocalization();
|
|
146
|
+
const { data: basket, isLoading } = useGetBasketQuery(undefined as never);
|
|
147
|
+
const [updateQuantity, { isLoading: isUpdating }] =
|
|
148
|
+
useUpdateQuantityMutation();
|
|
149
|
+
|
|
150
|
+
const contextBasket = (pageContext?.basket ?? null) as {
|
|
151
|
+
items?: BasketLineLike[];
|
|
152
|
+
} | null;
|
|
153
|
+
|
|
154
|
+
// The live query owns the post-mutation state; the SSR page context is only
|
|
155
|
+
// a first-paint fallback so the list is not empty before the query resolves.
|
|
156
|
+
const allItems: BasketLineLike[] =
|
|
157
|
+
(basket as { basketitem_set?: BasketLineLike[] } | undefined)
|
|
158
|
+
?.basketitem_set ??
|
|
159
|
+
contextBasket?.items ??
|
|
160
|
+
[];
|
|
161
|
+
|
|
162
|
+
const items = maxItems > 0 ? allItems.slice(0, maxItems) : allItems;
|
|
163
|
+
|
|
164
|
+
const changeQuantity = (item: BasketLineLike, nextQuantity: number) => {
|
|
165
|
+
const productPk = item.product?.pk;
|
|
166
|
+
|
|
167
|
+
if (isDesigner || productPk === undefined || nextQuantity < 0) return;
|
|
168
|
+
|
|
169
|
+
updateQuantity({
|
|
170
|
+
product: productPk,
|
|
171
|
+
quantity: nextQuantity,
|
|
172
|
+
attributes: (item.attributes as Record<string, unknown>) || {}
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Navigating inside the designer iframe would drop the author out of the
|
|
177
|
+
// page they are editing, so the product links are inert there.
|
|
178
|
+
const guard = (event: React.MouseEvent) => {
|
|
179
|
+
if (isDesigner) event.preventDefault();
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Plain objects so a designer binds item.name / item.price / item.image /
|
|
183
|
+
// item.url / item.quantity / item.total / item.variant in their own row.
|
|
184
|
+
const templateItems = items.map((item, index) => {
|
|
185
|
+
const currency = item.currency_type || localeCurrency;
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
key: String(item.id ?? item.product?.pk ?? index),
|
|
189
|
+
name: item.product?.name || '',
|
|
190
|
+
image: item.image || item.product?.image || '',
|
|
191
|
+
url: item.product?.absolute_url || '',
|
|
192
|
+
quantity: Number(item.quantity ?? 0),
|
|
193
|
+
variant: formatVariant(item.attributes, variantSeparator),
|
|
194
|
+
price: formatMoney(item.unit_price || item.price, { locale, currency }),
|
|
195
|
+
total: formatMoney(item.total_amount, { locale, currency }),
|
|
196
|
+
currency
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// A designer who marked a child as the row layout gets that layout repeated
|
|
201
|
+
// per line; otherwise the built-in row renders unchanged.
|
|
202
|
+
const customRows = hasItemTemplate(block, currentBreakpoint) ? (
|
|
203
|
+
<ItemTemplateList
|
|
204
|
+
block={block}
|
|
205
|
+
items={templateItems as unknown as Record<string, unknown>[]}
|
|
206
|
+
placeholderId={placeholderId}
|
|
207
|
+
sectionId={sectionId}
|
|
208
|
+
isDesigner={isDesigner}
|
|
209
|
+
selectedBlockId={selectedBlockId}
|
|
210
|
+
currentBreakpoint={currentBreakpoint}
|
|
211
|
+
/>
|
|
212
|
+
) : null;
|
|
213
|
+
|
|
214
|
+
if (items.length === 0 && !customRows) {
|
|
215
|
+
const authoredLabel = isLoading ? loadingLabel : emptyLabel;
|
|
216
|
+
|
|
217
|
+
// Authored empty copy is the one case the block speaks to the shopper; with
|
|
218
|
+
// none set the storefront stays silent exactly as before.
|
|
219
|
+
if (!isDesigner && !authoredLabel) return null;
|
|
220
|
+
|
|
221
|
+
const designerLabel = isLoading
|
|
222
|
+
? 'Basket items (loading)'
|
|
223
|
+
: 'Basket items (empty)';
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<div
|
|
227
|
+
data-basket-items=""
|
|
228
|
+
data-basket-items-state="empty"
|
|
229
|
+
{...partAttrs('empty', { empty: true, loading: isLoading })}
|
|
230
|
+
>
|
|
231
|
+
{authoredLabel || (isDesigner ? designerLabel : '')}
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const nameStyle: React.CSSProperties = {
|
|
237
|
+
color: 'inherit',
|
|
238
|
+
textDecoration: 'none',
|
|
239
|
+
...(nameLines > 0
|
|
240
|
+
? {
|
|
241
|
+
display: '-webkit-box',
|
|
242
|
+
WebkitBoxOrient: 'vertical',
|
|
243
|
+
WebkitLineClamp: nameLines,
|
|
244
|
+
overflow: 'hidden'
|
|
245
|
+
}
|
|
246
|
+
: null)
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<ul
|
|
251
|
+
data-basket-items=""
|
|
252
|
+
{...partAttrs('root', { loading: isUpdating })}
|
|
253
|
+
style={{
|
|
254
|
+
display: 'flex',
|
|
255
|
+
flexDirection: 'column',
|
|
256
|
+
gap: 'var(--basket-items-gap, 16px)',
|
|
257
|
+
listStyle: 'none',
|
|
258
|
+
margin: 0,
|
|
259
|
+
padding: 0
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
{customRows ??
|
|
263
|
+
items.map((item, index) => {
|
|
264
|
+
// The line reports the currency it was priced in; the storefront
|
|
265
|
+
// currency is only the fallback when commerce omits it.
|
|
266
|
+
const currency = item.currency_type || localeCurrency;
|
|
267
|
+
const image = item.image || item.product?.image || '';
|
|
268
|
+
const name = item.product?.name || '';
|
|
269
|
+
const href = item.product?.absolute_url || '';
|
|
270
|
+
const linkHref = isDesigner ? '#' : href || '#';
|
|
271
|
+
const quantity = Number(item.quantity ?? 0);
|
|
272
|
+
const variant = showVariant
|
|
273
|
+
? formatVariant(item.attributes, variantSeparator)
|
|
274
|
+
: '';
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<li
|
|
278
|
+
key={item.id ?? item.product?.pk ?? index}
|
|
279
|
+
data-basket-item=""
|
|
280
|
+
{...partAttrs('item')}
|
|
281
|
+
style={{
|
|
282
|
+
display: 'flex',
|
|
283
|
+
alignItems: 'flex-start',
|
|
284
|
+
gap: '12px',
|
|
285
|
+
paddingBottom: '16px',
|
|
286
|
+
...(showDivider
|
|
287
|
+
? {
|
|
288
|
+
borderBottom:
|
|
289
|
+
'1px solid var(--basket-items-border, #e5e7eb)'
|
|
290
|
+
}
|
|
291
|
+
: null)
|
|
292
|
+
}}
|
|
293
|
+
>
|
|
294
|
+
{showImage && image && (
|
|
295
|
+
<NextLink
|
|
296
|
+
href={linkHref}
|
|
297
|
+
onClick={guard}
|
|
298
|
+
style={{ flex: '0 0 auto', display: 'block' }}
|
|
299
|
+
>
|
|
300
|
+
{/* Plain img: basket line thumbnails come from arbitrary
|
|
301
|
+
commerce CDNs that the brand next.config may not allow. */}
|
|
302
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
303
|
+
<img
|
|
304
|
+
src={image}
|
|
305
|
+
alt={name}
|
|
306
|
+
width={imageWidth}
|
|
307
|
+
height={imageHeight}
|
|
308
|
+
{...partAttrs('image')}
|
|
309
|
+
style={{ objectFit: 'cover', display: 'block' }}
|
|
310
|
+
/>
|
|
311
|
+
</NextLink>
|
|
312
|
+
)}
|
|
313
|
+
|
|
314
|
+
<div
|
|
315
|
+
{...partAttrs('content')}
|
|
316
|
+
style={{ flex: '1 1 auto', minWidth: 0 }}
|
|
317
|
+
>
|
|
318
|
+
<NextLink
|
|
319
|
+
href={linkHref}
|
|
320
|
+
onClick={guard}
|
|
321
|
+
data-basket-item-name=""
|
|
322
|
+
{...partAttrs('name')}
|
|
323
|
+
style={nameStyle}
|
|
324
|
+
>
|
|
325
|
+
{name}
|
|
326
|
+
</NextLink>
|
|
327
|
+
|
|
328
|
+
{variant && (
|
|
329
|
+
<div
|
|
330
|
+
data-basket-item-variant=""
|
|
331
|
+
{...partAttrs('variant')}
|
|
332
|
+
style={{ marginTop: '4px', opacity: 0.8 }}
|
|
333
|
+
>
|
|
334
|
+
{variant}
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
|
|
338
|
+
{showPrice && (
|
|
339
|
+
<div
|
|
340
|
+
data-basket-item-price=""
|
|
341
|
+
{...partAttrs('price')}
|
|
342
|
+
style={{ marginTop: '4px', opacity: 0.8 }}
|
|
343
|
+
>
|
|
344
|
+
{formatMoney(item.unit_price || item.price, {
|
|
345
|
+
locale,
|
|
346
|
+
currency
|
|
347
|
+
})}
|
|
348
|
+
</div>
|
|
349
|
+
)}
|
|
350
|
+
|
|
351
|
+
{allowQuantityChange && (
|
|
352
|
+
<div
|
|
353
|
+
data-basket-item-quantity=""
|
|
354
|
+
{...partAttrs('quantity', { loading: isUpdating })}
|
|
355
|
+
style={{
|
|
356
|
+
display: 'inline-flex',
|
|
357
|
+
alignItems: 'center',
|
|
358
|
+
gap: '8px',
|
|
359
|
+
marginTop: '8px',
|
|
360
|
+
border: '1px solid var(--basket-items-border, #e5e7eb)',
|
|
361
|
+
borderRadius: '4px',
|
|
362
|
+
padding: '2px 6px'
|
|
363
|
+
}}
|
|
364
|
+
>
|
|
365
|
+
<button
|
|
366
|
+
type="button"
|
|
367
|
+
aria-label={decreaseAriaLabel}
|
|
368
|
+
disabled={isUpdating || quantity <= 1}
|
|
369
|
+
onClick={() => changeQuantity(item, quantity - 1)}
|
|
370
|
+
{...partAttrs('quantity-button', {
|
|
371
|
+
disabled: isUpdating || quantity <= 1
|
|
372
|
+
})}
|
|
373
|
+
style={{
|
|
374
|
+
background: 'none',
|
|
375
|
+
border: 'none',
|
|
376
|
+
cursor: 'pointer',
|
|
377
|
+
color: 'inherit',
|
|
378
|
+
padding: '2px 6px'
|
|
379
|
+
}}
|
|
380
|
+
>
|
|
381
|
+
{decreaseLabel}
|
|
382
|
+
</button>
|
|
383
|
+
<span {...partAttrs('quantity-input')}>{quantity}</span>
|
|
384
|
+
<button
|
|
385
|
+
type="button"
|
|
386
|
+
aria-label={increaseAriaLabel}
|
|
387
|
+
disabled={isUpdating}
|
|
388
|
+
onClick={() => changeQuantity(item, quantity + 1)}
|
|
389
|
+
{...partAttrs('quantity-button', {
|
|
390
|
+
disabled: isUpdating
|
|
391
|
+
})}
|
|
392
|
+
style={{
|
|
393
|
+
background: 'none',
|
|
394
|
+
border: 'none',
|
|
395
|
+
cursor: 'pointer',
|
|
396
|
+
color: 'inherit',
|
|
397
|
+
padding: '2px 6px'
|
|
398
|
+
}}
|
|
399
|
+
>
|
|
400
|
+
{increaseLabel}
|
|
401
|
+
</button>
|
|
402
|
+
</div>
|
|
403
|
+
)}
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
<div
|
|
407
|
+
{...partAttrs('actions')}
|
|
408
|
+
style={{
|
|
409
|
+
flex: '0 0 auto',
|
|
410
|
+
display: 'flex',
|
|
411
|
+
flexDirection: 'column',
|
|
412
|
+
alignItems: 'flex-end',
|
|
413
|
+
gap: '8px'
|
|
414
|
+
}}
|
|
415
|
+
>
|
|
416
|
+
{showTotal && (
|
|
417
|
+
<span
|
|
418
|
+
data-basket-item-total=""
|
|
419
|
+
{...partAttrs('line-total')}
|
|
420
|
+
>
|
|
421
|
+
{formatMoney(item.total_amount, { locale, currency })}
|
|
422
|
+
</span>
|
|
423
|
+
)}
|
|
424
|
+
|
|
425
|
+
{allowRemove && (
|
|
426
|
+
<button
|
|
427
|
+
type="button"
|
|
428
|
+
data-basket-item-remove=""
|
|
429
|
+
aria-label={removeAriaLabel}
|
|
430
|
+
disabled={isUpdating}
|
|
431
|
+
// Quantity 0 is how the commerce basket endpoint deletes a line.
|
|
432
|
+
onClick={() => changeQuantity(item, 0)}
|
|
433
|
+
{...partAttrs('remove', { disabled: isUpdating })}
|
|
434
|
+
style={{
|
|
435
|
+
background: 'none',
|
|
436
|
+
border: 'none',
|
|
437
|
+
cursor: 'pointer',
|
|
438
|
+
color: 'inherit',
|
|
439
|
+
padding: 0
|
|
440
|
+
}}
|
|
441
|
+
>
|
|
442
|
+
{removeIcon === 'text' ? (
|
|
443
|
+
removeLabel
|
|
444
|
+
) : (
|
|
445
|
+
<RemoveGlyph icon={removeIcon} />
|
|
446
|
+
)}
|
|
447
|
+
</button>
|
|
448
|
+
)}
|
|
449
|
+
</div>
|
|
450
|
+
</li>
|
|
451
|
+
);
|
|
452
|
+
})}
|
|
453
|
+
</ul>
|
|
454
|
+
);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
export default BasketItemsBlock;
|