@akinon/pz-theme 2.0.103-rc.0 → 2.0.103
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 +8 -33
- package/package.json +4 -4
- package/src/__tests__/collection-row-scope.test.tsx +187 -0
- package/src/__tests__/group-row-styles.test.tsx +110 -0
- package/src/__tests__/theme-typography-vars.test.tsx +125 -0
- package/src/blocks/__tests__/listing-row-swatches.test.ts +111 -0
- package/src/blocks/group-block.tsx +35 -1
- package/src/blocks/item-template-utils.ts +34 -0
- package/src/blocks/item-template.tsx +6 -1
- package/src/blocks/listing-context.ts +17 -622
- package/src/blocks/listing-row.ts +692 -0
- package/src/components/with-designer-features.tsx +6 -0
- package/src/sections/__tests__/posts-slider-row-visibility.test.tsx +147 -0
- package/src/sections/posts-slider-section.tsx +23 -2
- package/src/theme-block.tsx +63 -2
- package/src/theme-placeholder-client.tsx +4 -3
- package/src/theme-placeholder-wrapper.tsx +61 -5
- package/src/utils/__tests__/property-switches.test.tsx +250 -0
- package/src/utils/index.ts +55 -3
- package/src/utils/iterator-price-shapes.test.ts +465 -0
- package/src/utils/iterator-utils.ts +371 -35
- package/src/utils/property-switches.ts +230 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import ThemeBlock, { type Block } from '../../theme-block';
|
|
5
|
+
import type { Section } from '../../theme-section';
|
|
6
|
+
import {
|
|
7
|
+
applyPropertySwitchesToSections,
|
|
8
|
+
HOVER_REVEAL_CSS,
|
|
9
|
+
HOVER_SCOPE_ATTRIBUTE,
|
|
10
|
+
readSwitch,
|
|
11
|
+
REVEAL_ON_HOVER_ATTRIBUTE
|
|
12
|
+
} from '../property-switches';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* ZERO-4829 — the Product Card's badge, discount chip, favourite and colour
|
|
16
|
+
* swatches are switched from booleans on the card's root. A child names the
|
|
17
|
+
* switch it answers to; the nearest enclosing block that defines that switch
|
|
18
|
+
* decides, and a switch nobody defined leaves the child as it was.
|
|
19
|
+
*/
|
|
20
|
+
const block = (
|
|
21
|
+
id: string,
|
|
22
|
+
properties: Record<string, unknown> = {},
|
|
23
|
+
blocks: Block[] = [],
|
|
24
|
+
type = 'group'
|
|
25
|
+
): Block => ({
|
|
26
|
+
id,
|
|
27
|
+
type,
|
|
28
|
+
label: id,
|
|
29
|
+
value: type === 'text' ? id : undefined,
|
|
30
|
+
properties: { tag: 'div', ...properties },
|
|
31
|
+
styles: {},
|
|
32
|
+
order: 0,
|
|
33
|
+
hidden: false,
|
|
34
|
+
blocks
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const section = (
|
|
38
|
+
blocks: Block[],
|
|
39
|
+
properties: Record<string, unknown> = {}
|
|
40
|
+
): Section => ({
|
|
41
|
+
id: 's1',
|
|
42
|
+
type: 'custom',
|
|
43
|
+
name: 's1',
|
|
44
|
+
label: 'S1',
|
|
45
|
+
order: 0,
|
|
46
|
+
hidden: false,
|
|
47
|
+
properties,
|
|
48
|
+
styles: {},
|
|
49
|
+
blocks
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const ids = (blocks: Block[] | undefined): string[] =>
|
|
53
|
+
(blocks || []).map((entry) => entry.id);
|
|
54
|
+
|
|
55
|
+
const find = (blocks: Block[] | undefined, id: string): Block | undefined => {
|
|
56
|
+
for (const entry of blocks || []) {
|
|
57
|
+
if (entry.id === id) return entry;
|
|
58
|
+
const nested = find(entry.blocks, id);
|
|
59
|
+
if (nested) return nested;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
describe('readSwitch', () => {
|
|
65
|
+
it('reads booleans, the strings a select stores, and per-breakpoint objects', () => {
|
|
66
|
+
expect(readSwitch(true)).toBe(true);
|
|
67
|
+
expect(readSwitch('false')).toBe(false);
|
|
68
|
+
expect(readSwitch({ desktop: true })).toBe(true);
|
|
69
|
+
expect(readSwitch({ desktop: true, mobile: false }, 'mobile')).toBe(false);
|
|
70
|
+
// A tablet with nothing authored reads the mobile value, as the CSS does.
|
|
71
|
+
expect(readSwitch({ desktop: true, mobile: false }, 'tablet')).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('is undefined for nothing, so an undefined switch does not decide', () => {
|
|
75
|
+
expect(readSwitch(undefined)).toBeUndefined();
|
|
76
|
+
expect(readSwitch('')).toBeUndefined();
|
|
77
|
+
expect(readSwitch(null)).toBeUndefined();
|
|
78
|
+
expect(readSwitch('maybe')).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('visibleWhenProperty', () => {
|
|
83
|
+
const card = (switches: Record<string, unknown>) =>
|
|
84
|
+
block('card', switches, [
|
|
85
|
+
block('caption', {}, [
|
|
86
|
+
block('badge', { visibleWhenProperty: 'showBadge' }, [], 'text'),
|
|
87
|
+
block('name', {}, [], 'text')
|
|
88
|
+
])
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
it('drops the child when the nearest definer has the switch off', () => {
|
|
92
|
+
const [result] = applyPropertySwitchesToSections([
|
|
93
|
+
section([card({ showBadge: false })])
|
|
94
|
+
]);
|
|
95
|
+
expect(ids(find(result.blocks, 'caption')?.blocks)).toEqual(['name']);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('keeps the child when the switch is on, or when nobody defined it', () => {
|
|
99
|
+
const [on] = applyPropertySwitchesToSections([
|
|
100
|
+
section([card({ showBadge: true })])
|
|
101
|
+
]);
|
|
102
|
+
expect(ids(find(on.blocks, 'caption')?.blocks)).toEqual(['badge', 'name']);
|
|
103
|
+
|
|
104
|
+
const [undefinedSwitch] = applyPropertySwitchesToSections([
|
|
105
|
+
section([card({})])
|
|
106
|
+
]);
|
|
107
|
+
expect(ids(find(undefinedSwitch.blocks, 'caption')?.blocks)).toEqual([
|
|
108
|
+
'badge',
|
|
109
|
+
'name'
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('the nearest definer wins over an outer one', () => {
|
|
114
|
+
const outer = block('outer', { showBadge: true }, [
|
|
115
|
+
card({ showBadge: false })
|
|
116
|
+
]);
|
|
117
|
+
const [result] = applyPropertySwitchesToSections([section([outer])]);
|
|
118
|
+
expect(ids(find(result.blocks, 'caption')?.blocks)).toEqual(['name']);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('a section property can drive a switch too', () => {
|
|
122
|
+
const [result] = applyPropertySwitchesToSections([
|
|
123
|
+
section([card({})], { showBadge: false })
|
|
124
|
+
]);
|
|
125
|
+
expect(ids(find(result.blocks, 'caption')?.blocks)).toEqual(['name']);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('reads the switch at the rendered breakpoint', () => {
|
|
129
|
+
const [desktop] = applyPropertySwitchesToSections(
|
|
130
|
+
[section([card({ showBadge: { desktop: true, mobile: false } })])],
|
|
131
|
+
'desktop'
|
|
132
|
+
);
|
|
133
|
+
const [mobile] = applyPropertySwitchesToSections(
|
|
134
|
+
[section([card({ showBadge: { desktop: true, mobile: false } })])],
|
|
135
|
+
'mobile'
|
|
136
|
+
);
|
|
137
|
+
expect(ids(find(desktop.blocks, 'caption')?.blocks)).toEqual([
|
|
138
|
+
'badge',
|
|
139
|
+
'name'
|
|
140
|
+
]);
|
|
141
|
+
expect(ids(find(mobile.blocks, 'caption')?.blocks)).toEqual(['name']);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('leaves untouched trees untouched', () => {
|
|
145
|
+
const input = section([block('plain', {}, [block('child')])]);
|
|
146
|
+
const [result] = applyPropertySwitchesToSections([input]);
|
|
147
|
+
expect(result.blocks).toEqual(input.blocks);
|
|
148
|
+
expect('hoverScope' in result.blocks[0]).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('hoverRevealWhenProperty', () => {
|
|
153
|
+
const card = (switches: Record<string, unknown>) =>
|
|
154
|
+
block('card', switches, [
|
|
155
|
+
block('media', {}, [
|
|
156
|
+
block(
|
|
157
|
+
'favourite',
|
|
158
|
+
{
|
|
159
|
+
visibleWhenProperty: 'showFavourite',
|
|
160
|
+
hoverRevealWhenProperty: 'favouriteOnHover'
|
|
161
|
+
},
|
|
162
|
+
[],
|
|
163
|
+
'icon'
|
|
164
|
+
)
|
|
165
|
+
])
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
it('marks the child and the block that defines the switch when it is on', () => {
|
|
169
|
+
const [result] = applyPropertySwitchesToSections([
|
|
170
|
+
section([card({ favouriteOnHover: true })])
|
|
171
|
+
]);
|
|
172
|
+
expect(find(result.blocks, 'favourite')?.revealOnHover).toBe(true);
|
|
173
|
+
expect(find(result.blocks, 'card')?.hoverScope).toBe(true);
|
|
174
|
+
// The group in between is not the scope: hovering the caption reveals too.
|
|
175
|
+
expect(find(result.blocks, 'media')?.hoverScope).toBeUndefined();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('marks nothing when the switch is off or undefined — the child just shows', () => {
|
|
179
|
+
for (const switches of [{ favouriteOnHover: false }, {}]) {
|
|
180
|
+
const [result] = applyPropertySwitchesToSections([
|
|
181
|
+
section([card(switches)])
|
|
182
|
+
]);
|
|
183
|
+
expect(find(result.blocks, 'favourite')?.revealOnHover).toBeUndefined();
|
|
184
|
+
expect(find(result.blocks, 'card')?.hoverScope).toBeUndefined();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('a section is not a hover scope, so a section-defined switch alone keeps the child visible', () => {
|
|
189
|
+
const [result] = applyPropertySwitchesToSections([
|
|
190
|
+
section([card({})], { favouriteOnHover: true })
|
|
191
|
+
]);
|
|
192
|
+
expect(find(result.blocks, 'favourite')?.revealOnHover).toBeUndefined();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('a block that declares itself the hover scope is hovered, wherever the switch was defined', () => {
|
|
196
|
+
// The section (or a grid) says "on hover"; the card root says "hover ME".
|
|
197
|
+
const [fromSection] = applyPropertySwitchesToSections([
|
|
198
|
+
section([card({ hoverScope: true })], { favouriteOnHover: true })
|
|
199
|
+
]);
|
|
200
|
+
expect(find(fromSection.blocks, 'favourite')?.revealOnHover).toBe(true);
|
|
201
|
+
expect(find(fromSection.blocks, 'card')?.hoverScope).toBe(true);
|
|
202
|
+
|
|
203
|
+
// An outer block defines the switch, the card in it is still the scope.
|
|
204
|
+
const grid = block('grid', { favouriteOnHover: true }, [
|
|
205
|
+
card({ hoverScope: true })
|
|
206
|
+
]);
|
|
207
|
+
const [fromGrid] = applyPropertySwitchesToSections([section([grid])]);
|
|
208
|
+
expect(find(fromGrid.blocks, 'card')?.hoverScope).toBe(true);
|
|
209
|
+
expect(find(fromGrid.blocks, 'grid')?.hoverScope).toBeUndefined();
|
|
210
|
+
|
|
211
|
+
// A card that says nothing inherits; a card that says Hide overrides.
|
|
212
|
+
const [inherit] = applyPropertySwitchesToSections([
|
|
213
|
+
section([card({ hoverScope: true })], { showFavourite: false })
|
|
214
|
+
]);
|
|
215
|
+
expect(find(inherit.blocks, 'favourite')).toBeUndefined();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('the wrapper writes both marks as data attributes', () => {
|
|
219
|
+
const [result] = applyPropertySwitchesToSections([
|
|
220
|
+
section([card({ favouriteOnHover: true })])
|
|
221
|
+
]);
|
|
222
|
+
const { container } = render(
|
|
223
|
+
<ThemeBlock
|
|
224
|
+
block={result.blocks[0]}
|
|
225
|
+
placeholderId="p"
|
|
226
|
+
sectionId="s1"
|
|
227
|
+
isDesigner={false}
|
|
228
|
+
/>
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const scope = container.querySelector(`[${HOVER_SCOPE_ATTRIBUTE}]`);
|
|
232
|
+
const revealed = container.querySelector(`[${REVEAL_ON_HOVER_ATTRIBUTE}]`);
|
|
233
|
+
expect(scope?.getAttribute('data-block-id')).toBe('card');
|
|
234
|
+
expect(revealed?.getAttribute('data-block-id')).toBe('favourite');
|
|
235
|
+
expect(scope?.contains(revealed as Node)).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('the CSS hides the child until its scope is hovered or focused, on hovering desktops only', () => {
|
|
239
|
+
expect(HOVER_REVEAL_CSS).toContain('@media (min-width: 1024px) and (hover: hover)');
|
|
240
|
+
expect(HOVER_REVEAL_CSS).toContain(
|
|
241
|
+
`[${HOVER_SCOPE_ATTRIBUTE}] [${REVEAL_ON_HOVER_ATTRIBUTE}]{opacity:0`
|
|
242
|
+
);
|
|
243
|
+
expect(HOVER_REVEAL_CSS).toContain(
|
|
244
|
+
`[${HOVER_SCOPE_ATTRIBUTE}]:hover [${REVEAL_ON_HOVER_ATTRIBUTE}]`
|
|
245
|
+
);
|
|
246
|
+
expect(HOVER_REVEAL_CSS).toContain(
|
|
247
|
+
`[${HOVER_SCOPE_ATTRIBUTE}]:focus-within [${REVEAL_ON_HOVER_ATTRIBUTE}]`
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
});
|
package/src/utils/index.ts
CHANGED
|
@@ -86,6 +86,51 @@ const THEME_CSS_VARIABLE_MAP: Record<string, string> = {
|
|
|
86
86
|
|
|
87
87
|
const CSS_VAR_REGEX = /var\((--[\w-]+)(?:,\s*([^)]+))?\)/g;
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* `--theme-font-<variant>` — the family of one of the theme's typography
|
|
91
|
+
* variants (`heading01`, `heading02`, `subheading`, `body`), so a template can
|
|
92
|
+
* say "the theme's body face" without naming a brand's font.
|
|
93
|
+
*
|
|
94
|
+
* Resolved from the settings on both the server and the client, like the
|
|
95
|
+
* colour variables above. Reading it off the document instead (the
|
|
96
|
+
* `getComputedCssVariable` path) would answer only in the browser, and the
|
|
97
|
+
* fallback the server printed would then disagree with what hydration wrote.
|
|
98
|
+
*/
|
|
99
|
+
const THEME_FONT_VARIABLE_PREFIX = '--theme-font-';
|
|
100
|
+
|
|
101
|
+
export const readThemeTypographyFont = (
|
|
102
|
+
themeSettings: ThemeSettings | undefined,
|
|
103
|
+
variantKey: string
|
|
104
|
+
): string | undefined => {
|
|
105
|
+
const variants = themeSettings?.typography;
|
|
106
|
+
if (!Array.isArray(variants)) return undefined;
|
|
107
|
+
|
|
108
|
+
const variant = variants.find(
|
|
109
|
+
(entry) =>
|
|
110
|
+
entry &&
|
|
111
|
+
typeof entry === 'object' &&
|
|
112
|
+
(entry as { key?: unknown }).key === variantKey
|
|
113
|
+
) as { fontFamily?: unknown } | undefined;
|
|
114
|
+
const family = variant?.fontFamily;
|
|
115
|
+
|
|
116
|
+
return typeof family === 'string' &&
|
|
117
|
+
family.trim() &&
|
|
118
|
+
family.trim() !== 'inherit'
|
|
119
|
+
? family.trim()
|
|
120
|
+
: undefined;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const resolveThemeFontVariable = (
|
|
124
|
+
variableName: string,
|
|
125
|
+
themeSettings: ThemeSettings | undefined
|
|
126
|
+
): string | undefined =>
|
|
127
|
+
variableName.startsWith(THEME_FONT_VARIABLE_PREFIX)
|
|
128
|
+
? readThemeTypographyFont(
|
|
129
|
+
themeSettings,
|
|
130
|
+
variableName.slice(THEME_FONT_VARIABLE_PREFIX.length)
|
|
131
|
+
)
|
|
132
|
+
: undefined;
|
|
133
|
+
|
|
89
134
|
const stripQuotes = (value: string) => value.replace(/^['"]|['"]$/g, '').trim();
|
|
90
135
|
|
|
91
136
|
const getComputedCssVariable = (variableName: string): string | null => {
|
|
@@ -193,8 +238,8 @@ export const colorToRgba = (color: string, alpha: number): string | null => {
|
|
|
193
238
|
rawAuthored === undefined
|
|
194
239
|
? 1
|
|
195
240
|
: rawAuthored.endsWith('%')
|
|
196
|
-
|
|
197
|
-
|
|
241
|
+
? Number(rawAuthored.slice(0, -1)) / 100
|
|
242
|
+
: Number(rawAuthored);
|
|
198
243
|
const effective = Number.isNaN(authored) ? alpha : authored * alpha;
|
|
199
244
|
return `rgba(${r}, ${g}, ${b}, ${effective})`;
|
|
200
245
|
}
|
|
@@ -230,10 +275,17 @@ export const resolveThemeCssVariables = (
|
|
|
230
275
|
const themeColor = mappedKey
|
|
231
276
|
? (themeSettings?.[mappedKey] as string | undefined)
|
|
232
277
|
: undefined;
|
|
278
|
+
const themeFont = resolveThemeFontVariable(variableName, themeSettings);
|
|
233
279
|
const computedValue = getComputedCssVariable(variableName);
|
|
234
280
|
const fallback = fallbackValue ? stripQuotes(fallbackValue) : undefined;
|
|
235
281
|
|
|
236
|
-
return
|
|
282
|
+
return (
|
|
283
|
+
themeColor ||
|
|
284
|
+
themeFont ||
|
|
285
|
+
computedValue ||
|
|
286
|
+
fallback ||
|
|
287
|
+
`var(${variableName})`
|
|
288
|
+
);
|
|
237
289
|
}
|
|
238
290
|
);
|
|
239
291
|
};
|