popover-attribute-basic.html (17067B)
1 <!DOCTYPE html> 2 <meta charset="utf-8"> 3 <link rel="author" href="mailto:masonf@chromium.org"> 4 <link rel=help href="https://html.spec.whatwg.org/multipage/popover.html"> 5 <link rel=help href="https://open-ui.org/components/popover.research.explainer"> 6 <meta name="timeout" content="long"> 7 <script src="/resources/testharness.js"></script> 8 <script src="/resources/testharnessreport.js"></script> 9 <script src="/resources/testdriver.js"></script> 10 <script src="/resources/testdriver-actions.js"></script> 11 <script src="/resources/testdriver-vendor.js"></script> 12 <script src="resources/popover-utils.js"></script> 13 14 <div id=popovers> 15 <div popover id=boolean>Popover</div> 16 <div popover="">Popover</div> 17 <div popover=auto>Popover</div> 18 <div popover=hint>Popover</div> 19 <div popover=manual>Popover</div> 20 <article popover>Different element type</article> 21 <header popover>Different element type</header> 22 <nav popover>Different element type</nav> 23 <input type=text popover value="Different element type"> 24 <dialog popover>Dialog with popover attribute</dialog> 25 <dialog popover="manual">Dialog with popover=manual</dialog> 26 <div popover=true>Invalid popover value - defaults to popover=manual</div> 27 <div popover=popover>Invalid popover value - defaults to popover=manual</div> 28 <div popover=invalid>Invalid popover value - defaults to popover=manual</div> 29 </div> 30 31 <div id=nonpopovers> 32 <div>Not a popover</div> 33 <dialog open>Dialog without popover attribute</dialog> 34 </div> 35 36 <div id=outside></div> 37 <style> 38 [popover] { 39 inset:auto; 40 top:0; 41 left:0; 42 } 43 #outside { 44 position:fixed; 45 top:200px; 46 left:200px; 47 height:10px; 48 width:10px; 49 } 50 </style> 51 52 <script> 53 setup({ explicit_done: true }); 54 window.onload = () => { 55 const outsideElement = document.getElementById('outside'); 56 57 // Start with the provided examples: 58 Array.from(document.getElementById('popovers').children).forEach(popover => { 59 test((t) => { 60 assertIsFunctionalPopover(popover, true); 61 }, `The element ${popover.outerHTML} should behave as a popover.`); 62 }); 63 Array.from(document.getElementById('nonpopovers').children).forEach(nonPopover => { 64 test((t) => { 65 assertNotAPopover(nonPopover); 66 }, `The element ${nonPopover.outerHTML} should *not* behave as a popover.`); 67 }); 68 69 function createPopover(t) { 70 const popover = document.createElement('div'); 71 document.body.appendChild(popover); 72 t.add_cleanup(() => popover.remove()); 73 popover.setAttribute('popover','auto'); 74 return popover; 75 } 76 77 test((t) => { 78 // You can set the `popover` attribute to anything. 79 // Setting the `popover` IDL to a string sets the content attribute to exactly that, always. 80 // Getting the `popover` IDL value only retrieves valid values. 81 const popover = createPopover(t); 82 assert_equals(popover.popover,'auto'); 83 popover.setAttribute('popover','auto'); 84 assert_equals(popover.popover,'auto'); 85 popover.setAttribute('popover','AuTo'); 86 assert_equals(popover.popover,'auto','Case is normalized in IDL'); 87 assert_equals(popover.getAttribute('popover'),'AuTo','Case is *not* normalized/changed in the content attribute'); 88 popover.popover='aUtO'; 89 assert_equals(popover.popover,'auto','Case is normalized in IDL'); 90 assert_equals(popover.getAttribute('popover'),'aUtO','Value set from IDL is propagated exactly to the content attribute'); 91 popover.setAttribute('popover','invalid'); 92 assert_equals(popover.popover,'manual','Invalid values should reflect as "manual"'); 93 popover.removeAttribute('popover'); 94 assert_equals(popover.popover,null,'No value should reflect as null'); 95 popover.popover='hint'; 96 assert_equals(popover.getAttribute('popover'),'hint'); 97 popover.popover='auto'; 98 assert_equals(popover.getAttribute('popover'),'auto'); 99 popover.popover=''; 100 assert_equals(popover.getAttribute('popover'),''); 101 assert_equals(popover.popover,'auto'); 102 popover.popover='AuTo'; 103 assert_equals(popover.getAttribute('popover'),'AuTo'); 104 assert_equals(popover.popover,'auto'); 105 popover.popover='invalid'; 106 assert_equals(popover.getAttribute('popover'),'invalid','IDL setter allows any value'); 107 assert_equals(popover.popover,'manual','but IDL getter reflects "manual"'); 108 popover.popover=''; 109 assert_equals(popover.getAttribute('popover'),'','IDL setter propagates exactly'); 110 assert_equals(popover.popover,'auto','Empty should map to auto in IDL'); 111 popover.popover='auto'; 112 popover.popover=null; 113 assert_equals(popover.getAttribute('popover'),null,'Setting null for the IDL property should remove the content attribute'); 114 assert_equals(popover.popover,null,'Null returns null'); 115 popover.popover='auto'; 116 popover.popover=undefined; 117 assert_equals(popover.getAttribute('popover'),null,'Setting undefined for the IDL property should remove the content attribute'); 118 assert_equals(popover.popover,null,'undefined returns null'); 119 },'IDL attribute reflection'); 120 121 test((t) => { 122 const popover = createPopover(t); 123 assertIsFunctionalPopover(popover, true); 124 popover.removeAttribute('popover'); 125 assertNotAPopover(popover); 126 popover.setAttribute('popover','AuTo'); 127 assertIsFunctionalPopover(popover, true); 128 popover.removeAttribute('popover'); 129 popover.setAttribute('PoPoVeR','AuTo'); 130 assertIsFunctionalPopover(popover, true); 131 // Via IDL also 132 popover.popover = 'auto'; 133 assertIsFunctionalPopover(popover, true); 134 popover.popover = 'aUtO'; 135 assertIsFunctionalPopover(popover, true); 136 popover.popover = 'invalid'; // treated as "manual" 137 assertIsFunctionalPopover(popover, true); 138 },'Popover attribute value should be case insensitive'); 139 140 test((t) => { 141 const popover = createPopover(t); 142 assertIsFunctionalPopover(popover, true); 143 popover.setAttribute('popover','manual'); // Change popover type 144 assertIsFunctionalPopover(popover, true); 145 popover.setAttribute('popover','invalid'); // Change popover type to something invalid 146 assertIsFunctionalPopover(popover, true); 147 popover.popover = 'manual'; // Change popover type via IDL 148 assertIsFunctionalPopover(popover, true); 149 popover.popover = 'invalid'; // Make invalid via IDL (treated as "manual") 150 assertIsFunctionalPopover(popover, true); 151 },'Changing attribute values for popover should work'); 152 153 test((t) => { 154 const popover = createPopover(t); 155 popover.showPopover(); 156 assert_true(popover.matches(':popover-open')); 157 popover.setAttribute('popover','hint'); // Change popover type 158 assert_false(popover.matches(':popover-open')); 159 popover.showPopover(); 160 assert_true(popover.matches(':popover-open')); 161 popover.setAttribute('popover','manual'); 162 assert_false(popover.matches(':popover-open')); 163 popover.showPopover(); 164 assert_true(popover.matches(':popover-open')); 165 popover.setAttribute('popover','invalid'); 166 assert_true(popover.matches(':popover-open'),'From "manual" to "invalid" (which is interpreted as "manual") should not close the popover'); 167 popover.setAttribute('popover','auto'); 168 assert_false(popover.matches(':popover-open'),'From "invalid" ("manual") to "auto" should hide the popover'); 169 popover.showPopover(); 170 assert_true(popover.matches(':popover-open')); 171 popover.setAttribute('popover','invalid'); 172 assert_false(popover.matches(':popover-open'),'From "auto" to "invalid" (which is interpreted as "manual") should close the popover'); 173 },'Changing attribute values should close open popovers'); 174 175 const validTypes = ["auto","hint","manual"]; 176 validTypes.forEach(type => { 177 test((t) => { 178 const popover = createPopover(t); 179 popover.setAttribute('popover',type); 180 popover.showPopover(); 181 assert_true(popover.matches(':popover-open')); 182 popover.remove(); 183 assert_false(popover.matches(':popover-open')); 184 document.body.appendChild(popover); 185 assert_false(popover.matches(':popover-open')); 186 },`Removing a visible popover=${type} element from the document should close the popover`); 187 188 test((t) => { 189 const popover = createPopover(t); 190 popover.setAttribute('popover',type); 191 popover.showPopover(); 192 assert_true(popover.matches(':popover-open')); 193 assert_false(popover.matches(':modal')); 194 popover.hidePopover(); 195 },`A showing popover=${type} does not match :modal`); 196 197 test((t) => { 198 const popover = createPopover(t); 199 popover.setAttribute('popover',type); 200 assert_false(popover.matches(':popover-open')); 201 // FIXME: Once :open/:closed are defined in HTML we should remove these two constants. 202 const openPseudoClassIsSupported = CSS.supports('selector(:open))'); 203 const closePseudoClassIsSupported = CSS.supports('selector(:closed))'); 204 assert_false(openPseudoClassIsSupported && popover.matches(':open'),'popovers never match :open'); 205 assert_false(closePseudoClassIsSupported && popover.matches(':closed'),'popovers never match :closed'); 206 popover.showPopover(); 207 assert_true(popover.matches(':popover-open')); 208 assert_false(openPseudoClassIsSupported && popover.matches(':open'),'popovers never match :open'); 209 assert_false(closePseudoClassIsSupported && popover.matches(':closed'),'popovers never match :closed'); 210 popover.hidePopover(); 211 },`A popover=${type} never matches :open or :closed`); 212 }); 213 214 test((t) => { 215 const other_popover = createPopover(t); 216 other_popover.setAttribute('popover','auto'); 217 other_popover.showPopover(); 218 const popover = createPopover(t); 219 popover.setAttribute('popover','auto'); 220 other_popover.addEventListener('beforetoggle', (e) => { 221 if (e.newState !== "closed") 222 return; 223 popover.setAttribute('popover','manual'); 224 },{once: true}); 225 assert_true(other_popover.matches(':popover-open')); 226 assert_false(popover.matches(':popover-open')); 227 assert_throws_dom('InvalidStateError', () => popover.showPopover()); 228 assert_false(other_popover.matches(':popover-open'),'unrelated popover is hidden'); 229 assert_false(popover.matches(':popover-open'),'popover is not shown if its type changed during show'); 230 },`Changing the popover type in a "beforetoggle" event handler should throw an exception (during showPopover())`); 231 232 test((t) => { 233 const other_popover = createPopover(t); 234 other_popover.setAttribute('popover','auto'); 235 other_popover.showPopover(); 236 const popover = createPopover(t); 237 popover.setAttribute('popover','auto'); 238 other_popover.addEventListener('beforetoggle', (e) => { 239 if (e.newState !== "closed") 240 return; 241 popover.setAttribute('popover','manual'); 242 },{once: true}); 243 assert_true(other_popover.matches(':popover-open')); 244 assert_false(popover.matches(':popover-open')); 245 246 popover.id = 'type-change-test'; 247 const invoker = document.createElement('button'); 248 document.body.appendChild(invoker); 249 t.add_cleanup(() => invoker.remove()); 250 invoker.setAttribute('popovertarget', 'type-change-test'); 251 invoker.click(); 252 253 assert_false(other_popover.matches(':popover-open'),'unrelated popover is hidden'); 254 assert_false(popover.matches(':popover-open'),'popover is not shown if its type changed during show'); 255 },`Changing the popover type in a "beforetoggle" event handler should not show the popover (during popovertarget invoking)`); 256 257 test((t) => { 258 const popover = createPopover(t); 259 popover.setAttribute('popover','auto'); 260 const other_popover = createPopover(t); 261 other_popover.setAttribute('popover','auto'); 262 popover.appendChild(other_popover); 263 popover.showPopover(); 264 other_popover.showPopover(); 265 let nested_popover_hidden=false; 266 other_popover.addEventListener('beforetoggle', (e) => { 267 if (e.newState !== "closed") 268 return; 269 nested_popover_hidden = true; 270 popover.setAttribute('popover','manual'); 271 },{once: true}); 272 popover.addEventListener('beforetoggle', (e) => { 273 if (e.newState !== "closed") 274 return; 275 assert_true(nested_popover_hidden,'The nested popover should be hidden first'); 276 },{once: true}); 277 assert_true(popover.matches(':popover-open')); 278 assert_true(other_popover.matches(':popover-open')); 279 popover.hidePopover(); // Calling hidePopover on a hidden popover should not throw. 280 assert_false(other_popover.matches(':popover-open'),'unrelated popover is hidden'); 281 assert_false(popover.matches(':popover-open'),'popover is still hidden if its type changed during hide event'); 282 other_popover.hidePopover(); // Calling hidePopover on a hidden popover should not throw. 283 },`Changing the popover type in a "beforetoggle" event handler during hidePopover() should not throw an exception`); 284 285 test(t => { 286 const popover = document.createElement('div'); 287 assert_throws_dom('NotSupportedError', () => popover.hidePopover(), 288 'Calling hidePopover on an element without a popover attribute should throw.'); 289 popover.setAttribute('popover', 'auto'); 290 popover.hidePopover(); // Calling hidePopover on a disconnected popover should not throw. 291 assert_throws_dom('InvalidStateError', () => popover.showPopover(), 292 'Calling showPopover on a disconnected popover should throw.'); 293 },'Calling hidePopover on a disconnected popover should not throw.'); 294 295 function interpretedType(typeString,method) { 296 if (validTypes.includes(typeString)) 297 return typeString; 298 if (typeString === undefined) 299 return "invalid-value-undefined"; 300 if (method === "idl" && typeString === null) 301 return "invalid-value-idl-null"; 302 return "manual"; // Invalid types default to "manual" 303 } 304 function setPopoverValue(popover,type,method) { 305 switch (method) { 306 case "attr": 307 if (type === undefined) { 308 popover.removeAttribute('popover'); 309 } else { 310 popover.setAttribute('popover',type); 311 } 312 break; 313 case "idl": 314 popover.popover = type; 315 break; 316 default: 317 assert_unreached(); 318 } 319 } 320 ["attr","idl"].forEach(method => { 321 validTypes.forEach(type => { 322 [...validTypes,"invalid",null,undefined].forEach(newType => { 323 [...validTypes,"invalid",null,undefined].forEach(inEventType => { 324 promise_test(async (t) => { 325 const popover = createPopover(t); 326 setPopoverValue(popover,type,method); 327 popover.showPopover(); 328 assert_true(popover.matches(':popover-open')); 329 let gotEvent = false; 330 popover.addEventListener('beforetoggle', (e) => { 331 if (e.newState !== "closed") 332 return; 333 gotEvent = true; 334 setPopoverValue(popover,inEventType,method); 335 },{once:true}); 336 setPopoverValue(popover,newType,method); 337 if (type===interpretedType(newType,method)) { 338 // Keeping the type the same should not hide it or fire events. 339 assert_true(popover.matches(':popover-open'),'popover should remain open when not changing the type'); 340 assert_false(gotEvent); 341 try { 342 popover.hidePopover(); // Cleanup 343 } catch (e) {} 344 } else { 345 // Changing the type at all should hide the popover. The hide event 346 // handler should run, set a new type, and that type should end up 347 // as the final result. 348 assert_false(popover.matches(':popover-open')); 349 assert_true(gotEvent); 350 if (inEventType === undefined || (method ==="idl" && inEventType === null)) { 351 assert_throws_dom("NotSupportedError",() => popover.showPopover(),'We should have removed the popover attribute, so showPopover should throw'); 352 } else { 353 // Make sure the attribute is correct. 354 assert_equals(popover.getAttribute('popover'),String(inEventType),'Content attribute'); 355 assert_equals(popover.popover, interpretedType(inEventType,method),'IDL attribute'); 356 // Make sure the type is really correct, via behavior. 357 popover.showPopover(); // Show it 358 assert_true(popover.matches(':popover-open'),'Popover should function'); 359 await clickOn(outsideElement); // Try to light dismiss 360 switch (interpretedType(inEventType,method)) { 361 case 'manual': 362 assert_true(popover.matches(':popover-open'),'A popover=manual should not light-dismiss'); 363 popover.hidePopover(); 364 break; 365 case 'auto': 366 case 'hint': 367 assert_false(popover.matches(':popover-open'),'A popover=auto should light-dismiss'); 368 break; 369 } 370 } 371 } 372 },`Changing a popover from ${type} to ${newType} (via ${method}), and then ${inEventType} during 'beforetoggle' works`); 373 }); 374 }); 375 }); 376 }); 377 378 done(); 379 }; 380 </script>