tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

interestfor-keyboard-behavior.tentative.html (11017B)


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