380 lines
17 KiB
HTML
380 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<meta charset="utf-8">
|
|
<link rel="author" href="mailto:masonf@chromium.org">
|
|
<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>
|