Bug 1460613 - Add keyboard navigation for reveal link in slotted nodes. r=jdescottes,yzen draft
authorBelén Albeza <balbeza@mozilla.com>
Wed, 18 Jul 2018 14:17:23 +0200
changeset 822432 9d98588cb45c08180cf3ff3ddcc9ba8afd889014
parent 819645 8dab948a10f073a46f13f55f94d1f6514c7360ac
push id117370
push userbmo:balbeza@mozilla.com
push dateWed, 25 Jul 2018 09:44:21 +0000
reviewersjdescottes, yzen
bugs1460613
milestone63.0a1
Bug 1460613 - Add keyboard navigation for reveal link in slotted nodes. r=jdescottes,yzen MozReview-Commit-ID: GAm1bJNcZPz
devtools/client/inspector/markup/test/browser_markup_shadowdom_clickreveal.js
devtools/client/inspector/markup/test/head.js
devtools/client/inspector/markup/views/slotted-node-container.js
devtools/client/inspector/markup/views/slotted-node-editor.js
--- a/devtools/client/inspector/markup/test/browser_markup_shadowdom_clickreveal.js
+++ b/devtools/client/inspector/markup/test/browser_markup_shadowdom_clickreveal.js
@@ -19,21 +19,37 @@ const TEST_URL = `data:text/html;charset
       constructor() {
         super();
         let shadowRoot = this.attachShadow({mode: 'open'});
         shadowRoot.innerHTML = '<slot name="slot1"></slot>';
       }
     });
   </script>`;
 
+// Test reveal link with mouse navigation
 add_task(async function() {
+  const checkWithMouse = checkRevealLink.bind(null, clickOnRevealLink);
+  await testRevealLink(checkWithMouse, checkWithMouse);
+});
+
+// Test reveal link with keyboard navigation (Enter and Spacebar keys)
+add_task(async function() {
+  const checkWithEnter = checkRevealLink.bind(null,
+    keydownOnRevealLink.bind(null, "KEY_Enter"));
+  const checkWithSpacebar = checkRevealLink.bind(null,
+    keydownOnRevealLink.bind(null, " "));
+
+  await testRevealLink(checkWithEnter, checkWithSpacebar);
+});
+
+async function testRevealLink(revealFnFirst, revealFnSecond) {
   await enableWebComponents();
 
-  const {inspector} = await openInspectorForURL(TEST_URL);
-  const {markup} = inspector;
+  const { inspector } = await openInspectorForURL(TEST_URL);
+  const { markup } = inspector;
 
   info("Find and expand the test-component shadow DOM host.");
   const hostFront = await getNodeFront("test-component", inspector);
   const hostContainer = markup.getContainer(hostFront);
   await expandContainer(inspector, hostContainer);
 
   info("Expand the shadow root");
   const shadowRootContainer = hostContainer.getChildContainers()[0];
@@ -41,33 +57,36 @@ add_task(async function() {
 
   info("Expand the slot");
   const slotContainer = shadowRootContainer.getChildContainers()[0];
   await expandContainer(inspector, slotContainer);
 
   const slotChildContainers = slotContainer.getChildContainers();
   is(slotChildContainers.length, 2, "Expecting 2 slotted children");
 
-  await checkRevealLink(inspector, slotChildContainers[0].node);
+  await revealFnFirst(inspector, slotChildContainers[0].node);
   is(inspector.selection.nodeFront.id, "el1", "The right node was selected");
   is(hostContainer.getChildContainers()[1].node, inspector.selection.nodeFront);
 
-  await checkRevealLink(inspector, slotChildContainers[1].node);
+  await revealFnSecond(inspector, slotChildContainers[1].node);
   is(inspector.selection.nodeFront.id, "el2", "The right node was selected");
   is(hostContainer.getChildContainers()[2].node, inspector.selection.nodeFront);
-});
+}
 
