Bug 1654956 - adding TabbingOrderHighlighter that highlights a sequence of nodes within the tabbing order. r=jdescottes

Differential Revision: https://phabricator.services.mozilla.com/D94924
This commit is contained in:
Yura Zenevich 2020-11-03 15:33:01 +00:00
Родитель 265472b1e2
Коммит e669493035
5 изменённых файлов: 342 добавлений и 0 удалений

Просмотреть файл

@ -24,4 +24,5 @@ DevToolsModules(
"rulers.js",
"selector.js",
"shapes.js",
"tabbing-order.js",
)

Просмотреть файл

@ -0,0 +1,237 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Services = require("Services");
loader.lazyRequireGetter(
this,
"ContentDOMReference",
"resource://gre/modules/ContentDOMReference.jsm",
true
);
loader.lazyRequireGetter(
this,
["isRemoteFrame", "isWindowIncluded"],
"devtools/shared/layout/utils",
true
);
loader.lazyRequireGetter(
this,
"NodeTabbingOrderHighlighter",
"devtools/server/actors/highlighters/node-tabbing-order",
true
);
const DEFAULT_FOCUS_FLAGS = Services.focus.FLAG_NOSCROLL;
/**
* The TabbingOrderHighlighter uses focus manager to traverse all focusable
* nodes on the page and then uses the NodeTabbingOrderHighlighter to highlight
* these nodes.
*/
class TabbingOrderHighlighter {
constructor(highlighterEnv) {
this.highlighterEnv = highlighterEnv;
this._highlighters = new Map();
this.onMutation = this.onMutation.bind(this);
this.onPageHide = this.onPageHide.bind(this);
this.onWillNavigate = this.onWillNavigate.bind(this);
this.highlighterEnv.on("will-navigate", this.onWillNavigate);
const { pageListenerTarget } = highlighterEnv;
pageListenerTarget.addEventListener("pagehide", this.onPageHide);
}
/**
* Static getter that indicates that TabbingOrderHighlighter supports
* highlighting in XUL windows.
*/
static get XULSupported() {
return true;
}
get win() {
return this.highlighterEnv.window;
}
get focusedElement() {
return Services.focus.getFocusedElementForWindow(this.win, true, {});
}
set focusedElement(element) {
Services.focus.setFocus(element, DEFAULT_FOCUS_FLAGS);
}
moveFocus(startElement) {
return Services.focus.moveFocus(
this.win,
startElement.nodeType === Node.DOCUMENT_NODE
? startElement.documentElement
: startElement,
Services.focus.MOVEFOCUS_FORWARD,
DEFAULT_FOCUS_FLAGS
);
}
/**
* Show NodeTabbingOrderHighlighter on each node that belongs to the keyboard
* tabbing order.
*
* @param {DOMNode} startElm
* Starting element to calculate tabbing order from.
*
* @param {JSON} options
* - options.index
* Start index for the tabbing order. Starting index will be 0 at
* the start of the tabbing order highlighting; in remote frames
* starting index will, typically, be greater than 0 (unless there
* was nothing to focus in the top level content document prior to
* the remote frame).
*/
async show(startElm, { index }) {
const focusableElements = [];
const originalFocusedElement = this.focusedElement;
let currentFocusedElement = this.moveFocus(startElm);
while (
currentFocusedElement &&
isWindowIncluded(this.win, currentFocusedElement.ownerGlobal)
) {
focusableElements.push(currentFocusedElement);
currentFocusedElement = this.moveFocus(currentFocusedElement);
}
// Allow to flush pending notifications to ensure the PresShell and frames
// are updated.
await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
let endElm = this.focusedElement;
if (
currentFocusedElement &&
!isWindowIncluded(this.win, currentFocusedElement.ownerGlobal)
) {
endElm = null;
}
if (
!endElm &&
focusableElements.length > 0 &&
isRemoteFrame(focusableElements[focusableElements.length - 1])
) {
endElm = focusableElements[focusableElements.length - 1];
}
if (originalFocusedElement && originalFocusedElement !== endElm) {
this.focusedElement = originalFocusedElement;
}
const highlighters = [];
for (let i = 0; i < focusableElements.length; i++) {
highlighters.push(
this._accumulateHighlighter(focusableElements[i], index++)
);
}
await Promise.all(highlighters);
this._trackMutations();
return {
contentDOMReference: endElm && ContentDOMReference.get(endElm),
index,
};
}
async _accumulateHighlighter(node, index) {
const highlighter = new NodeTabbingOrderHighlighter(this.highlighterEnv);
await highlighter.isReady;
highlighter.show(node, { index: index + 1 });
this._highlighters.set(node, highlighter);
}
hide() {
this._untrackMutations();
for (const highlighter of this._highlighters.values()) {
highlighter.destroy();
}
this._highlighters.clear();
}
/**
* Track mutations in the top level document subtree so that the appropriate
* NodeTabbingOrderHighlighter infobar's could be updated to reflect the
* attribute mutations on relevant nodes.
*/
_trackMutations() {
const { win } = this;
this.currentMutationObserver = new win.MutationObserver(this.onMutation);
this.currentMutationObserver.observe(win.document.documentElement, {
subtree: true,
attributes: true,
});
}
_untrackMutations() {
if (!this.currentMutationObserver) {
return;
}
this.currentMutationObserver.disconnect();
this.currentMutationObserver = null;
}
onMutation(mutationList) {
for (const { target } of mutationList) {
const highlighter = this._highlighters.get(target);
if (highlighter) {
highlighter.update();
}
}
}
/**
* Update NodeTabbingOrderHighlighter focus styling for a node that,
* potentially, belongs to the tabbing order.
* @param {Object} options
* Options specifying the node and its focused state.
*/
updateFocus({ node, focused }) {
const highlighter = this._highlighters.get(node);
if (!highlighter) {
return;
}
highlighter.updateFocus(focused);
}
destroy() {
this.highlighterEnv.off("will-navigate", this.onWillNavigate);
const { pageListenerTarget } = this.highlighterEnv;
if (pageListenerTarget) {
pageListenerTarget.removeEventListener("pagehide", this.onPageHide);
}
this.hide();
this.highlighterEnv = null;
}
onPageHide({ target }) {
// If a pagehide event is triggered for current window's highlighter, hide
// the highlighter.
if (target.defaultView === this.win) {
this.hide();
}
}
onWillNavigate({ isTopLevel }) {
if (isTopLevel) {
this.hide();
}
}
}
exports.TabbingOrderHighlighter = TabbingOrderHighlighter;

