tabbing-order.js (6826B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const lazy = {}; 8 loader.lazyGetter( 9 lazy, 10 "ContentDOMReference", 11 () => 12 ChromeUtils.importESModule( 13 "resource://gre/modules/ContentDOMReference.sys.mjs", 14 // ContentDOMReference needs to be retrieved from the shared global 15 // since it is a shared singleton. 16 { global: "shared" } 17 ).ContentDOMReference 18 ); 19 loader.lazyRequireGetter( 20 this, 21 ["isFrameWithChildTarget", "isWindowIncluded"], 22 "resource://devtools/shared/layout/utils.js", 23 true 24 ); 25 loader.lazyRequireGetter( 26 this, 27 "NodeTabbingOrderHighlighter", 28 "resource://devtools/server/actors/highlighters/node-tabbing-order.js", 29 true 30 ); 31 32 const DEFAULT_FOCUS_FLAGS = Services.focus.FLAG_NOSCROLL; 33 34 /** 35 * The TabbingOrderHighlighter uses focus manager to traverse all focusable 36 * nodes on the page and then uses the NodeTabbingOrderHighlighter to highlight 37 * these nodes. 38 */ 39 class TabbingOrderHighlighter { 40 constructor(highlighterEnv) { 41 this.highlighterEnv = highlighterEnv; 42 this._highlighters = new Map(); 43 44 this.onMutation = this.onMutation.bind(this); 45 this.onPageHide = this.onPageHide.bind(this); 46 this.onWillNavigate = this.onWillNavigate.bind(this); 47 48 this.highlighterEnv.on("will-navigate", this.onWillNavigate); 49 50 const { pageListenerTarget } = highlighterEnv; 51 pageListenerTarget.addEventListener("pagehide", this.onPageHide); 52 } 53 54 /** 55 * Static getter that indicates that TabbingOrderHighlighter supports 56 * highlighting in XUL windows. 57 */ 58 static get XULSupported() { 59 return true; 60 } 61 62 get win() { 63 return this.highlighterEnv.window; 64 } 65 66 get focusedElement() { 67 return Services.focus.getFocusedElementForWindow(this.win, true, {}); 68 } 69 70 set focusedElement(element) { 71 Services.focus.setFocus(element, DEFAULT_FOCUS_FLAGS); 72 } 73 74 moveFocus(startElement) { 75 return Services.focus.moveFocus( 76 this.win, 77 startElement.nodeType === Node.DOCUMENT_NODE 78 ? startElement.documentElement 79 : startElement, 80 Services.focus.MOVEFOCUS_FORWARD, 81 DEFAULT_FOCUS_FLAGS 82 ); 83 } 84 85 /** 86 * Show NodeTabbingOrderHighlighter on each node that belongs to the keyboard 87 * tabbing order. 88 * 89 * @param {DOMNode} startElm 90 * Starting element to calculate tabbing order from. 91 * 92 * @param {JSON} options 93 * - options.index 94 * Start index for the tabbing order. Starting index will be 0 at 95 * the start of the tabbing order highlighting; in remote frames 96 * starting index will, typically, be greater than 0 (unless there 97 * was nothing to focus in the top level content document prior to 98 * the remote frame). 99 */ 100 async show(startElm, { index }) { 101 const focusableElements = []; 102 const originalFocusedElement = this.focusedElement; 103 let currentFocusedElement = this.moveFocus(startElm); 104 while ( 105 currentFocusedElement && 106 isWindowIncluded(this.win, currentFocusedElement.ownerGlobal) 107 ) { 108 focusableElements.push(currentFocusedElement); 109 currentFocusedElement = this.moveFocus(currentFocusedElement); 110 } 111 112 // Allow to flush pending notifications to ensure the PresShell and frames 113 // are updated. 114 await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); 115 let endElm = this.focusedElement; 116 if ( 117 currentFocusedElement && 118 !isWindowIncluded(this.win, currentFocusedElement.ownerGlobal) 119 ) { 120 endElm = null; 121 } 122 123 if ( 124 !endElm && 125 !!focusableElements.length && 126 isFrameWithChildTarget( 127 this.highlighterEnv.targetActor, 128 focusableElements[focusableElements.length - 1] 129 ) 130 ) { 131 endElm = focusableElements[focusableElements.length - 1]; 132 } 133 134 if (originalFocusedElement && originalFocusedElement !== endElm) { 135 this.focusedElement = originalFocusedElement; 136 } 137 138 const highlighters = []; 139 for (let i = 0; i < focusableElements.length; i++) { 140 highlighters.push( 141 this._accumulateHighlighter(focusableElements[i], index++) 142 ); 143 } 144 await Promise.all(highlighters); 145 146 this._trackMutations(); 147 148 return { 149 contentDOMReference: endElm && lazy.ContentDOMReference.get(endElm), 150 index, 151 }; 152 } 153 154 async _accumulateHighlighter(node, index) { 155 const highlighter = new NodeTabbingOrderHighlighter(this.highlighterEnv); 156 await highlighter.isReady; 157 158 highlighter.show(node, { index: index + 1 }); 159 this._highlighters.set(node, highlighter); 160 } 161 162 hide() { 163 this._untrackMutations(); 164 for (const highlighter of this._highlighters.values()) { 165 highlighter.destroy(); 166 } 167 168 this._highlighters.clear(); 169 } 170 171 /** 172 * Track mutations in the top level document subtree so that the appropriate 173 * NodeTabbingOrderHighlighter infobar's could be updated to reflect the 174 * attribute mutations on relevant nodes. 175 */ 176 _trackMutations() { 177 const { win } = this; 178 this.currentMutationObserver = new win.MutationObserver(this.onMutation); 179 this.currentMutationObserver.observe(win.document.documentElement, { 180 subtree: true, 181 attributes: true, 182 }); 183 } 184 185 _untrackMutations() { 186 if (!this.currentMutationObserver) { 187 return; 188 } 189 190 this.currentMutationObserver.disconnect(); 191 this.currentMutationObserver = null; 192 } 193 194 onMutation(mutationList) { 195 for (const { target } of mutationList) { 196 const highlighter = this._highlighters.get(target); 197 if (highlighter) { 198 highlighter.update(); 199 } 200 } 201 } 202 203 /** 204 * Update NodeTabbingOrderHighlighter focus styling for a node that, 205 * potentially, belongs to the tabbing order. 206 * 207 * @param {object} options 208 * Options specifying the node and its focused state. 209 */ 210 updateFocus({ node, focused }) { 211 const highlighter = this._highlighters.get(node); 212 if (!highlighter) { 213 return; 214 } 215 216 highlighter.updateFocus(focused); 217 } 218 219 destroy() { 220 this.highlighterEnv.off("will-navigate", this.onWillNavigate); 221 222 const { pageListenerTarget } = this.highlighterEnv; 223 if (pageListenerTarget) { 224 pageListenerTarget.removeEventListener("pagehide", this.onPageHide); 225 } 226 227 this.hide(); 228 this.highlighterEnv = null; 229 } 230 231 onPageHide({ target }) { 232 // If a pagehide event is triggered for current window's highlighter, hide 233 // the highlighter. 234 if (target.defaultView === this.win) { 235 this.hide(); 236 } 237 } 238 239 onWillNavigate({ isTopLevel }) { 240 if (isTopLevel) { 241 this.hide(); 242 } 243 } 244 } 245 246 exports.TabbingOrderHighlighter = TabbingOrderHighlighter;