-async function checkRevealLink(inspector, node) {
+async function checkRevealLink(actionFn, inspector, node) {
   const slottedContainer = inspector.markup.getContainer(node, true);
   info("Select the slotted container for the element");
   await selectNode(node, inspector, "no-reason", true);
   ok(inspector.selection.isSlotted(), "The selection is the slotted version");
   ok(inspector.markup.getSelectedContainer().isSlotted(),
     "The selected container is slotted");
 
+  const link = slottedContainer.elt.querySelector(".reveal-link");
+  is(link.getAttribute("role"), "link", "Reveal link has the role=link attribute");
+
   info("Click on the reveal link and wait for the new node to be selected");
-  await clickOnRevealLink(inspector, slottedContainer);
+  await actionFn(inspector, slottedContainer);
   const selectedFront = inspector.selection.nodeFront;
   is(selectedFront, node, "The same node front is still selected");
   ok(!inspector.selection.isSlotted(), "The selection is not the slotted version");
   ok(!inspector.markup.getSelectedContainer().isSlotted(),
     "The selected container is not slotted");
 }
--- a/devtools/client/inspector/markup/test/head.js
+++ b/devtools/client/inspector/markup/test/head.js
@@ -726,8 +726,33 @@ async function clickOnRevealLink(inspect
   const win = inspector.markup.doc.defaultView;
 
   // First send a mouseover on the tagline to force the link to be displayed.
   EventUtils.synthesizeMouseAtCenter(tagline, {type: "mouseover"}, win);
   EventUtils.synthesizeMouseAtCenter(revealLink, {}, win);
 
   await onSelection;
 }
+
+/**
+ * Hit `key` on the reveal link in the provided slotted container.
+ * Will resolve when selection emits "new-node-front".
+ */
+async function keydownOnRevealLink(key, inspector, container) {
+  const revealLink = container.elt.querySelector(".reveal-link");
+  const win = inspector.markup.doc.defaultView;
+
+  const root = inspector.markup.getContainer(inspector.markup._rootNode);
+  root.elt.focus();
+
+  // we need to go through a ENTER + TAB  key sequence to focus on
+  // the .reveal-link element with the keyboard
+  const revealFocused = once(revealLink, "focus");
+  EventUtils.synthesizeKey("KEY_Enter", {}, win);
+  EventUtils.synthesizeKey("KEY_Tab", {}, win);
+  info("Waiting for .reveal-link to be focused");
+  await revealFocused;
+
+  // hit `key` on the .reveal-link
+  const onSelection = inspector.selection.once("new-node-front");
+  EventUtils.synthesizeKey(key, {}, win);
+  await onSelection;
+}
--- a/devtools/client/inspector/markup/views/slotted-node-container.js
+++ b/devtools/client/inspector/markup/views/slotted-node-container.js
@@ -29,24 +29,35 @@ SlottedNodeContainer.prototype = extend(
 
   /**
    * Slotted node containers never display children and should not react to toggle.
    */
   _onToggle: function(event) {
     event.stopPropagation();
   },
 
+  _revealFromSlot() {
+    const reason = "reveal-from-slot";
+    this.markup.inspector.selection.setNodeFront(this.node, { reason });
+    this.markup.telemetry.scalarSet("devtools.shadowdom.reveal_link_clicked", true);
+  },
+
+  _onKeyDown: function(event) {
+    const isActionKey = event.code == "Enter" || event.code == "Space";
+    if (event.target.classList.contains("reveal-link") && isActionKey) {
+      this._revealFromSlot();
+    }
+  },
+
   onContainerClick: async function(event) {
     if (!event.target.classList.contains("reveal-link")) {
       return;
     }
 
-    const reason = "reveal-from-slot";
-    this.markup.inspector.selection.setNodeFront(this.node, { reason });
-    this.markup.telemetry.scalarSet("devtools.shadowdom.reveal_link_clicked", true);
+    this._revealFromSlot();
   },
 
   isDraggable: function() {
     return false;
   },
 
   isSlotted: function() {
     return true;
--- a/devtools/client/inspector/markup/views/slotted-node-editor.js
+++ b/devtools/client/inspector/markup/views/slotted-node-editor.js
@@ -25,18 +25,20 @@ SlottedNodeEditor.prototype = {
     this.elt = doc.createElement("span");
     this.elt.classList.add("editor");
 
     this.tag = doc.createElement("span");
     this.tag.classList.add("tag");
     this.elt.appendChild(this.tag);
 
     this.revealLink = doc.createElement("span");
+    this.revealLink.setAttribute("role", "link");
+    this.revealLink.setAttribute("tabindex", -1);
+    this.revealLink.title = INSPECTOR_L10N.getStr("markupView.revealLink.tooltip");
     this.revealLink.classList.add("reveal-link");
-    this.revealLink.title = INSPECTOR_L10N.getStr("markupView.revealLink.tooltip");
     this.elt.appendChild(this.revealLink);
   },
 
   destroy: function() {
     // We might be already destroyed.
     if (!this.elt) {
       return;
     }