@akinon/pz-theme 2.0.47-beta.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.
@@ -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;