Просмотреть файл

@ -70,6 +70,8 @@ skip-if = (os == 'win' && processor == 'aarch64') # bug 1533184
[browser_accessibility_simple.js]
skip-if = (os == 'win' && processor == 'aarch64') # bug 1533184
[browser_accessibility_simulator.js]
[browser_accessibility_tabbing_order_highlighter.js]
skip-if = (os == 'win' && processor == 'aarch64') # bug 1533184
[browser_accessibility_text_label_audit_frame.js]
skip-if =
(os == 'win' && processor == 'aarch64') # bug 1533184

Просмотреть файл

@ -0,0 +1,101 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Checks for the TabbingOrderHighlighter.
add_task(async function() {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: MAIN_DOMAIN + "doc_accessibility_infobar.html",
},
async function(browser) {
await SpecialPowers.spawn(browser, [], async function() {
const { require } = ChromeUtils.import(
"resource://devtools/shared/Loader.jsm"
);
const {
HighlighterEnvironment,
} = require("devtools/server/actors/highlighters");
const {
TabbingOrderHighlighter,
} = require("devtools/server/actors/highlighters/tabbing-order");
// Start testing. First, create highlighter environment and initialize.
const env = new HighlighterEnvironment();
env.initFromWindow(content.window);
// Wait for loading highlighter environment content to complete before
// creating the highlighter.
await new Promise(resolve => {
const doc = env.document;
function onContentLoaded() {
if (
doc.readyState === "interactive" ||
doc.readyState === "complete"
) {
resolve();
} else {
doc.addEventListener("DOMContentLoaded", onContentLoaded, {
once: true,
});
}
}
onContentLoaded();
});
// Now, we can test the Infobar's index content.
const node = content.document.createElement("div");
content.document.body.append(node);
const highlighter = new TabbingOrderHighlighter(env);
await highlighter.isReady;
info("Showing tabbing order highlighter for all tabbable nodes");
const { contentDOMReference, index } = await highlighter.show(
content.document,
{
index: 0,
}
);
is(
contentDOMReference,
null,
"No current element when at the end of the tab order"
);
is(index, 2, "Current index is correct");
is(
highlighter._highlighters.size,
2,
"Number of node tabbing order highlighters is correct"
);
for (let i = 0; i < highlighter._highlighters.size; i++) {
const nodeHighlighter = [...highlighter._highlighters.values()][i];
const infoBarText = nodeHighlighter.getElement("infobar-text");
is(
parseInt(infoBarText.getTextContent(), 10),
i + 1,
"infobar text content is correct"
);
}
info("Showing focus highlighting");
const input = content.document.getElementById("input");
highlighter.updateFocus({ node: input, focused: true });
const nodeHighlighter = highlighter._highlighters.get(input);
const { classList } = nodeHighlighter.getElement("root");
ok(classList.contains("focused"), "Focus styling is applied");
highlighter.updateFocus({ node: input, focused: false });
ok(!classList.contains("focused"), "Focus styling is removed");
highlighter.hide();
});
}
);
});

Просмотреть файл

@ -7,5 +7,6 @@
<h1 id="h1">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</h1>
<button id="button">Accessible Button</button>
<p id="p" style="font-size: 0;">This is a paragraph that has no bounds.</p>
<label>Enter text: <input id="input" type="text"></text></label>
</body>
</html>