AccessibilityRow.js (8504B)
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 "use strict"; 5 6 /* global EVENTS */ 7 8 // React & Redux 9 const { 10 Component, 11 createFactory, 12 } = require("resource://devtools/client/shared/vendor/react.mjs"); 13 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 14 const { 15 findDOMNode, 16 } = require("resource://devtools/client/shared/vendor/react-dom.mjs"); 17 const { 18 connect, 19 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 20 21 const TreeRow = ChromeUtils.importESModule( 22 "resource://devtools/client/shared/components/tree/TreeRow.mjs", 23 { global: "current" } 24 ).default; 25 const AuditFilter = createFactory( 26 require("resource://devtools/client/accessibility/components/AuditFilter.js") 27 ); 28 const AuditController = createFactory( 29 require("resource://devtools/client/accessibility/components/AuditController.js") 30 ); 31 32 // Utils 33 const { 34 flashElementOn, 35 flashElementOff, 36 } = require("resource://devtools/client/inspector/markup/utils.js"); 37 const { openDocLink } = require("resource://devtools/client/shared/link.js"); 38 const { 39 PREFS, 40 VALUE_FLASHING_DURATION, 41 VALUE_HIGHLIGHT_DURATION, 42 } = require("resource://devtools/client/accessibility/constants.js"); 43 44 const nodeConstants = require("resource://devtools/shared/dom-node-constants.js"); 45 46 // Actions 47 const { 48 updateDetails, 49 } = require("resource://devtools/client/accessibility/actions/details.js"); 50 const { 51 unhighlight, 52 } = require("resource://devtools/client/accessibility/actions/accessibles.js"); 53 54 const { 55 L10N, 56 } = require("resource://devtools/client/accessibility/utils/l10n.js"); 57 58 loader.lazyRequireGetter( 59 this, 60 "Menu", 61 "resource://devtools/client/framework/menu.js" 62 ); 63 loader.lazyRequireGetter( 64 this, 65 "MenuItem", 66 "resource://devtools/client/framework/menu-item.js" 67 ); 68 69 const { scrollIntoView } = ChromeUtils.importESModule( 70 "resource://devtools/client/shared/scroll.mjs" 71 ); 72 73 const JSON_URL_PREFIX = "data:application/json;charset=UTF-8,"; 74 75 class HighlightableTreeRowClass extends TreeRow { 76 shouldComponentUpdate(nextProps) { 77 const shouldTreeRowUpdate = super.shouldComponentUpdate(nextProps); 78 if (shouldTreeRowUpdate) { 79 return shouldTreeRowUpdate; 80 } 81 82 if ( 83 nextProps.highlighted !== this.props.highlighted || 84 nextProps.filtered !== this.props.filtered 85 ) { 86 return true; 87 } 88 89 return false; 90 } 91 } 92 93 const HighlightableTreeRow = createFactory(HighlightableTreeRowClass); 94 95 // Component that expands TreeView's own TreeRow and is responsible for 96 // rendering an accessible object. 97 class AccessibilityRow extends Component { 98 static get propTypes() { 99 return { 100 ...TreeRow.propTypes, 101 dispatch: PropTypes.func.isRequired, 102 toolboxDoc: PropTypes.object.isRequired, 103 scrollContentNodeIntoView: PropTypes.bool.isRequired, 104 highlightAccessible: PropTypes.func.isRequired, 105 unhighlightAccessible: PropTypes.func.isRequired, 106 }; 107 } 108 109 componentDidMount() { 110 const { 111 member: { selected, object }, 112 scrollContentNodeIntoView, 113 } = this.props; 114 if (selected) { 115 this.unhighlight(object); 116 this.update(); 117 this.highlight( 118 object, 119 { duration: VALUE_HIGHLIGHT_DURATION }, 120 scrollContentNodeIntoView 121 ); 122 } 123 124 if (this.props.highlighted) { 125 this.scrollIntoView(); 126 } 127 } 128 129 /** 130 * Update accessible object details that are going to be rendered inside the 131 * accessible panel sidebar. 132 */ 133 componentDidUpdate(prevProps) { 134 const { 135 member: { selected, object }, 136 scrollContentNodeIntoView, 137 } = this.props; 138 // If row is selected, update corresponding accessible details. 139 if (!prevProps.member.selected && selected) { 140 this.unhighlight(object); 141 this.update(); 142 this.highlight( 143 object, 144 { duration: VALUE_HIGHLIGHT_DURATION }, 145 scrollContentNodeIntoView 146 ); 147 } 148 149 if (this.props.highlighted) { 150 this.scrollIntoView(); 151 } 152 153 if (!selected && prevProps.member.value !== this.props.member.value) { 154 this.flashValue(); 155 } 156 } 157 158 scrollIntoView() { 159 const row = findDOMNode(this); 160 // Row might not be rendered in the DOM tree if it is filtered out during 161 // audit. 162 if (!row) { 163 return; 164 } 165 166 scrollIntoView(row); 167 } 168 169 update() { 170 const { 171 dispatch, 172 member: { object }, 173 } = this.props; 174 if (!object.actorID) { 175 return; 176 } 177 178 dispatch(updateDetails(object)); 179 window.emit(EVENTS.NEW_ACCESSIBLE_FRONT_SELECTED, object); 180 } 181 182 flashValue() { 183 const row = findDOMNode(this); 184 // Row might not be rendered in the DOM tree if it is filtered out during 185 // audit. 186 if (!row) { 187 return; 188 } 189 190 const value = row.querySelector(".objectBox"); 191 192 flashElementOn(value); 193 if (this._flashMutationTimer) { 194 clearTimeout(this._flashMutationTimer); 195 this._flashMutationTimer = null; 196 } 197 this._flashMutationTimer = setTimeout(() => { 198 flashElementOff(value); 199 }, VALUE_FLASHING_DURATION); 200 } 201 202 /** 203 * Scroll the node that corresponds to a current accessible object into view. 204 * 205 * @param {object} 206 * Accessible front that is rendered for this node. 207 * 208 * @returns {Promise} 209 * Promise that resolves when the node is scrolled into view if 210 * possible. 211 */ 212 async scrollNodeIntoViewIfNeeded(accessibleFront) { 213 if (accessibleFront.isDestroyed()) { 214 return; 215 } 216 217 const domWalker = (await accessibleFront.targetFront.getFront("inspector")) 218 .walker; 219 if (accessibleFront.isDestroyed()) { 220 return; 221 } 222 223 const node = await domWalker.getNodeFromActor(accessibleFront.actorID, [ 224 "rawAccessible", 225 "DOMNode", 226 ]); 227 if (!node) { 228 return; 229 } 230 231 if (node.nodeType == nodeConstants.ELEMENT_NODE) { 232 await node.scrollIntoView(); 233 } else if (node.nodeType != nodeConstants.DOCUMENT_NODE) { 234 // scrollIntoView method is only part of the Element interface, in cases 235 // where node is a text node (and not a document node) scroll into view 236 // its parent. 237 await node.parentNode().scrollIntoView(); 238 } 239 } 240 241 async highlight(accessibleFront, options, scrollContentNodeIntoView) { 242 this.props.dispatch(unhighlight()); 243 // If necessary scroll the node into view before showing the accessibility 244 // highlighter. 245 if (scrollContentNodeIntoView) { 246 await this.scrollNodeIntoViewIfNeeded(accessibleFront); 247 } 248 249 this.props.highlightAccessible(accessibleFront, options); 250 } 251 252 unhighlight(accessibleFront) { 253 this.props.dispatch(unhighlight()); 254 this.props.unhighlightAccessible(accessibleFront); 255 } 256 257 async printToJSON() { 258 Glean.devtoolsAccessibility.accessibleContextMenuItemActivated[ 259 "print-to-json" 260 ].add(1); 261 262 const snapshot = await this.props.member.object.snapshot(); 263 openDocLink( 264 `${JSON_URL_PREFIX}${encodeURIComponent(JSON.stringify(snapshot))}` 265 ); 266 } 267 268 onContextMenu(e) { 269 e.stopPropagation(); 270 e.preventDefault(); 271 272 if (!this.props.toolboxDoc) { 273 return; 274 } 275 276 const menu = new Menu({ id: "accessibility-row-contextmenu" }); 277 menu.append( 278 new MenuItem({ 279 id: "menu-printtojson", 280 label: L10N.getStr("accessibility.tree.menu.printToJSON"), 281 click: () => this.printToJSON(), 282 }) 283 ); 284 285 menu.popup(e.screenX, e.screenY, this.props.toolboxDoc); 286 287 Glean.devtoolsAccessibility.accessibleContextMenuOpened.add(1); 288 } 289 290 /** 291 * Render accessible row component. 292 * 293 * @returns acecssible-row React component. 294 */ 295 render() { 296 const { member } = this.props; 297 const props = { 298 ...this.props, 299 onContextMenu: e => this.onContextMenu(e), 300 onMouseOver: () => this.highlight(member.object), 301 onMouseOut: () => this.unhighlight(member.object), 302 key: `${member.path}-${member.active ? "active" : "inactive"}`, 303 }; 304 305 return AuditController( 306 { 307 accessibleFront: member.object, 308 }, 309 AuditFilter({}, HighlightableTreeRow(props)) 310 ); 311 } 312 } 313 314 const mapStateToProps = ({ 315 ui: { [PREFS.SCROLL_INTO_VIEW]: scrollContentNodeIntoView }, 316 }) => ({ 317 scrollContentNodeIntoView, 318 }); 319 320 module.exports = connect(mapStateToProps, null, null, { withRef: true })( 321 AccessibilityRow 322 );