@akinon/pz-theme 2.0.100-rc.0 → 2.0.100
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__/registry-parity.test.ts +1 -0
- package/src/blocks/__tests__/collapsible-block.test.tsx +521 -0
- package/src/blocks/__tests__/image-block.test.tsx +173 -0
- package/src/blocks/block-renderer-registry.tsx +2 -0
- package/src/blocks/collapsible-block.tsx +595 -0
- package/src/blocks/image-block.tsx +150 -4
- package/src/utils/breakpoints.ts +30 -0
|
@@ -257,3 +257,176 @@ describe('ImageBlock leaves its box to the sheet', () => {
|
|
|
257
257
|
expect(screen.getByText('No image uploaded').style.minHeight).toBe('');
|
|
258
258
|
});
|
|
259
259
|
});
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Per-breakpoint art direction.
|
|
263
|
+
*
|
|
264
|
+
* Two invariants carry this feature and both are easy to lose in a refactor:
|
|
265
|
+
*
|
|
266
|
+
* 1. A block with no override must emit BYTE-IDENTICAL markup to the old
|
|
267
|
+
* renderer — no `<picture>`, no extra element. 198 image blocks exist in
|
|
268
|
+
* this repo's fixtures alone and none of them has an override.
|
|
269
|
+
* 2. `<picture>` resolves by FIRST MATCH, not by cascade. The phone band is
|
|
270
|
+
* therefore exclusive when a tablet image exists and widened to 1023 when it
|
|
271
|
+
* does not, which is what reproduces the stylesheet's "mobile covers tablet
|
|
272
|
+
* unless tablet overrides" rule the merchant already knows.
|
|
273
|
+
*/
|
|
274
|
+
describe('ImageBlock — per-breakpoint sources', () => {
|
|
275
|
+
const withSources = (
|
|
276
|
+
overrides: Record<string, unknown>,
|
|
277
|
+
extra: Record<string, unknown> = {}
|
|
278
|
+
) => ({
|
|
279
|
+
id: 'b-art',
|
|
280
|
+
value: 'https://example.com/desktop.jpg',
|
|
281
|
+
properties: { alt: 'Kampanya', ...overrides },
|
|
282
|
+
...extra
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const sources = (container: HTMLElement) =>
|
|
286
|
+
Array.from(container.querySelectorAll('source')).map((el) => ({
|
|
287
|
+
media: el.getAttribute('media'),
|
|
288
|
+
srcSet: el.getAttribute('srcset')
|
|
289
|
+
}));
|
|
290
|
+
|
|
291
|
+
it('emits no <picture> when only the desktop image is authored', () => {
|
|
292
|
+
const { container } = renderBlock(withSources({}));
|
|
293
|
+
|
|
294
|
+
expect(container.querySelector('picture')).toBeNull();
|
|
295
|
+
expect(getImg(container)?.getAttribute('src')).toBe(
|
|
296
|
+
'https://example.com/desktop.jpg'
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('widens the phone band to the tablet edge when no tablet image is authored', () => {
|
|
301
|
+
const { container } = renderBlock(
|
|
302
|
+
withSources({ imageMobile: 'https://example.com/phone.jpg' })
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
expect(container.querySelector('picture')).not.toBeNull();
|
|
306
|
+
expect(sources(container)).toEqual([
|
|
307
|
+
{ media: '(max-width: 1023px)', srcSet: 'https://example.com/phone.jpg' }
|
|
308
|
+
]);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('narrows the phone band and orders phone before tablet when both exist', () => {
|
|
312
|
+
const { container } = renderBlock(
|
|
313
|
+
withSources({
|
|
314
|
+
imageMobile: 'https://example.com/phone.jpg',
|
|
315
|
+
imageTablet: 'https://example.com/tablet.jpg'
|
|
316
|
+
})
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
expect(sources(container)).toEqual([
|
|
320
|
+
{ media: '(max-width: 767px)', srcSet: 'https://example.com/phone.jpg' },
|
|
321
|
+
{
|
|
322
|
+
media: '(min-width: 768px) and (max-width: 1023px)',
|
|
323
|
+
srcSet: 'https://example.com/tablet.jpg'
|
|
324
|
+
}
|
|
325
|
+
]);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('emits only the tablet band when the phone image is left empty', () => {
|
|
329
|
+
const { container } = renderBlock(
|
|
330
|
+
withSources({ imageTablet: 'https://example.com/tablet.jpg' })
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
expect(sources(container)).toEqual([
|
|
334
|
+
{
|
|
335
|
+
media: '(min-width: 768px) and (max-width: 1023px)',
|
|
336
|
+
srcSet: 'https://example.com/tablet.jpg'
|
|
337
|
+
}
|
|
338
|
+
]);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('drops an override that is the same file as the desktop image', () => {
|
|
342
|
+
const { container } = renderBlock(
|
|
343
|
+
withSources({ imageMobile: 'https://example.com/desktop.jpg' })
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
expect(container.querySelector('picture')).toBeNull();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('keeps the <img> as the desktop candidate and the only carrier of alt', () => {
|
|
350
|
+
const { container } = renderBlock(
|
|
351
|
+
withSources({ imageMobile: 'https://example.com/phone.jpg' })
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const img = getImg(container);
|
|
355
|
+
expect(img?.getAttribute('src')).toBe('https://example.com/desktop.jpg');
|
|
356
|
+
expect(img?.getAttribute('alt')).toBe('Kampanya');
|
|
357
|
+
// alt is illegal on <source>; the browser reads it from the <img> whichever
|
|
358
|
+
// candidate it picks.
|
|
359
|
+
expect(container.querySelector('source')?.hasAttribute('alt')).toBe(false);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Without `display: contents` the <picture> becomes the flex item the parent
|
|
364
|
+
* lays out and the percentage parent the <img> resolves against, and an
|
|
365
|
+
* authored `height: 100%` collapses against an auto-height box.
|
|
366
|
+
*/
|
|
367
|
+
it('keeps the <picture> out of layout', () => {
|
|
368
|
+
const { container } = renderBlock(
|
|
369
|
+
withSources({ imageMobile: 'https://example.com/phone.jpg' })
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
expect(
|
|
373
|
+
(container.querySelector('picture') as HTMLElement).style.display
|
|
374
|
+
).toBe('contents');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe('the canvas resolves in JS instead, because <source media> answers the real viewport', () => {
|
|
378
|
+
const designer = (breakpoint: string, overrides: Record<string, unknown>) =>
|
|
379
|
+
render(
|
|
380
|
+
<ImageBlock
|
|
381
|
+
{...({
|
|
382
|
+
block: withSources(overrides),
|
|
383
|
+
isDesigner: true,
|
|
384
|
+
currentBreakpoint: breakpoint
|
|
385
|
+
} as unknown as BlockRendererProps)}
|
|
386
|
+
/>
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
it('shows the phone image at the phone preview', () => {
|
|
390
|
+
const { container } = designer('mobile', {
|
|
391
|
+
imageMobile: 'https://example.com/phone.jpg'
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
expect(container.querySelector('picture')).toBeNull();
|
|
395
|
+
expect(getImg(container)?.getAttribute('src')).toBe(
|
|
396
|
+
'https://example.com/phone.jpg'
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('falls through to the phone image at the tablet preview when no tablet image exists', () => {
|
|
401
|
+
const { container } = designer('tablet', {
|
|
402
|
+
imageMobile: 'https://example.com/phone.jpg'
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
expect(getImg(container)?.getAttribute('src')).toBe(
|
|
406
|
+
'https://example.com/phone.jpg'
|
|
407
|
+
);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('shows the desktop image at the desktop preview', () => {
|
|
411
|
+
const { container } = designer('desktop', {
|
|
412
|
+
imageMobile: 'https://example.com/phone.jpg',
|
|
413
|
+
imageTablet: 'https://example.com/tablet.jpg'
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
expect(getImg(container)?.getAttribute('src')).toBe(
|
|
417
|
+
'https://example.com/desktop.jpg'
|
|
418
|
+
);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('leaves the empty-state placeholder alone', () => {
|
|
423
|
+
const { container } = renderBlock({
|
|
424
|
+
id: 'b-empty',
|
|
425
|
+
value: '',
|
|
426
|
+
properties: { imageMobile: 'https://example.com/phone.jpg' }
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
expect(container.querySelector('picture')).toBeNull();
|
|
430
|
+
expect(screen.getByText('No image uploaded')).toBeInTheDocument();
|
|
431
|
+
});
|
|
432
|
+
});
|
|
@@ -91,6 +91,7 @@ import StoreLocatorBlock from './store-locator-block';
|
|
|
91
91
|
import FaqAccordionBlock from './faq-accordion-block';
|
|
92
92
|
import ShareBlock from './share-block';
|
|
93
93
|
import ContainerBlock from './container-block';
|
|
94
|
+
import CollapsibleBlock from './collapsible-block';
|
|
94
95
|
import CarouselItemBlock from './carousel-item-block';
|
|
95
96
|
import HoverShowcaseItemBlock from './hover-showcase-item-block';
|
|
96
97
|
import IconGroupBlock from './icon-group-block';
|
|
@@ -143,6 +144,7 @@ blockRendererRegistry.register('button', ButtonBlock);
|
|
|
143
144
|
blockRendererRegistry.register('icon', IconBlock);
|
|
144
145
|
blockRendererRegistry.register('video', VideoBlock);
|
|
145
146
|
blockRendererRegistry.register('accordion', AccordionBlock);
|
|
147
|
+
blockRendererRegistry.register('collapsible', CollapsibleBlock);
|
|
146
148
|
blockRendererRegistry.register('tab', TabBlock);
|
|
147
149
|
blockRendererRegistry.register('counter', CounterBlock);
|
|
148
150
|
blockRendererRegistry.register('image-gallery', ImageGalleryBlock);
|