@akinon/pz-theme 2.0.100-rc.0 → 2.0.101-beta.0
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 +16 -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
package/CHANGELOG.md
CHANGED
|
@@ -1,38 +1,21 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- Updated dependencies [
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
- Updated dependencies [57d7eb305]
|
|
20
|
-
- Updated dependencies [d99a6a7d5]
|
|
21
|
-
- Updated dependencies [9db81a714]
|
|
22
|
-
- Updated dependencies [591e345e1]
|
|
23
|
-
- Updated dependencies [4de5303c5]
|
|
24
|
-
- Updated dependencies [95b139dc1]
|
|
25
|
-
- Updated dependencies [1d00f2d06]
|
|
26
|
-
- Updated dependencies [4ac7b2a1e]
|
|
27
|
-
- Updated dependencies [e9598c71d]
|
|
28
|
-
- Updated dependencies [4998a9631]
|
|
29
|
-
- Updated dependencies [804d2bd6c]
|
|
30
|
-
- Updated dependencies [3909d3224]
|
|
31
|
-
- Updated dependencies [6a3d8a631]
|
|
32
|
-
- Updated dependencies [e18836b20]
|
|
33
|
-
- Updated dependencies [b1111d7b]
|
|
34
|
-
- @akinon/next@2.0.100-rc.0
|
|
35
|
-
- @akinon/theme-contract@2.0.100-rc.0
|
|
3
|
+
## 2.0.101-beta.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [cbbbfd757]
|
|
8
|
+
- @akinon/next@2.0.101-beta.0
|
|
9
|
+
- @akinon/theme-contract@2.0.101-beta.0
|
|
10
|
+
|
|
11
|
+
## 2.0.100
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- dabdde88: ZERO-5014: Add the collapsible block — any stack of composed children clamped to a line budget behind a fading read-more trigger, with the whole composition kept in the server HTML.
|
|
16
|
+
- 6a8255c1: ZERO-5015: Let the image block serve a different image per device — optional tablet and phone uploads are emitted as `<picture><source media>` so the browser picks before any JS runs, while a block with no override keeps rendering exactly the `<img>` it renders today.
|
|
17
|
+
- @akinon/next@2.0.100
|
|
18
|
+
- @akinon/theme-contract@2.0.100
|
|
36
19
|
|
|
37
20
|
## 2.0.99
|
|
38
21
|
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
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.
|
|
4
|
+
"version": "2.0.101-beta.0",
|
|
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.
|
|
11
|
+
"@akinon/next": "2.0.101-beta.0",
|
|
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.
|
|
16
|
+
"@akinon/theme-contract": "2.0.101-beta.0",
|
|
17
17
|
"clsx": "2.1.1",
|
|
18
18
|
"lottie-web": "^5.13.0",
|
|
19
19
|
"react-multi-carousel": "2.8.4",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"zod": "4.4.1"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@akinon/next": "2.0.
|
|
24
|
+
"@akinon/next": "2.0.101-beta.0",
|
|
25
25
|
"@testing-library/jest-dom": "5.16.4",
|
|
26
26
|
"@testing-library/react": "16.3.2",
|
|
27
27
|
"@testing-library/user-event": "14.1.1",
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The collapsible block: what it emits, and the two things that would silently
|
|
3
|
+
* destroy it.
|
|
4
|
+
*
|
|
5
|
+
* The first is SEO. The whole point of this block is that the clipped half is
|
|
6
|
+
* present in the server HTML, so there is a `renderToString` test that asserts
|
|
7
|
+
* every child marker survives the collapsed render. A refactor that reaches for
|
|
8
|
+
* `{isExpanded && …}` passes every interaction test in this file and fails that
|
|
9
|
+
* one.
|
|
10
|
+
*
|
|
11
|
+
* The second is the clamp mechanism. `-webkit-line-clamp` and
|
|
12
|
+
* `overflow: hidden` both LOOK correct in a browser and are both wrong here
|
|
13
|
+
* (they cannot count lines across block children, and a hidden box scrolls the
|
|
14
|
+
* visible lines away when a clipped link takes focus), so the emitted sheet is
|
|
15
|
+
* asserted for `1lh`, for `overflow:clip`, and for the `hidden` fallback that
|
|
16
|
+
* has to precede it.
|
|
17
|
+
*/
|
|
18
|
+
import React from 'react';
|
|
19
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
20
|
+
import { renderToString } from 'react-dom/server';
|
|
21
|
+
|
|
22
|
+
jest.mock('../../theme-block', () => ({
|
|
23
|
+
__esModule: true,
|
|
24
|
+
default: ({ block }: any) => <div data-testid="tb" data-id={block.id} />
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
import CollapsibleBlock, {
|
|
28
|
+
COLLAPSIBLE_FALLBACK_LABELS
|
|
29
|
+
} from '../collapsible-block';
|
|
30
|
+
|
|
31
|
+
const child = (id: string, over: Record<string, unknown> = {}) => ({
|
|
32
|
+
id,
|
|
33
|
+
type: 'text',
|
|
34
|
+
label: `child ${id}`,
|
|
35
|
+
hidden: false,
|
|
36
|
+
order: 0,
|
|
37
|
+
properties: {},
|
|
38
|
+
styles: {},
|
|
39
|
+
...over
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const props = (over: Record<string, unknown> = {}) =>
|
|
43
|
+
({
|
|
44
|
+
block: {
|
|
45
|
+
id: 'col-1',
|
|
46
|
+
type: 'collapsible',
|
|
47
|
+
label: 'Collapsible',
|
|
48
|
+
properties: {},
|
|
49
|
+
styles: {},
|
|
50
|
+
blocks: [child('a'), child('b', { order: 1 })]
|
|
51
|
+
},
|
|
52
|
+
placeholderId: 'p',
|
|
53
|
+
sectionId: 's',
|
|
54
|
+
isDesigner: false,
|
|
55
|
+
currentBreakpoint: 'desktop',
|
|
56
|
+
...over
|
|
57
|
+
}) as any;
|
|
58
|
+
|
|
59
|
+
const withProperties = (
|
|
60
|
+
properties: Record<string, unknown>,
|
|
61
|
+
over: Record<string, unknown> = {}
|
|
62
|
+
) =>
|
|
63
|
+
props({
|
|
64
|
+
block: {
|
|
65
|
+
...props().block,
|
|
66
|
+
properties
|
|
67
|
+
},
|
|
68
|
+
...over
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const sheet = (container: HTMLElement) =>
|
|
72
|
+
Array.from(container.querySelectorAll('style'))
|
|
73
|
+
.map((el) => el.textContent || '')
|
|
74
|
+
.join('');
|
|
75
|
+
|
|
76
|
+
describe('CollapsibleBlock', () => {
|
|
77
|
+
describe('the hidden half is never unmounted', () => {
|
|
78
|
+
it('ships every child in the collapsed server render', () => {
|
|
79
|
+
const html = renderToString(<CollapsibleBlock {...props()} />);
|
|
80
|
+
|
|
81
|
+
expect(html).toContain('data-id="a"');
|
|
82
|
+
expect(html).toContain('data-id="b"');
|
|
83
|
+
expect(html).toContain('aria-expanded="false"');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('keeps the same children mounted across a toggle', () => {
|
|
87
|
+
render(<CollapsibleBlock {...props()} />);
|
|
88
|
+
|
|
89
|
+
expect(screen.getAllByTestId('tb')).toHaveLength(2);
|
|
90
|
+
fireEvent.click(screen.getByRole('button'));
|
|
91
|
+
expect(screen.getAllByTestId('tb')).toHaveLength(2);
|
|
92
|
+
fireEvent.click(screen.getByRole('button'));
|
|
93
|
+
expect(screen.getAllByTestId('tb')).toHaveLength(2);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('the clamp', () => {
|
|
98
|
+
it('clips in line units with the em fallback ahead of it', () => {
|
|
99
|
+
const { container } = render(<CollapsibleBlock {...props()} />);
|
|
100
|
+
const css = sheet(container);
|
|
101
|
+
|
|
102
|
+
expect(css).toContain(
|
|
103
|
+
'max-height:calc(var(--pz-collapse-lines) * 1.5em)'
|
|
104
|
+
);
|
|
105
|
+
expect(css).toContain('max-height:calc(var(--pz-collapse-lines) * 1lh)');
|
|
106
|
+
expect(css.indexOf('1.5em')).toBeLessThan(css.indexOf('1lh'));
|
|
107
|
+
expect(css).not.toContain('-webkit-line-clamp');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('clips rather than hides, so a focused child cannot scroll the visible lines away', () => {
|
|
111
|
+
const { container } = render(<CollapsibleBlock {...props()} />);
|
|
112
|
+
const css = sheet(container);
|
|
113
|
+
|
|
114
|
+
expect(css).toContain('overflow:hidden;overflow:clip');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('reads the collapsed and fade heights per breakpoint on the storefront', () => {
|
|
118
|
+
const { container } = render(
|
|
119
|
+
<CollapsibleBlock
|
|
120
|
+
{...withProperties({
|
|
121
|
+
collapsedLines: { desktop: 10, tablet: 7, mobile: 5 },
|
|
122
|
+
fadeLines: { desktop: 4, mobile: 2 }
|
|
123
|
+
})}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
const css = sheet(container);
|
|
127
|
+
|
|
128
|
+
expect(css).toContain('--pz-collapse-lines:10');
|
|
129
|
+
expect(css).toContain(
|
|
130
|
+
'@media (max-width:1023px){[data-pz-collapsible="col-1"]{--pz-collapse-lines:5'
|
|
131
|
+
);
|
|
132
|
+
expect(css).toContain('--pz-collapse-lines:7');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('resolves one band in the designer, because the canvas frame would answer a media query', () => {
|
|
136
|
+
const { container } = render(
|
|
137
|
+
<CollapsibleBlock
|
|
138
|
+
{...withProperties(
|
|
139
|
+
{ collapsedLines: { desktop: 10, mobile: 5 } },
|
|
140
|
+
{ isDesigner: true, currentBreakpoint: 'mobile' }
|
|
141
|
+
)}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
const css = sheet(container);
|
|
145
|
+
|
|
146
|
+
expect(css).toContain('--pz-collapse-lines:5');
|
|
147
|
+
expect(css).not.toContain('@media (max-width');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('clamps a stored line count that would emit an invalid declaration', () => {
|
|
151
|
+
const { container } = render(
|
|
152
|
+
<CollapsibleBlock
|
|
153
|
+
{...withProperties({ collapsedLines: { desktop: -4 } })}
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(sheet(container)).toContain('--pz-collapse-lines:2');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('drops the clamp and the trigger entirely while the designer has it open', () => {
|
|
161
|
+
const { container } = render(
|
|
162
|
+
<CollapsibleBlock {...props({ isDesigner: true })} />
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(container.querySelector('[data-designer-open]')).toHaveAttribute(
|
|
166
|
+
'data-designer-open',
|
|
167
|
+
'true'
|
|
168
|
+
);
|
|
169
|
+
expect(sheet(container)).toContain(
|
|
170
|
+
'[data-pz-collapsible="col-1"][data-designer-open="true"] [data-part="clip"]{max-height:none;overflow:visible'
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('the fade', () => {
|
|
176
|
+
it('masks the text by default, so it is correct over any section background', () => {
|
|
177
|
+
const { container } = render(<CollapsibleBlock {...props()} />);
|
|
178
|
+
const css = sheet(container);
|
|
179
|
+
|
|
180
|
+
expect(css).toContain('-webkit-mask-image:linear-gradient(');
|
|
181
|
+
expect(css).toContain('mask-image:linear-gradient(');
|
|
182
|
+
expect(container.querySelector('[data-part="fade"]')).toBeNull();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('paints an authored colour band instead when asked', () => {
|
|
186
|
+
const { container } = render(
|
|
187
|
+
<CollapsibleBlock
|
|
188
|
+
{...withProperties({
|
|
189
|
+
fadeStyle: { desktop: 'color' },
|
|
190
|
+
fadeColor: { desktop: '#101010' }
|
|
191
|
+
})}
|
|
192
|
+
/>
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
expect(sheet(container)).not.toContain('mask-image:linear-gradient(');
|
|
196
|
+
const fade = container.querySelector('[data-part="fade"]');
|
|
197
|
+
expect(fade).toBeInTheDocument();
|
|
198
|
+
expect(fade).toHaveAttribute('aria-hidden', 'true');
|
|
199
|
+
expect(sheet(container)).toContain(
|
|
200
|
+
'background-image:linear-gradient(to bottom,transparent,var(--pz-collapse-fade-color'
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The authored colour reaches CSS as a custom property VALUE, never as a
|
|
206
|
+
* substring of the sheet — which is what stops a crafted colour from
|
|
207
|
+
* closing the rule and writing its own.
|
|
208
|
+
*/
|
|
209
|
+
it('hands the authored colour to the sheet as a variable, not as text', () => {
|
|
210
|
+
const { container } = render(
|
|
211
|
+
<CollapsibleBlock
|
|
212
|
+
{...withProperties({
|
|
213
|
+
fadeStyle: { desktop: 'color' },
|
|
214
|
+
fadeColor: { desktop: '#101010' }
|
|
215
|
+
})}
|
|
216
|
+
/>
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(
|
|
220
|
+
container.querySelector('[data-pz-collapsible]')?.getAttribute('style')
|
|
221
|
+
).toContain('--pz-collapse-fade-color: #101010');
|
|
222
|
+
expect(sheet(container)).not.toContain('#101010');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('falls back to the theme background when the colour is not authored', () => {
|
|
226
|
+
const { container } = render(
|
|
227
|
+
<CollapsibleBlock
|
|
228
|
+
{...withProperties({ fadeStyle: { desktop: 'color' } })}
|
|
229
|
+
/>
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
expect(sheet(container)).toContain('var(--theme-background,#fff)');
|
|
233
|
+
expect(
|
|
234
|
+
container.querySelector('[data-pz-collapsible]')?.getAttribute('style')
|
|
235
|
+
).not.toContain('--pz-collapse-fade-color');
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('the trigger', () => {
|
|
240
|
+
it('is a real button wired to the panel it controls', () => {
|
|
241
|
+
const { container } = render(<CollapsibleBlock {...props()} />);
|
|
242
|
+
const toggle = screen.getByRole('button');
|
|
243
|
+
|
|
244
|
+
expect(toggle).toHaveAttribute('type', 'button');
|
|
245
|
+
expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
|
246
|
+
expect(toggle.getAttribute('aria-controls')).toBe(
|
|
247
|
+
container.querySelector('[data-part="content"]')?.getAttribute('id')
|
|
248
|
+
);
|
|
249
|
+
expect(toggle.getAttribute('aria-controls')).toBeTruthy();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('swaps the caption and marks itself open', () => {
|
|
253
|
+
render(<CollapsibleBlock {...props()} />);
|
|
254
|
+
const toggle = screen.getByRole('button');
|
|
255
|
+
|
|
256
|
+
expect(toggle).toHaveTextContent(COLLAPSIBLE_FALLBACK_LABELS.more);
|
|
257
|
+
|
|
258
|
+
fireEvent.click(toggle);
|
|
259
|
+
|
|
260
|
+
expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
|
261
|
+
expect(toggle).toHaveAttribute('data-state', 'open');
|
|
262
|
+
expect(toggle).toHaveTextContent(COLLAPSIBLE_FALLBACK_LABELS.less);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('prefers the authored captions over the fallback constant', () => {
|
|
266
|
+
render(
|
|
267
|
+
<CollapsibleBlock
|
|
268
|
+
{...withProperties({ moreLabel: 'Daha Fazla', lessLabel: 'Daha Az' })}
|
|
269
|
+
/>
|
|
270
|
+
);
|
|
271
|
+
const toggle = screen.getByRole('button');
|
|
272
|
+
|
|
273
|
+
expect(toggle).toHaveTextContent('Daha Fazla');
|
|
274
|
+
fireEvent.click(toggle);
|
|
275
|
+
expect(toggle).toHaveTextContent('Daha Az');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('reads a locale map caption at the active locale', () => {
|
|
279
|
+
render(
|
|
280
|
+
<CollapsibleBlock
|
|
281
|
+
{...withProperties({
|
|
282
|
+
moreLabel: { 'en-us': 'Read more', 'tr-tr': 'Devamını Gör' }
|
|
283
|
+
})}
|
|
284
|
+
/>
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
expect(screen.getByRole('button')).toHaveTextContent('Read more');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('carries an icon the stylesheet can rotate, hidden from assistive tech', () => {
|
|
291
|
+
const { container } = render(<CollapsibleBlock {...props()} />);
|
|
292
|
+
|
|
293
|
+
expect(
|
|
294
|
+
container.querySelector('[data-part="toggle-icon"]')
|
|
295
|
+
).toHaveAttribute('aria-hidden', 'true');
|
|
296
|
+
expect(sheet(container)).toContain(
|
|
297
|
+
'[data-part="toggle"][data-state~="open"] [data-part="toggle-icon"]{transform:rotate(180deg)}'
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('can be dropped', () => {
|
|
302
|
+
const { container } = render(
|
|
303
|
+
<CollapsibleBlock
|
|
304
|
+
{...withProperties({ showToggleIcon: { desktop: false } })}
|
|
305
|
+
/>
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
expect(container.querySelector('[data-part="toggle-icon"]')).toBeNull();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('is aligned with a margin, so it works on the flex box it is', () => {
|
|
312
|
+
const { container } = render(
|
|
313
|
+
<CollapsibleBlock
|
|
314
|
+
{...withProperties({ toggleAlign: { desktop: 'center' } })}
|
|
315
|
+
/>
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
expect(
|
|
319
|
+
container.querySelector('[data-part="toggle"]')?.getAttribute('style')
|
|
320
|
+
).toContain('margin: 0px auto');
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe('reaching the clipped half', () => {
|
|
325
|
+
it('opens when a child inside the clipped region takes focus', () => {
|
|
326
|
+
const { container } = render(<CollapsibleBlock {...props()} />);
|
|
327
|
+
const viewport = container.querySelector(
|
|
328
|
+
'[data-part="viewport"]'
|
|
329
|
+
) as HTMLElement;
|
|
330
|
+
|
|
331
|
+
fireEvent.focus(viewport, { bubbles: true });
|
|
332
|
+
|
|
333
|
+
expect(screen.getByRole('button')).toHaveAttribute(
|
|
334
|
+
'aria-expanded',
|
|
335
|
+
'true'
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('leaves it shut when the merchant turned that off', () => {
|
|
340
|
+
const { container } = render(
|
|
341
|
+
<CollapsibleBlock
|
|
342
|
+
{...withProperties({ expandOnFocus: { desktop: false } })}
|
|
343
|
+
/>
|
|
344
|
+
);
|
|
345
|
+
const viewport = container.querySelector(
|
|
346
|
+
'[data-part="viewport"]'
|
|
347
|
+
) as HTMLElement;
|
|
348
|
+
|
|
349
|
+
fireEvent.focus(viewport, { bubbles: true });
|
|
350
|
+
|
|
351
|
+
expect(screen.getByRole('button')).toHaveAttribute(
|
|
352
|
+
'aria-expanded',
|
|
353
|
+
'false'
|
|
354
|
+
);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('does not open when the trigger itself takes focus', () => {
|
|
358
|
+
render(<CollapsibleBlock {...props()} />);
|
|
359
|
+
|
|
360
|
+
fireEvent.focus(screen.getByRole('button'), { bubbles: true });
|
|
361
|
+
|
|
362
|
+
expect(screen.getByRole('button')).toHaveAttribute(
|
|
363
|
+
'aria-expanded',
|
|
364
|
+
'false'
|
|
365
|
+
);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe('degrading', () => {
|
|
370
|
+
/**
|
|
371
|
+
* The server render is where this matters: the affordance is only ever
|
|
372
|
+
* taken AWAY by the observer, so a long article — what the block exists
|
|
373
|
+
* for — never shifts on hydration. (In jsdom the observer then measures a
|
|
374
|
+
* zero-height content box and removes it, which is the next assertion.)
|
|
375
|
+
*/
|
|
376
|
+
it('starts overflowing in the server render, so a long article never shifts', () => {
|
|
377
|
+
expect(renderToString(<CollapsibleBlock {...props()} />)).toContain(
|
|
378
|
+
'data-overflowing="true"'
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('hides the trigger once the content is measured as fitting', () => {
|
|
383
|
+
const { container } = render(<CollapsibleBlock {...props()} />);
|
|
384
|
+
|
|
385
|
+
expect(container.querySelector('[data-overflowing]')).toHaveAttribute(
|
|
386
|
+
'data-overflowing',
|
|
387
|
+
'false'
|
|
388
|
+
);
|
|
389
|
+
expect(sheet(container)).toContain(
|
|
390
|
+
'[data-pz-collapsible="col-1"][data-overflowing="false"] [data-part="toggle"]{display:none}'
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('unclips itself with JS off', () => {
|
|
395
|
+
const { container } = render(<CollapsibleBlock {...props()} />);
|
|
396
|
+
const noscript = container.querySelector('noscript');
|
|
397
|
+
|
|
398
|
+
expect(noscript?.innerHTML).toContain('max-height:none');
|
|
399
|
+
expect(noscript?.innerHTML).toContain(
|
|
400
|
+
'[data-part="toggle"]{display:none}'
|
|
401
|
+
);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('stops animating for a reduced-motion visitor', () => {
|
|
405
|
+
const { container } = render(<CollapsibleBlock {...props()} />);
|
|
406
|
+
|
|
407
|
+
expect(sheet(container)).toContain(
|
|
408
|
+
'@media (prefers-reduced-motion:reduce)'
|
|
409
|
+
);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('opens on its own when the merchant asked for it', () => {
|
|
413
|
+
render(
|
|
414
|
+
<CollapsibleBlock
|
|
415
|
+
{...withProperties({ defaultExpanded: { desktop: true } })}
|
|
416
|
+
/>
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
expect(screen.getByRole('button')).toHaveAttribute(
|
|
420
|
+
'aria-expanded',
|
|
421
|
+
'true'
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
describe('composing', () => {
|
|
427
|
+
it('renders children in order and drops hidden ones on the storefront', () => {
|
|
428
|
+
render(
|
|
429
|
+
<CollapsibleBlock
|
|
430
|
+
{...props({
|
|
431
|
+
block: {
|
|
432
|
+
...props().block,
|
|
433
|
+
blocks: [
|
|
434
|
+
child('second', { order: 2 }),
|
|
435
|
+
child('first', { order: 1 }),
|
|
436
|
+
child('gone', { order: 3, hidden: true })
|
|
437
|
+
]
|
|
438
|
+
}
|
|
439
|
+
})}
|
|
440
|
+
/>
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
expect(
|
|
444
|
+
screen.getAllByTestId('tb').map((el) => el.getAttribute('data-id'))
|
|
445
|
+
).toEqual(['first', 'second']);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('keeps hidden children selectable on the canvas', () => {
|
|
449
|
+
render(
|
|
450
|
+
<CollapsibleBlock
|
|
451
|
+
{...props({
|
|
452
|
+
isDesigner: true,
|
|
453
|
+
block: {
|
|
454
|
+
...props().block,
|
|
455
|
+
blocks: [
|
|
456
|
+
child('shown'),
|
|
457
|
+
child('gone', { order: 1, hidden: true })
|
|
458
|
+
]
|
|
459
|
+
}
|
|
460
|
+
})}
|
|
461
|
+
/>
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
expect(screen.getAllByTestId('tb')).toHaveLength(2);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('says what it is missing on the canvas and renders nothing on the storefront', () => {
|
|
468
|
+
const empty = { ...props().block, blocks: [] };
|
|
469
|
+
|
|
470
|
+
const { container } = render(
|
|
471
|
+
<CollapsibleBlock {...props({ block: empty, isDesigner: true })} />
|
|
472
|
+
);
|
|
473
|
+
expect(
|
|
474
|
+
container.querySelector('[data-part="empty"]')
|
|
475
|
+
).toBeInTheDocument();
|
|
476
|
+
|
|
477
|
+
const live = render(<CollapsibleBlock {...props({ block: empty })} />);
|
|
478
|
+
expect(live.container.querySelector('[data-part="empty"]')).toBeNull();
|
|
479
|
+
expect(live.container.querySelector('[data-part="toggle"]')).toBeNull();
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
describe('every part the editor can style is emitted', () => {
|
|
484
|
+
it.each([
|
|
485
|
+
'viewport',
|
|
486
|
+
'clip',
|
|
487
|
+
'content',
|
|
488
|
+
'toggle',
|
|
489
|
+
'toggle-label',
|
|
490
|
+
'toggle-icon'
|
|
491
|
+
])('draws %s', (part) => {
|
|
492
|
+
const { container } = render(<CollapsibleBlock {...props()} />);
|
|
493
|
+
expect(
|
|
494
|
+
container.querySelector(`[data-part="${part}"]`)
|
|
495
|
+
).toBeInTheDocument();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('draws fade and empty in the states that have them', () => {
|
|
499
|
+
const colour = render(
|
|
500
|
+
<CollapsibleBlock
|
|
501
|
+
{...withProperties({ fadeStyle: { desktop: 'color' } })}
|
|
502
|
+
/>
|
|
503
|
+
);
|
|
504
|
+
expect(
|
|
505
|
+
colour.container.querySelector('[data-part="fade"]')
|
|
506
|
+
).toBeInTheDocument();
|
|
507
|
+
|
|
508
|
+
const bare = render(
|
|
509
|
+
<CollapsibleBlock
|
|
510
|
+
{...props({
|
|
511
|
+
block: { ...props().block, blocks: [] },
|
|
512
|
+
isDesigner: true
|
|
513
|
+
})}
|
|
514
|
+
/>
|
|
515
|
+
);
|
|
516
|
+
expect(
|
|
517
|
+
bare.container.querySelector('[data-part="empty"]')
|
|
518
|
+
).toBeInTheDocument();
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
});
|