@akinon/pz-theme 2.0.90-beta.0 → 2.0.90
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
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.90
|
|
3
|
+
## 2.0.90
|
|
4
4
|
|
|
5
5
|
### Patch Changes
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- @akinon/next@2.0.90
|
|
9
|
-
- @akinon/theme-contract@2.0.90
|
|
7
|
+
- dfe76c5c: ZERO-4984: Stop an unnamed slot from adopting a custom section's regions
|
|
8
|
+
- @akinon/next@2.0.90
|
|
9
|
+
- @akinon/theme-contract@2.0.90
|
|
10
10
|
|
|
11
11
|
## 2.0.89
|
|
12
12
|
|
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-theme",
|
|
3
3
|
"description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
|
|
4
|
-
"version": "2.0.90
|
|
4
|
+
"version": "2.0.90",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"@akinon/next": "2.0.90
|
|
11
|
+
"@akinon/next": "2.0.90",
|
|
12
12
|
"react": "^18.0.0 || ^19.0.0",
|
|
13
13
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@akinon/theme-contract": "2.0.90
|
|
16
|
+
"@akinon/theme-contract": "2.0.90",
|
|
17
17
|
"clsx": "2.1.1",
|
|
18
18
|
"lottie-web": "^5.13.0",
|
|
19
19
|
"tailwind-merge": "^2.5.4",
|
|
20
20
|
"zod": "4.4.1"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@akinon/next": "2.0.90
|
|
23
|
+
"@akinon/next": "2.0.90",
|
|
24
24
|
"@testing-library/jest-dom": "5.16.4",
|
|
25
25
|
"@testing-library/react": "16.3.2",
|
|
26
26
|
"@testing-library/user-event": "14.1.1",
|
|
@@ -162,6 +162,47 @@ const registerHeader = () =>
|
|
|
162
162
|
})
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
+
/**
|
|
166
|
+
* The same header, with its one slot spelled as a bare `<ThemeSlot />`.
|
|
167
|
+
*
|
|
168
|
+
* That spelling is not exotic — it is what every block written against the
|
|
169
|
+
* original single-slot API uses, and a developer moving a header into a section
|
|
170
|
+
* carries it over unchanged. On a section it resolves against the SECTION node,
|
|
171
|
+
* whose direct children are the seeded regions.
|
|
172
|
+
*/
|
|
173
|
+
function ZsaHeaderBareSlot() {
|
|
174
|
+
return (
|
|
175
|
+
<header>
|
|
176
|
+
<div data-testid="logo-area">
|
|
177
|
+
<ThemeRegion name="logo" />
|
|
178
|
+
</div>
|
|
179
|
+
<div data-testid="search-area">
|
|
180
|
+
<ThemeRegion name="search" label="Store search" />
|
|
181
|
+
</div>
|
|
182
|
+
<div data-testid="basket-area">
|
|
183
|
+
<ThemeRegion name="basket" />
|
|
184
|
+
</div>
|
|
185
|
+
<div data-testid="extras-area">
|
|
186
|
+
<ThemeSlot label="Extra header actions" />
|
|
187
|
+
</div>
|
|
188
|
+
</header>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const registerHeaderWithBareSlot = () =>
|
|
193
|
+
registerCustomSections('zsazsazsu', {
|
|
194
|
+
header: defineCustomSection(ZsaHeaderBareSlot, {
|
|
195
|
+
label: 'ZSA ZSA ZSU Header',
|
|
196
|
+
regions: [
|
|
197
|
+
{ name: 'logo', label: 'Logo', block: 'image' },
|
|
198
|
+
{ name: 'search', label: 'Store search', block: 'search-box' },
|
|
199
|
+
{ name: 'basket', label: 'Basket', block: 'mini-basket' }
|
|
200
|
+
],
|
|
201
|
+
slots: [{ label: 'Extra header actions' }],
|
|
202
|
+
lockRootTransforms: true
|
|
203
|
+
})
|
|
204
|
+
});
|
|
205
|
+
|
|
165
206
|
const renderComposition = (section: Section, isDesigner = false) =>
|
|
166
207
|
render(
|
|
167
208
|
<ThemeSettingsProvider value={null}>
|
|
@@ -280,6 +321,35 @@ describe('the region blocks the editor actually seeds', () => {
|
|
|
280
321
|
expect(childIdsIn('extras-area')).toEqual(['extras-child']);
|
|
281
322
|
});
|
|
282
323
|
|
|
324
|
+
it('are never adopted by an unnamed slot the merchant has not opened', () => {
|
|
325
|
+
registerHeaderWithBareSlot();
|
|
326
|
+
|
|
327
|
+
// THE OTHER HALF OF THE ANTI-SLOT-FALLBACK INVARIANT, from the slot's side.
|
|
328
|
+
// The test above pins that a region never falls back to the section's
|
|
329
|
+
// direct children; this one pins that the slot's legacy fallback never
|
|
330
|
+
// claims them either. A merchant who has never opened the extras slot has
|
|
331
|
+
// no `custom-slot` node in the composition, so `resolveSlotChildren` takes
|
|
332
|
+
// that fallback — and every region is an ordinary block, invisible to
|
|
333
|
+
// `isSlotNode`. Without the region filter the whole header renders a second
|
|
334
|
+
// time inside the slot, with two `data-block-id` wrappers per region.
|
|
335
|
+
const neverOpened = composition();
|
|
336
|
+
neverOpened.blocks = neverOpened.blocks.filter(
|
|
337
|
+
(block) => block.id !== 'block-t6qz0'
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
renderComposition(neverOpened);
|
|
341
|
+
|
|
342
|
+
expect(childIdsIn('extras-area')).toEqual([]);
|
|
343
|
+
expect(childIdsIn('logo-area')).toEqual(['block-h3n1a']);
|
|
344
|
+
expect(childIdsIn('search-area')).toEqual(['block-k92rd']);
|
|
345
|
+
expect(childIdsIn('basket-area')).toEqual(['block-p47xv']);
|
|
346
|
+
|
|
347
|
+
// Counted across the whole render, not per area: the doubling this guards
|
|
348
|
+
// against puts the second copy somewhere the per-area assertions above
|
|
349
|
+
// cannot reach.
|
|
350
|
+
expect(screen.queryAllByTestId('region-child')).toHaveLength(3);
|
|
351
|
+
});
|
|
352
|
+
|
|
283
353
|
it('are never spilled onto the page by a build that cannot draw the section', () => {
|
|
284
354
|
const warn = jest
|
|
285
355
|
.spyOn(console, 'warn')
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
slotNameError,
|
|
7
7
|
slotNodeName
|
|
8
8
|
} from '../slot-nodes';
|
|
9
|
+
import { REGION_NAME_PROPERTY } from '../section-types';
|
|
9
10
|
import { CUSTOM_SLOT_BLOCK_TYPE, SLOT_NAME_PROPERTY } from '../types';
|
|
10
11
|
|
|
11
12
|
const block = (over: Partial<Block> = {}): Block => ({
|
|
@@ -27,6 +28,19 @@ const slotNode = (properties: Record<string, unknown>, blocks: Block[] = []) =>
|
|
|
27
28
|
blocks
|
|
28
29
|
});
|
|
29
30
|
|
|
31
|
+
/**
|
|
32
|
+
* A seeded region as the editor writes one: an ORDINARY block of a real type
|
|
33
|
+
* carrying the reserved name property, NOT a reserved-type wrapper. That is
|
|
34
|
+
* precisely why `isSlotNode` cannot recognise it — see the region case below.
|
|
35
|
+
*/
|
|
36
|
+
const regionBlock = (name: string): Block =>
|
|
37
|
+
block({
|
|
38
|
+
id: `region-${name}`,
|
|
39
|
+
type: 'image',
|
|
40
|
+
label: name,
|
|
41
|
+
properties: { [REGION_NAME_PROPERTY]: name }
|
|
42
|
+
});
|
|
43
|
+
|
|
30
44
|
describe('slotNodeName', () => {
|
|
31
45
|
it('reads the bare string the editor stamps', () => {
|
|
32
46
|
expect(slotNodeName(slotNode({ [SLOT_NAME_PROPERTY]: 'promo' }))).toBe(
|
|
@@ -90,6 +104,20 @@ describe('resolveSlotChildren', () => {
|
|
|
90
104
|
expect(resolveSlotChildren(halfMigrated, '')).toEqual([inAside]);
|
|
91
105
|
});
|
|
92
106
|
|
|
107
|
+
it('keeps the regions a section seeds out of the unnamed slot', () => {
|
|
108
|
+
// The legacy branch reads a host's direct children as the merchant's own
|
|
109
|
+
// content, which is true of a custom block and false of a custom section:
|
|
110
|
+
// there the host is the section and its direct children are its regions.
|
|
111
|
+
// Regions are ordinary blocks, so `isSlotNode` waves every one of them
|
|
112
|
+
// through and a bare `<ThemeSlot />` on a section would render the whole
|
|
113
|
+
// header a second time for shoppers.
|
|
114
|
+
const section = block({
|
|
115
|
+
blocks: [regionBlock('logo'), regionBlock('search'), inPromo]
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(resolveSlotChildren(section, '')).toEqual([inPromo]);
|
|
119
|
+
});
|
|
120
|
+
|
|
93
121
|
it('never falls back for a named slot', () => {
|
|
94
122
|
// A named slot cannot have legacy content to find: naming one was refused
|
|
95
123
|
// at define time before slots were nodes.
|
package/src/custom/slot-nodes.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Block } from '../theme-block';
|
|
2
|
+
import { isRegionNode } from './region-nodes';
|
|
2
3
|
import {
|
|
3
4
|
CUSTOM_SLOT_BLOCK_TYPE,
|
|
4
5
|
SLOT_NAME_PATTERN,
|
|
@@ -58,6 +59,20 @@ export const slotNodeName = (block: Block): string => {
|
|
|
58
59
|
* unnamed one — a named slot was refused at define time, so it can have no
|
|
59
60
|
* legacy content to find. Slot nodes are filtered out of that branch so a block
|
|
60
61
|
* re-saved with nodes does not render another slot's content here as well.
|
|
62
|
+
*
|
|
63
|
+
* REGION BLOCKS ARE FILTERED OUT OF IT TOO, and the slot-node filter does not
|
|
64
|
+
* cover them. The legacy branch reads a host's direct children as the
|
|
65
|
+
* merchant's own content, which is true of a custom BLOCK and false of a custom
|
|
66
|
+
* SECTION: `ThemeSlot` resolves against whatever node the context carries, and
|
|
67
|
+
* for a section that is the section itself, whose direct children are its
|
|
68
|
+
* seeded regions. A region is an ORDINARY block carrying
|
|
69
|
+
* `properties.regionName` rather than a reserved type, so `isSlotNode` waves
|
|
70
|
+
* every one of them through — a section that renders a bare `<ThemeSlot />`
|
|
71
|
+
* would draw its whole region set a SECOND time inside the slot for shoppers,
|
|
72
|
+
* with two `data-block-id` wrappers answering to each region's id. Reaching
|
|
73
|
+
* across to `isRegionNode` costs one import and both modules are
|
|
74
|
+
* directive-free, so nothing about the client boundary changes;
|
|
75
|
+
* `region-nodes.ts` states the same asymmetry from its own side.
|
|
61
76
|
*/
|
|
62
77
|
export const resolveSlotChildren = (
|
|
63
78
|
block: Block,
|
|
@@ -71,7 +86,10 @@ export const resolveSlotChildren = (
|
|
|
71
86
|
|
|
72
87
|
if (node) return node.blocks ?? [];
|
|
73
88
|
|
|
74
|
-
if (name === '')
|
|
89
|
+
if (name === '')
|
|
90
|
+
return children.filter(
|
|
91
|
+
(child) => !isSlotNode(child) && !isRegionNode(child)
|
|
92
|
+
);
|
|
75
93
|
|
|
76
94
|
return null;
|
|
77
95
|
};
|