Source code
Revision control
Copy as Markdown
Other Tools
<!DOCTYPE html>
<meta charset="utf-8" />
<meta name="timeout" content="long">
<link rel="author" href="mailto:masonf@chromium.org">
<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/invoker-utils.js"></script>
<div id="examples">
<button id="<button>" interesttarget=target>Button</button>
<a id="<a>" href=foo interesttarget=target>Link</a>
<img src="/images/blue.png" usemap="#map">
<map id=map>
<area id="<area>" interesttarget=target href="/" shape=default id=interestarea>
</map>
<a id="SVG <a>" href=foo interesttarget=target>
<text x=50 y=90>SVG A</text>
</a>
</svg>
</div>
<div id=target popover>Popover</div>
<button id="otherbutton">Other button</button>
<button id="another" interesttarget=anothertarget>Another Button</button>
<div id=anothertarget popover>Another Popover</div>
<style>
[interesttarget] {
interest-target-delay: 0s;
}
[interesttarget].longhide {
interest-target-hide-delay: 10000s;
}
</style>
<script>
const kEscape = '\uE00C';
function sendLoseInterestHotkey() {
return new test_driver.Actions()
.keyDown(kEscape)
.keyUp(kEscape)
.send();
}
const allInterestTargetElements = document.querySelectorAll('#examples [interesttarget]');
function verifyInterest(onlyElements,description) {
if (!(onlyElements instanceof Array)) {
onlyElements = [onlyElements];
}
[...allInterestTargetElements, another].forEach(el => {
const expectInterest = onlyElements.includes(el);
assert_equals(el.matches(':has-interest'),expectInterest,`${description}, element ${el.id} should ${expectInterest ? "" : "NOT "}have interest`);
})
}
allInterestTargetElements.forEach(el => {
const description = `${el.id}`;
promise_test(async function (t) {
t.add_cleanup(() => otherbutton.focus());
target.hidePopover(); // Just in case
await focusOn(el);
assert_equals(document.activeElement,el,'Elements should all be focusable');
assert_true(target.matches(':popover-open'),'Focusing should trigger interest');
verifyInterest(el,`After show interest in ${el.id}`);
await focusOn(otherbutton);
assert_not_equals(document.activeElement,el);
assert_false(target.matches(':popover-open'),'Blurring should trigger lose interest');
verifyInterest(undefined,`After lose interest in ${el.id}`);
},`Basic keyboard focus behavior, ${description}`);
promise_test(async function (t) {
t.add_cleanup(() => otherbutton.focus());
target.hidePopover(); // Just in case
await focusOn(el);
assert_true(target.matches(':popover-open'),'Focusing should trigger interest');
verifyInterest(el,`After show interest in ${el.id}`);
await sendLoseInterestHotkey();
assert_false(target.matches(':popover-open'),'Pressing lose interest hot key should trigger lose interest');
verifyInterest(undefined,`After lose interest in ${el.id}`);
await focusOn(otherbutton);
assert_not_equals(document.activeElement,el);
assert_false(target.matches(':popover-open'),'Blurring should do nothing at this point');
verifyInterest(undefined,`After blurring ${el.id}`);
},`Lose interest hot key behavior, ${description}`);
promise_test(async function (t) {
t.add_cleanup(() => otherbutton.focus());
// Ensure blurring doesn't immediately lose interest:
el.classList.add('longhide');
t.add_cleanup(() => (el.classList.remove('longhide')));
target.hidePopover(); // Just in case
await focusOn(el);
assert_true(target.matches(':popover-open'),'Focusing should trigger interest');
verifyInterest(el,`After show interest in ${el.id}`);
await focusOn(otherbutton);
assert_not_equals(document.activeElement,el);
assert_true(target.matches(':popover-open'),'Blurring should not immediately lose interest');
verifyInterest(el,`After blurring ${el.id}`);
// Send lose interest hot key to the other button (not the invoker):
await sendLoseInterestHotkey();
assert_false(target.matches(':popover-open'),'Pressing lose interest hot key should trigger lose interest');
verifyInterest(undefined,`After lose interest in ${el.id}`);
},`Lose interest hot key behavior with element not focused, ${description}`);
promise_test(async function (t) {
t.add_cleanup(() => otherbutton.focus());
target.hidePopover(); // Just in case
target.addEventListener('interest', (e) => e.preventDefault(), {once: true});
await focusOn(el);
assert_false(target.matches(':popover-open'));
verifyInterest(undefined,`Nothing has interest, ${el.id}`);
}, `canceling the interest event stops behavior, ${description}`);
let events = [];
function addListeners(t,element) {
const controller = new AbortController();
const signal = controller.signal;
t.add_cleanup(() => controller.abort());
element.addEventListener('interest',(e) => events.push(`${e.target.id} interest`),{signal});
element.addEventListener('loseinterest',(e) => events.push(`${e.target.id} loseinterest`),{signal});
}
promise_test(async function (t) {
t.add_cleanup(() => otherbutton.focus());
target.hidePopover(); // Just in case
anothertarget.hidePopover(); // Just in case
events = [];
addListeners(t,target);
addListeners(t,anothertarget);
await focusOn(el);
assert_array_equals(events,['target interest'],'first hotkey');
verifyInterest(el,`After show interest in ${el.id}`);
await focusOn(another);
assert_array_equals(events,['target interest','target loseinterest','anothertarget interest'],
'showing interest in another trigger should lose interest in the first, then gain interest in second');
verifyInterest(another,`After show interest in ${another.id}`);
await sendLoseInterestHotkey();
assert_array_equals(events,['target interest','target loseinterest','anothertarget interest','anothertarget loseinterest']);
verifyInterest(undefined,`After lose interest in ${another.id}`);
assert_false(target.matches(':popover-open'));
assert_false(anothertarget.matches(':popover-open'));
}, `Showing interest in a second element loses interest in the first, ${description}`);
promise_test(async function (t) {
t.add_cleanup(() => otherbutton.focus());
target.hidePopover(); // Just in case
anothertarget.hidePopover(); // Just in case
events = [];
addListeners(t,target);
addListeners(t,anothertarget);
await focusOn(el);
assert_array_equals(events,['target interest'],'setup');
verifyInterest(el,`After show interest in ${el.id}`);
target.addEventListener('loseinterest',(e) => e.preventDefault(),{once: true});
await focusOn(another);
assert_array_equals(events,['target interest','target loseinterest','anothertarget interest'],
'the loseinterest listener should fire, and anothertarget should still get interest');
events = [];
verifyInterest([el,another],`${el.id} should still have interest because loseinterest was cancelled`);
assert_false(target.matches(':popover-open'),'anothertarget popover opens, closing target');
assert_true(anothertarget.matches(':popover-open'));
await sendLoseInterestHotkey();
assert_array_equals(events,['target loseinterest','anothertarget loseinterest'],'Fire loseinterest in both active elements');
assert_false(target.matches(':popover-open'));
assert_false(anothertarget.matches(':popover-open'));
verifyInterest(undefined,`Nothing has interest now`);
}, `Cancelling loseinterest caused by keyboard-gained interest cancels interest, ${description}`);
});
</script>