popover-events.html (8916B)
1 <!DOCTYPE html> 2 <meta charset="utf-8" /> 3 <title>Popover events</title> 4 <link rel="author" href="mailto:masonf@chromium.org"> 5 <link rel=help href="https://open-ui.org/components/popover.research.explainer"> 6 <link rel=help href="https://html.spec.whatwg.org/multipage/popover.html"> 7 <script src="/resources/testharness.js"></script> 8 <script src="/resources/testharnessreport.js"></script> 9 <script src="resources/popover-utils.js"></script> 10 11 <div popover>Popover</div> 12 13 <script> 14 function getPopoverAndSignal(t) { 15 const popover = document.querySelector('[popover]'); 16 const controller = new AbortController(); 17 const signal = controller.signal; 18 t.add_cleanup(() => controller.abort()); 19 return {popover, signal}; 20 } 21 window.onload = () => { 22 for(const method of ["listener","attribute"]) { 23 promise_test(async t => { 24 const {popover,signal} = getPopoverAndSignal(t); 25 assert_false(popover.matches(':popover-open')); 26 let showCount = 0; 27 let afterShowCount = 0; 28 let hideCount = 0; 29 let afterHideCount = 0; 30 function listener(e) { 31 assert_false(e.bubbles,'toggle events should not bubble'); 32 if (e.type === "beforetoggle") { 33 if (e.newState === "open") { 34 ++showCount; 35 assert_equals(e.oldState,"closed",'The "beforetoggle" event should be fired before the popover is open'); 36 assert_false(e.target.matches(':popover-open'),'The popover should *not* be in the :popover-open state when the opening event fires.'); 37 assert_true(e.cancelable,'beforetoggle should be cancelable only for the "show" transition'); 38 } else { 39 ++hideCount; 40 assert_equals(e.newState,"closed",'Popover toggleevent states should be "open" and "closed"'); 41 assert_equals(e.oldState,"open",'The "beforetoggle" event should be fired before the popover is closed') 42 assert_true(e.target.matches(':popover-open'),'The popover should be in the :popover-open state when the hiding event fires.'); 43 assert_false(e.cancelable,'beforetoggle should be cancelable only for the "show" transition'); 44 e.preventDefault(); // beforetoggle should be cancelable only for the "show" transition 45 } 46 } else { 47 assert_equals(e.type,"toggle",'Popover events should be "beforetoggle" and "toggle"') 48 assert_false(e.cancelable,'toggle should never be cancelable'); 49 e.preventDefault(); // toggle should never be cancelable 50 if (e.newState === "open") { 51 ++afterShowCount; 52 if (document.body.contains(e.target)) { 53 assert_true(e.target.matches(':popover-open'),'The popover should be in the :popover-open state when the after opening event fires.'); 54 } 55 } else { 56 ++afterHideCount; 57 assert_equals(e.newState,"closed",'Popover toggleevent states should be "open" and "closed"'); 58 assert_false(e.target.matches(':popover-open'),'The popover should *not* be in the :popover-open state when the after hiding event fires.'); 59 } 60 e.preventDefault(); // "toggle" should not be cancelable. 61 } 62 }; 63 switch (method) { 64 case "listener": 65 // These events do *not* bubble. 66 popover.addEventListener('beforetoggle', listener, {signal}); 67 popover.addEventListener('toggle', listener, {signal}); 68 break; 69 case "attribute": 70 assert_false(popover.hasAttribute('onbeforetoggle')); 71 t.add_cleanup(() => popover.removeAttribute('onbeforetoggle')); 72 popover.onbeforetoggle = listener; 73 assert_false(popover.hasAttribute('ontoggle')); 74 t.add_cleanup(() => popover.removeAttribute('ontoggle')); 75 popover.ontoggle = listener; 76 break; 77 default: assert_unreached(); 78 } 79 assert_equals(0,showCount); 80 assert_equals(0,hideCount); 81 assert_equals(0,afterShowCount); 82 assert_equals(0,afterHideCount); 83 popover.showPopover(); 84 assert_true(popover.matches(':popover-open')); 85 assert_equals(1,showCount); 86 assert_equals(0,hideCount); 87 assert_equals(0,afterShowCount); 88 assert_equals(0,afterHideCount); 89 await waitForRender(); 90 assert_equals(1,afterShowCount,'toggle show is fired asynchronously'); 91 assert_equals(0,afterHideCount); 92 assert_true(popover.matches(':popover-open')); 93 popover.hidePopover(); 94 assert_false(popover.matches(':popover-open')); 95 assert_equals(1,showCount); 96 assert_equals(1,hideCount); 97 assert_equals(1,afterShowCount); 98 assert_equals(0,afterHideCount); 99 await waitForRender(); 100 assert_equals(1,afterShowCount); 101 assert_equals(1,afterHideCount,'toggle hide is fired asynchronously'); 102 // No additional events 103 await waitForRender(); 104 await waitForRender(); 105 assert_false(popover.matches(':popover-open')); 106 assert_equals(1,showCount); 107 assert_equals(1,hideCount); 108 assert_equals(1,afterShowCount); 109 assert_equals(1,afterHideCount); 110 }, `The "beforetoggle" event (${method}) get properly dispatched for popovers`); 111 } 112 113 promise_test(async t => { 114 const {popover,signal} = getPopoverAndSignal(t); 115 let cancel = true; 116 popover.addEventListener('beforetoggle',(e) => { 117 if (e.newState !== "open") 118 return; 119 if (cancel) 120 e.preventDefault(); 121 }, {signal}); 122 assert_false(popover.matches(':popover-open')); 123 popover.showPopover(); 124 assert_false(popover.matches(':popover-open'),'The "beforetoggle" event should be cancelable for the "opening" transition'); 125 cancel = false; 126 popover.showPopover(); 127 assert_true(popover.matches(':popover-open')); 128 popover.hidePopover(); 129 assert_false(popover.matches(':popover-open')); 130 }, 'The "beforetoggle" event is cancelable for the "opening" transition'); 131 132 promise_test(async t => { 133 const {popover,signal} = getPopoverAndSignal(t); 134 popover.addEventListener('beforetoggle',(e) => { 135 assert_not_equals(e.newState,"closed",'The "beforetoggle" event was fired for the closing transition'); 136 }, {signal}); 137 assert_false(popover.matches(':popover-open')); 138 popover.showPopover(); 139 assert_true(popover.matches(':popover-open')); 140 t.add_cleanup(() => {document.body.appendChild(popover);}); 141 popover.remove(); 142 await waitForRender(); // Check for async events also 143 await waitForRender(); // Check for async events also 144 assert_false(popover.matches(':popover-open')); 145 }, 'The "beforetoggle" event is not fired for element removal'); 146 147 promise_test(async t => { 148 const {popover,signal} = getPopoverAndSignal(t); 149 let events; 150 function resetEvents() { 151 events = { 152 singleShow: false, 153 singleHide: false, 154 coalescedShow: false, 155 coalescedHide: false, 156 }; 157 } 158 function setEvent(type) { 159 assert_equals(events[type],false,'event repeated'); 160 events[type] = true; 161 } 162 function assertOnly(type,msg) { 163 Object.keys(events).forEach(val => { 164 assert_equals(events[val],val===type,`${msg} (${val})`); 165 }); 166 } 167 popover.addEventListener('toggle',(e) => { 168 switch (e.newState) { 169 case "open": 170 switch (e.oldState) { 171 case "open": setEvent('coalescedShow'); break; 172 case "closed": setEvent('singleShow'); break; 173 default: assert_unreached(); 174 } 175 break; 176 case "closed": 177 switch (e.oldState) { 178 case "closed": setEvent('coalescedHide'); break; 179 case "open": setEvent('singleHide'); break; 180 default: assert_unreached(); 181 } 182 break; 183 default: assert_unreached(); 184 } 185 }, {signal}); 186 187 resetEvents(); 188 assertOnly('none'); 189 assert_false(popover.matches(':popover-open')); 190 popover.showPopover(); 191 await waitForRender(); 192 assert_true(popover.matches(':popover-open')); 193 assertOnly('singleShow','Single event should have been fired, which is a "show"'); 194 195 resetEvents(); 196 popover.hidePopover(); 197 popover.showPopover(); // Immediate re-show 198 await waitForRender(); 199 assert_true(popover.matches(':popover-open')); 200 assertOnly('coalescedShow','Single coalesced event should have been fired, which is a "show"'); 201 202 resetEvents(); 203 popover.hidePopover(); 204 await waitForRender(); 205 assertOnly('singleHide','Single event should have been fired, which is a "hide"'); 206 assert_false(popover.matches(':popover-open')); 207 208 resetEvents(); 209 popover.showPopover(); 210 popover.hidePopover(); // Immediate re-hide 211 await waitForRender(); 212 assertOnly('coalescedHide','Single coalesced event should have been fired, which is a "hide"'); 213 assert_false(popover.matches(':popover-open')); 214 }, 'The "toggle" event is coalesced'); 215 }; 216 </script>