AccessibilityTree.js (8616B)
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 connect, 16 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 17 18 const TreeView = createFactory( 19 ChromeUtils.importESModule( 20 "resource://devtools/client/shared/components/tree/TreeView.mjs" 21 ).default 22 ); 23 // Reps 24 const { MODE } = ChromeUtils.importESModule( 25 "resource://devtools/client/shared/components/reps/index.mjs" 26 ); 27 28 const { 29 fetchChildren, 30 } = require("resource://devtools/client/accessibility/actions/accessibles.js"); 31 32 const { 33 L10N, 34 } = require("resource://devtools/client/accessibility/utils/l10n.js"); 35 const { 36 isFiltered, 37 } = require("resource://devtools/client/accessibility/utils/audit.js"); 38 const AccessibilityRow = createFactory( 39 require("resource://devtools/client/accessibility/components/AccessibilityRow.js") 40 ); 41 const AccessibilityRowValue = createFactory( 42 require("resource://devtools/client/accessibility/components/AccessibilityRowValue.js") 43 ); 44 const { 45 Provider, 46 } = require("resource://devtools/client/accessibility/provider.js"); 47 48 const { scrollIntoView } = ChromeUtils.importESModule( 49 "resource://devtools/client/shared/scroll.mjs" 50 ); 51 52 /** 53 * Renders Accessibility panel tree. 54 */ 55 class AccessibilityTree extends Component { 56 static get propTypes() { 57 return { 58 toolboxDoc: PropTypes.object.isRequired, 59 dispatch: PropTypes.func.isRequired, 60 accessibles: PropTypes.object, 61 expanded: PropTypes.object, 62 selected: PropTypes.string, 63 highlighted: PropTypes.object, 64 filtered: PropTypes.bool, 65 getAccessibilityTreeRoot: PropTypes.func.isRequired, 66 startListeningForAccessibilityEvents: PropTypes.func.isRequired, 67 stopListeningForAccessibilityEvents: PropTypes.func.isRequired, 68 highlightAccessible: PropTypes.func.isRequired, 69 unhighlightAccessible: PropTypes.func.isRequired, 70 }; 71 } 72 73 constructor(props) { 74 super(props); 75 76 this.onNameChange = this.onNameChange.bind(this); 77 this.onReorder = this.onReorder.bind(this); 78 this.onTextChange = this.onTextChange.bind(this); 79 this.renderValue = this.renderValue.bind(this); 80 this.scrollSelectedRowIntoView = this.scrollSelectedRowIntoView.bind(this); 81 } 82 83 /** 84 * Add accessibility event listeners that affect tree rendering and updates. 85 */ 86 // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 87 UNSAFE_componentWillMount() { 88 this.props.startListeningForAccessibilityEvents({ 89 reorder: this.onReorder, 90 "name-change": this.onNameChange, 91 "text-change": this.onTextChange, 92 }); 93 window.on( 94 EVENTS.NEW_ACCESSIBLE_FRONT_INSPECTED, 95 this.scrollSelectedRowIntoView 96 ); 97 return null; 98 } 99 100 componentDidUpdate(prevProps) { 101 // When filtering is toggled, make sure that the selected row remains in 102 // view. 103 if (this.props.filtered !== prevProps.filtered) { 104 this.scrollSelectedRowIntoView(); 105 } 106 107 window.emit(EVENTS.ACCESSIBILITY_INSPECTOR_UPDATED); 108 } 109 110 /** 111 * Remove accessible event listeners. 112 */ 113 componentWillUnmount() { 114 this.props.stopListeningForAccessibilityEvents({ 115 reorder: this.onReorder, 116 "name-change": this.onNameChange, 117 "text-change": this.onTextChange, 118 }); 119 120 window.off( 121 EVENTS.NEW_ACCESSIBLE_FRONT_INSPECTED, 122 this.scrollSelectedRowIntoView 123 ); 124 } 125 126 /** 127 * Handle accessible reorder event. If the accessible is cached and rendered 128 * within the accessibility tree, re-fetch its children and re-render the 129 * corresponding subtree. 130 * 131 * @param {object} accessibleFront 132 * accessible front that had its subtree reordered. 133 */ 134 onReorder(accessibleFront) { 135 if (this.props.accessibles.has(accessibleFront.actorID)) { 136 this.props.dispatch(fetchChildren(accessibleFront)); 137 } 138 } 139 140 scrollSelectedRowIntoView() { 141 const { treeview } = this.refs; 142 if (!treeview) { 143 return; 144 } 145 146 const treeEl = treeview.treeRef.current; 147 if (!treeEl) { 148 return; 149 } 150 151 const selected = treeEl.ownerDocument.querySelector( 152 ".treeTable .treeRow.selected" 153 ); 154 if (selected) { 155 scrollIntoView(selected, { center: true }); 156 } 157 } 158 159 /** 160 * Handle accessible name change event. If the name of an accessible changes 161 * and that accessible is cached and rendered within the accessibility tree, 162 * re-fetch its parent's children and re-render the corresponding subtree. 163 * 164 * @param {object} accessibleFront 165 * accessible front that had its name changed. 166 * @param {object} parentFront 167 * optional parent accessible front. Note: if it parent is not 168 * present, we assume that the top level document's name has changed 169 * and use accessible walker as a parent. 170 */ 171 onNameChange(accessibleFront, parentFront) { 172 const { accessibles, dispatch } = this.props; 173 const accessibleWalkerFront = accessibleFront.getParent(); 174 parentFront = parentFront || accessibleWalkerFront; 175 176 if ( 177 accessibles.has(accessibleFront.actorID) || 178 accessibles.has(parentFront.actorID) 179 ) { 180 dispatch(fetchChildren(parentFront)); 181 } 182 } 183 184 /** 185 * Handle accessible text change (change/insert/remove) event. If the text of 186 * an accessible changes and that accessible is cached and rendered within the 187 * accessibility tree, re-fetch its children and re-render the corresponding 188 * subtree. 189 * 190 * @param {object} accessibleFront 191 * accessible front that had its child text changed. 192 */ 193 onTextChange(accessibleFront) { 194 const { accessibles, dispatch } = this.props; 195 if (accessibles.has(accessibleFront.actorID)) { 196 dispatch(fetchChildren(accessibleFront)); 197 } 198 } 199 200 renderValue(props) { 201 return AccessibilityRowValue(props); 202 } 203 204 /** 205 * Render Accessibility panel content 206 */ 207 render() { 208 const columns = [ 209 { 210 id: "default", 211 title: L10N.getStr("accessibility.role"), 212 }, 213 { 214 id: "value", 215 title: L10N.getStr("accessibility.name"), 216 }, 217 ]; 218 219 const { 220 accessibles, 221 dispatch, 222 expanded, 223 selected, 224 highlighted: highlightedItem, 225 toolboxDoc, 226 filtered, 227 getAccessibilityTreeRoot, 228 highlightAccessible, 229 unhighlightAccessible, 230 } = this.props; 231 232 const renderRow = rowProps => { 233 const { object } = rowProps.member; 234 const highlighted = object === highlightedItem; 235 return AccessibilityRow( 236 Object.assign({}, rowProps, { 237 toolboxDoc, 238 highlighted, 239 decorator: { 240 getRowClass() { 241 return highlighted ? ["highlighted"] : []; 242 }, 243 }, 244 highlightAccessible, 245 unhighlightAccessible, 246 }) 247 ); 248 }; 249 const className = filtered ? "filtered" : undefined; 250 251 return TreeView({ 252 ref: "treeview", 253 object: getAccessibilityTreeRoot(), 254 mode: MODE.SHORT, 255 provider: new Provider(accessibles, filtered, dispatch), 256 columns, 257 className, 258 renderValue: this.renderValue, 259 renderRow, 260 label: L10N.getStr("accessibility.treeName"), 261 header: true, 262 expandedNodes: expanded, 263 selected, 264 onClickRow(nodePath, event) { 265 if (event.target.classList.contains("theme-twisty")) { 266 this.toggle(nodePath); 267 } 268 269 this.selectRow( 270 this.rows.find(row => row.props.member.path === nodePath), 271 { preventAutoScroll: true } 272 ); 273 274 return true; 275 }, 276 onContextMenuTree(e) { 277 // If context menu event is triggered on (or bubbled to) the TreeView, it was 278 // done via keyboard. Open context menu for currently selected row. 279 let row = this.getSelectedRow(); 280 if (!row) { 281 return; 282 } 283 284 row = row.getWrappedInstance(); 285 row.onContextMenu(e); 286 }, 287 }); 288 } 289 } 290 291 const mapStateToProps = ({ 292 accessibles, 293 ui: { expanded, selected, highlighted }, 294 audit: { filters }, 295 }) => ({ 296 accessibles, 297 expanded, 298 selected, 299 highlighted, 300 filtered: isFiltered(filters), 301 }); 302 // Exports from this module 303 module.exports = connect(mapStateToProps)(AccessibilityTree);