tor-browser

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

popover-focus-5.html (9193B)


      1 <!doctype html>
      2 <meta charset="utf-8" />
      3 <title>Popover focus behaviors in slot elements</title>
      4 <meta name="timeout" content="long" />
      5 <link rel="author" title="Keith Cirkel" href="mailto:wpt@keithcirkel.co.uk" />
      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/popover-utils.js"></script>
     12 
     13 <meta name="variant" content="?commandforelement" />
     14 <meta name="variant" content="?popovertargetelement" />
     15 <meta name="variant" content="?commandfor" />
     16 <meta name="variant" content="?imperative" />
     17 
     18 <div data-candidate="form controls inside" data-count="2">
     19  <button tabindex="0" command="toggle-popover">Toggle popover</button>
     20  <div popover id="popover1">
     21    <input tabindex="0" type="text" data-expected="1" />
     22    <input tabindex="0" type="text" data-expected="2" />
     23    <button tabindex="0" command="hide-popover" data-expected="close">
     24      Close popover
     25    </button>
     26  </div>
     27 </div>
     28 <button tabindex="0" data-expected="after">
     29  This button is where focus should land after traversing this popover
     30 </button>
     31 
     32 <div data-candidate="a details element inside" data-count="1">
     33  <button tabindex="0" command="toggle-popover">Toggle popover</button>
     34  <div popover id="popover2">
     35    <details>
     36      <summary tabindex="0" data-expected="1">A details element</summary>
     37    </details>
     38    <button tabindex="0" command="hide-popover" data-expected="close">
     39      Close popover
     40    </button>
     41  </div>
     42 </div>
     43 <button tabindex="0" data-expected="after">
     44  This button is where focus should land after traversing this popover
     45 </button>
     46 
     47 <div
     48  data-candidate="a details element inside, and the after button adjacent to invoker"
     49  data-count="1"
     50 >
     51  <button tabindex="0" command="toggle-popover">Toggle popover</button>
     52  <button tabindex="0" data-expected="after">
     53    This button is where focus should land after traversing this popover
     54  </button>
     55  <div popover id="popover3">
     56    <details data-expected="1">A details element</details>
     57    <button tabindex="0" command="hide-popover" data-expected="close">
     58      Close popover
     59    </button>
     60  </div>
     61 </div>
     62 
     63 <div
     64  data-candidate="a custom-element with delegatesfocus inside"
     65  data-count="1"
     66 >
     67  <button tabindex="0" command="toggle-popover">Toggle popover</button>
     68  <div popover id="popover4">
     69    <my-element data-expected="1">
     70      <template shadowrootmode="open" shadowrootdelegatesfocus>
     71        <button tabindex="0" data-expected="shadow-1"></button>
     72      </template>
     73    </my-element>
     74    <button tabindex="0" command="hide-popover" data-expected="close">
     75      Close popover
     76    </button>
     77  </div>
     78 </div>
     79 <button tabindex="0" data-expected="after">
     80  This button is where focus should land after traversing popover
     81 </button>
     82 
     83 <div
     84  data-candidate="a custom-element with a slotted button inside"
     85  data-count="1"
     86 >
     87  <button tabindex="0" command="toggle-popover">Toggle popover</button>
     88  <div popover id="popover5">
     89    <my-element>
     90      <template shadowrootmode="open">
     91        <slot></slot>
     92      </template>
     93      <button tabindex="0" data-expected="1"></button>
     94    </my-element>
     95    <button tabindex="0" command="hide-popover" data-expected="close">
     96      Close popover
     97    </button>
     98  </div>
     99 </div>
    100 <button tabindex="0" data-expected="after">
    101  This button is where focus should land after traversing popover
    102 </button>
    103 
    104 <div
    105  data-candidate="custom-element with a slotted button, followed by a details element, where the after button is adjacent to the invoker"
    106  data-count="2"
    107 >
    108  <button tabindex="0" command="toggle-popover">Toggle popover</button>
    109  <button tabindex="0" data-expected="after">
    110    This button is where focus should land after traversing popover
    111  </button>
    112  <div popover id="popover6">
    113    <my-element data-expected="2">
    114      <template shadowrootmode="open">
    115        <slot></slot>
    116        <details>
    117          <summary tabindex="0" data-expected="shadow-2">A details element</summary>
    118        </details>
    119      </template>
    120      <button tabindex="0" data-expected="1"></button>
    121    </my-element>
    122    <button tabindex="0" command="hide-popover" data-expected="close">
    123      Close popover
    124    </button>
    125  </div>
    126 </div>
    127 
    128 <script>
    129  // The active element might be an element within the expected focus candidate,e.g. a summary
    130  // inside a details, we should traverse the tree to find the nearest `[data-expected]` element
    131  // to ensure we're not failing a test for chosing to focus an interior element.
    132  function getExpectedValue()  {
    133    return document.activeElement?.closest('[data-expected]')?.getAttribute("data-expected")
    134  }
    135 
    136  async function testCandidate(el, style, signal) {
    137    const count = parseInt(el.getAttribute("data-count"));
    138    const invoker = el.querySelector("button:first-child");
    139    const popover = el.querySelector("[popover]");
    140    const popoverClose = el.querySelector('[data-expected="close"]');
    141    assert_greater_than_equal(count, 0, "test candidate had an invalid count");
    142    assert_not_equals(
    143      invoker,
    144      null,
    145      "could not find invoker in test candidate",
    146    );
    147    assert_not_equals(
    148      popover,
    149      null,
    150      "could not find popover in test candidate",
    151    );
    152    assert_not_equals(
    153      popoverClose,
    154      null,
    155      "could not find popover close button in test candidate",
    156    );
    157    switch (style) {
    158      case "popovertarget":
    159        invoker.setAttribute("popovertarget", popover.id);
    160        popoverClose.setAttribute("popovertarget", popover.id);
    161        break;
    162      case "popovertargetelement":
    163        invoker.popoverTargetElement = popover;
    164        popoverClose.popoverTargetElement = popover;
    165        break;
    166      case "commandfor":
    167        invoker.setAttribute("commandfor", popover.id);
    168        popoverClose.setAttribute("commandfor", popover.id);
    169        break;
    170      case "commandforelement":
    171        invoker.commandForElement = popover;
    172        popoverClose.commandForElement = popover;
    173        break;
    174      case "imperative":
    175        invoker.addEventListener(
    176          "click",
    177          () => {
    178            popover.togglePopover({ source: invoker });
    179          },
    180          { signal },
    181        );
    182        popoverClose.addEventListener(
    183          "click",
    184          () => {
    185            popover.hidePopover({ source: invoker });
    186          },
    187          { signal },
    188        );
    189        break;
    190      default:
    191        assert_unreached();
    192    }
    193    invoker.focus();
    194    assert_equals(document.activeElement, invoker);
    195    invoker.click();
    196    assert_true(
    197      popover.matches(":popover-open"),
    198      "popover should be invoked by invoker",
    199    );
    200    assert_equals(
    201      document.activeElement,
    202      invoker,
    203      "invoker should still be focused",
    204    );
    205    for (let i = 1; i <= count; i += 1) {
    206      await sendTab();
    207      assert_equals(
    208        getExpectedValue(),
    209        String(i),
    210        `tab press should move to active element ${i}`,
    211      );
    212      const shadowActive = document.activeElement?.shadowRoot?.activeElement;
    213      if (shadowActive) {
    214        const expected = `shadow-${i}`;
    215        assert_equals(
    216          shadowActive.getAttribute("data-expected"),
    217          expected,
    218          `tab press should move to active element ${expected} in shadow dom`,
    219        );
    220      }
    221    }
    222    await sendTab();
    223    assert_equals(
    224      getExpectedValue(),
    225      "close",
    226      "tab press should move to close popover button",
    227    );
    228    await sendShiftTab();
    229    assert_equals(
    230      getExpectedValue(),
    231      String(count),
    232      "shift+tab should move back to last active element",
    233    );
    234    await sendTab();
    235    assert_equals(
    236      document.activeElement,
    237      popoverClose,
    238      "tab should move forward to re-land on close popover again",
    239    );
    240    await sendTab();
    241    assert_equals(
    242      getExpectedValue(),
    243      "after",
    244      "tab should move forward to land on the button after the popover contents",
    245    );
    246    await sendShiftTab();
    247    popoverClose.click();
    248    assert_equals(
    249      document.activeElement,
    250      invoker,
    251      "popover now closed - focus should have returned to initial invoker",
    252    );
    253  }
    254 
    255  const style = window.location.search.substring(1) || "popovertarget";
    256  for (const candidate of document.querySelectorAll("[data-candidate]")) {
    257    promise_test(
    258      async (t) => {
    259        const controller = new AbortController();
    260        t.add_cleanup(() => {
    261          controller.abort();
    262          for (const el of document.querySelectorAll(
    263            ":is([popovertarget],[commandfor],[disabled],[tabindex])",
    264          )) {
    265            el.removeAttribute("popovertarget");
    266            el.removeAttribute("commandfor");
    267            el.removeAttribute("disabled");
    268            el.removeAttribute("tabindex");
    269          }
    270          for (const el of document.querySelectorAll(":popover-open")) {
    271            el.hidePopover();
    272          }
    273        });
    274        await testCandidate(candidate, style);
    275      },
    276      `Focusing elements inside a popover with ${candidate.getAttribute("data-candidate")}, using ${style}`,
    277    );
    278  }
    279 </script>