@akinon/pz-theme 2.0.38-rc.0 → 2.0.39-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 +15 -31
- package/package.json +4 -4
- package/src/__tests__/chrome-takeover-gate.test.tsx +252 -0
- package/src/__tests__/registry-parity.test.ts +14 -1
- package/src/__tests__/route-slug.test.ts +116 -0
- package/src/blocks/__tests__/overlay-sweep.test.tsx +592 -0
- package/src/blocks/account-button-block.tsx +133 -0
- package/src/blocks/announcement-bar-block.tsx +407 -0
- package/src/blocks/block-renderer-registry.tsx +26 -0
- package/src/blocks/currency-selector-block.tsx +177 -0
- package/src/blocks/hamburger-menu-block.tsx +461 -0
- package/src/blocks/language-selector-block.tsx +172 -0
- package/src/blocks/mega-menu-block.tsx +712 -0
- package/src/blocks/mini-basket-block.tsx +173 -0
- package/src/blocks/nav-menu-block.tsx +309 -0
- package/src/blocks/newsletter-form-block.tsx +184 -0
- package/src/blocks/payment-badges-block.tsx +393 -0
- package/src/blocks/search-box-block.tsx +277 -0
- package/src/blocks/social-links-block.tsx +391 -0
- package/src/blocks/wishlist-button-block.tsx +115 -0
- package/src/chrome/chrome-fallback.tsx +147 -0
- package/src/chrome/chrome-slot.tsx +68 -0
- package/src/chrome/composition-state.ts +92 -0
- package/src/chrome/page-context.ts +387 -0
- package/src/chrome/placeholder-slugs.ts +86 -0
- package/src/chrome/route-slug.ts +159 -0
- package/src/index.ts +20 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
const push = jest.fn();
|
|
5
|
+
|
|
6
|
+
jest.mock('next/navigation', () => ({
|
|
7
|
+
useRouter: () => ({ push: (...args: unknown[]) => push(...args) }),
|
|
8
|
+
usePathname: () => '/list',
|
|
9
|
+
useSearchParams: () => new URLSearchParams('')
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
jest.mock('@akinon/next/data/client/misc', () => ({
|
|
13
|
+
useGetMenuQuery: () => ({
|
|
14
|
+
data: [
|
|
15
|
+
{ pk: 1, label: 'Kadın', url: '/kadin', parent_pk: null },
|
|
16
|
+
{ pk: 2, label: 'Ayakkabı', url: '/kadin/ayakkabi', parent_pk: 1 }
|
|
17
|
+
],
|
|
18
|
+
isLoading: false,
|
|
19
|
+
isFetching: false,
|
|
20
|
+
isSuccess: true,
|
|
21
|
+
isError: false
|
|
22
|
+
})
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
import FacetFiltersBlock from '../facet-filters-block';
|
|
26
|
+
import SizeGuideBlock from '../size-guide-block';
|
|
27
|
+
import HamburgerMenuBlock from '../hamburger-menu-block';
|
|
28
|
+
import MegaMenuBlock from '../mega-menu-block';
|
|
29
|
+
import SortSelectBlock from '../sort-select-block';
|
|
30
|
+
import { useOverlay } from '../../hooks/use-overlay';
|
|
31
|
+
import { ThemePageContextProvider } from '../../theme-page-context';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The sweep, from the outside.
|
|
35
|
+
*
|
|
36
|
+
* Every one of these blocks used to own a piece of state the whole PAGE shares
|
|
37
|
+
* — the focus trap, the `aria-modal` claim, Escape, the scroll lock — and a
|
|
38
|
+
* listing page composes several of them at once. A shopper opening the filter
|
|
39
|
+
* drawer over a store panel, a size picker or a quick view is the ordinary
|
|
40
|
+
* case, not an edge one, so each pair below is a composition a brand ships.
|
|
41
|
+
*
|
|
42
|
+
* The overlay these blocks are tested AGAINST is a plain `useOverlay`
|
|
43
|
+
* consumer: it is the same primitive `store-locator`, `variant-selector`,
|
|
44
|
+
* `product-gallery` and `product-recommendations` were migrated onto, so a
|
|
45
|
+
* block that behaves next to it behaves next to all four.
|
|
46
|
+
*/
|
|
47
|
+
function MigratedPanel({
|
|
48
|
+
open,
|
|
49
|
+
modal = true
|
|
50
|
+
}: {
|
|
51
|
+
open: boolean;
|
|
52
|
+
modal?: boolean;
|
|
53
|
+
}) {
|
|
54
|
+
const overlay = useOverlay<HTMLDivElement>({ open, modal });
|
|
55
|
+
|
|
56
|
+
if (!open) return null;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
{...overlay.overlayProps}
|
|
61
|
+
role="dialog"
|
|
62
|
+
tabIndex={-1}
|
|
63
|
+
data-testid="migrated-panel"
|
|
64
|
+
>
|
|
65
|
+
<button type="button" data-testid="migrated-first">
|
|
66
|
+
Kadıköy
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const facet = (key: string, name: string, choices: unknown[]) => ({
|
|
73
|
+
key,
|
|
74
|
+
name,
|
|
75
|
+
search_key: `attributes_${key}`,
|
|
76
|
+
widget_type: 'multiselect',
|
|
77
|
+
data: { choices }
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const listingContext = {
|
|
81
|
+
listing: {
|
|
82
|
+
title: 'Shoes',
|
|
83
|
+
path: '/list',
|
|
84
|
+
query: '',
|
|
85
|
+
products: [],
|
|
86
|
+
facets: [
|
|
87
|
+
facet('color', 'Renk', [
|
|
88
|
+
{ label: 'Siyah', value: 'black', quantity: 12, is_selected: false },
|
|
89
|
+
{ label: 'Beyaz', value: 'white', quantity: 7, is_selected: false }
|
|
90
|
+
])
|
|
91
|
+
],
|
|
92
|
+
sorters: [
|
|
93
|
+
{ key: 'newest', label: 'Newest' },
|
|
94
|
+
{ key: 'price_asc', label: 'Price low to high' }
|
|
95
|
+
],
|
|
96
|
+
pagination: {},
|
|
97
|
+
breadcrumbs: []
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const mkBlock = (
|
|
102
|
+
type: string,
|
|
103
|
+
properties: Record<string, unknown>,
|
|
104
|
+
blocks: unknown[] = []
|
|
105
|
+
) =>
|
|
106
|
+
({
|
|
107
|
+
id: `${type}-1`,
|
|
108
|
+
type,
|
|
109
|
+
label: type,
|
|
110
|
+
order: 0,
|
|
111
|
+
hidden: false,
|
|
112
|
+
value: '',
|
|
113
|
+
properties,
|
|
114
|
+
styles: {},
|
|
115
|
+
blocks
|
|
116
|
+
} as never);
|
|
117
|
+
|
|
118
|
+
interface PageProps {
|
|
119
|
+
facetProperties?: Record<string, unknown>;
|
|
120
|
+
migratedOpen?: boolean;
|
|
121
|
+
isDesigner?: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const Page = ({
|
|
125
|
+
facetProperties = { layout: 'panel' },
|
|
126
|
+
migratedOpen = false,
|
|
127
|
+
isDesigner = false
|
|
128
|
+
}: PageProps) => (
|
|
129
|
+
<ThemePageContextProvider value={listingContext}>
|
|
130
|
+
<button type="button" data-testid="outside">
|
|
131
|
+
outside
|
|
132
|
+
</button>
|
|
133
|
+
<FacetFiltersBlock
|
|
134
|
+
block={mkBlock('facet-filters', facetProperties)}
|
|
135
|
+
placeholderId="p"
|
|
136
|
+
sectionId="s"
|
|
137
|
+
isDesigner={isDesigner}
|
|
138
|
+
currentBreakpoint="desktop"
|
|
139
|
+
/>
|
|
140
|
+
<MigratedPanel open={migratedOpen} />
|
|
141
|
+
</ThemePageContextProvider>
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const openDrawer = () =>
|
|
145
|
+
fireEvent.click(screen.getByRole('button', { name: /Filters/ }));
|
|
146
|
+
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
push.mockClear();
|
|
149
|
+
document.body.style.overflow = '';
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('the filter drawer next to an overlay already on the primitive', () => {
|
|
153
|
+
/**
|
|
154
|
+
* The tab-killer itself. Two DOCUMENT-level `focusin` listeners, each
|
|
155
|
+
* pulling focus back into its own panel, read each other's panel as
|
|
156
|
+
* "outside" and hand focus back and forth until the call stack runs out —
|
|
157
|
+
* `RangeError: Maximum call stack size exceeded`, and a tab a shopper has to
|
|
158
|
+
* kill. One move of focus is all it takes.
|
|
159
|
+
*/
|
|
160
|
+
it('does not trade focus with the overlay on top until the stack runs out', () => {
|
|
161
|
+
const { rerender } = render(<Page />);
|
|
162
|
+
openDrawer();
|
|
163
|
+
// The shopper opens a store panel over the drawer. Both are up.
|
|
164
|
+
rerender(<Page migratedOpen />);
|
|
165
|
+
|
|
166
|
+
const seen: string[] = [];
|
|
167
|
+
const recordMove = (event: Event) => {
|
|
168
|
+
// Bounded: the point is WHICH panels answer, and an unbounded log of a
|
|
169
|
+
// runaway loop is unreadable.
|
|
170
|
+
if (seen.length < 8) {
|
|
171
|
+
seen.push(
|
|
172
|
+
(event.target as HTMLElement).getAttribute('data-testid') ||
|
|
173
|
+
(event.target as HTMLElement).getAttribute('data-facet-choice') ||
|
|
174
|
+
(event.target as HTMLElement).tagName
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
document.addEventListener('focusin', recordMove);
|
|
180
|
+
|
|
181
|
+
let thrown: unknown = null;
|
|
182
|
+
try {
|
|
183
|
+
screen.getByTestId('outside').focus();
|
|
184
|
+
} catch (error) {
|
|
185
|
+
thrown = error;
|
|
186
|
+
} finally {
|
|
187
|
+
document.removeEventListener('focusin', recordMove);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
expect(thrown).toBeNull();
|
|
191
|
+
// Focus landing outside, and one pull back into the overlay on top. That
|
|
192
|
+
// is the whole exchange a single stack allows. (Order is jsdom's: the
|
|
193
|
+
// nested dispatch finishes before the outer listener list does.)
|
|
194
|
+
expect(seen).toHaveLength(2);
|
|
195
|
+
expect(seen).toContain('outside');
|
|
196
|
+
expect(seen).toContain('migrated-first');
|
|
197
|
+
|
|
198
|
+
expect(
|
|
199
|
+
screen.getByTestId('migrated-panel').contains(document.activeElement)
|
|
200
|
+
).toBe(true);
|
|
201
|
+
// The drawer beneath is inert, not gone: it stays mounted and intact and
|
|
202
|
+
// simply stops reaching for the page.
|
|
203
|
+
expect(screen.getByRole('dialog', { name: 'Filters' })).toBeInTheDocument();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('leaves the trap to the overlay on top, and takes it back when that one closes', () => {
|
|
207
|
+
const { rerender } = render(<Page />);
|
|
208
|
+
openDrawer();
|
|
209
|
+
rerender(<Page migratedOpen />);
|
|
210
|
+
|
|
211
|
+
expect(screen.getByTestId('migrated-panel')).toHaveAttribute(
|
|
212
|
+
'aria-modal',
|
|
213
|
+
'true'
|
|
214
|
+
);
|
|
215
|
+
// Two live dialogs both claiming modality makes neither claim true.
|
|
216
|
+
expect(screen.getByRole('dialog', { name: 'Filters' })).not.toHaveAttribute(
|
|
217
|
+
'aria-modal'
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
rerender(<Page migratedOpen={false} />);
|
|
221
|
+
|
|
222
|
+
const drawer = screen.getByRole('dialog', { name: 'Filters' });
|
|
223
|
+
expect(drawer).toHaveAttribute('aria-modal', 'true');
|
|
224
|
+
|
|
225
|
+
// And the drawer traps again now that it is the one hiding the page.
|
|
226
|
+
screen.getByTestId('outside').focus();
|
|
227
|
+
expect(drawer.contains(document.activeElement)).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('leaves Escape to the overlay on top', () => {
|
|
231
|
+
const { rerender } = render(<Page />);
|
|
232
|
+
openDrawer();
|
|
233
|
+
rerender(<Page migratedOpen />);
|
|
234
|
+
|
|
235
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
236
|
+
|
|
237
|
+
// The drawer is underneath: Escape backed out of the panel on top, not out
|
|
238
|
+
// of every panel on the page.
|
|
239
|
+
expect(screen.getByRole('dialog', { name: 'Filters' })).toBeInTheDocument();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('counts its scroll lock with every other overlay and gives the page its own overflow back', () => {
|
|
243
|
+
document.body.style.overflow = 'auto';
|
|
244
|
+
|
|
245
|
+
const { rerender } = render(<Page />);
|
|
246
|
+
openDrawer();
|
|
247
|
+
expect(document.body.style.overflow).toBe('hidden');
|
|
248
|
+
|
|
249
|
+
rerender(<Page migratedOpen />);
|
|
250
|
+
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
|
|
251
|
+
// One holder left: the page must stay frozen behind the panel still up.
|
|
252
|
+
expect(document.body.style.overflow).toBe('hidden');
|
|
253
|
+
|
|
254
|
+
rerender(<Page migratedOpen={false} />);
|
|
255
|
+
expect(document.body.style.overflow).toBe('auto');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('still moves focus into the drawer on open and hands it back on close', () => {
|
|
259
|
+
render(<Page />);
|
|
260
|
+
|
|
261
|
+
const trigger = screen.getByRole('button', { name: /Filters/ });
|
|
262
|
+
trigger.focus();
|
|
263
|
+
fireEvent.click(trigger);
|
|
264
|
+
|
|
265
|
+
expect(
|
|
266
|
+
screen.getByRole('dialog', { name: 'Filters' }).contains(document.activeElement)
|
|
267
|
+
).toBe(true);
|
|
268
|
+
|
|
269
|
+
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
|
|
270
|
+
expect(document.activeElement).toBe(trigger);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('stays inert in the designer', () => {
|
|
274
|
+
document.body.style.overflow = 'auto';
|
|
275
|
+
render(<Page isDesigner />);
|
|
276
|
+
openDrawer();
|
|
277
|
+
|
|
278
|
+
const drawer = screen.getByRole('dialog', { name: 'Filters' });
|
|
279
|
+
|
|
280
|
+
// The canvas is the author's workspace: nothing may steal focus into it,
|
|
281
|
+
// freeze the page being styled, or claim the page is hidden.
|
|
282
|
+
expect(drawer.contains(document.activeElement)).toBe(false);
|
|
283
|
+
expect(document.body.style.overflow).toBe('auto');
|
|
284
|
+
expect(drawer).not.toHaveAttribute('aria-modal');
|
|
285
|
+
|
|
286
|
+
// And it must not close itself out from under an author who pressed
|
|
287
|
+
// Escape while typing in a property field. The block used to answer
|
|
288
|
+
// Escape on the canvas too; the primitive is inert there.
|
|
289
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
290
|
+
expect(screen.getByRole('dialog', { name: 'Filters' })).toBeInTheDocument();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('leaves the page alone when the designer turned the lock off', () => {
|
|
294
|
+
document.body.style.overflow = 'auto';
|
|
295
|
+
render(<Page facetProperties={{ layout: 'panel', lockScroll: false }} />);
|
|
296
|
+
openDrawer();
|
|
297
|
+
|
|
298
|
+
expect(document.body.style.overflow).toBe('auto');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('makes no modal claim for a drawer with no backdrop behind it', () => {
|
|
302
|
+
render(<Page facetProperties={{ layout: 'panel', showBackdrop: false }} />);
|
|
303
|
+
openDrawer();
|
|
304
|
+
|
|
305
|
+
// The page underneath stays clickable, so the claim would be a lie.
|
|
306
|
+
expect(screen.getByRole('dialog', { name: 'Filters' })).not.toHaveAttribute(
|
|
307
|
+
'aria-modal'
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('still answers Escape when it is the panel on top', () => {
|
|
312
|
+
render(<Page />);
|
|
313
|
+
openDrawer();
|
|
314
|
+
|
|
315
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
316
|
+
|
|
317
|
+
expect(screen.queryByRole('dialog', { name: 'Filters' })).toBeNull();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('honours a composition that turned Escape off', () => {
|
|
321
|
+
render(
|
|
322
|
+
<Page facetProperties={{ layout: 'panel', closeOnEscape: false }} />
|
|
323
|
+
);
|
|
324
|
+
openDrawer();
|
|
325
|
+
|
|
326
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
327
|
+
|
|
328
|
+
expect(screen.getByRole('dialog', { name: 'Filters' })).toBeInTheDocument();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Every pointer route out is a designer toggle, so a composition can legally
|
|
333
|
+
* turn all of them off. Escape is then re-armed no matter what the property
|
|
334
|
+
* says — an authored composition must never seal a shopper in.
|
|
335
|
+
*/
|
|
336
|
+
it('re-arms Escape for a panel with no pointer route out', () => {
|
|
337
|
+
render(
|
|
338
|
+
<Page
|
|
339
|
+
facetProperties={{
|
|
340
|
+
layout: 'panel',
|
|
341
|
+
showPanelHeader: false,
|
|
342
|
+
showClose: false,
|
|
343
|
+
showBackdrop: false,
|
|
344
|
+
closeOnEscape: false
|
|
345
|
+
}}
|
|
346
|
+
/>
|
|
347
|
+
);
|
|
348
|
+
openDrawer();
|
|
349
|
+
|
|
350
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
351
|
+
|
|
352
|
+
expect(screen.queryByRole('dialog', { name: 'Filters' })).toBeNull();
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe('the size guide next to an overlay already on the primitive', () => {
|
|
357
|
+
const SizeGuidePage = ({ migratedOpen = false }: { migratedOpen?: boolean }) => (
|
|
358
|
+
<>
|
|
359
|
+
<SizeGuideBlock
|
|
360
|
+
block={mkBlock('size-guide', {}, [
|
|
361
|
+
{
|
|
362
|
+
id: 'row-1',
|
|
363
|
+
type: 'text',
|
|
364
|
+
label: 'Row',
|
|
365
|
+
order: 0,
|
|
366
|
+
hidden: false,
|
|
367
|
+
value: 'S / M / L',
|
|
368
|
+
properties: {},
|
|
369
|
+
styles: {},
|
|
370
|
+
blocks: []
|
|
371
|
+
}
|
|
372
|
+
])}
|
|
373
|
+
placeholderId="p"
|
|
374
|
+
sectionId="s"
|
|
375
|
+
isDesigner={false}
|
|
376
|
+
currentBreakpoint="desktop"
|
|
377
|
+
/>
|
|
378
|
+
<MigratedPanel open={migratedOpen} />
|
|
379
|
+
</>
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
const openGuide = () =>
|
|
383
|
+
fireEvent.click(screen.getByRole('button', { name: 'Size guide' }));
|
|
384
|
+
|
|
385
|
+
it('drops its modal claim while another dialog is on top', () => {
|
|
386
|
+
const { rerender } = render(<SizeGuidePage />);
|
|
387
|
+
openGuide();
|
|
388
|
+
rerender(<SizeGuidePage migratedOpen />);
|
|
389
|
+
|
|
390
|
+
// It used to hard-code `aria-modal="true"`, so a quick view over it left
|
|
391
|
+
// two dialogs each telling assistive tech the other was hidden.
|
|
392
|
+
expect(
|
|
393
|
+
screen.getByRole('dialog', { name: 'Size guide' })
|
|
394
|
+
).not.toHaveAttribute('aria-modal');
|
|
395
|
+
expect(screen.getByTestId('migrated-panel')).toHaveAttribute(
|
|
396
|
+
'aria-modal',
|
|
397
|
+
'true'
|
|
398
|
+
);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('leaves Escape to the overlay on top', () => {
|
|
402
|
+
const { rerender } = render(<SizeGuidePage />);
|
|
403
|
+
openGuide();
|
|
404
|
+
rerender(<SizeGuidePage migratedOpen />);
|
|
405
|
+
|
|
406
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
407
|
+
|
|
408
|
+
expect(
|
|
409
|
+
screen.getByRole('dialog', { name: 'Size guide' })
|
|
410
|
+
).toBeInTheDocument();
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('still closes on Escape when it is the one on top', () => {
|
|
414
|
+
render(<SizeGuidePage />);
|
|
415
|
+
openGuide();
|
|
416
|
+
|
|
417
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
418
|
+
|
|
419
|
+
expect(screen.queryByRole('dialog', { name: 'Size guide' })).toBeNull();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('honours a composition that turned Escape off', () => {
|
|
423
|
+
render(
|
|
424
|
+
<SizeGuideBlock
|
|
425
|
+
block={mkBlock('size-guide', { closeOnEscape: false }, [
|
|
426
|
+
{
|
|
427
|
+
id: 'row-1',
|
|
428
|
+
type: 'text',
|
|
429
|
+
label: 'Row',
|
|
430
|
+
order: 0,
|
|
431
|
+
hidden: false,
|
|
432
|
+
value: 'S / M / L',
|
|
433
|
+
properties: {},
|
|
434
|
+
styles: {},
|
|
435
|
+
blocks: []
|
|
436
|
+
}
|
|
437
|
+
])}
|
|
438
|
+
placeholderId="p"
|
|
439
|
+
sectionId="s"
|
|
440
|
+
isDesigner={false}
|
|
441
|
+
currentBreakpoint="desktop"
|
|
442
|
+
/>
|
|
443
|
+
);
|
|
444
|
+
openGuide();
|
|
445
|
+
|
|
446
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
447
|
+
|
|
448
|
+
expect(
|
|
449
|
+
screen.getByRole('dialog', { name: 'Size guide' })
|
|
450
|
+
).toBeInTheDocument();
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
describe('the hamburger drawer next to an overlay already on the primitive', () => {
|
|
455
|
+
const HamburgerPage = ({
|
|
456
|
+
migratedOpen = false,
|
|
457
|
+
properties = {}
|
|
458
|
+
}: {
|
|
459
|
+
migratedOpen?: boolean;
|
|
460
|
+
properties?: Record<string, unknown>;
|
|
461
|
+
}) => (
|
|
462
|
+
<>
|
|
463
|
+
<HamburgerMenuBlock
|
|
464
|
+
block={mkBlock('hamburger-menu', properties)}
|
|
465
|
+
placeholderId="p"
|
|
466
|
+
sectionId="s"
|
|
467
|
+
isDesigner={false}
|
|
468
|
+
currentBreakpoint="desktop"
|
|
469
|
+
/>
|
|
470
|
+
<MigratedPanel open={migratedOpen} />
|
|
471
|
+
</>
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
const openDrawerMenu = () =>
|
|
475
|
+
fireEvent.click(
|
|
476
|
+
document.querySelector('[data-hamburger-menu]') as HTMLElement
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* The drawer captured `document.body.style.overflow` itself, with no counter
|
|
481
|
+
* at all. Opened over a panel that had already frozen the page it recorded
|
|
482
|
+
* `hidden` as "the page's own value", so the other panel closing thawed the
|
|
483
|
+
* page under a drawer still up — and the drawer closing wrote `hidden` back
|
|
484
|
+
* for good, leaving a storefront no one can scroll.
|
|
485
|
+
*/
|
|
486
|
+
it('counts its scroll lock with every other overlay', () => {
|
|
487
|
+
document.body.style.overflow = 'auto';
|
|
488
|
+
|
|
489
|
+
const { rerender } = render(<HamburgerPage migratedOpen />);
|
|
490
|
+
openDrawerMenu();
|
|
491
|
+
expect(document.body.style.overflow).toBe('hidden');
|
|
492
|
+
|
|
493
|
+
// The panel that froze the page first closes while the drawer is still up.
|
|
494
|
+
rerender(<HamburgerPage migratedOpen={false} />);
|
|
495
|
+
expect(document.body.style.overflow).toBe('hidden');
|
|
496
|
+
|
|
497
|
+
fireEvent.click(
|
|
498
|
+
document.querySelector('[data-hamburger-close]') as HTMLElement
|
|
499
|
+
);
|
|
500
|
+
expect(document.body.style.overflow).toBe('auto');
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it('leaves the page alone when the designer turned the lock off', () => {
|
|
504
|
+
document.body.style.overflow = 'auto';
|
|
505
|
+
render(<HamburgerPage properties={{ lockScroll: false }} />);
|
|
506
|
+
openDrawerMenu();
|
|
507
|
+
|
|
508
|
+
expect(document.body.style.overflow).toBe('auto');
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Neither of these is an overlay — no backdrop, nothing covered, no scroll
|
|
514
|
+
* lock, no focus trap — so they keep their own focus handling. What they do
|
|
515
|
+
* share with the page is Escape, and a document listener each meant a key
|
|
516
|
+
* aimed at the modal on top collapsed them underneath it at the same time.
|
|
517
|
+
*/
|
|
518
|
+
describe('the popups that share the page keyboard', () => {
|
|
519
|
+
const SortPage = ({ migratedOpen = false }: { migratedOpen?: boolean }) => (
|
|
520
|
+
<ThemePageContextProvider value={listingContext}>
|
|
521
|
+
<SortSelectBlock
|
|
522
|
+
block={mkBlock('sort-select', { variant: 'panel' })}
|
|
523
|
+
placeholderId="p"
|
|
524
|
+
sectionId="s"
|
|
525
|
+
isDesigner={false}
|
|
526
|
+
currentBreakpoint="desktop"
|
|
527
|
+
/>
|
|
528
|
+
<MigratedPanel open={migratedOpen} />
|
|
529
|
+
</ThemePageContextProvider>
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
const MegaPage = ({ migratedOpen = false }: { migratedOpen?: boolean }) => (
|
|
533
|
+
<>
|
|
534
|
+
<MegaMenuBlock
|
|
535
|
+
block={mkBlock('mega-menu', { trigger: 'click' })}
|
|
536
|
+
placeholderId="p"
|
|
537
|
+
sectionId="s"
|
|
538
|
+
isDesigner={false}
|
|
539
|
+
currentBreakpoint="desktop"
|
|
540
|
+
/>
|
|
541
|
+
<MigratedPanel open={migratedOpen} />
|
|
542
|
+
</>
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
it('leaves Escape to the overlay on top of the sort panel', () => {
|
|
546
|
+
const { rerender } = render(<SortPage />);
|
|
547
|
+
|
|
548
|
+
fireEvent.click(screen.getByRole('button', { name: /Sort/ }));
|
|
549
|
+
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
550
|
+
|
|
551
|
+
rerender(<SortPage migratedOpen />);
|
|
552
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
553
|
+
|
|
554
|
+
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('still closes the sort panel on Escape and hands focus back', () => {
|
|
558
|
+
render(<SortPage />);
|
|
559
|
+
|
|
560
|
+
const trigger = screen.getByRole('button', { name: /Sort/ });
|
|
561
|
+
fireEvent.click(trigger);
|
|
562
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
563
|
+
|
|
564
|
+
expect(screen.queryByRole('listbox')).toBeNull();
|
|
565
|
+
expect(document.activeElement).toBe(trigger);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('leaves Escape to the overlay on top of the mega menu panel', () => {
|
|
569
|
+
const { rerender } = render(<MegaPage />);
|
|
570
|
+
|
|
571
|
+
fireEvent.click(screen.getByRole('button', { name: 'Kadın' }));
|
|
572
|
+
expect(
|
|
573
|
+
document.querySelector('[data-mega-menu-panel]')
|
|
574
|
+
).toBeInTheDocument();
|
|
575
|
+
|
|
576
|
+
rerender(<MegaPage migratedOpen />);
|
|
577
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
578
|
+
|
|
579
|
+
expect(
|
|
580
|
+
document.querySelector('[data-mega-menu-panel]')
|
|
581
|
+
).toBeInTheDocument();
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('still closes the mega menu on Escape when it is the one on top', () => {
|
|
585
|
+
render(<MegaPage />);
|
|
586
|
+
|
|
587
|
+
fireEvent.click(screen.getByRole('button', { name: 'Kadın' }));
|
|
588
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
589
|
+
|
|
590
|
+
expect(document.querySelector('[data-mega-menu-panel]')).toBeNull();
|
|
591
|
+
});
|
|
592
|
+
});
|