diff --git a/devtools/server/actors/accessibility/accessible.js b/devtools/server/actors/accessibility/accessible.js index 44d6317520be..da6e6cc90b72 100644 --- a/devtools/server/actors/accessibility/accessible.js +++ b/devtools/server/actors/accessibility/accessible.js @@ -241,7 +241,7 @@ const AccessibleActor = ActorClassWithSpec(accessibleSpec, { let targetAcc; try { - targetAcc = this.walker.attachAccessible(target, doc); + targetAcc = this.walker.attachAccessible(target, doc.rawAccessible); } catch (e) { // Target is not available. } diff --git a/devtools/server/actors/accessibility/walker.js b/devtools/server/actors/accessibility/walker.js index 37916e0a413c..3343d5acacd1 100644 --- a/devtools/server/actors/accessibility/walker.js +++ b/devtools/server/actors/accessibility/walker.js @@ -13,12 +13,16 @@ loader.lazyRequireGetter(this, "AccessibleActor", "devtools/server/actors/access loader.lazyRequireGetter(this, "CustomHighlighterActor", "devtools/server/actors/highlighters", true); loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/shared/DevToolsUtils"); loader.lazyRequireGetter(this, "events", "devtools/shared/event-emitter"); +loader.lazyRequireGetter(this, "getCurrentZoom", "devtools/shared/layout/utils", true); +loader.lazyRequireGetter(this, "InspectorUtils", "InspectorUtils"); loader.lazyRequireGetter(this, "isDefunct", "devtools/server/actors/utils/accessibility", true); loader.lazyRequireGetter(this, "isTypeRegistered", "devtools/server/actors/highlighters", true); loader.lazyRequireGetter(this, "isWindowIncluded", "devtools/shared/layout/utils", true); loader.lazyRequireGetter(this, "isXUL", "devtools/server/actors/highlighters/utils/markup", true); loader.lazyRequireGetter(this, "register", "devtools/server/actors/highlighters", true); +const kStateHover = 0x00000004; // NS_EVENT_STATE_HOVER + const nsIAccessibleEvent = Ci.nsIAccessibleEvent; const nsIAccessibleStateChangeEvent = Ci.nsIAccessibleStateChangeEvent; const nsIAccessibleRole = Ci.nsIAccessibleRole; @@ -126,6 +130,7 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, { this.setA11yServiceGetter(); this.onPick = this.onPick.bind(this); this.onHovered = this.onHovered.bind(this); + this._preventContentEvent = this._preventContentEvent.bind(this); this.onKey = this.onKey.bind(this); this.onHighlighterEvent = this.onHighlighterEvent.bind(this); }, @@ -513,6 +518,24 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, { _preventContentEvent(event) { event.stopPropagation(); event.preventDefault(); + + const target = event.originalTarget || event.target; + if (target !== this._currentTarget) { + this._resetStateAndReleaseTarget(); + this._currentTarget = target; + // We use InspectorUtils to save the original hover content state of the target + // element (that includes its hover state). In order to not trigger any visual + // changes to the element that depend on its hover state we remove the state while + // the element is the most current target of the highlighter. + // + // TODO: This logic can be removed if/when we can use elementsAtPoint API for + // determining topmost DOMNode that corresponds to specific coordinates. We would + // then be able to use a highlighter overlay that would prevent all pointer events + // to content but still render highlighter for the node/element correctly. + this._currentTargetHoverState = + InspectorUtils.getContentState(target) & kStateHover; + InspectorUtils.removeContentState(target, kStateHover); + } }, /** @@ -521,7 +544,7 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, { * @param {Object} event * Current click event. */ - async onPick(event) { + onPick(event) { if (!this._isPicking) { return; } @@ -535,16 +558,16 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, { // the client, but don't stop picking. if (event.shiftKey) { if (!this._currentAccessible) { - this._currentAccessible = await this._findAndAttachAccessible(event); + this._currentAccessible = this._findAndAttachAccessible(event); } events.emit(this, "picker-accessible-previewed", this._currentAccessible); return; } - this._stopPickerListeners(); + this._unsetPickerEnvironment(); this._isPicking = false; if (!this._currentAccessible) { - this._currentAccessible = await this._findAndAttachAccessible(event); + this._currentAccessible = this._findAndAttachAccessible(event); } events.emit(this, "picker-accessible-picked", this._currentAccessible); }, @@ -555,7 +578,7 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, { * @param {Object} event * Current hover event. */ - async onHovered(event) { + onHovered(event) { if (!this._isPicking) { return; } @@ -565,7 +588,7 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, { return; } - const accessible = await this._findAndAttachAccessible(event); + const accessible = this._findAndAttachAccessible(event); if (!accessible) { return; } @@ -626,7 +649,7 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, { pick: function() { if (!this._isPicking) { this._isPicking = true; - this._startPickerListeners(); + this._setPickerEnvironment(); } }, @@ -651,7 +674,7 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, { // defunct and accessing it via parent property will throw. try { let parent = accessible; - while (parent && parent != accessibleDocument) { + while (parent && parent.rawAccessible != accessibleDocument) { parent = parent.parentAcc; } } catch (error) { @@ -662,39 +685,59 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, { }, /** - * Find accessible object that corresponds to a DOMNode and attach (lookup its - * ancestry to the root doc) to the AccessibilityWalker tree. + * When RDM is used, users can set custom DPR values that are different from the device + * they are using. Store true screenPixelsPerCSSPixel value to be able to use accessible + * highlighter features correctly. + */ + get pixelRatio() { + const { contentViewer } = this.targetActor.docShell; + const { windowUtils } = this.rootWin; + const overrideDPPX = contentViewer.overrideDPPX; + let ratio; + if (overrideDPPX) { + contentViewer.overrideDPPX = 0; + ratio = windowUtils.screenPixelsPerCSSPixel; + contentViewer.overrideDPPX = overrideDPPX; + } else { + ratio = windowUtils.screenPixelsPerCSSPixel; + } + + return ratio; + }, + + /** + * Find deepest accessible object that corresponds to the screen coordinates of the + * mouse pointer and attach it to the AccessibilityWalker tree. * * @param {Object} event * Correspoinding content event. * @return {null|Object} * Accessible object, if available, that corresponds to a DOM node. */ - async _findAndAttachAccessible(event) { - let target = event.originalTarget || event.target; - let rawAccessible; - // Find a first accessible object in the target's ancestry, including - // target. Note: not all DOM nodes have corresponding accessible objects - // (for example, a
element that is used as a container for other - // things) thus we need to find one that does. - while (!rawAccessible && target) { - rawAccessible = this.getRawAccessibleFor(target); - target = target.parentNode; - } - - const doc = await this.getDocument(); - return this.attachAccessible(rawAccessible, doc); + _findAndAttachAccessible(event) { + const target = event.originalTarget || event.target; + const docAcc = this.getRawAccessibleFor(this.rootDoc); + const win = target.ownerGlobal; + const scale = this.pixelRatio / getCurrentZoom(win); + const rawAccessible = docAcc.getDeepestChildAtPoint( + event.screenX * scale, + event.screenY * scale); + return this.attachAccessible(rawAccessible, docAcc); }, /** * Start picker content listeners. */ - _startPickerListeners: function() { + _setPickerEnvironment: function() { const target = this.targetActor.chromeEventHandler; target.addEventListener("mousemove", this.onHovered, true); target.addEventListener("click", this.onPick, true); target.addEventListener("mousedown", this._preventContentEvent, true); target.addEventListener("mouseup", this._preventContentEvent, true); + target.addEventListener("mouseover", this._preventContentEvent, true); + target.addEventListener("mouseout", this._preventContentEvent, true); + target.addEventListener("mouseleave", this._preventContentEvent, true); + target.addEventListener("mouseenter", this._preventContentEvent, true); target.addEventListener("dblclick", this._preventContentEvent, true); target.addEventListener("keydown", this.onKey, true); target.addEventListener("keyup", this._preventContentEvent, true); @@ -703,7 +746,7 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, { /** * If content is still alive, stop picker content listeners. */ - _stopPickerListeners: function() { + _unsetPickerEnvironment: function() { const target = this.targetActor.chromeEventHandler; if (!target) { @@ -714,9 +757,42 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, { target.removeEventListener("click", this.onPick, true); target.removeEventListener("mousedown", this._preventContentEvent, true); target.removeEventListener("mouseup", this._preventContentEvent, true); + target.removeEventListener("mouseover", this._preventContentEvent, true); + target.removeEventListener("mouseout", this._preventContentEvent, true); + target.removeEventListener("mouseleave", this._preventContentEvent, true); + target.removeEventListener("mouseenter", this._preventContentEvent, true); target.removeEventListener("dblclick", this._preventContentEvent, true); target.removeEventListener("keydown", this.onKey, true); target.removeEventListener("keyup", this._preventContentEvent, true); + + this._resetStateAndReleaseTarget(); + }, + + /** + * When using accessibility highlighter, we keep track of the most current event pointer + * event target. In order to update or release the target, we need to make sure we set + * the content state (using InspectorUtils) to its original value. + * + * TODO: This logic can be removed if/when we can use elementsAtPoint API for + * determining topmost DOMNode that corresponds to specific coordinates. We would then + * be able to use a highlighter overlay that would prevent all pointer events to content + * but still render highlighter for the node/element correctly. + */ + _resetStateAndReleaseTarget() { + if (!this._currentTarget) { + return; + } + + try { + if (this._currentTargetHoverState) { + InspectorUtils.setContentState(this._currentTarget, kStateHover); + } + } catch (e) { + // DOMNode is already dead. + } + + this._currentTarget = null; + this._currentTargetState = null; }, /** @@ -728,7 +804,7 @@ const AccessibleWalkerActor = ActorClassWithSpec(accessibleWalkerSpec, { } if (this._isPicking) { - this._stopPickerListeners(); + this._unsetPickerEnvironment(); this._isPicking = false; this._currentAccessible = null; }