Bug 1460613 - Add keyboard navigation for reveal link in slotted nodes. r=jdescottes,yzen
MozReview-Commit-ID: GAm1bJNcZPz
--- 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;
}