| <!DOCTYPE html> |
| <meta charset="utf-8"> |
| <link rel="author" href="mailto:[email protected]"> |
| <link rel=help href="https://html.spec.whatwg.org/multipage/popover.html"> |
| <link rel=help href="https://open-ui.org/components/popover.research.explainer"> |
| <meta name="timeout" content="long"> |
| <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> |
| |
| <div id=popovers> |
| <div popover id=boolean>Popover</div> |
| <div popover="">Popover</div> |
| <div popover=auto>Popover</div> |
| <div popover=hint>Popover</div> |
| <div popover=manual>Popover</div> |
| <article popover>Different element type</article> |
| <header popover>Different element type</header> |
| <nav popover>Different element type</nav> |
| <input type=text popover value="Different element type"> |
| <dialog popover>Dialog with popover attribute</dialog> |
| <dialog popover="manual">Dialog with popover=manual</dialog> |
| <div popover=true>Invalid popover value - defaults to popover=manual</div> |
| <div popover=popover>Invalid popover value - defaults to popover=manual</div> |
| <div popover=invalid>Invalid popover value - defaults to popover=manual</div> |
| </div> |
| |
| <div id=nonpopovers> |
| <div>Not a popover</div> |
| <dialog open>Dialog without popover attribute</dialog> |
| </div> |
| |
| <div id=outside></div> |
| <style> |
| [popover] { |
| inset:auto; |
| top:0; |
| left:0; |
| } |
| #outside { |
| position:fixed; |
| top:200px; |
| left:200px; |
| height:10px; |
| width:10px; |
| } |
| </style> |
| |
| <script> |
| setup({ explicit_done: true }); |
| window.onload = () => { |
| const outsideElement = document.getElementById('outside'); |
| |
| // Start with the provided examples: |
| Array.from(document.getElementById('popovers').children).forEach(popover => { |
| test((t) => { |
| assertIsFunctionalPopover(popover, true); |
| }, `The element ${popover.outerHTML} should behave as a popover.`); |
| }); |
| Array.from(document.getElementById('nonpopovers').children).forEach(nonPopover => { |
| test((t) => { |
| assertNotAPopover(nonPopover); |
| }, `The element ${nonPopover.outerHTML} should *not* behave as a popover.`); |
| }); |
| |
| function createPopover(t) { |
| const popover = document.createElement('div'); |
| document.body.appendChild(popover); |
| t.add_cleanup(() => popover.remove()); |
| popover.setAttribute('popover','auto'); |
| return popover; |
| } |
| |
| test((t) => { |
| // You can set the `popover` attribute to anything. |
| // Setting the `popover` IDL to a string sets the content attribute to exactly that, always. |
| // Getting the `popover` IDL value only retrieves valid values. |
| const popover = createPopover(t); |
| assert_equals(popover.popover,'auto'); |
| popover.setAttribute('popover','auto'); |
| assert_equals(popover.popover,'auto'); |
| popover.setAttribute('popover','AuTo'); |
| assert_equals(popover.popover,'auto','Case is normalized in IDL'); |
| assert_equals(popover.getAttribute('popover'),'AuTo','Case is *not* normalized/changed in the content attribute'); |
| popover.popover='aUtO'; |
| assert_equals(popover.popover,'auto','Case is normalized in IDL'); |
| assert_equals(popover.getAttribute('popover'),'aUtO','Value set from IDL is propagated exactly to the content attribute'); |
| popover.setAttribute('popover','invalid'); |
| assert_equals(popover.popover,'manual','Invalid values should reflect as "manual"'); |
| popover.removeAttribute('popover'); |
| assert_equals(popover.popover,null,'No value should reflect as null'); |
| popover.popover='hint'; |
| assert_equals(popover.getAttribute('popover'),'hint'); |
| popover.popover='auto'; |
| assert_equals(popover.getAttribute('popover'),'auto'); |
| popover.popover=''; |
| assert_equals(popover.getAttribute('popover'),''); |
| assert_equals(popover.popover,'auto'); |
| popover.popover='AuTo'; |
| assert_equals(popover.getAttribute('popover'),'AuTo'); |
| assert_equals(popover.popover,'auto'); |
| popover.popover='invalid'; |
| assert_equals(popover.getAttribute('popover'),'invalid','IDL setter allows any value'); |
| assert_equals(popover.popover,'manual','but IDL getter reflects "manual"'); |
| popover.popover=''; |
| assert_equals(popover.getAttribute('popover'),'','IDL setter propagates exactly'); |
| assert_equals(popover.popover,'auto','Empty should map to auto in IDL'); |
| popover.popover='auto'; |
| popover.popover=null; |
| assert_equals(popover.getAttribute('popover'),null,'Setting null for the IDL property should remove the content attribute'); |
| assert_equals(popover.popover,null,'Null returns null'); |
| popover.popover='auto'; |
| popover.popover=undefined; |
| assert_equals(popover.getAttribute('popover'),null,'Setting undefined for the IDL property should remove the content attribute'); |
| assert_equals(popover.popover,null,'undefined returns null'); |
| },'IDL attribute reflection'); |
| |
| test((t) => { |
| const popover = createPopover(t); |
| assertIsFunctionalPopover(popover, true); |
| popover.removeAttribute('popover'); |
| assertNotAPopover(popover); |
| popover.setAttribute('popover','AuTo'); |
| assertIsFunctionalPopover(popover, true); |
| popover.removeAttribute('popover'); |
| popover.setAttribute('PoPoVeR','AuTo'); |
| assertIsFunctionalPopover(popover, true); |
| // Via IDL also |
| popover.popover = 'auto'; |
| assertIsFunctionalPopover(popover, true); |
| popover.popover = 'aUtO'; |
| assertIsFunctionalPopover(popover, true); |
| popover.popover = 'invalid'; // treated as "manual" |
| assertIsFunctionalPopover(popover, true); |
| },'Popover attribute value should be case insensitive'); |
| |
| test((t) => { |
| const popover = createPopover(t); |
| assertIsFunctionalPopover(popover, true); |
| popover.setAttribute('popover','manual'); // Change popover type |
| assertIsFunctionalPopover(popover, true); |
| popover.setAttribute('popover','invalid'); // Change popover type to something invalid |
| assertIsFunctionalPopover(popover, true); |
| popover.popover = 'manual'; // Change popover type via IDL |
| assertIsFunctionalPopover(popover, true); |
| popover.popover = 'invalid'; // Make invalid via IDL (treated as "manual") |
| assertIsFunctionalPopover(popover, true); |
| },'Changing attribute values for popover should work'); |
| |
| test((t) => { |
| const popover = createPopover(t); |
| popover.showPopover(); |
| assert_true(popover.matches(':popover-open')); |
| popover.setAttribute('popover','hint'); // Change popover type |
| assert_false(popover.matches(':popover-open')); |
| popover.showPopover(); |
| assert_true(popover.matches(':popover-open')); |
| popover.setAttribute('popover','manual'); |
| assert_false(popover.matches(':popover-open')); |
| popover.showPopover(); |
| assert_true(popover.matches(':popover-open')); |
| popover.setAttribute('popover','invalid'); |
| assert_true(popover.matches(':popover-open'),'From "manual" to "invalid" (which is interpreted as "manual") should not close the popover'); |
| popover.setAttribute('popover','auto'); |
| assert_false(popover.matches(':popover-open'),'From "invalid" ("manual") to "auto" should hide the popover'); |
| popover.showPopover(); |
| assert_true(popover.matches(':popover-open')); |
| popover.setAttribute('popover','invalid'); |
| assert_false(popover.matches(':popover-open'),'From "auto" to "invalid" (which is interpreted as "manual") should close the popover'); |
| },'Changing attribute values should close open popovers'); |
| |
| const validTypes = ["auto","hint","manual"]; |
| validTypes.forEach(type => { |
| test((t) => { |
| const popover = createPopover(t); |
| popover.setAttribute('popover',type); |
| popover.showPopover(); |
| assert_true(popover.matches(':popover-open')); |
| popover.remove(); |
| assert_false(popover.matches(':popover-open')); |
| document.body.appendChild(popover); |
| assert_false(popover.matches(':popover-open')); |
| },`Removing a visible popover=${type} element from the document should close the popover`); |
| |
| test((t) => { |
| const popover = createPopover(t); |
| popover.setAttribute('popover',type); |
| popover.showPopover(); |
| assert_true(popover.matches(':popover-open')); |
| assert_false(popover.matches(':modal')); |
| popover.hidePopover(); |
| },`A showing popover=${type} does not match :modal`); |
| |
| test((t) => { |
| const popover = createPopover(t); |
| popover.setAttribute('popover',type); |
| assert_false(popover.matches(':popover-open')); |
| // FIXME: Once :open/:closed are defined in HTML we should remove these two constants. |
| const openPseudoClassIsSupported = CSS.supports('selector(:open))'); |
| const closePseudoClassIsSupported = CSS.supports('selector(:closed))'); |
| assert_false(openPseudoClassIsSupported && popover.matches(':open'),'popovers never match :open'); |
| assert_false(closePseudoClassIsSupported && popover.matches(':closed'),'popovers never match :closed'); |
| popover.showPopover(); |
| assert_true(popover.matches(':popover-open')); |
| assert_false(openPseudoClassIsSupported && popover.matches(':open'),'popovers never match :open'); |
| assert_false(closePseudoClassIsSupported && popover.matches(':closed'),'popovers never match :closed'); |
| popover.hidePopover(); |
| },`A popover=${type} never matches :open or :closed`); |
| }); |
| |
| test((t) => { |
| const other_popover = createPopover(t); |
| other_popover.setAttribute('popover','auto'); |
| other_popover.showPopover(); |
| const popover = createPopover(t); |
| popover.setAttribute('popover','auto'); |
| other_popover.addEventListener('beforetoggle', (e) => { |
| if (e.newState !== "closed") |
| return; |
| popover.setAttribute('popover','manual'); |
| },{once: true}); |
| assert_true(other_popover.matches(':popover-open')); |
| assert_false(popover.matches(':popover-open')); |
| assert_throws_dom('InvalidStateError', () => popover.showPopover()); |
| assert_false(other_popover.matches(':popover-open'),'unrelated popover is hidden'); |
| assert_false(popover.matches(':popover-open'),'popover is not shown if its type changed during show'); |
| },`Changing the popover type in a "beforetoggle" event handler should throw an exception (during showPopover())`); |
| |
| test((t) => { |
| const other_popover = createPopover(t); |
| other_popover.setAttribute('popover','auto'); |
| other_popover.showPopover(); |
| const popover = createPopover(t); |
| popover.setAttribute('popover','auto'); |
| other_popover.addEventListener('beforetoggle', (e) => { |
| if (e.newState !== "closed") |
| return; |
| popover.setAttribute('popover','manual'); |
| },{once: true}); |
| assert_true(other_popover.matches(':popover-open')); |
| assert_false(popover.matches(':popover-open')); |
| |
| popover.id = 'type-change-test'; |
| const invoker = document.createElement('button'); |
| document.body.appendChild(invoker); |
| t.add_cleanup(() => invoker.remove()); |
| invoker.setAttribute('popovertarget', 'type-change-test'); |
| invoker.click(); |
| |
| assert_false(other_popover.matches(':popover-open'),'unrelated popover is hidden'); |
| assert_false(popover.matches(':popover-open'),'popover is not shown if its type changed during show'); |
| },`Changing the popover type in a "beforetoggle" event handler should not show the popover (during popovertarget invoking)`); |
| |
| test((t) => { |
| const popover = createPopover(t); |
| popover.setAttribute('popover','auto'); |
| const other_popover = createPopover(t); |
| other_popover.setAttribute('popover','auto'); |
| popover.appendChild(other_popover); |
| popover.showPopover(); |
| other_popover.showPopover(); |
| let nested_popover_hidden=false; |
| other_popover.addEventListener('beforetoggle', (e) => { |
| if (e.newState !== "closed") |
| return; |
| nested_popover_hidden = true; |
| popover.setAttribute('popover','manual'); |
| },{once: true}); |
| popover.addEventListener('beforetoggle', (e) => { |
| if (e.newState !== "closed") |
| return; |
| assert_true(nested_popover_hidden,'The nested popover should be hidden first'); |
| },{once: true}); |
| assert_true(popover.matches(':popover-open')); |
| assert_true(other_popover.matches(':popover-open')); |
| popover.hidePopover(); // Calling hidePopover on a hidden popover should not throw. |
| assert_false(other_popover.matches(':popover-open'),'unrelated popover is hidden'); |
| assert_false(popover.matches(':popover-open'),'popover is still hidden if its type changed during hide event'); |
| other_popover.hidePopover(); // Calling hidePopover on a hidden popover should not throw. |
| },`Changing the popover type in a "beforetoggle" event handler during hidePopover() should not throw an exception`); |
| |
| test(t => { |
| const popover = document.createElement('div'); |
| assert_throws_dom('NotSupportedError', () => popover.hidePopover(), |
| 'Calling hidePopover on an element without a popover attribute should throw.'); |
| popover.setAttribute('popover', 'auto'); |
| popover.hidePopover(); // Calling hidePopover on a disconnected popover should not throw. |
| assert_throws_dom('InvalidStateError', () => popover.showPopover(), |
| 'Calling showPopover on a disconnected popover should throw.'); |
| },'Calling hidePopover on a disconnected popover should not throw.'); |
| |
| function interpretedType(typeString,method) { |
| if (validTypes.includes(typeString)) |
| return typeString; |
| if (typeString === undefined) |
| return "invalid-value-undefined"; |
| if (method === "idl" && typeString === null) |
| return "invalid-value-idl-null"; |
| return "manual"; // Invalid types default to "manual" |
| } |
| function setPopoverValue(popover,type,method) { |
| switch (method) { |
| case "attr": |
| if (type === undefined) { |
| popover.removeAttribute('popover'); |
| } else { |
| popover.setAttribute('popover',type); |
| } |
| break; |
| case "idl": |
| popover.popover = type; |
| break; |
| default: |
| assert_unreached(); |
| } |
| } |
| ["attr","idl"].forEach(method => { |
| validTypes.forEach(type => { |
| [...validTypes,"invalid",null,undefined].forEach(newType => { |
| [...validTypes,"invalid",null,undefined].forEach(inEventType => { |
| promise_test(async (t) => { |
| const popover = createPopover(t); |
| setPopoverValue(popover,type,method); |
| popover.showPopover(); |
| assert_true(popover.matches(':popover-open')); |
| let gotEvent = false; |
| popover.addEventListener('beforetoggle', (e) => { |
| if (e.newState !== "closed") |
| return; |
| gotEvent = true; |
| setPopoverValue(popover,inEventType,method); |
| },{once:true}); |
| setPopoverValue(popover,newType,method); |
| if (type===interpretedType(newType,method)) { |
| // Keeping the type the same should not hide it or fire events. |
| assert_true(popover.matches(':popover-open'),'popover should remain open when not changing the type'); |
| assert_false(gotEvent); |
| try { |
| popover.hidePopover(); // Cleanup |
| } catch (e) {} |
| } else { |
| // Changing the type at all should hide the popover. The hide event |
| // handler should run, set a new type, and that type should end up |
| // as the final result. |
| assert_false(popover.matches(':popover-open')); |
| assert_true(gotEvent); |
| if (inEventType === undefined || (method ==="idl" && inEventType === null)) { |
| assert_throws_dom("NotSupportedError",() => popover.showPopover(),'We should have removed the popover attribute, so showPopover should throw'); |
| } else { |
| // Make sure the attribute is correct. |
| assert_equals(popover.getAttribute('popover'),String(inEventType),'Content attribute'); |
| assert_equals(popover.popover, interpretedType(inEventType,method),'IDL attribute'); |
| // Make sure the type is really correct, via behavior. |
| popover.showPopover(); // Show it |
| assert_true(popover.matches(':popover-open'),'Popover should function'); |
| await clickOn(outsideElement); // Try to light dismiss |
| switch (interpretedType(inEventType,method)) { |
| case 'manual': |
| assert_true(popover.matches(':popover-open'),'A popover=manual should not light-dismiss'); |
| popover.hidePopover(); |
| break; |
| case 'auto': |
| case 'hint': |
| assert_false(popover.matches(':popover-open'),'A popover=auto should light-dismiss'); |
| break; |
| } |
| } |
| } |
| },`Changing a popover from ${type} to ${newType} (via ${method}), and then ${inEventType} during 'beforetoggle' works`); |
| }); |
| }); |
| }); |
| }); |
| |
| done(); |
| }; |
| </script> |