@akinon/pz-theme 2.0.37-rc.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 +7 -31
- 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,215 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import NextLink from 'next/link';
|
|
5
|
+
import { getResponsiveValue, partAttrs } from '../utils';
|
|
6
|
+
import { useThemePageContext } from '../theme-page-context';
|
|
7
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
8
|
+
import {
|
|
9
|
+
DesignerPlaceholder,
|
|
10
|
+
getProductName,
|
|
11
|
+
pickProductField,
|
|
12
|
+
readToggle,
|
|
13
|
+
useThemeProduct
|
|
14
|
+
} from './product-block-utils';
|
|
15
|
+
|
|
16
|
+
interface ProductCrumb {
|
|
17
|
+
label: string;
|
|
18
|
+
url: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const toRecord = (value: unknown): Record<string, unknown> =>
|
|
22
|
+
value && typeof value === 'object' && !Array.isArray(value)
|
|
23
|
+
? (value as Record<string, unknown>)
|
|
24
|
+
: {};
|
|
25
|
+
|
|
26
|
+
const toText = (value: unknown): string =>
|
|
27
|
+
typeof value === 'string'
|
|
28
|
+
? value
|
|
29
|
+
: typeof value === 'number' && Number.isFinite(value)
|
|
30
|
+
? String(value)
|
|
31
|
+
: '';
|
|
32
|
+
|
|
33
|
+
const normalizeCrumb = (entry: unknown): ProductCrumb | null => {
|
|
34
|
+
// Category payloads are sometimes a bare name with no URL behind it.
|
|
35
|
+
if (typeof entry === 'string') {
|
|
36
|
+
return entry.trim() ? { label: entry.trim(), url: '' } : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const crumb = toRecord(entry);
|
|
40
|
+
const label =
|
|
41
|
+
toText(crumb.label) || toText(crumb.name) || toText(crumb.text) || '';
|
|
42
|
+
if (!label) return null;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
label,
|
|
46
|
+
url: toText(crumb.url) || toText(crumb.absolute_url)
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const collectCrumbs = (entries: unknown): ProductCrumb[] =>
|
|
51
|
+
Array.isArray(entries)
|
|
52
|
+
? entries
|
|
53
|
+
.map((entry) => normalizeCrumb(entry))
|
|
54
|
+
.filter((crumb): crumb is ProductCrumb => crumb !== null)
|
|
55
|
+
: [];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Functional PDP block: the breadcrumb trail of the product page. Unlike the
|
|
59
|
+
* listing page context, `buildProductPageContext()` publishes no breadcrumb
|
|
60
|
+
* array, so the trail is assembled from whatever the payload does carry —
|
|
61
|
+
* an explicit trail when a brand adds one, otherwise the product categories —
|
|
62
|
+
* and the product itself always closes the trail as the current page.
|
|
63
|
+
* Properties: separator, ariaLabel, gap, showHome, homeLabel, homeHref,
|
|
64
|
+
* showCurrent, maxLabelLength.
|
|
65
|
+
*/
|
|
66
|
+
const ProductBreadcrumbBlock = ({
|
|
67
|
+
block,
|
|
68
|
+
currentBreakpoint = 'desktop',
|
|
69
|
+
isDesigner = false
|
|
70
|
+
}: BlockRendererProps) => {
|
|
71
|
+
const product = useThemeProduct();
|
|
72
|
+
const pageContext = useThemePageContext();
|
|
73
|
+
|
|
74
|
+
const prop = (key: string, fallback: string) =>
|
|
75
|
+
String(
|
|
76
|
+
getResponsiveValue(block.properties?.[key], currentBreakpoint, fallback)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const separator = prop('separator', '/');
|
|
80
|
+
const ariaLabel = prop('ariaLabel', 'Breadcrumb');
|
|
81
|
+
const homeLabel = prop('homeLabel', 'Home');
|
|
82
|
+
const homeHref = prop('homeHref', '/');
|
|
83
|
+
const showHome = readToggle(
|
|
84
|
+
block.properties?.showHome,
|
|
85
|
+
currentBreakpoint,
|
|
86
|
+
false
|
|
87
|
+
);
|
|
88
|
+
const showCurrent = readToggle(
|
|
89
|
+
block.properties?.showCurrent,
|
|
90
|
+
currentBreakpoint,
|
|
91
|
+
true
|
|
92
|
+
);
|
|
93
|
+
const gapValue = Number(
|
|
94
|
+
getResponsiveValue(block.properties?.gap, currentBreakpoint, 8)
|
|
95
|
+
);
|
|
96
|
+
const gap = Number.isFinite(gapValue) && gapValue >= 0 ? gapValue : 8;
|
|
97
|
+
const maxLabelLengthValue = Number(
|
|
98
|
+
getResponsiveValue(block.properties?.maxLabelLength, currentBreakpoint, 0)
|
|
99
|
+
);
|
|
100
|
+
const maxLabelLength =
|
|
101
|
+
Number.isFinite(maxLabelLengthValue) && maxLabelLengthValue > 0
|
|
102
|
+
? Math.round(maxLabelLengthValue)
|
|
103
|
+
: 0;
|
|
104
|
+
|
|
105
|
+
const title = getProductName(product);
|
|
106
|
+
|
|
107
|
+
// Probed in order of specificity: an explicit trail on the product wins over
|
|
108
|
+
// a page-level one, and categories are only a fallback shape.
|
|
109
|
+
const trail = collectCrumbs(pickProductField(product, 'breadcrumbs'));
|
|
110
|
+
const contextTrail = collectCrumbs(
|
|
111
|
+
(pageContext as Record<string, unknown> | null)?.breadcrumbs
|
|
112
|
+
);
|
|
113
|
+
const categoryTrail = collectCrumbs(pickProductField(product, 'categories'));
|
|
114
|
+
|
|
115
|
+
const resolvedParents = trail.length
|
|
116
|
+
? trail
|
|
117
|
+
: contextTrail.length
|
|
118
|
+
? contextTrail
|
|
119
|
+
: categoryTrail;
|
|
120
|
+
|
|
121
|
+
const parents = showHome
|
|
122
|
+
? [{ label: homeLabel, url: homeHref }].concat(resolvedParents)
|
|
123
|
+
: resolvedParents;
|
|
124
|
+
|
|
125
|
+
if (!product || (!title && parents.length === 0)) {
|
|
126
|
+
return isDesigner ? (
|
|
127
|
+
<DesignerPlaceholder
|
|
128
|
+
label="Product breadcrumb (no product data)"
|
|
129
|
+
attribute="data-product-breadcrumb"
|
|
130
|
+
/>
|
|
131
|
+
) : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The product is the current page, so it must not be duplicated when the
|
|
135
|
+
// resolved trail already ends on it.
|
|
136
|
+
const lastLabel = parents.length ? parents[parents.length - 1].label : '';
|
|
137
|
+
const items =
|
|
138
|
+
showCurrent && title && lastLabel !== title
|
|
139
|
+
? parents.concat({ label: title, url: '' })
|
|
140
|
+
: parents;
|
|
141
|
+
|
|
142
|
+
const displayLabel = (label: string) =>
|
|
143
|
+
maxLabelLength > 0 && label.length > maxLabelLength
|
|
144
|
+
? `${label.slice(0, maxLabelLength).trimEnd()}…`
|
|
145
|
+
: label;
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<nav data-product-breadcrumb="" aria-label={ariaLabel}>
|
|
149
|
+
<ol
|
|
150
|
+
{...partAttrs('list')}
|
|
151
|
+
style={{
|
|
152
|
+
listStyle: 'none',
|
|
153
|
+
margin: 0,
|
|
154
|
+
padding: 0,
|
|
155
|
+
display: 'flex',
|
|
156
|
+
alignItems: 'center',
|
|
157
|
+
flexWrap: 'wrap',
|
|
158
|
+
gap: `var(--product-breadcrumb-gap, ${gap}px)`
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
{items.map((item, index) => {
|
|
162
|
+
const isLast = index === items.length - 1;
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<li
|
|
166
|
+
key={`${item.label}-${index}`}
|
|
167
|
+
data-breadcrumb-item={isLast ? 'current' : 'link'}
|
|
168
|
+
{...partAttrs('item', { selected: isLast })}
|
|
169
|
+
style={{
|
|
170
|
+
display: 'inline-flex',
|
|
171
|
+
alignItems: 'center',
|
|
172
|
+
gap: '8px'
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
{isLast || !item.url ? (
|
|
176
|
+
<span
|
|
177
|
+
{...partAttrs('label', { selected: isLast })}
|
|
178
|
+
aria-current={isLast ? 'page' : undefined}
|
|
179
|
+
>
|
|
180
|
+
{displayLabel(item.label)}
|
|
181
|
+
</span>
|
|
182
|
+
) : (
|
|
183
|
+
<NextLink
|
|
184
|
+
// NextLink warns when href resolves to undefined, so a crumb
|
|
185
|
+
// whose url the payload omitted still gets a resolved value.
|
|
186
|
+
href={isDesigner ? '#' : item.url || '#'}
|
|
187
|
+
{...partAttrs('link')}
|
|
188
|
+
// Navigating inside the designer iframe would drop the author
|
|
189
|
+
// out of the page they are editing, so crumbs are inert there.
|
|
190
|
+
onClick={(event) => {
|
|
191
|
+
if (isDesigner) event.preventDefault();
|
|
192
|
+
}}
|
|
193
|
+
style={{ color: 'inherit', textDecoration: 'none' }}
|
|
194
|
+
>
|
|
195
|
+
{displayLabel(item.label)}
|
|
196
|
+
</NextLink>
|
|
197
|
+
)}
|
|
198
|
+
{!isLast && (
|
|
199
|
+
<span
|
|
200
|
+
data-breadcrumb-separator=""
|
|
201
|
+
{...partAttrs('separator')}
|
|
202
|
+
aria-hidden="true"
|
|
203
|
+
>
|
|
204
|
+
{separator}
|
|
205
|
+
</span>
|
|
206
|
+
)}
|
|
207
|
+
</li>
|
|
208
|
+
);
|
|
209
|
+
})}
|
|
210
|
+
</ol>
|
|
211
|
+
</nav>
|
|
212
|
+
);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export default ProductBreadcrumbBlock;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { getResponsiveValue, partAttrs } from '../utils';
|
|
5
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
6
|
+
import {
|
|
7
|
+
DesignerPlaceholder,
|
|
8
|
+
pickProductField,
|
|
9
|
+
readToggle,
|
|
10
|
+
stripHtml,
|
|
11
|
+
useThemeProduct
|
|
12
|
+
} from './product-block-utils';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Same word-boundary rule as the shared helper, with the ellipsis authored
|
|
16
|
+
* instead of baked in — the character differs per language and per brand.
|
|
17
|
+
*/
|
|
18
|
+
const truncate = (text: string, maxLength: number, suffix: string): string => {
|
|
19
|
+
if (!text || text.length <= maxLength) return text;
|
|
20
|
+
|
|
21
|
+
const sliced = text.slice(0, maxLength);
|
|
22
|
+
const lastSpace = sliced.lastIndexOf(' ');
|
|
23
|
+
const safe = lastSpace > maxLength * 0.6 ? sliced.slice(0, lastSpace) : sliced;
|
|
24
|
+
|
|
25
|
+
return `${safe.trimEnd()}${suffix}`;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Functional PDP block: the product description coming from commerce. Full
|
|
30
|
+
* mode keeps the merchandiser's markup, excerpt mode flattens it to plain
|
|
31
|
+
* text so a teaser never leaks half-open tags. The editor owns mode, length,
|
|
32
|
+
* the optional read-more toggle and — through named parts — the styling.
|
|
33
|
+
* Properties: mode, excerptLength, ellipsis, showToggle, collapsedLines,
|
|
34
|
+
* moreLabel, lessLabel.
|
|
35
|
+
*/
|
|
36
|
+
const ProductDescriptionBlock = ({
|
|
37
|
+
block,
|
|
38
|
+
currentBreakpoint = 'desktop',
|
|
39
|
+
isDesigner = false
|
|
40
|
+
}: BlockRendererProps) => {
|
|
41
|
+
const product = useThemeProduct();
|
|
42
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
43
|
+
|
|
44
|
+
const prop = (key: string, fallback: string) =>
|
|
45
|
+
String(
|
|
46
|
+
getResponsiveValue(block.properties?.[key], currentBreakpoint, fallback)
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const modeValue = String(
|
|
50
|
+
getResponsiveValue(block.properties?.mode, currentBreakpoint, 'full')
|
|
51
|
+
);
|
|
52
|
+
const mode = modeValue === 'excerpt' ? 'excerpt' : 'full';
|
|
53
|
+
const excerptLength = Number(
|
|
54
|
+
getResponsiveValue(block.properties?.excerptLength, currentBreakpoint, 160)
|
|
55
|
+
);
|
|
56
|
+
const ellipsis = prop('ellipsis', '…');
|
|
57
|
+
const showToggle = readToggle(
|
|
58
|
+
block.properties?.showToggle,
|
|
59
|
+
currentBreakpoint,
|
|
60
|
+
false
|
|
61
|
+
);
|
|
62
|
+
const moreLabel = prop('moreLabel', 'Read more');
|
|
63
|
+
const lessLabel = prop('lessLabel', 'Read less');
|
|
64
|
+
const collapsedLinesValue = Number(
|
|
65
|
+
getResponsiveValue(block.properties?.collapsedLines, currentBreakpoint, 4)
|
|
66
|
+
);
|
|
67
|
+
const collapsedLines =
|
|
68
|
+
Number.isFinite(collapsedLinesValue) && collapsedLinesValue > 0
|
|
69
|
+
? Math.round(collapsedLinesValue)
|
|
70
|
+
: 4;
|
|
71
|
+
|
|
72
|
+
const description = String(
|
|
73
|
+
pickProductField<string>(product, 'description') || ''
|
|
74
|
+
).trim();
|
|
75
|
+
|
|
76
|
+
if (!description) {
|
|
77
|
+
return isDesigner ? (
|
|
78
|
+
<DesignerPlaceholder
|
|
79
|
+
label="Product description (empty)"
|
|
80
|
+
attribute="data-product-description"
|
|
81
|
+
/>
|
|
82
|
+
) : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const toggleNode = showToggle ? (
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
data-product-description-toggle=""
|
|
89
|
+
{...partAttrs('toggle', { open: isExpanded })}
|
|
90
|
+
aria-expanded={isExpanded}
|
|
91
|
+
onClick={() => setIsExpanded((current) => !current)}
|
|
92
|
+
style={{
|
|
93
|
+
display: 'inline-flex',
|
|
94
|
+
alignItems: 'center',
|
|
95
|
+
marginTop: '8px',
|
|
96
|
+
padding: 0,
|
|
97
|
+
border: 'none',
|
|
98
|
+
background: 'transparent',
|
|
99
|
+
color: 'inherit',
|
|
100
|
+
font: 'inherit',
|
|
101
|
+
textDecoration: 'underline',
|
|
102
|
+
cursor: 'pointer'
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
{isExpanded ? lessLabel : moreLabel}
|
|
106
|
+
</button>
|
|
107
|
+
) : null;
|
|
108
|
+
|
|
109
|
+
if (mode === 'excerpt') {
|
|
110
|
+
const safeLength =
|
|
111
|
+
Number.isFinite(excerptLength) && excerptLength > 0 ? excerptLength : 160;
|
|
112
|
+
const plain = stripHtml(description);
|
|
113
|
+
// With the toggle on, "expanded" means the whole text — truncating an
|
|
114
|
+
// expanded excerpt would make the control look broken.
|
|
115
|
+
const excerpt =
|
|
116
|
+
showToggle && isExpanded ? plain : truncate(plain, safeLength, ellipsis);
|
|
117
|
+
|
|
118
|
+
if (!excerpt) {
|
|
119
|
+
return isDesigner ? (
|
|
120
|
+
<DesignerPlaceholder
|
|
121
|
+
label="Product description (empty)"
|
|
122
|
+
attribute="data-product-description"
|
|
123
|
+
/>
|
|
124
|
+
) : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<>
|
|
129
|
+
<div
|
|
130
|
+
data-product-description=""
|
|
131
|
+
data-product-description-mode="excerpt"
|
|
132
|
+
{...partAttrs('content', { open: showToggle && isExpanded })}
|
|
133
|
+
>
|
|
134
|
+
{excerpt}
|
|
135
|
+
</div>
|
|
136
|
+
{toggleNode}
|
|
137
|
+
</>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The clamp is only added once a brand asked for a toggle, so an unclamped
|
|
142
|
+
// description keeps exactly the box it renders today.
|
|
143
|
+
const clampStyle: React.CSSProperties =
|
|
144
|
+
showToggle && !isExpanded
|
|
145
|
+
? {
|
|
146
|
+
display: '-webkit-box',
|
|
147
|
+
WebkitBoxOrient: 'vertical',
|
|
148
|
+
WebkitLineClamp: collapsedLines,
|
|
149
|
+
overflow: 'hidden'
|
|
150
|
+
}
|
|
151
|
+
: {};
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<>
|
|
155
|
+
<div
|
|
156
|
+
data-product-description=""
|
|
157
|
+
data-product-description-mode="full"
|
|
158
|
+
{...partAttrs('content', { open: showToggle && isExpanded })}
|
|
159
|
+
{...(showToggle ? { style: clampStyle } : {})}
|
|
160
|
+
// Same trust boundary as text-block: the markup is merchandiser-authored
|
|
161
|
+
// commerce content, not shopper input.
|
|
162
|
+
dangerouslySetInnerHTML={{ __html: description }}
|
|
163
|
+
/>
|
|
164
|
+
{toggleNode}
|
|
165
|
+
</>
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export default ProductDescriptionBlock;
|