Bug 1648161 - use accessible highlighter instead of xul-accessible highlighter in browser toolbox. Remove xul-accessible highlighter altogether. r=ochameau

Differential Revision: https://phabricator.services.mozilla.com/D80005
This commit is contained in:
Yura Zenevich 2020-07-30 13:59:36 +00:00
parent d0686776d2
commit c894b69ce1
8 changed files with 243 additions and 710 deletions

View File

@ -1385,13 +1385,9 @@ toolbar[keyNav=true]:not([collapsed=true]):not([customizing=true]) toolbartabsto
}
/* Frame used for rendering the DevTools inspector highlighters */
iframe.devtools-highlighter-renderer {
:root > iframe.devtools-highlighter-renderer {
border: none;
pointer-events: none;
}
/* Highlighter for the Browser Toolbox */
:root > iframe.devtools-highlighter-renderer {
position: fixed;
top: 0;
left: 0;
@ -1399,8 +1395,3 @@ iframe.devtools-highlighter-renderer {
height: 100%;
z-index: 2;
}
/* Highlighter for web content */
.browserStack > iframe.devtools-highlighter-renderer {
-moz-box-flex: 1;
}

View File

@ -287,25 +287,10 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
get highlighter() {
if (!this._highlighter) {
if (isXUL(this.rootWin)) {
if (!isTypeRegistered("XULWindowAccessibleHighlighter")) {
register("XULWindowAccessibleHighlighter", "xul-accessible");
}
this._highlighter = CustomHighlighterActor(
this,
"XULWindowAccessibleHighlighter"
);
} else {
if (!isTypeRegistered("AccessibleHighlighter")) {
register("AccessibleHighlighter", "accessible");
}
this._highlighter = CustomHighlighterActor(
this,
"AccessibleHighlighter"
);
if (!isTypeRegistered("AccessibleHighlighter")) {
register("AccessibleHighlighter", "accessible");
}
this._highlighter = CustomHighlighterActor(this, "AccessibleHighlighter");
this.manage(this._highlighter);
this._highlighter.on("highlighter-event", this.onHighlighterEvent);
@ -331,6 +316,10 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
return this.targetActor && this.targetActor.window.document;
},
get isXUL() {
return isXUL(this.rootWin);
},
get colorMatrix() {
if (!this.targetActor.docShell) {
return null;
@ -468,7 +457,7 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
return this.once("document-ready").then(docAcc => this.addRef(docAcc));
}
if (isXUL(this.rootWin)) {
if (this.isXUL) {
const doc = this.addRef(this.getRawAccessibleFor(this.rootDoc));
return Promise.resolve(doc);
}
@ -851,7 +840,7 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
const { name, role } = accessible;
const shown = this.highlighter.show(
{ rawNode },
{ ...options, ...bounds, name, role, audit }
{ ...options, ...bounds, name, role, audit, isXUL: this.isXUL }
);
this._highlightingAccessible = null;
@ -1119,7 +1108,8 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, {
const target = event.originalTarget || event.target;
const docAcc = this.getRawAccessibleFor(this.rootDoc);
const win = target.ownerGlobal;
const scale = this.pixelRatio / getCurrentZoom(win);
const zoom = this.isXUL ? 1 : getCurrentZoom(win);
const scale = this.pixelRatio / zoom;
const rawAccessible = docAcc.getDeepestChildAtPointInProcess(
event.screenX * scale,
event.screenY * scale

View File

@ -17,7 +17,10 @@ const {
TEXT_NODE,
DOCUMENT_NODE,
} = require("devtools/shared/dom-node-constants");
const { setIgnoreLayoutChanges } = require("devtools/shared/layout/utils");
const {
getCurrentZoom,
setIgnoreLayoutChanges,
} = require("devtools/shared/layout/utils");
loader.lazyRequireGetter(
this,
@ -25,6 +28,12 @@ loader.lazyRequireGetter(
"devtools/server/actors/highlighters/utils/accessibility",
true
);
loader.lazyRequireGetter(
this,
"getBoundsXUL",
"devtools/server/actors/highlighters/utils/accessibility",
true
);
loader.lazyRequireGetter(
this,
"Infobar",
@ -94,6 +103,14 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
this.pageListenerTarget.addEventListener("pagehide", this.onPageHide);
}
/**
* Static getter that indicates that AccessibleHighlighter supports
* highlighting in XUL windows.
*/
static get XULSupported() {
return true;
}
/**
* Build highlighter markup.
*
@ -318,7 +335,20 @@ class AccessibleHighlighter extends AutoRefreshHighlighter {
* information for the accessible object.
*/
get _bounds() {
return getBounds(this.win, this.options);
let { win, options } = this;
let getBoundsFn = getBounds;
if (this.options.isXUL) {
// Zoom level for the top level browser window does not change and only
// inner frames do. So we need to get the zoom level of the current node's
// parent window.
let zoom = getCurrentZoom(this.currentNode);
zoom *= zoom;
options = { ...options, zoom };
getBoundsFn = getBoundsXUL;
win = this.win.parent.ownerGlobal;
}
return getBoundsFn(win, options);
}
/**

View File

@ -24,6 +24,5 @@ DevToolsModules(
'rulers.js',
'selector.js',
'shapes.js',
'simple-outline.js',
'xul-accessible.js'
'simple-outline.js'
)

View File

@ -5,10 +5,7 @@
"use strict";
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
const {
getCurrentZoom,
getViewportDimensions,
} = require("devtools/shared/layout/utils");
const { getCurrentZoom } = require("devtools/shared/layout/utils");
const {
moveInfobar,
createNode,
@ -278,148 +275,6 @@ class Infobar {
}
}
/**
* The XULAccessibleInfobar handles building the XUL infobar markup where it isn't
* possible with the regular accessible highlighter.
*/
class XULWindowInfobar extends Infobar {
/**
* A helper function that calculates the positioning of a XUL accessible's infobar.
*
* @param {Object} container
* The infobar container.
*/
_moveInfobar(container) {
const arrow = this.getElement("arrow");
// Show the container and arrow elements first.
container.removeAttribute("hidden");
arrow.removeAttribute("hidden");
// Set the left value of the infobar container in relation to
// highlighter's bounds position.
const {
left: boundsLeft,
right: boundsRight,
top: boundsTop,
bottom: boundsBottom,
} = this.bounds;
const boundsMidPoint = (boundsLeft + boundsRight) / 2;
container.style.left = `${boundsMidPoint}px`;
const zoom = getCurrentZoom(this.win);
let {
width: viewportWidth,
height: viewportHeight,
} = getViewportDimensions(this.win);
const { width, height, left } = container.getBoundingClientRect();
const containerHalfWidth = width / 2;
const containerHeight = height;
const margin = 100 * zoom;
viewportHeight *= zoom;
viewportWidth *= zoom;
// Determine viewport boundaries for infobar.
const topBoundary = margin;
const bottomBoundary = viewportHeight - containerHeight;
const leftBoundary = containerHalfWidth;
const rightBoundary = viewportWidth - containerHalfWidth;
// Determine if an infobar's position is offscreen.
const isOffScreenOnTop = boundsBottom < topBoundary;
const isOffScreenOnBottom = boundsBottom > bottomBoundary;
const isOffScreenOnLeft = left < leftBoundary;
const isOffScreenOnRight = left > rightBoundary;
// Check if infobar is offscreen on either left/right of viewport and position.
if (isOffScreenOnLeft) {
container.style.left = `${leftBoundary + boundsLeft}px`;
arrow.setAttribute("hidden", "true");
} else if (isOffScreenOnRight) {
const leftOffset = rightBoundary - boundsRight;
container.style.left = `${rightBoundary -
leftOffset -
containerHalfWidth}px`;
arrow.setAttribute("hidden", "true");
}
// Check if infobar is offscreen on either top/bottom of viewport and position.
const bubbleArrowSize = "var(--highlighter-bubble-arrow-size)";
if (isOffScreenOnTop) {
if (boundsTop < 0) {
container.style.top = bubbleArrowSize;
} else {
container.style.top = `calc(${boundsBottom}px + ${bubbleArrowSize})`;
}
arrow.setAttribute("class", "accessible-arrow top");
} else if (isOffScreenOnBottom) {
container.style.top = `calc(${bottomBoundary}px - ${bubbleArrowSize})`;
arrow.setAttribute("hidden", "true");
} else {
container.style.top = `calc(${boundsTop}px -
(${containerHeight}px + ${bubbleArrowSize}))`;
arrow.setAttribute("class", "accessible-arrow bottom");
}
}
/**
* Build markup for XUL window infobar.
*
* @param {Element} root
* Root element to build infobar with.
*/
buildMarkup(root) {
super.buildMarkup(root, createNode);
createNode(this.win, {
parent: this.getElement("infobar"),
attributes: {
class: "arrow",
id: "arrow",
},
prefix: this.prefix,
});
}
/**
* Override of Infobar class's getTextContent method.
*
* @param {String} id
* Element ID to retrieve text content from.
* @return {String} Returns the text content of the element.
*/
getTextContent(id) {
return this.getElement(id).textContent;
}
/**
* Override of Infobar class's getElement method.
*
* @param {String} id
* Element ID.
* @return {String} Returns the specified element.
*/
getElement(id) {
return this.win.document.getElementById(`${this.prefix}${id}`);
}
/**
* Override of Infobar class's setTextContent method.
*
* @param {Element} el
* Element to set text content on.
* @param {String} text
* Text for content.
*/
setTextContent(el, text) {
el.textContent = text;
}
}
/**
* Audit component used within the accessible highlighter infobar. This component is
* responsible for rendering and updating its containing AuditReport components that
@ -834,38 +689,70 @@ class TextLabel extends AuditReport {
* width of the the accessible object
* - {Number} h
* height of the the accessible object
* - {Number} zoom
* zoom level of the accessible object's parent window
* @return {Object|null} Returns, if available, positioning and bounds information for
* the accessible object.
*/
function getBounds(win, { x, y, w, h, zoom }) {
let { mozInnerScreenX, mozInnerScreenY, scrollX, scrollY } = win;
let zoomFactor = getCurrentZoom(win);
function getBounds(win, { x, y, w, h }) {
const { mozInnerScreenX, mozInnerScreenY, scrollX, scrollY } = win;
const zoom = getCurrentZoom(win);
let left = x;
let right = x + w;
let top = y;
let bottom = y + h;
// For a XUL accessible, normalize the top-level window with its current zoom level.
// We need to do this because top-level browser content does not allow zooming.
if (zoom) {
zoomFactor = zoom;
mozInnerScreenX /= zoomFactor;
mozInnerScreenY /= zoomFactor;
scrollX /= zoomFactor;
scrollY /= zoomFactor;
}
left -= mozInnerScreenX - scrollX;
right -= mozInnerScreenX - scrollX;
top -= mozInnerScreenY - scrollY;
bottom -= mozInnerScreenY - scrollY;
left *= zoomFactor;
right *= zoomFactor;
top *= zoomFactor;
bottom *= zoomFactor;
left *= zoom;
right *= zoom;
top *= zoom;
bottom *= zoom;
const width = right - left;
const height = bottom - top;
return { left, right, top, bottom, width, height };
}
/**
* A helper function that calculate accessible object bounds and positioning to
* be used for highlighting in browser toolbox.
*
* @param {Object} win
* window that contains accessible object.
* @param {Object} options
* Object used for passing options:
* - {Number} x
* x coordinate of the top left corner of the accessible object
* - {Number} y
* y coordinate of the top left corner of the accessible object
* - {Number} w
* width of the the accessible object
* - {Number} h
* height of the the accessible object
* - {Number} zoom
* zoom level of the accessible object's parent window
* @return {Object|null} Returns, if available, positioning and bounds information for
* the accessible object.
*/
function getBoundsXUL(win, { x, y, w, h, zoom }) {
const { mozInnerScreenX, mozInnerScreenY } = win;
let left = x;
let right = x + w;
let top = y;
let bottom = y + h;
left *= zoom;
right *= zoom;
top *= zoom;
bottom *= zoom;
left -= mozInnerScreenX;
right -= mozInnerScreenX;
top -= mozInnerScreenY;
bottom -= mozInnerScreenY;
const width = right - left;
const height = bottom - top;
@ -875,5 +762,5 @@ function getBounds(win, { x, y, w, h, zoom }) {
exports.MAX_STRING_LENGTH = MAX_STRING_LENGTH;
exports.getBounds = getBounds;
exports.getBoundsXUL = getBoundsXUL;
exports.Infobar = Infobar;
exports.XULWindowInfobar = XULWindowInfobar;

View File

@ -10,6 +10,7 @@ const {
getWindowDimensions,
getViewportDimensions,
loadSheet,
removeSheet,
} = require("devtools/shared/layout/utils");
const EventEmitter = require("devtools/shared/event-emitter");
const InspectorUtils = require("InspectorUtils");
@ -40,6 +41,22 @@ exports.removePseudoClassLock = (...args) =>
const SVG_NS = "http://www.w3.org/2000/svg";
const XHTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
// Highlighter in parent process will create an iframe relative to its target
// window. We need to make sure that the iframe is styled correctly. Note:
// this styles are taken from browser/base/content/browser.css
// iframe.devtools-highlighter-renderer rules.
const XUL_HIGHLIGHTER_STYLES_SHEET = `data:text/css;charset=utf-8,
:root > iframe.devtools-highlighter-renderer {
border: none;
pointer-events: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
}`;
const STYLESHEET_URI =
"resource://devtools/server/actors/" + "highlighters.css";
@ -236,11 +253,9 @@ exports.createNode = createNode;
function CanvasFrameAnonymousContentHelper(highlighterEnv, nodeBuilder) {
this.highlighterEnv = highlighterEnv;
this.nodeBuilder = nodeBuilder;
this.anonymousContentDocument = this.highlighterEnv.document;
// XXX the next line is a wallpaper for bug 1123362.
this.anonymousContentGlobal = Cu.getGlobalForObject(
this.anonymousContentDocument
);
this._insert = this._insert.bind(this);
this._onWindowReady = this._onWindowReady.bind(this);
// Only try to create the highlighter when the document is loaded,
// otherwise, wait for the window-ready event to fire.
@ -249,7 +264,6 @@ function CanvasFrameAnonymousContentHelper(highlighterEnv, nodeBuilder) {
this._insert();
}
this._onWindowReady = this._onWindowReady.bind(this);
this.highlighterEnv.on("window-ready", this._onWindowReady);
this.listeners = new Map();
@ -259,35 +273,94 @@ function CanvasFrameAnonymousContentHelper(highlighterEnv, nodeBuilder) {
CanvasFrameAnonymousContentHelper.prototype = {
destroy() {
this._remove();
if (this._iframe) {
// If iframe is used, remove one ref count from its numberOfHighlighters
// data attribute.
const numberOfHighlighters =
parseInt(this._iframe.dataset.numberOfHighlighters, 10) - 1;
this._iframe.dataset.numberOfHighlighters = numberOfHighlighters;
// If we reached 0, we can now remove the iframe and its styling from
// target window.
if (numberOfHighlighters === 0) {
this._iframe.remove();
removeSheet(this.highlighterEnv.window, XUL_HIGHLIGHTER_STYLES_SHEET);
}
this._iframe = null;
}
this.highlighterEnv.off("window-ready", this._onWindowReady);
this.highlighterEnv = this.nodeBuilder = this._content = null;
this.anonymousContentDocument = null;
this.anonymousContentGlobal = null;
this.anonymousContentWindow = null;
this.pageListenerTarget = null;
this._removeAllListeners();
this.elements.clear();
},
_insert() {
const doc = this.highlighterEnv.document;
// Wait for DOMContentLoaded before injecting the anonymous content.
if (doc.readyState != "interactive" && doc.readyState != "complete") {
doc.addEventListener("DOMContentLoaded", this._insert.bind(this), {
once: true,
});
async _insert() {
await waitForContentLoaded(this.highlighterEnv.window);
if (!this.highlighterEnv) {
// CanvasFrameAnonymousContentHelper was already destroyed.
return;
}
// Reject XUL documents. Check that after DOMContentLoaded as we query
// documentElement which is only available after this event.
if (isXUL(this.highlighterEnv.window)) {
return;
// In order to use anonymous content, we need to create and use an IFRAME
// inside a XUL document first and use its window/document the same way we
// would normally use highlighter environment's window/document. See
// TODO: bug 1594587 for more details.
//
// Note: xul:window is not necessarily the top chrome window (as it's the
// case with about:devtools-toolbox). We need to ensure that we use the
// top chrome window to look up or create the iframe.
if (!this._iframe) {
const { documentElement } = this.highlighterEnv.window.document;
this._iframe = documentElement.querySelector(
":scope > .devtools-highlighter-renderer"
);
if (this._iframe) {
// If iframe is used and already exists, add one ref count to its
// numberOfHighlighters data attribute.
const numberOfHighlighters =
parseInt(this._iframe.dataset.numberOfHighlighters, 10) + 1;
this._iframe.dataset.numberOfHighlighters = numberOfHighlighters;
} else {
this._iframe = this.highlighterEnv.window.document.createElement(
"iframe"
);
this._iframe.classList.add("devtools-highlighter-renderer");
// If iframe is used for the first time, add ref count of one to its
// numberOfHighlighters data attribute.
this._iframe.dataset.numberOfHighlighters = 1;
documentElement.append(this._iframe);
loadSheet(this.highlighterEnv.window, XUL_HIGHLIGHTER_STYLES_SHEET);
}
}
await waitForContentLoaded(this._iframe);
if (!this.highlighterEnv) {
// CanvasFrameAnonymousContentHelper was already destroyed.
return;
}
// If it's a XUL window anonymous content will be inserted inside a newly
// created IFRAME in the chrome window.
this.anonymousContentDocument = this._iframe.contentDocument;
this.anonymousContentWindow = this._iframe.contentWindow;
this.pageListenerTarget = this._iframe.contentWindow;
} else {
// Regular highlighters are drawn inside the anonymous content of the
// highlighter environment document.
this.anonymousContentDocument = this.highlighterEnv.document;
this.anonymousContentWindow = this.highlighterEnv.window;
this.pageListenerTarget = this.highlighterEnv.pageListenerTarget;
}
// For now highlighters.css is injected in content as a ua sheet because
// we no longer support scoped style sheets (see bug 1345702).
// If it did, highlighters.css would be injected as an anonymous content
// node using CanvasFrameAnonymousContentHelper instead.
loadSheet(this.highlighterEnv.window, STYLESHEET_URI);
loadSheet(this.anonymousContentWindow, STYLESHEET_URI);
const node = this.nodeBuilder();
@ -297,7 +370,9 @@ CanvasFrameAnonymousContentHelper.prototype = {
// that scenario, fixes when we're adding anonymous content in a tab that
// is not the active one (see bug 1260043 and bug 1260044)
try {
this._content = doc.insertAnonymousContent(node);
this._content = this.anonymousContentDocument.insertAnonymousContent(
node
);
} catch (e) {
// If the `insertAnonymousContent` fails throwing a `NS_ERROR_UNEXPECTED`, it means
// we don't have access to a `CustomContentContainer` yet (see bug 1365075).
@ -306,15 +381,18 @@ CanvasFrameAnonymousContentHelper.prototype = {
// again.
if (
e.result === Cr.NS_ERROR_UNEXPECTED &&
doc.readyState === "interactive"
this.anonymousContentDocument.readyState === "interactive"
) {
// The next state change will be "complete" since the current is "interactive"
doc.addEventListener(
"readystatechange",
() => {
this._content = doc.insertAnonymousContent(node);
},
{ once: true }
await new Promise(resolve => {
this.anonymousContentDocument.addEventListener(
"readystatechange",
resolve,
{ once: true }
);
});
this._content = this.anonymousContentDocument.insertAnonymousContent(
node
);
} else {
throw e;
@ -324,8 +402,7 @@ CanvasFrameAnonymousContentHelper.prototype = {
_remove() {
try {
const doc = this.anonymousContentDocument;
doc.removeAnonymousContent(this._content);
this.anonymousContentDocument.removeAnonymousContent(this._content);
} catch (e) {
// If the current window isn't the one the content was inserted into, this
// will fail, but that's fine.
@ -343,8 +420,16 @@ CanvasFrameAnonymousContentHelper.prototype = {
if (isTopLevel) {
this._removeAllListeners();
this.elements.clear();
if (this._iframe) {
// When we are switching top level targets, we can remove the iframe and
// its styling as well, since it will be re-created for the new top
// level target document.
this._iframe.remove();
removeSheet(this.highlighterEnv.window, XUL_HIGHLIGHTER_STYLES_SHEET);
this._iframe = null;
}
this._insert();
this.anonymousContentDocument = this.highlighterEnv.document;
}
},
@ -433,7 +518,7 @@ CanvasFrameAnonymousContentHelper.prototype = {
// If no one is listening for this type of event yet, add one listener.
if (!this.listeners.has(type)) {
const target = this.highlighterEnv.pageListenerTarget;
const target = this.pageListenerTarget;
target.addEventListener(type, this, true);
// Each type entry in the map is a map of ids:handlers.
this.listeners.set(type, new Map());
@ -458,7 +543,7 @@ CanvasFrameAnonymousContentHelper.prototype = {
// If no one is listening for event type anymore, remove the listener.
if (!this.listeners.has(type)) {
const target = this.highlighterEnv.pageListenerTarget;
const target = this.pageListenerTarget;
target.removeEventListener(type, this, true);
}
},
@ -501,8 +586,8 @@ CanvasFrameAnonymousContentHelper.prototype = {
},
_removeAllListeners() {
if (this.highlighterEnv && this.highlighterEnv.pageListenerTarget) {
const target = this.highlighterEnv.pageListenerTarget;
if (this.pageListenerTarget) {
const target = this.pageListenerTarget;
for (const [type] of this.listeners) {
target.removeEventListener(type, this, true);
}
@ -599,6 +684,33 @@ CanvasFrameAnonymousContentHelper.prototype = {
};
exports.CanvasFrameAnonymousContentHelper = CanvasFrameAnonymousContentHelper;
/**
* Wait for document readyness.
* @param {Object} iframeOrWindow
* IFrame or Window for which the content should be loaded.
*/
function waitForContentLoaded(iframeOrWindow) {
let loadEvent = "DOMContentLoaded";
// If we are waiting for an iframe to load and it is for a XUL window
// highlighter that is not browser toolbox, we must wait for IFRAME's "load".
if (
iframeOrWindow.contentWindow &&
iframeOrWindow.ownerGlobal !==
iframeOrWindow.contentWindow.browsingContext.topChromeWindow
) {
loadEvent = "load";
}
const doc = iframeOrWindow.contentDocument || iframeOrWindow.document;
if (doc.readyState == "interactive" || doc.readyState == "complete") {
return Promise.resolve();
}
return new Promise(resolve => {
iframeOrWindow.addEventListener(loadEvent, resolve, { once: true });
});
}
/**
* Move the infobar to the right place in the highlighter. This helper method is utilized
* in both css-grid.js and box-model.js to help position the infobar in an appropriate

View File

@ -1,469 +0,0 @@
/* 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 {
getBounds,
XULWindowInfobar,
} = require("devtools/server/actors/highlighters/utils/accessibility");
const {
createNode,
isNodeValid,
} = require("devtools/server/actors/highlighters/utils/markup");
const { getCurrentZoom, loadSheet } = require("devtools/shared/layout/utils");
const { TEXT_NODE } = require("devtools/shared/dom-node-constants");
/**
* Stylesheet used for highlighter styling of accessible objects in chrome. It
* is consistent with the styling of an in-content accessible highlighter.
*/
const ACCESSIBLE_BOUNDS_SHEET =
"data:text/css;charset=utf-8," +
encodeURIComponent(`
.highlighter-container {
--highlighter-bubble-background-color: hsl(214, 13%, 24%);
--highlighter-bubble-border-color: rgba(255, 255, 255, 0.2);
--highlighter-bubble-arrow-size: 8px;
--grey-40: #b1b1b3;
--red-40: #ff3b6b;
--yellow-60: #d7b600;
}
.accessible-bounds {
position: fixed;
pointer-events: none;
z-index: 10;
display: block;
background-color: #6a5acd!important;
opacity: 0.6;
}
.accessible-infobar-container {
position: fixed;
max-width: 90%;
z-index: 11;
}
.accessible-infobar {
position: relative;
left: -50%;
background-color: var(--highlighter-bubble-background-color);
border: 1px solid var(--highlighter-bubble-border-color);
border-radius: 3px;
padding: 5px;
}
.accessible-arrow {
position: absolute;
width: 0;
height: 0;
border-left: var(--highlighter-bubble-arrow-size) solid transparent;
border-right: var(--highlighter-bubble-arrow-size) solid transparent;
left: calc(50% - var(--highlighter-bubble-arrow-size));
}
.top {
border-bottom: var(--highlighter-bubble-arrow-size) solid
var(--highlighter-bubble-background-color);
top: calc(-1 * var(--highlighter-bubble-arrow-size));
}
.bottom {
border-top: var(--highlighter-bubble-arrow-size) solid
var(--highlighter-bubble-background-color);
bottom: calc(-1 * var(--highlighter-bubble-arrow-size));
}
.accessible-infobar-text {
display: grid;
grid-template-areas:
"role name"
"audit audit";
grid-template-columns: min-content 1fr;
overflow: hidden;
white-space: nowrap;
justify-content: center;
}
.accessible-infobar-role {
color: #9CDCFE;
grid-area: role;
}
.accessible-infobar-name {
grid-area: name;
}
.accessible-infobar-audit {
grid-area: audit;
padding-top: 5px;
padding-bottom: 2px;
}
.accessible-infobar-name,
.accessible-infobar-audit {
color: hsl(210, 30%, 85%);
}
.accessible-infobar-audit .accessible-contrast-ratio:empty::before,
.accessible-infobar-audit .accessible-contrast-ratio:empty::after,
.accessible-infobar-name:empty {
display: none;
}
.accessible-infobar-audit .accessible-contrast-ratio::before {
content: "";
height: 8px;
width: 8px;
display: inline-flex;
background-color: var(--accessibility-highlighter-contrast-ratio-color);
box-shadow: 0 0 0 1px var(--grey-40),
4px 3px var(--accessibility-highlighter-contrast-ratio-bg),
4px 3px 0 1px var(--grey-40);
margin-inline-start: 3px;
margin-inline-end: 9px;
}
.accessible-infobar-audit .accessible-contrast-ratio::after {
margin-inline-start: 2px;
}
.accessible-infobar-audit .accessible-contrast-ratio.AA::after,
.accessible-infobar-audit .accessible-contrast-ratio.AAA::after {
color: #90E274;
}
.accessible-infobar-audit .accessible-audit::before,
.accessible-infobar-audit .accessible-contrast-ratio.FAIL::after {
display: inline-block;
width: 12px;
height: 12px;
content: "";
vertical-align: -2px;
background-position: center;
background-repeat: no-repeat;
-moz-context-properties: fill;
}
.accessible-infobar-audit .accessible-contrast-ratio.FAIL:after {
color: #E57180;
margin-inline-start: 3px;
background-image: url(chrome://devtools/skin/images/error-small.svg);
fill: var(--red-40);
}
.accessible-infobar-audit .accessible-contrast-ratio.AA::after {
content: "AA\u2713";
}
.accessible-infobar-audit .accessible-contrast-ratio.AAA::after {
content: "AAA\u2713";
}
.accessible-infobar-audit .accessible-contrast-ratio-label,
.accessible-infobar-audit .accessible-contrast-ratio-separator::before {
margin-inline-end: 3px;
}
.accessible-infobar-audit .accessible-contrast-ratio-separator::before {
content: "-";
margin-inline-start: 3px;
}
.accessible-infobar-audit .accessible-audit {
display: block;
padding-block-end: 5px;
}
.accessible-infobar-audit .accessible-audit:last-child {
padding-block-end: 0;
}
.accessible-infobar-audit .accessible-audit::before {
margin-inline-end: 4px;
background-image: none;
fill: currentColor;
}
.accessible-infobar-audit .accessible-audit.FAIL::before {
background-image: url(chrome://devtools/skin/images/error-small.svg);
fill: var(--red-40);
}
.accessible-infobar-audit .accessible-audit.WARNING::before {
background-image: url(chrome://devtools/skin/images/alert-small.svg);
fill: var(--yellow-60);
}
.accessible-infobar-audit .accessible-audit.BEST_PRACTICES::before {
background-image: url(chrome://devtools/skin/images/info-small.svg);
}
.accessible-infobar-name {
border-inline-start: 1px solid #5a6169;
margin-inline-start: 6px;
padding-inline-start: 6px;
}`);
/**
* The XULWindowAccessibleHighlighter is a class that has the same API as the
* AccessibleHighlighter, and by extension other highlighters that implement
* auto-refresh highlighter, but instead of drawing in canvas frame anonymous
* content (that is not available for chrome accessible highlighting) it adds a
* transparrent inactionable element with the same position and bounds as the
* accessible object highlighted. Unlike SimpleOutlineHighlighter, we can't use
* element (that corresponds to accessible object) itself because the accessible
* position and bounds are calculated differently.
*
* It is used when canvasframe-based AccessibleHighlighter can't be used. This
* is the case for XUL windows.
*/
class XULWindowAccessibleHighlighter {
constructor(highlighterEnv) {
this.ID_CLASS_PREFIX = "accessible-";
this.highlighterEnv = highlighterEnv;
this.win = highlighterEnv.window;
this.accessibleInfobar = new XULWindowInfobar(this);
}
/**
* Static getter that indicates that XULWindowAccessibleHighlighter supports
* highlighting in XUL windows.
*/
static get XULSupported() {
return true;
}
/**
* Build highlighter markup.
*/
_buildMarkup() {
const doc = this.win.document;
loadSheet(doc.ownerGlobal, ACCESSIBLE_BOUNDS_SHEET);
this.container = createNode(this.win, {
parent: doc.body || doc.documentElement,
attributes: {
class: "highlighter-container",
"aria-hidden": "true",
},
});
this.bounds = createNode(this.win, {
parent: this.container,
attributes: {
class: "accessible-bounds",
},
});
this.accessibleInfobar.buildMarkup(this.container);
}
/**
* Get current accessible bounds.
*
* @return {Object|null} Returns, if available, positioning and bounds
* information for the accessible object.
*/
get _bounds() {
// Zoom level for the top level browser window does not change and only inner frames
// do. So we need to get the zoom level of the current node's parent window.
const zoom = getCurrentZoom(this.currentNode);
return getBounds(this.win, { ...this.options, zoom });
}
/**
* Show the highlighter on a given accessible.
*
* @param {DOMNode} node
* A dom node that corresponds to the accessible object.
* @param {Object} options
* Object used for passing options. Available options:
* - {Number} x
* x coordinate of the top left corner of the accessible object
* - {Number} y
* y coordinate of the top left corner of the accessible object
* - {Number} w
* width of the the accessible object
* - {Number} h
* height of the the accessible object
* - duration {Number}
* Duration of time that the highlighter should be shown.
* - {String|null} name
* name of the the accessible object
* - {String} role
* role of the the accessible object
*
* @return {Boolean} True if accessible is highlighted, false otherwise.
*/
show(node, options = {}) {
const isSameNode = node === this.currentNode;
const hasBounds =
options &&
typeof options.x == "number" &&
typeof options.y == "number" &&
typeof options.w == "number" &&
typeof options.h == "number";
if (!hasBounds || !this._isNodeValid(node) || isSameNode) {
return false;
}
this.options = options;
this.currentNode = node;
return this._show();
}
/**
* Internal show method that updates bounds and tracks duration based
* highlighting.
*
* @return {Boolean} True if accessible is highlighted, false otherwise.
*/
_show() {
if (this._highlightTimer) {
clearTimeout(this._highlightTimer);
this._highlightTimer = null;
}
const shown = this._update();
const { duration } = this.options;
if (shown && duration) {
this._highlightTimer = setTimeout(() => {
this._hideAccessibleBounds();
}, duration);
}
return shown;
}
/**
* Update accessible bounds for a current accessible. Re-draw highlighter
* markup.
*
* @return {Boolean} True if accessible is highlighted, false otherwise.
*/
_update() {
this._hideAccessibleBounds();
const bounds = this._bounds;
if (!bounds) {
return false;
}
let boundsEl = this.bounds;
if (!boundsEl) {
this._buildMarkup();
boundsEl = this.bounds;
}
const { left, top, width, height } = bounds;
boundsEl.style.top = `${top}px`;
boundsEl.style.left = `${left}px`;
boundsEl.style.width = `${width}px`;
boundsEl.style.height = `${height}px`;
this._showAccessibleBounds();
this.accessibleInfobar.show();
return true;
}
/**
* Hide the highlighter
*/
hide() {
if (!this.currentNode || !this.highlighterEnv.window) {
return;
}
this._hideAccessibleBounds();
this.accessibleInfobar.hide();
this.currentNode = null;
this.options = null;
}
/**
* Check if node is a valid element or text node.
*
* @param {DOMNode} node
* The node to highlight.
* @return {Boolean} whether or not node is valid.
*/
_isNodeValid(node) {
return isNodeValid(node) || isNodeValid(node, TEXT_NODE);
}
/**
* Public API method to temporarily hide accessible bounds for things like
* color contrast calculation.
*/
hideAccessibleBounds() {
if (this.container.hasAttribute("hidden")) {
return;
}
this._hideAccessibleBounds();
this._shouldRestoreBoundsVisibility = true;
}
/**
* Public API method to show accessible bounds in case they were temporarily
* hidden.
*/
showAccessibleBounds() {
if (this._shouldRestoreBoundsVisibility) {
this._showAccessibleBounds();
}
}
/**
* Show accessible bounds highlighter.
*/
_showAccessibleBounds() {
this._shouldRestoreBoundsVisibility = null;
if (this.container) {
if (!this.currentNode || !this.highlighterEnv.window) {
return;
}
this.container.removeAttribute("hidden");
}
}
/**
* Hide accessible bounds highlighter.
*/
_hideAccessibleBounds() {
this._shouldRestoreBoundsVisibility = null;
if (this.container) {
this.container.setAttribute("hidden", "true");
}
}
/**
* Hide accessible highlighter, clean up and remove the markup.
*/
destroy() {
if (this._highlightTimer) {
clearTimeout(this._highlightTimer);
this._highlightTimer = null;
}
this.hide();
if (this.container) {
this.container.remove();
}
this.accessibleInfobar.destroy();
this.accessibleInfobar = null;
this.win = null;
}
}
exports.XULWindowAccessibleHighlighter = XULWindowAccessibleHighlighter;

View File

@ -23,9 +23,6 @@ add_task(async function() {
const {
AccessibleHighlighter,
} = require("devtools/server/actors/highlighters/accessible");
const {
XULWindowAccessibleHighlighter,
} = require("devtools/server/actors/highlighters/xul-accessible");
/**
* Get whether or not infobar container is hidden.
@ -177,10 +174,6 @@ add_task(async function() {
info("Checks for Infobar's show method");
const highlighter = new AccessibleHighlighter(env);
testInfobar(node, highlighter);
info("Checks for XULWindowInfobar's show method");
const xulWindowHighlighter = new XULWindowAccessibleHighlighter(env);
testInfobar(node, xulWindowHighlighter);
});
}
);