tor-browser

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

popover-focus.html (13750B)


      1 <!DOCTYPE html>
      2 <meta charset="utf-8" />
      3 <title>Popover focus behaviors</title>
      4 <link rel="author" href="mailto:masonf@chromium.org">
      5 <link rel=help href="https://open-ui.org/components/popover.research.explainer">
      6 <meta name="timeout" content="long">
      7 <script src="/resources/testharness.js"></script>
      8 <script src="/resources/testharnessreport.js"></script>
      9 <script src="/resources/testdriver.js"></script>
     10 <script src="/resources/testdriver-actions.js"></script>
     11 <script src="/resources/testdriver-vendor.js"></script>
     12 <script src="resources/popover-utils.js"></script>
     13 
     14 <div popover data-test='default behavior - popover is not focused' data-no-focus>
     15  <p>This is a popover</p>
     16  <button tabindex="0">first button</button>
     17 </div>
     18 
     19 <div popover data-test='autofocus popover' autofocus tabindex=-1 class=should-be-focused>
     20  <p>This is a popover</p>
     21 </div>
     22 
     23 <div popover data-test='autofocus empty popover' autofocus tabindex=-1 class=should-be-focused></div>
     24 
     25 <div popover data-test='autofocus popover with button' autofocus tabindex=-1 class=should-be-focused>
     26  <p>This is a popover</p>
     27  <button tabindex="0">button</button>
     28 </div>
     29 
     30 <div popover data-test='autofocus child'>
     31  <p>This is a popover</p>
     32  <button autofocus class=should-be-focused tabindex="0">autofocus button</button>
     33 </div>
     34 
     35 <div popover data-test='autofocus on tabindex=0 element'>
     36  <p autofocus tabindex=0 class=should-be-focused>This is a popover with autofocus on a tabindex=0 element</p>
     37  <button tabindex="0">button</button>
     38 </div>
     39 
     40 <div popover data-test='autofocus multiple children'>
     41  <p>This is a popover</p>
     42  <button autofocus class=should-be-focused tabindex="0">autofocus button</button>
     43  <button autofocus tabindex="0">second autofocus button</button>
     44 </div>
     45 
     46 <div popover autofocus tabindex=-1 data-test='autofocus popover and multiple autofocus children' class=should-be-focused>
     47  <p>This is a popover</p>
     48  <button autofocus tabindex="0">autofocus button</button>
     49  <button autofocus tabindex="0">second autofocus button</button>
     50 </div>
     51 
     52 <dialog popover=auto data-test='Opening dialogs as popovers should use dialog initial focus algorithm.'>
     53  <button class=should-be-focused tabindex="0">button</button>
     54 </dialog>
     55 
     56 <dialog popover=auto autofocus class=should-be-focused data-test='Opening dialogs as popovers which have autofocus should focus the dialog.'>
     57  <button tabindex="0">button</button>
     58 </dialog>
     59 
     60 <style>
     61  [popover] {
     62    border: 2px solid black;
     63    top:150px;
     64    left:150px;
     65  }
     66  :focus-within { border: 5px dashed red; }
     67  :focus { border: 5px solid lime; }
     68 </style>
     69 
     70 <script>
     71  function addInvoker(t, popover) {
     72    const button = document.createElement('button');
     73    button.innerText = 'Click me';
     74    const popoverId = 'popover-id';
     75    assert_equals(document.querySelectorAll('#' + popoverId).length, 0);
     76    document.body.appendChild(button);
     77    t.add_cleanup(function() {
     78      popover.removeAttribute('id');
     79      button.remove();
     80    });
     81    popover.id = popoverId;
     82    button.setAttribute('tabindex', '0');
     83    button.setAttribute('popovertarget', popoverId);
     84    return button;
     85  }
     86  function addPriorFocus(t) {
     87    const priorFocus = document.createElement('button');
     88    priorFocus.setAttribute("tabindex", "0");
     89    priorFocus.id = 'priorFocus';
     90    document.body.appendChild(priorFocus);
     91    t.add_cleanup(() => priorFocus.remove());
     92    return priorFocus;
     93  }
     94  function activateAndVerify(popover) {
     95    const testName = popover.getAttribute('data-test');
     96    promise_test(async t => {
     97      const priorFocus = addPriorFocus(t);
     98      let expectedFocusedElement = popover.matches('.should-be-focused') ? popover : popover.querySelector('.should-be-focused');
     99      const changesFocus = !popover.hasAttribute('data-no-focus');
    100      if (!changesFocus) {
    101        expectedFocusedElement = priorFocus;
    102      }
    103      assert_true(!!expectedFocusedElement);
    104      assert_false(popover.matches(':popover-open'));
    105 
    106      // Directly show and hide the popover:
    107      priorFocus.focus();
    108      assert_equals(document.activeElement, priorFocus);
    109      popover.showPopover();
    110      assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popover.showPopover()`);
    111      popover.hidePopover();
    112      assert_equals(document.activeElement, priorFocus, 'prior element should get focus on hide, or if focus didn\'t shift on show, focus should stay where it was');
    113      assert_false(isElementVisible(popover));
    114 
    115      // Manual popover does not restore focus
    116      popover.popover = 'manual';
    117      priorFocus.focus();
    118      assert_equals(document.activeElement, priorFocus);
    119      popover.showPopover();
    120      assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popover.showPopover()`);
    121      popover.hidePopover();
    122      if (!popover.hasAttribute('data-no-focus')) {
    123        assert_not_equals(document.activeElement, priorFocus, 'prior element should *not* get focus when the popover is manual');
    124      }
    125      assert_false(isElementVisible(popover));
    126      popover.popover = 'auto';
    127 
    128      // Hit Escape:
    129      priorFocus.focus();
    130      assert_equals(document.activeElement, priorFocus);
    131      popover.showPopover();
    132      assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popover.showPopover()`);
    133      await sendEscape();
    134      assert_equals(document.activeElement, priorFocus, 'prior element should get focus after Escape');
    135      assert_false(isElementVisible(popover));
    136 
    137      // Move focus into the popover, then hit Escape:
    138      let containedButton = popover.querySelector('button');
    139      if (containedButton) {
    140        priorFocus.focus();
    141        assert_equals(document.activeElement, priorFocus);
    142        popover.showPopover();
    143        containedButton.focus();
    144        assert_equals(document.activeElement, containedButton);
    145        await sendEscape();
    146        assert_equals(document.activeElement, priorFocus, 'prior element should get focus after Escape');
    147        assert_false(isElementVisible(popover));
    148      }
    149 
    150      // Change the popover type:
    151      priorFocus.focus();
    152      popover.showPopover();
    153      assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popover.showPopover()`);
    154      assert_equals(popover.popover, 'auto', 'All popovers in this test should start as popover=auto');
    155      popover.popover = 'manual';
    156      assert_false(popover.matches(':popover-open'), 'Changing the popover type should hide the popover');
    157      assert_equals(document.activeElement, priorFocus, 'prior element should get focus when the type is changed');
    158      assert_false(isElementVisible(popover));
    159      popover.popover = 'auto';
    160 
    161      // Remove from the document:
    162      priorFocus.focus();
    163      popover.showPopover();
    164      assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popover.showPopover()`);
    165      popover.remove();
    166      assert_false(isElementVisible(popover), 'Removing the popover should hide it immediately');
    167      if (!popover.hasAttribute('data-no-focus')) {
    168        assert_not_equals(document.activeElement, priorFocus, 'prior element should *not* get focus when the popover is removed from the document');
    169      }
    170      document.body.appendChild(popover);
    171 
    172      // Show a modal dialog:
    173      priorFocus.focus();
    174      popover.showPopover();
    175      assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by popover.showPopover()`);
    176      const dialog = document.body.appendChild(document.createElement('dialog'));
    177      dialog.showModal();
    178      assert_false(popover.matches(':popover-open'), 'Opening a modal dialog should hide the popover');
    179      assert_not_equals(document.activeElement, priorFocus, 'prior element should *not* get focus when a modal dialog is shown');
    180      assert_false(isElementVisible(popover));
    181      dialog.close();
    182      dialog.remove();
    183 
    184      // Use an activating element:
    185      const button = addInvoker(t, popover);
    186      priorFocus.focus();
    187      button.click();
    188      assert_true(popover.matches(':popover-open'));
    189      assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by button.click()`);
    190 
    191      // Make sure Escape works in the invoker case:
    192      await sendEscape();
    193      assert_equals(document.activeElement, priorFocus, 'prior element should get focus after Escape (via invoker)');
    194      assert_false(isElementVisible(popover));
    195 
    196      // Make sure we can directly focus the (already open) popover:
    197      priorFocus.focus();
    198      button.click();
    199      assert_true(popover.matches(':popover-open'));
    200      assert_equals(document.activeElement, expectedFocusedElement, `${testName} activated by button.click()`);
    201      popover.focus();
    202      assert_equals(document.activeElement, popover.hasAttribute('tabindex') || popover.tagName === 'DIALOG' ? popover : expectedFocusedElement, `${testName} directly focus with popover.focus()`);
    203      button.click(); // Button is set to toggle the popover
    204      assert_false(popover.matches(':popover-open'));
    205      assert_equals(document.activeElement, priorFocus, 'prior element should get focus on button-toggled hide');
    206      assert_false(isElementVisible(popover));
    207    }, "Popover focus test: " + testName);
    208 
    209    promise_test(async t => {
    210      const priorFocus = addPriorFocus(t);
    211      assert_false(popover.matches(':popover-open'), 'popover should start out hidden');
    212      let button = addInvoker(t, popover);
    213      assert_equals(button.getAttribute('popovertarget'), popover.id, 'This test assumes the button uses `popovertarget`.');
    214      assert_not_equals(button, priorFocus, 'Stranger things have happened');
    215      assert_false(popover.contains(button), 'Start with a non-contained button');
    216      priorFocus.focus();
    217      assert_equals(document.activeElement, priorFocus);
    218      popover.showPopover();
    219      assert_true(popover.matches(':popover-open'));
    220      await clickOn(button); // This will *not* light dismiss, but will "toggle" the popover.
    221      assert_false(popover.matches(':popover-open'));
    222      assert_equals(document.activeElement, button, 'focus should move to the button when clicked, and should stay there when the popover closes');
    223      assert_false(isElementVisible(popover));
    224 
    225      // Same thing, but the button is contained within the popover
    226      button.setAttribute('popovertarget', popover.id);
    227      button.setAttribute('popovertargetaction', 'hide');
    228      popover.appendChild(button);
    229      t.add_cleanup(() => button.remove());
    230      priorFocus.focus();
    231      popover.showPopover();
    232      assert_true(popover.matches(':popover-open'));
    233      const changesFocus = !popover.hasAttribute('data-no-focus');
    234      if (changesFocus) {
    235        assert_not_equals(document.activeElement, priorFocus, 'focus should shift for this element');
    236      }
    237      await clickOn(button);
    238      assert_false(popover.matches(':popover-open'), 'clicking button should hide the popover');
    239      assert_equals(document.activeElement, priorFocus, 'Contained button should return focus to the previously focused element');
    240      assert_false(isElementVisible(popover));
    241 
    242      // Same thing, but the button is unrelated (no popovertarget)
    243      button = document.createElement('button');
    244      button.setAttribute("tabindex", "0");
    245      document.body.appendChild(button);
    246      priorFocus.focus();
    247      popover.showPopover();
    248      assert_true(popover.matches(':popover-open'));
    249      await clickOn(button); // This will light dismiss the popover, focus the prior focus, then focus this button.
    250      assert_false(popover.matches(':popover-open'), 'clicking button should hide the popover (via light dismiss)');
    251      assert_equals(document.activeElement, button, 'Focus should go to unrelated button on light dismiss');
    252      assert_false(isElementVisible(popover));
    253    }, "Popover button click focus test: " + testName);
    254 
    255    promise_test(async t => {
    256      if (popover.hasAttribute('data-no-focus')) {
    257        // This test only applies if the popover changes focus
    258        return;
    259      }
    260      const priorFocus = addPriorFocus(t);
    261      assert_false(popover.matches(':popover-open'), 'popover should start out hidden');
    262 
    263      // Move the prior focus out of the document
    264      priorFocus.focus();
    265      popover.showPopover();
    266      assert_true(popover.matches(':popover-open'));
    267      const newFocus = document.activeElement;
    268      assert_not_equals(newFocus, priorFocus, 'focus should shift for this element');
    269      priorFocus.remove();
    270      assert_equals(document.activeElement, newFocus, 'focus should not change when prior focus is removed');
    271      popover.hidePopover();
    272      assert_not_equals(document.activeElement, priorFocus, 'focused element has been removed');
    273      assert_false(isElementVisible(popover));
    274      document.body.appendChild(priorFocus); // Put it back
    275 
    276      // Move the prior focus inside the (already open) popover
    277      priorFocus.focus();
    278      popover.showPopover();
    279      assert_true(popover.matches(':popover-open'));
    280      assert_false(popover.contains(priorFocus), 'Start with a non-contained prior focus');
    281      popover.appendChild(priorFocus); // Move inside the popover
    282      assert_true(popover.contains(priorFocus));
    283      assert_true(popover.matches(':popover-open'), 'popover should stay open');
    284      popover.hidePopover();
    285      assert_false(isElementVisible(popover));
    286      assert_not_equals(document.activeElement, priorFocus, 'focused element is display:none inside the popover');
    287      document.body.appendChild(priorFocus); // Put it back
    288    }, "Popover corner cases test: " + testName);
    289  }
    290 
    291  document.querySelectorAll('body > [popover]').forEach(popover => activateAndVerify(popover));
    292 </script>