1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
|
<!DOCTYPE html>
<meta charset="utf-8" />
<title>Popover light dismiss behavior</title>
<meta name="timeout" content="long">
<link rel="author" href="mailto:masonf@chromium.org">
<link rel=help href="https://open-ui.org/components/popup.research.explainer">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="resources/popover-utils.js"></script>
<button id=b1t popovertoggletarget='p1'>Popover 1</button>
<button id=b1s popovershowtarget='p1'>Popover 1</button>
<button id=p1anchor>Popover1 anchor (no action)</button>
<span id=outside>Outside all popovers</span>
<div popover id=p1 anchor=p1anchor>
<span id=inside1>Inside popover 1</span>
<button id=b2 popovershowtarget='p2'>Popover 2</button>
<span id=inside1after>Inside popover 1 after button</span>
</div>
<div popover id=p2 anchor=b2>
<span id=inside2>Inside popover 2</span>
</div>
<button id=after_p1>Next control after popover1</button>
<style>
#p1 {top: 50px;}
#p2 {top: 120px;}
[popover] {bottom:auto;}
[popover]::backdrop {
/* This should *not* affect anything: */
pointer-events: auto;
}
</style>
<script>
const popover1 = document.querySelector('#p1');
const button1toggle = document.querySelector('#b1t');
const button1show = document.querySelector('#b1s');
const popover1anchor = document.querySelector('#p1anchor');
const inside1After = document.querySelector('#inside1after');
const button2 = document.querySelector('#b2');
const popover2 = document.querySelector('#p2');
const outside = document.querySelector('#outside');
const inside1 = document.querySelector('#inside1');
const inside2 = document.querySelector('#inside2');
const afterp1 = document.querySelector('#after_p1');
let popover1HideCount = 0;
popover1.addEventListener('beforetoggle',(e) => {
if (e.newState !== "closed")
return;
++popover1HideCount;
e.preventDefault(); // 'beforetoggle' should not be cancellable.
});
let popover2HideCount = 0;
popover2.addEventListener('beforetoggle',(e) => {
if (e.newState !== "closed")
return;
++popover2HideCount;
e.preventDefault(); // 'beforetoggle' should not be cancellable.
});
promise_test(async () => {
assert_false(popover1.matches(':open'));
popover1.showPopover();
assert_true(popover1.matches(':open'));
let p1HideCount = popover1HideCount;
await clickOn(outside);
assert_false(popover1.matches(':open'));
assert_equals(popover1HideCount,p1HideCount+1);
},'Clicking outside a popover will dismiss the popover');
promise_test(async (t) => {
const controller = new AbortController();
t.add_cleanup(() => controller.abort());
function addListener(eventName) {
document.addEventListener(eventName,(e) => e.preventDefault(),{signal:controller.signal,capture: true});
}
addListener('pointerdown');
addListener('pointerup');
addListener('mousedown');
addListener('mouseup');
assert_false(popover1.matches(':open'));
popover1.showPopover();
assert_true(popover1.matches(':open'));
let p1HideCount = popover1HideCount;
await clickOn(outside);
assert_false(popover1.matches(':open'),'preventDefault should not prevent light dismiss');
assert_equals(popover1HideCount,p1HideCount+1);
},'Canceling pointer events should not keep clicks from light dismissing popovers');
promise_test(async () => {
assert_false(popover1.matches(':open'));
popover1.showPopover();
await waitForRender();
p1HideCount = popover1HideCount;
await clickOn(inside1);
assert_true(popover1.matches(':open'));
assert_equals(popover1HideCount,p1HideCount);
popover1.hidePopover();
},'Clicking inside a popover does not close that popover');
promise_test(async () => {
assert_false(popover1.matches(':open'));
popover1.showPopover();
await waitForRender();
assert_true(popover1.matches(':open'));
const actions = new test_driver.Actions();
await actions.pointerMove(0, 0, {origin: outside})
.pointerDown({button: actions.ButtonType.LEFT})
.send();
await waitForRender();
assert_true(popover1.matches(':open'),'pointerdown (outside the popover) should not hide the popover');
await actions.pointerUp({button: actions.ButtonType.LEFT})
.send();
await waitForRender();
assert_false(popover1.matches(':open'),'pointerup (outside the popover) should trigger light dismiss');
},'Popovers close on pointerup, not pointerdown');
promise_test(async () => {
assert_false(popover1.matches(':open'));
popover1.showPopover();
assert_true(popover1.matches(':open'));
async function testOne(eventName) {
document.body.dispatchEvent(new PointerEvent(eventName));
document.body.dispatchEvent(new MouseEvent(eventName));
document.body.dispatchEvent(new ProgressEvent(eventName));
await waitForRender();
assert_true(popover1.matches(':open'),`A synthetic "${eventName}" event should not hide the popover`);
}
await testOne('pointerup');
await testOne('pointerdown');
await testOne('mouseup');
await testOne('mousedown');
popover1.hidePopover();
},'Synthetic events can\'t close popovers');
promise_test(async () => {
popover1.showPopover();
await clickOn(inside1After);
assert_true(popover1.matches(':open'));
await sendTab();
assert_equals(document.activeElement,afterp1,'Focus should move to a button outside the popover');
assert_true(popover1.matches(':open'));
popover1.hidePopover();
},'Moving focus outside the popover should not dismiss the popover');
promise_test(async () => {
popover1.showPopover();
popover2.showPopover();
await waitForRender();
p1HideCount = popover1HideCount;
let p2HideCount = popover2HideCount;
await clickOn(inside2);
assert_true(popover1.matches(':open'),'popover1 should be open');
assert_true(popover2.matches(':open'),'popover2 should be open');
assert_equals(popover1HideCount,p1HideCount,'popover1');
assert_equals(popover2HideCount,p2HideCount,'popover2');
popover1.hidePopover();
assert_false(popover1.matches(':open'));
assert_false(popover2.matches(':open'));
},'Clicking inside a child popover shouldn\'t close either popover');
promise_test(async () => {
popover1.showPopover();
popover2.showPopover();
await waitForRender();
p1HideCount = popover1HideCount;
p2HideCount = popover2HideCount;
await clickOn(inside1);
assert_true(popover1.matches(':open'));
assert_equals(popover1HideCount,p1HideCount);
assert_false(popover2.matches(':open'));
assert_equals(popover2HideCount,p2HideCount+1);
popover1.hidePopover();
},'Clicking inside a parent popover should close child popover');
promise_test(async () => {
await clickOn(button1show);
assert_true(popover1.matches(':open'));
await waitForRender();
p1HideCount = popover1HideCount;
await clickOn(button1show);
assert_true(popover1.matches(':open'),'popover1 should stay open');
assert_equals(popover1HideCount,p1HideCount,'popover1 should not get hidden and reshown');
popover1.hidePopover(); // Cleanup
assert_false(popover1.matches(':open'));
},'Clicking on invoking element, after using it for activation, shouldn\'t close its popover');
promise_test(async () => {
popover1.showPopover();
assert_true(popover1.matches(':open'));
assert_false(popover2.matches(':open'));
await clickOn(button2);
assert_true(popover2.matches(':open'),'button2 should activate popover2');
p2HideCount = popover2HideCount;
await clickOn(button2);
assert_true(popover2.matches(':open'),'popover2 should stay open');
assert_equals(popover2HideCount,p2HideCount,'popover2 should not get hidden and reshown');
popover1.hidePopover(); // Cleanup
assert_false(popover1.matches(':open'));
assert_false(popover2.matches(':open'));
},'Clicking on invoking element, after using it for activation, shouldn\'t close its popover (nested case)');
promise_test(async () => {
popover1.showPopover();
popover2.showPopover();
assert_true(popover1.matches(':open'));
assert_true(popover2.matches(':open'));
p2HideCount = popover2HideCount;
await clickOn(button2);
assert_true(popover2.matches(':open'),'popover2 should stay open');
assert_equals(popover2HideCount,p2HideCount,'popover2 should not get hidden and reshown');
popover1.hidePopover(); // Cleanup
assert_false(popover1.matches(':open'));
assert_false(popover2.matches(':open'));
},'Clicking on invoking element, after using it for activation, shouldn\'t close its popover (nested case, not used for invocation)');
promise_test(async () => {
popover1.showPopover(); // Directly show the popover
assert_true(popover1.matches(':open'));
await waitForRender();
p1HideCount = popover1HideCount;
await clickOn(button1show);
assert_true(popover1.matches(':open'),'popover1 should stay open');
assert_equals(popover1HideCount,p1HideCount,'popover1 should not get hidden and reshown');
popover1.hidePopover(); // Cleanup
assert_false(popover1.matches(':open'));
},'Clicking on invoking element, even if it wasn\'t used for activation, shouldn\'t close its popover');
promise_test(async () => {
popover1.showPopover(); // Directly show the popover
assert_true(popover1.matches(':open'));
await waitForRender();
p1HideCount = popover1HideCount;
await clickOn(button1toggle);
assert_false(popover1.matches(':open'),'popover1 should be hidden by popovertoggletarget');
assert_equals(popover1HideCount,p1HideCount+1,'popover1 should get hidden only once by popovertoggletarget');
},'Clicking on popovertoggletarget element, even if it wasn\'t used for activation, should hide it exactly once');
promise_test(async () => {
popover1.showPopover();
assert_true(popover1.matches(':open'));
await waitForRender();
await clickOn(popover1anchor);
assert_false(popover1.matches(':open'),'popover1 should close');
},'Clicking on anchor element (that isn\'t an invoking element) shouldn\'t prevent its popover from being closed');
promise_test(async () => {
popover1.showPopover();
popover2.showPopover(); // Popover1 is an ancestral element for popover2.
assert_true(popover1.matches(':open'));
assert_true(popover2.matches(':open'));
const drag_actions = new test_driver.Actions();
// Drag *from* popover2 *to* popover1 (its ancestor).
await drag_actions.pointerMove(0,0,{origin: popover2})
.pointerDown({button: drag_actions.ButtonType.LEFT})
.pointerMove(0,0,{origin: popover1})
.pointerUp({button: drag_actions.ButtonType.LEFT})
.send();
assert_true(popover1.matches(':open'),'popover1 should be open');
assert_true(popover2.matches(':open'),'popover1 should be open');
popover1.hidePopover();
assert_false(popover2.matches(':open'));
},'Dragging from an open popover outside an open popover should leave the popover open');
</script>
<button id=b3 popovertoggletarget=p3>Popover 3 - button 3
<div popover id=p4>Inside popover 4</div>
</button>
<div popover id=p3>Inside popover 3</div>
<div popover id=p5>Inside popover 5
<button popovertoggletarget=p3>Popover 3 - button 4 - unused</button>
</div>
<style>
#p3 {top:100px;}
#p4 {top:200px;}
#p5 {top:200px;}
</style>
<script>
const popover3 = document.querySelector('#p3');
const popover4 = document.querySelector('#p4');
const popover5 = document.querySelector('#p5');
const button3 = document.querySelector('#b3');
promise_test(async () => {
await clickOn(button3);
assert_true(popover3.matches(':open'),'invoking element should open popover');
popover4.showPopover();
assert_true(popover4.matches(':open'));
assert_false(popover3.matches(':open'),'popover3 is unrelated to popover4');
popover4.hidePopover(); // Cleanup
assert_false(popover4.matches(':open'));
},'A popover inside an invoking element doesn\'t participate in that invoker\'s ancestor chain');
promise_test(async () => {
popover5.showPopover();
assert_true(popover5.matches(':open'));
assert_false(popover3.matches(':open'));
popover3.showPopover();
assert_true(popover3.matches(':open'));
assert_true(popover5.matches(':open'));
popover5.hidePopover();
assert_false(popover3.matches(':open'));
assert_false(popover5.matches(':open'));
},'An invoking element that was not used to invoke the popover can still be part of the ancestor chain');
</script>
<div popover id=p6>Inside popover 6
<div style="height:2000px;background:lightgreen"></div>
Bottom of popover6
</div>
<button popovertoggletarget=p6>Popover 6</button>
<style>
#p6 {
width: 300px;
height: 300px;
overflow-y: scroll;
}
</style>
<script>
const popover6 = document.querySelector('#p6');
promise_test(async () => {
popover6.showPopover();
assert_equals(popover6.scrollTop,0,'popover6 should start non-scrolled');
await new test_driver.Actions()
.scroll(0, 0, 0, 50, {origin: popover6})
.send();
assert_true(popover6.matches(':open'),'popover6 should stay open');
assert_equals(popover6.scrollTop,50,'popover6 should be scrolled');
popover6.hidePopover();
},'Scrolling within a popover should not close the popover');
</script>
<my-element id="myElement">
<template shadowroot="open">
<button id=b7 onclick='showPopover7()'>Popover7</button>
<div popover id=p7 anchor=b7 style="top: 100px;">
<p>Popover content.</p>
<input id="inside7" type="text" placeholder="some text">
</div>
</template>
</my-element>
<script>
const button7 = document.querySelector('#myElement').shadowRoot.querySelector('#b7');
const popover7 = document.querySelector('#myElement').shadowRoot.querySelector('#p7');
const inside7 = document.querySelector('#myElement').shadowRoot.querySelector('#inside7');
function showPopover7() {
popover7.showPopover();
}
promise_test(async () => {
button7.click();
assert_true(popover7.matches(':open'),'invoking element should open popover');
inside7.click();
assert_true(popover7.matches(':open'));
popover7.hidePopover();
},'Clicking inside a shadow DOM popover does not close that popover');
promise_test(async () => {
button7.click();
inside7.click();
assert_true(popover7.matches(':open'));
await clickOn(outside);
assert_false(popover7.matches(':open'));
},'Clicking outside a shadow DOM popover should close that popover');
</script>
<div popover id=p8 anchor=p8anchor>
<button>Button</button>
<span id=inside8after>Inside popover 8 after button</span>
</div>
<button id=p8anchor>Popover8 anchor (no action)</button>
<script>
promise_test(async () => {
const popover8 = document.querySelector('#p8');
const inside8After = document.querySelector('#inside8after');
const popover8Anchor = document.querySelector('#p8anchor');
assert_false(popover8.matches(':open'));
popover8.showPopover();
await clickOn(inside8After);
assert_true(popover8.matches(':open'));
await sendTab();
assert_equals(document.activeElement,popover8Anchor,'Focus should move to the anchor element');
assert_true(popover8.matches(':open'),'popover should stay open');
popover8.hidePopover();
},'Moving focus back to the anchor element should not dismiss the popover');
</script>
<!-- Convoluted ancestor relationship -->
<div popover id=convoluted_p1>Popover 1
<div id=convoluted_anchor>Anchor
<button popovertoggletarget=convoluted_p2>Open Popover 2</button>
<div popover id=convoluted_p4><p>Popover 4</p></div>
</div>
</div>
<div popover id=convoluted_p2 anchor=convoluted_p2>Popover 2 (self-anchor-linked)
<button popovertoggletarget=convoluted_p3>Open Popover 3</button>
<button popovershowtarget=convoluted_p2>Self-linked invoker</button>
</div>
<div popover id=convoluted_p3 anchor=convoluted_anchor>Popover 3
<button popovertoggletarget=convoluted_p4>Open Popover 4</button>
</div>
<button onclick="convoluted_p1.showPopover()">Open convoluted popover</button>
<style>
#convoluted_p1 {top:50px;}
#convoluted_p2 {top:150px;}
#convoluted_p3 {top:250px;}
#convoluted_p4 {top:350px;}
</style>
<script>
const convPopover1 = document.querySelector('#convoluted_p1');
const convPopover2 = document.querySelector('#convoluted_p2');
const convPopover3 = document.querySelector('#convoluted_p3');
const convPopover4 = document.querySelector('#convoluted_p4');
promise_test(async () => {
convPopover1.showPopover(); // Programmatically open p1
assert_true(convPopover1.matches(':open'));
convPopover1.querySelector('button').click(); // Click to invoke p2
assert_true(convPopover1.matches(':open'));
assert_true(convPopover2.matches(':open'));
convPopover2.querySelector('button').click(); // Click to invoke p3
assert_true(convPopover1.matches(':open'));
assert_true(convPopover2.matches(':open'));
assert_true(convPopover3.matches(':open'));
convPopover3.querySelector('button').click(); // Click to invoke p4
assert_true(convPopover1.matches(':open'));
assert_true(convPopover2.matches(':open'));
assert_true(convPopover3.matches(':open'));
assert_true(convPopover4.matches(':open'));
convPopover4.firstElementChild.click(); // Click within p4
assert_true(convPopover1.matches(':open'));
assert_true(convPopover2.matches(':open'));
assert_true(convPopover3.matches(':open'));
assert_true(convPopover4.matches(':open'));
convPopover1.hidePopover();
assert_false(convPopover1.matches(':open'));
assert_false(convPopover2.matches(':open'));
assert_false(convPopover3.matches(':open'));
assert_false(convPopover4.matches(':open'));
},'Ensure circular/convoluted ancestral relationships are functional');
promise_test(async () => {
convPopover1.showPopover(); // Programmatically open p1
convPopover1.querySelector('button').click(); // Click to invoke p2
assert_true(convPopover1.matches(':open'));
assert_true(convPopover2.matches(':open'));
assert_false(convPopover3.matches(':open'));
assert_false(convPopover4.matches(':open'));
convPopover4.showPopover(); // Programmatically open p4
assert_true(convPopover1.matches(':open'),'popover1 stays open because it is a DOM ancestor of popover4');
assert_false(convPopover2.matches(':open'),'popover2 closes because it isn\'t connected to popover4 via active invokers');
assert_true(convPopover4.matches(':open'));
convPopover4.firstElementChild.click(); // Click within p4
assert_true(convPopover1.matches(':open'),'nothing changes');
assert_false(convPopover2.matches(':open'));
assert_true(convPopover4.matches(':open'));
convPopover1.hidePopover();
assert_false(convPopover1.matches(':open'));
assert_false(convPopover2.matches(':open'));
assert_false(convPopover3.matches(':open'));
assert_false(convPopover4.matches(':open'));
},'Ensure circular/convoluted ancestral relationships are functional, with a direct showPopover()');
</script>
<div popover id=p13>Popover 1
<div popover id=p14>Popover 2
<div popover id=p15>Popover 3</div>
</div>
</div>
<style>
#p13 {top: 100px;}
#p14 {top: 200px;}
#p15 {top: 300px;}
</style>
<script>
promise_test(async () => {
const p13 = document.querySelector('#p13');
const p14 = document.querySelector('#p14');
const p15 = document.querySelector('#p15');
p13.showPopover();
p14.showPopover();
p15.showPopover();
p15.addEventListener('beforetoggle', (e) => {
if (e.newState !== "closed")
return;
p14.hidePopover();
},{once:true});
assert_true(p13.matches(':open') && p14.matches(':open') && p15.matches(':open'),'all three should be open');
p14.hidePopover();
assert_true(p13.matches(':open'),'p13 should still be open');
assert_false(p14.matches(':open'));
assert_false(p15.matches(':open'));
},'Hide the target popover during "hide all popovers until"');
</script>
|