ToolbarBadgeHub.sys.mjs (8952B)
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 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 EveryWindow: "resource:///modules/EveryWindow.sys.mjs", 9 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 10 clearTimeout: "resource://gre/modules/Timer.sys.mjs", 11 requestIdleCallback: "resource://gre/modules/Timer.sys.mjs", 12 setTimeout: "resource://gre/modules/Timer.sys.mjs", 13 }); 14 15 let notificationsByWindow = new WeakMap(); 16 17 export class _ToolbarBadgeHub { 18 constructor() { 19 this.id = "toolbar-badge-hub"; 20 this.state = {}; 21 this.removeAllNotifications = this.removeAllNotifications.bind(this); 22 this.removeToolbarNotification = this.removeToolbarNotification.bind(this); 23 this.addToolbarNotification = this.addToolbarNotification.bind(this); 24 this.registerBadgeToAllWindows = this.registerBadgeToAllWindows.bind(this); 25 this._sendPing = this._sendPing.bind(this); 26 this.sendUserEventTelemetry = this.sendUserEventTelemetry.bind(this); 27 28 this._handleMessageRequest = null; 29 this._addImpression = null; 30 this._blockMessageById = null; 31 this._sendTelemetry = null; 32 this._initialized = false; 33 } 34 35 async init( 36 waitForInitialized, 37 { 38 handleMessageRequest, 39 addImpression, 40 blockMessageById, 41 unblockMessageById, 42 sendTelemetry, 43 } 44 ) { 45 if (this._initialized) { 46 return; 47 } 48 49 this._initialized = true; 50 this._handleMessageRequest = handleMessageRequest; 51 this._blockMessageById = blockMessageById; 52 this._unblockMessageById = unblockMessageById; 53 this._addImpression = addImpression; 54 this._sendTelemetry = sendTelemetry; 55 // Need to wait for ASRouter to initialize before trying to fetch messages 56 await waitForInitialized; 57 this.messageRequest({ 58 triggerId: "toolbarBadgeUpdate", 59 template: "toolbar_badge", 60 }); 61 } 62 63 maybeInsertFTL(win) { 64 win.MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl"); 65 } 66 67 _clearBadgeTimeout() { 68 if (this.state.showBadgeTimeoutId) { 69 lazy.clearTimeout(this.state.showBadgeTimeoutId); 70 } 71 } 72 73 removeAllNotifications(event) { 74 if (event) { 75 // ignore right clicks 76 if ( 77 (event.type === "mousedown" || event.type === "click") && 78 event.button !== 0 79 ) { 80 return; 81 } 82 // ignore keyboard access that is not one of the usual accessor keys 83 if ( 84 event.type === "keypress" && 85 event.key !== " " && 86 event.key !== "Enter" 87 ) { 88 return; 89 } 90 91 event.target.removeEventListener( 92 "mousedown", 93 this.removeAllNotifications 94 ); 95 event.target.removeEventListener("keypress", this.removeAllNotifications); 96 // If we have an event it means the user interacted with the badge 97 // we should send telemetry 98 if (this.state.notification) { 99 this.sendUserEventTelemetry("CLICK", this.state.notification); 100 } 101 } 102 // Will call uninit on every window 103 lazy.EveryWindow.unregisterCallback(this.id); 104 if (this.state.notification) { 105 this._blockMessageById(this.state.notification.id); 106 } 107 this._clearBadgeTimeout(); 108 this.state = {}; 109 } 110 111 removeToolbarNotification(toolbarButton) { 112 // Remove it from the element that displays the badge 113 toolbarButton 114 .querySelector(".toolbarbutton-badge") 115 .classList.remove("feature-callout"); 116 toolbarButton.removeAttribute("badged"); 117 toolbarButton.removeAttribute("showing-callout"); 118 // Remove id used for for aria-label badge description 119 const notificationDescription = toolbarButton.querySelector( 120 "#toolbarbutton-notification-description" 121 ); 122 if (notificationDescription) { 123 notificationDescription.remove(); 124 toolbarButton.removeAttribute("aria-labelledby"); 125 toolbarButton.removeAttribute("aria-describedby"); 126 } 127 } 128 129 addToolbarNotification(win, message) { 130 const document = win.browser.ownerDocument; 131 let toolbarbutton = document.getElementById(message.content.target); 132 if (toolbarbutton) { 133 const badge = toolbarbutton.querySelector(".toolbarbutton-badge"); 134 badge.classList.add("feature-callout"); 135 toolbarbutton.setAttribute("badged", true); 136 toolbarbutton.setAttribute("showing-callout", true); 137 // If we have additional aria-label information for the notification 138 // we add this content to the hidden `toolbarbutton-text` node. 139 // We then use `aria-labelledby` to link this description to the button 140 // that received the notification badge. 141 if (message.content.badgeDescription) { 142 // Insert strings as soon as we know we're showing them 143 this.maybeInsertFTL(win); 144 toolbarbutton.setAttribute( 145 "aria-labelledby", 146 `toolbarbutton-notification-description ${message.content.target}` 147 ); 148 // Because tooltiptext is different to the label, it gets duplicated as 149 // the description. Setting `describedby` to the same value as 150 // `labelledby` will be detected by the a11y code and the description 151 // will be removed. 152 toolbarbutton.setAttribute( 153 "aria-describedby", 154 `toolbarbutton-notification-description ${message.content.target}` 155 ); 156 const descriptionEl = document.createElement("span"); 157 descriptionEl.setAttribute( 158 "id", 159 "toolbarbutton-notification-description" 160 ); 161 descriptionEl.hidden = true; 162 document.l10n.setAttributes( 163 descriptionEl, 164 message.content.badgeDescription.string_id 165 ); 166 toolbarbutton.appendChild(descriptionEl); 167 } 168 // `mousedown` event required because of the `onmousedown` defined on 169 // the button that prevents `click` events from firing 170 toolbarbutton.addEventListener("mousedown", this.removeAllNotifications); 171 // `keypress` event required for keyboard accessibility 172 toolbarbutton.addEventListener("keypress", this.removeAllNotifications); 173 this.state = { notification: { id: message.id } }; 174 175 // Impression should be added when the badge becomes visible 176 this._addImpression(message); 177 // Send a telemetry ping when adding the notification badge 178 this.sendUserEventTelemetry("IMPRESSION", message); 179 180 return toolbarbutton; 181 } 182 183 return null; 184 } 185 186 registerBadgeToAllWindows(message) { 187 lazy.EveryWindow.registerCallback( 188 this.id, 189 win => { 190 if (notificationsByWindow.has(win)) { 191 // nothing to do 192 return; 193 } 194 const el = this.addToolbarNotification(win, message); 195 notificationsByWindow.set(win, el); 196 }, 197 win => { 198 const el = notificationsByWindow.get(win); 199 if (el) { 200 this.removeToolbarNotification(el); 201 } 202 notificationsByWindow.delete(win); 203 } 204 ); 205 } 206 207 registerBadgeNotificationListener(message, options = {}) { 208 // We need to clear any existing notifications and only show 209 // the one set by devtools 210 if (options.force) { 211 this.removeAllNotifications(); 212 // When debugging immediately show the badge 213 this.registerBadgeToAllWindows(message); 214 return; 215 } 216 217 if (message.content.delay) { 218 this.state.showBadgeTimeoutId = lazy.setTimeout(() => { 219 lazy.requestIdleCallback(() => this.registerBadgeToAllWindows(message)); 220 }, message.content.delay); 221 } else { 222 this.registerBadgeToAllWindows(message); 223 } 224 } 225 226 async messageRequest({ triggerId, template }) { 227 const timerId = Glean.messagingSystem.messageRequestTime.start(); 228 const message = await this._handleMessageRequest({ 229 triggerId, 230 template, 231 }); 232 Glean.messagingSystem.messageRequestTime.stopAndAccumulate(timerId); 233 if (message) { 234 this.registerBadgeNotificationListener(message); 235 } 236 } 237 238 _sendPing(ping) { 239 this._sendTelemetry({ 240 type: "TOOLBAR_BADGE_TELEMETRY", 241 data: { action: "badge_user_event", ...ping }, 242 }); 243 } 244 245 sendUserEventTelemetry(event, message) { 246 const win = Services.wm.getMostRecentWindow("navigator:browser"); 247 // Only send pings for non private browsing windows 248 if ( 249 win && 250 !lazy.PrivateBrowsingUtils.isBrowserPrivate( 251 win.ownerGlobal.gBrowser.selectedBrowser 252 ) 253 ) { 254 this._sendPing({ 255 message_id: message.id, 256 event, 257 }); 258 } 259 } 260 261 uninit() { 262 this._clearBadgeTimeout(); 263 this.state = {}; 264 this._initialized = false; 265 notificationsByWindow = new WeakMap(); 266 } 267 } 268 269 /** 270 * ToolbarBadgeHub - singleton instance of _ToolbarBadgeHub that can initiate 271 * message requests and render messages. 272 */ 273 export const ToolbarBadgeHub = new _ToolbarBadgeHub();