зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
265472b1e2
Коммит
e669493035
|
@ -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>
|
||||
|
|
Загрузка…
Ссылка в новой задаче