1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
|
<!DOCTYPE html>
<meta charset="utf-8" />
<meta name="timeout" content="long">
<link rel="author" href="mailto:masonf@chromium.org">
<link rel="help" href="https://open-ui.org/components/interest-invokers.explainer/" />
<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>
<script src="/html/semantics/popovers/resources/popover-utils.js"></script>
<button data-testcase="<button>" interestfor=target>Button</button>
<a data-testcase="<a>" href=foo interestfor=target>Link</a>
<img src="/images/blue.png" usemap="#map">
<map id=map>
<area data-testcase="<area>" interestfor=target href="/" shape=default>
</map>
<svg viewBox="0 0 100 100" style="width: 100px" xmlns="http://www.w3.org/2000/svg">
<a data-testcase="SVG <a>" href=foo interestfor=target>
<text x=50 y=90>SVG A</text>
</a>
</svg>
<div id=target popover>Popover</div>
<button id="otherbutton">Other button</button>
<button id="another" interestfor=anothertarget>Another Button</button>
<div id=anothertarget popover>Another Popover</div>
<style>
[interestfor] {
interest-delay: 0s;
}
[interestfor].longhide {
interest-hide-delay: 10000s;
}
</style>
<script>
const allInterestForElements = document.querySelectorAll('[data-testcase]');
assert_true(allInterestForElements.length > 0);
function verifyInterest(onlyElements,description) {
if (!(onlyElements instanceof Array)) {
onlyElements = [onlyElements];
}
[...allInterestForElements, another].forEach(el => {
const expectInterest = onlyElements.includes(el);
assert_equals(el.matches(':interest-source'),expectInterest,`${description}, element ${el.dataset.testcase} should ${expectInterest ? "" : "NOT "}have interest`);
})
}
allInterestForElements.forEach(el => {
const description = el.dataset.testcase;
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 ${description}`);
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 ${description}`);
},`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 ${description}`);
await sendLoseInterestHotkey();
assert_false(target.matches(':popover-open'),'Pressing lose interest hot key should trigger lose interest');
verifyInterest(undefined,`After lose interest in ${description}`);
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 ${description}`);
},`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 ${description}`);
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 ${description}`);
// 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 ${description}`);
},`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, ${description}`);
}, `canceling the interest event stops behavior, ${description}`);
let events = [];
function addListeners(t,element) {
const signal = t.get_signal();
element.addEventListener('interest',(e) => events.push(`${e.target.id} interest`),{signal});
element.addEventListener('loseinterest',(e) => events.push(`${e.target.id} loseinterest (${e.cancelable ? 'cancelable' : 'not cancelable'})`),{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 ${description}`);
await focusOn(another);
assert_array_equals(events,['target interest','target loseinterest (cancelable)','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 (cancelable)','anothertarget interest','anothertarget loseinterest (not cancelable)']);
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 ${description}`);
const signal = t.get_signal();
let shouldCancelLoseInterest = true;
target.addEventListener('loseinterest',(e) => {
if (shouldCancelLoseInterest) {
e.preventDefault();
}
},{signal});
await focusOn(another);
assert_array_equals(events,['target interest','target loseinterest (cancelable)','anothertarget interest','target loseinterest (cancelable)'],
'the loseinterest listener should fire but get cancelled, anothertarget should still get interest, and that should close the first target popover firing another loseinterest');
events = [];
verifyInterest([el,another],`${description} 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,['anothertarget loseinterest (not cancelable)', 'target loseinterest (not cancelable)'],'Lose interest hot key loses interest in all elements');
assert_false(target.matches(':popover-open'));
assert_false(anothertarget.matches(':popover-open'));
verifyInterest(undefined,`Nothing has interest now`);
}, `Canceling loseinterest caused by keyboard-gained interest cancels interest, ${description}`);
});
</script>
<button id="esc_invoker1" class="longhide" interestfor="esc_target1">ESC Invoker 1</button>
<div id="esc_target1">Non-popover target for ESC test</div>
<button id="esc_invoker2" class="longhide" interestfor="esc_target2">ESC Invoker 2</button>
<div id="esc_target2">Non-popover target for ESC test</div>
<button id="esc_invoker3" class="longhide" interestfor="esc_target3">ESC Invoker 3</button>
<div id="esc_target3">Non-popover target for ESC test</div>
<script>
promise_test(async function (t) {
const invoker1 = document.getElementById('esc_invoker1');
const target1 = document.getElementById('esc_target1');
const invoker2 = document.getElementById('esc_invoker2');
const target2 = document.getElementById('esc_target2');
const invoker3 = document.getElementById('esc_invoker3');
const target3 = document.getElementById('esc_target3');
const otherbutton = document.getElementById('otherbutton');
t.add_cleanup(() => otherbutton.focus());
let events = [];
const signal = t.get_signal();
[target1, target2, target3].forEach(target => {
target.addEventListener('interest',(e) => events.push(`${e.source.id} interest`),{signal});
target.addEventListener('loseinterest',(e) => events.push(`${e.source.id} loseinterest`),{signal});
// These loseinterest events should not be cancelable:
target.addEventListener('loseinterest',(e) => e.preventDefault(),{signal});
});
// Invoke them in non-tree order:
await focusOn(invoker1);
await focusOn(invoker3);
await focusOn(invoker2);
assert_array_equals(events,
['esc_invoker1 interest','esc_invoker3 interest','esc_invoker2 interest'],
'Events after gaining interest');
events = [];
// Hit ESC once, while focused on the body
document.body.focus();
await waitForRender();
assert_true(invoker1.matches(':interest-source'), 'invoker1 should still have interest');
assert_true(invoker3.matches(':interest-source'), 'invoker3 should still have interest');
assert_true(invoker2.matches(':interest-source'), 'invoker2 should still have interest');
const kEscape = '\uE00C';
await new test_driver.Actions()
.keyDown(kEscape)
.keyUp(kEscape)
.send();
await waitForRender();
assert_false(invoker2.matches(':interest-source'), 'invoker2 should lose interest');
assert_false(invoker1.matches(':interest-source'), 'invoker1 should lose interest');
assert_false(invoker3.matches(':interest-source'), 'invoker3 should lose interest');
assert_array_equals(events, [
'esc_invoker2 loseinterest', 'esc_invoker3 loseinterest', 'esc_invoker1 loseinterest'],
'ESC should lose interest in all invokers, in reverse order');
}, 'ESC key dismisses all interest invokers');
</script>
|