@akinon/pz-theme 2.0.47-rc.0 → 2.0.47
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__/registry-parity.test.ts +1 -0
- package/src/blocks/__tests__/force-open.test.tsx +92 -0
- package/src/blocks/__tests__/overlay-sweep.test.tsx +39 -3
- package/src/blocks/__tests__/panel-block.test.tsx +329 -0
- package/src/blocks/__tests__/panel-legacy-golden.json +7 -0
- package/src/blocks/block-renderer-registry.tsx +5 -2
- package/src/blocks/panel-block.tsx +560 -0
- package/src/blocks/size-guide-block.tsx +0 -313
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React, { useRef, useState } from 'react';
|
|
4
|
-
import { getResponsiveValue, partAttrs } from '../utils';
|
|
5
|
-
import { useOverlay } from '../hooks/use-overlay';
|
|
6
|
-
import ThemeBlock from '../theme-block';
|
|
7
|
-
import { BlockRendererProps } from './block-renderer-registry';
|
|
8
|
-
import { DesignerPlaceholder, readToggle } from './product-block-utils';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Functional PDP block: a trigger that opens the size guide.
|
|
12
|
-
*
|
|
13
|
-
* The size table itself is authored in the editor as CHILD blocks (a table
|
|
14
|
-
* drawn with text/image/group blocks), so nothing about the content is
|
|
15
|
-
* hardcoded here. In the designer the children render inline and stay
|
|
16
|
-
* selectable — a real modal would put them behind a click the author cannot
|
|
17
|
-
* perform inside the preview iframe — while the storefront gets the modal.
|
|
18
|
-
* Properties: triggerLabel, modalTitle, closeLabel, showClose, closeOnOverlay,
|
|
19
|
-
* closeOnEscape, modalWidth.
|
|
20
|
-
*/
|
|
21
|
-
const SizeGuideBlock = ({
|
|
22
|
-
block,
|
|
23
|
-
placeholderId,
|
|
24
|
-
sectionId,
|
|
25
|
-
isDesigner = false,
|
|
26
|
-
selectedBlockId,
|
|
27
|
-
currentBreakpoint = 'desktop'
|
|
28
|
-
}: BlockRendererProps) => {
|
|
29
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
30
|
-
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
|
31
|
-
|
|
32
|
-
const prop = (key: string, fallback: string) =>
|
|
33
|
-
String(
|
|
34
|
-
getResponsiveValue(block.properties?.[key], currentBreakpoint, fallback)
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
const triggerLabel = prop('triggerLabel', 'Size guide');
|
|
38
|
-
const modalTitle = prop('modalTitle', '');
|
|
39
|
-
const closeLabel = prop('closeLabel', 'Close');
|
|
40
|
-
const showClose = readToggle(
|
|
41
|
-
block.properties?.showClose,
|
|
42
|
-
currentBreakpoint,
|
|
43
|
-
true
|
|
44
|
-
);
|
|
45
|
-
const closeOnOverlay = readToggle(
|
|
46
|
-
block.properties?.closeOnOverlay,
|
|
47
|
-
currentBreakpoint,
|
|
48
|
-
true
|
|
49
|
-
);
|
|
50
|
-
const closeOnEscape = readToggle(
|
|
51
|
-
block.properties?.closeOnEscape,
|
|
52
|
-
currentBreakpoint,
|
|
53
|
-
true
|
|
54
|
-
);
|
|
55
|
-
const modalWidthValue = Number(
|
|
56
|
-
getResponsiveValue(block.properties?.modalWidth, currentBreakpoint, 640)
|
|
57
|
-
);
|
|
58
|
-
const modalWidth =
|
|
59
|
-
Number.isFinite(modalWidthValue) && modalWidthValue > 0
|
|
60
|
-
? modalWidthValue
|
|
61
|
-
: 640;
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Escape, the focus trap, returning focus to the trigger and the
|
|
65
|
-
* `aria-modal` claim are the shared overlay primitive's, not this block's.
|
|
66
|
-
*
|
|
67
|
-
* This modal hard-coded `aria-modal="true"` and answered Escape from its own
|
|
68
|
-
* document listener. A PDP composes several overlays — a size picker, a
|
|
69
|
-
* store panel, a quick view — and two of them open at once meant two dialogs
|
|
70
|
-
* each telling assistive tech the other was hidden, and one Escape closing
|
|
71
|
-
* both. The primitive keeps one stack for the page, so the claim and the key
|
|
72
|
-
* belong to whichever overlay is actually on top.
|
|
73
|
-
*
|
|
74
|
-
* The modal really does cover the page behind its backdrop, so `modal` is
|
|
75
|
-
* unconditionally true here — unlike the filter drawer, whose backdrop is a
|
|
76
|
-
* designer toggle. The designer never reaches this path at all: it returns
|
|
77
|
-
* the inline branch above, where the table is styled in place.
|
|
78
|
-
*/
|
|
79
|
-
const overlay = useOverlay<HTMLDivElement>({
|
|
80
|
-
open: isOpen,
|
|
81
|
-
onClose: () => setIsOpen(false),
|
|
82
|
-
isDesigner,
|
|
83
|
-
modal: true,
|
|
84
|
-
closeOnEscape,
|
|
85
|
-
returnFocusRef: triggerRef
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
const childBlocks = (block.blocks || [])
|
|
89
|
-
.filter((childBlock) => (isDesigner ? true : !childBlock.hidden))
|
|
90
|
-
.sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
91
|
-
|
|
92
|
-
const renderChildren = () =>
|
|
93
|
-
childBlocks.map((childBlock, index) => {
|
|
94
|
-
const createActionHandler = (actionType: string) => () => {
|
|
95
|
-
if (window.parent) {
|
|
96
|
-
window.parent.postMessage(
|
|
97
|
-
{
|
|
98
|
-
type: actionType,
|
|
99
|
-
data: { placeholderId, sectionId, blockId: childBlock.id }
|
|
100
|
-
},
|
|
101
|
-
'*'
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const handleRename = (newLabel: string) => {
|
|
107
|
-
if (window.parent) {
|
|
108
|
-
window.parent.postMessage(
|
|
109
|
-
{
|
|
110
|
-
type: 'RENAME_BLOCK',
|
|
111
|
-
data: {
|
|
112
|
-
placeholderId,
|
|
113
|
-
sectionId,
|
|
114
|
-
blockId: childBlock.id,
|
|
115
|
-
label: newLabel
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
'*'
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
return (
|
|
124
|
-
<ThemeBlock
|
|
125
|
-
key={childBlock.id || `size-guide-block-${index}`}
|
|
126
|
-
block={childBlock}
|
|
127
|
-
placeholderId={placeholderId}
|
|
128
|
-
sectionId={sectionId}
|
|
129
|
-
isDesigner={isDesigner}
|
|
130
|
-
isSelected={selectedBlockId === childBlock.id}
|
|
131
|
-
selectedBlockId={selectedBlockId}
|
|
132
|
-
currentBreakpoint={currentBreakpoint}
|
|
133
|
-
onMoveUp={createActionHandler('MOVE_BLOCK_UP')}
|
|
134
|
-
onMoveDown={createActionHandler('MOVE_BLOCK_DOWN')}
|
|
135
|
-
onDuplicate={createActionHandler('DUPLICATE_BLOCK')}
|
|
136
|
-
onToggleVisibility={createActionHandler('TOGGLE_BLOCK_VISIBILITY')}
|
|
137
|
-
onDelete={createActionHandler('DELETE_BLOCK')}
|
|
138
|
-
onRename={handleRename}
|
|
139
|
-
/>
|
|
140
|
-
);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// A trigger that opens an empty modal is a dead control, so on the storefront
|
|
144
|
-
// the block disappears until the author has put a table inside it.
|
|
145
|
-
if (childBlocks.length === 0 && !isDesigner) return null;
|
|
146
|
-
|
|
147
|
-
const trigger = (
|
|
148
|
-
<button
|
|
149
|
-
type="button"
|
|
150
|
-
data-size-guide-trigger=""
|
|
151
|
-
ref={triggerRef}
|
|
152
|
-
{...partAttrs('trigger', { open: isOpen })}
|
|
153
|
-
aria-haspopup="dialog"
|
|
154
|
-
aria-expanded={isOpen}
|
|
155
|
-
onClick={() => {
|
|
156
|
-
// The designer already shows the content inline, and opening a fixed
|
|
157
|
-
// overlay there would cover the canvas the author is working in.
|
|
158
|
-
if (isDesigner) return;
|
|
159
|
-
setIsOpen(true);
|
|
160
|
-
}}
|
|
161
|
-
style={{
|
|
162
|
-
display: 'inline-flex',
|
|
163
|
-
alignItems: 'center',
|
|
164
|
-
gap: '6px',
|
|
165
|
-
padding: 0,
|
|
166
|
-
margin: 0,
|
|
167
|
-
border: 'none',
|
|
168
|
-
background: 'transparent',
|
|
169
|
-
color: 'inherit',
|
|
170
|
-
font: 'inherit',
|
|
171
|
-
textDecoration: 'underline',
|
|
172
|
-
cursor: 'pointer'
|
|
173
|
-
}}
|
|
174
|
-
>
|
|
175
|
-
{triggerLabel}
|
|
176
|
-
</button>
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
const titleNode = modalTitle ? (
|
|
180
|
-
<div
|
|
181
|
-
data-size-guide-title=""
|
|
182
|
-
{...partAttrs('title')}
|
|
183
|
-
style={{ marginBottom: '12px', fontWeight: 600 }}
|
|
184
|
-
>
|
|
185
|
-
{modalTitle}
|
|
186
|
-
</div>
|
|
187
|
-
) : null;
|
|
188
|
-
|
|
189
|
-
if (isDesigner) {
|
|
190
|
-
return (
|
|
191
|
-
<div data-size-guide="" data-size-guide-state="inline">
|
|
192
|
-
{trigger}
|
|
193
|
-
<div
|
|
194
|
-
data-size-guide-content=""
|
|
195
|
-
data-size-guide-mode="inline"
|
|
196
|
-
// The same part as the storefront panel: the author has to see the
|
|
197
|
-
// panel styling they just typed, and the modal never opens here.
|
|
198
|
-
{...partAttrs('modal')}
|
|
199
|
-
style={{
|
|
200
|
-
marginTop: '8px',
|
|
201
|
-
padding: '12px',
|
|
202
|
-
border: '1px dashed #cbd5e1',
|
|
203
|
-
borderRadius: '4px'
|
|
204
|
-
}}
|
|
205
|
-
>
|
|
206
|
-
{titleNode}
|
|
207
|
-
{childBlocks.length > 0 ? (
|
|
208
|
-
renderChildren()
|
|
209
|
-
) : (
|
|
210
|
-
<DesignerPlaceholder label="Size guide (drop the size table blocks here)" />
|
|
211
|
-
)}
|
|
212
|
-
</div>
|
|
213
|
-
</div>
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return (
|
|
218
|
-
<div data-size-guide="" data-size-guide-state={isOpen ? 'open' : 'closed'}>
|
|
219
|
-
{trigger}
|
|
220
|
-
|
|
221
|
-
{isOpen && (
|
|
222
|
-
<div
|
|
223
|
-
data-size-guide-overlay=""
|
|
224
|
-
{...partAttrs('overlay', { open: true })}
|
|
225
|
-
onClick={() => {
|
|
226
|
-
if (closeOnOverlay) setIsOpen(false);
|
|
227
|
-
}}
|
|
228
|
-
style={{
|
|
229
|
-
position: 'fixed',
|
|
230
|
-
inset: 0,
|
|
231
|
-
zIndex: 1000,
|
|
232
|
-
display: 'flex',
|
|
233
|
-
alignItems: 'center',
|
|
234
|
-
justifyContent: 'center',
|
|
235
|
-
padding: '16px',
|
|
236
|
-
background: 'rgba(0, 0, 0, 0.5)'
|
|
237
|
-
}}
|
|
238
|
-
>
|
|
239
|
-
<div
|
|
240
|
-
data-size-guide-content=""
|
|
241
|
-
data-size-guide-mode="modal"
|
|
242
|
-
{...partAttrs('modal', { open: true })}
|
|
243
|
-
role="dialog"
|
|
244
|
-
// Carries the panel ref and the `aria-modal` claim, which is now
|
|
245
|
-
// made only while this really is the dialog on top. Spread after
|
|
246
|
-
// `role` so the attribute order stays exactly where it was.
|
|
247
|
-
//
|
|
248
|
-
// The primitive asks a panel to carry `tabIndex={-1}` as its focus
|
|
249
|
-
// target of last resort; this one cannot be given it without
|
|
250
|
-
// moving markup a published storefront already renders. The gap
|
|
251
|
-
// only shows in a composition with the close button off AND no
|
|
252
|
-
// focusable child, where focus simply stays put — the same place
|
|
253
|
-
// it stayed before the migration.
|
|
254
|
-
{...overlay.overlayProps}
|
|
255
|
-
aria-label={modalTitle || triggerLabel}
|
|
256
|
-
// The backdrop closes the modal; clicks on the panel itself must
|
|
257
|
-
// not bubble up into that handler.
|
|
258
|
-
onClick={(event) => event.stopPropagation()}
|
|
259
|
-
style={{
|
|
260
|
-
position: 'relative',
|
|
261
|
-
width: '100%',
|
|
262
|
-
maxWidth: `${modalWidth}px`,
|
|
263
|
-
maxHeight: '80vh',
|
|
264
|
-
overflow: 'auto',
|
|
265
|
-
padding: '24px',
|
|
266
|
-
borderRadius: '4px',
|
|
267
|
-
background: '#ffffff',
|
|
268
|
-
color: '#111827',
|
|
269
|
-
textAlign: 'left'
|
|
270
|
-
}}
|
|
271
|
-
>
|
|
272
|
-
{showClose && (
|
|
273
|
-
<button
|
|
274
|
-
type="button"
|
|
275
|
-
data-size-guide-close=""
|
|
276
|
-
{...partAttrs('close')}
|
|
277
|
-
aria-label={closeLabel}
|
|
278
|
-
onClick={() => setIsOpen(false)}
|
|
279
|
-
style={{
|
|
280
|
-
position: 'absolute',
|
|
281
|
-
top: '12px',
|
|
282
|
-
right: '12px',
|
|
283
|
-
border: 'none',
|
|
284
|
-
background: 'transparent',
|
|
285
|
-
color: 'inherit',
|
|
286
|
-
cursor: 'pointer',
|
|
287
|
-
padding: 0
|
|
288
|
-
}}
|
|
289
|
-
>
|
|
290
|
-
<svg
|
|
291
|
-
width="18"
|
|
292
|
-
height="18"
|
|
293
|
-
viewBox="0 0 24 24"
|
|
294
|
-
fill="none"
|
|
295
|
-
stroke="currentColor"
|
|
296
|
-
strokeWidth="1.6"
|
|
297
|
-
strokeLinecap="round"
|
|
298
|
-
>
|
|
299
|
-
<path d="M6 6l12 12M18 6L6 18" />
|
|
300
|
-
</svg>
|
|
301
|
-
</button>
|
|
302
|
-
)}
|
|
303
|
-
|
|
304
|
-
{titleNode}
|
|
305
|
-
{renderChildren()}
|
|
306
|
-
</div>
|
|
307
|
-
</div>
|
|
308
|
-
)}
|
|
309
|
-
</div>
|
|
310
|
-
);
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
export default SizeGuideBlock;
|