sidebar-tab-list.mjs (9510B)
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 import { 6 classMap, 7 html, 8 ifDefined, 9 when, 10 } from "chrome://global/content/vendor/lit.all.mjs"; 11 12 import { 13 FxviewTabListBase, 14 FxviewTabRowBase, 15 } from "chrome://browser/content/firefoxview/fxview-tab-list.mjs"; 16 17 export class SidebarTabList extends FxviewTabListBase { 18 constructor() { 19 super(); 20 // Panel is open, assume we always want to react to updates. 21 this.updatesPaused = false; 22 this.multiSelect = true; 23 this.selectedGuids = new Set(); 24 this.shortcutsLocalization = new Localization( 25 ["toolkit/global/textActions.ftl"], 26 true 27 ); 28 } 29 30 static queries = { 31 ...FxviewTabListBase.queries, 32 rowEls: { 33 all: "sidebar-tab-row", 34 }, 35 }; 36 37 /** 38 * Only handle vertical navigation in sidebar. 39 * 40 * @param {KeyboardEvent} e 41 */ 42 handleFocusElementInRow(e) { 43 // Handle vertical navigation. 44 if ( 45 (e.code == "ArrowUp" && this.activeIndex > 0) || 46 (e.code == "ArrowDown" && this.activeIndex < this.rowEls.length - 1) 47 ) { 48 super.handleFocusElementInRow(e); 49 } else if ( 50 (e.code == "ArrowUp" && this.activeIndex == 0) || 51 e.code === "ArrowLeft" 52 ) { 53 this.#focusParentHeader(e.target); 54 } else if ( 55 e.code == "ArrowDown" && 56 this.activeIndex == this.rowEls.length - 1 57 ) { 58 this.#focusNextHeader(e.target); 59 } 60 61 // Update or clear multi-selection (depending on whether shift key is used). 62 if (this.multiSelect && (e.code === "ArrowUp" || e.code === "ArrowDown")) { 63 this.#updateSelection(e); 64 } 65 66 // (Ctrl / Cmd) + A should select all rows. 67 if ( 68 e.getModifierState("Accel") && 69 e.key.toUpperCase() === this.selectAllShortcut 70 ) { 71 e.preventDefault(); 72 this.#selectAll(); 73 } 74 } 75 76 #focusParentHeader(row) { 77 let parentCard = row.getRootNode().host.closest("moz-card"); 78 if (parentCard) { 79 parentCard.summaryEl.focus(); 80 } 81 } 82 83 #focusNextHeader(row) { 84 let parentCard = row.getRootNode().host.closest("moz-card"); 85 if ( 86 this.sortOption == "datesite" && 87 parentCard.classList.contains("last-card") 88 ) { 89 // If we're going down from the last site, then focus the next date. 90 const dateCard = parentCard.parentElement; 91 const nextDate = dateCard.nextElementSibling; 92 nextDate?.summaryEl.focus(); 93 } 94 let nextCard = parentCard.nextElementSibling; 95 if (nextCard && nextCard.localName == "moz-card") { 96 nextCard.summaryEl.focus(); 97 } 98 } 99 100 #updateSelection(event) { 101 if (!event.shiftKey) { 102 // Clear the selection when navigating without shift key. 103 // Dispatch event so that other lists will also clear their selection. 104 this.clearSelection(); 105 this.dispatchEvent( 106 new CustomEvent("clear-selection", { 107 bubbles: true, 108 composed: true, 109 }) 110 ); 111 return; 112 } 113 114 // Select the current row. 115 const row = event.target; 116 const { 117 guid, 118 previousElementSibling: prevRow, 119 nextElementSibling: nextRow, 120 } = row; 121 this.selectedGuids.add(guid); 122 123 // Select the previous or next sibling, depending on which arrow key was used. 124 if (event.code === "ArrowUp" && prevRow) { 125 this.selectedGuids.add(prevRow.guid); 126 } else if (event.code === "ArrowDown" && nextRow) { 127 this.selectedGuids.add(nextRow.guid); 128 } else { 129 this.requestVirtualListUpdate(); 130 } 131 132 // Notify the host component. 133 this.dispatchEvent( 134 new CustomEvent("update-selection", { 135 bubbles: true, 136 composed: true, 137 }) 138 ); 139 } 140 141 clearSelection() { 142 this.selectedGuids.clear(); 143 this.requestVirtualListUpdate(); 144 } 145 146 get selectAllShortcut() { 147 const [l10nMessage] = this.shortcutsLocalization.formatMessagesSync([ 148 "text-action-select-all-shortcut", 149 ]); 150 const shortcutKey = l10nMessage.attributes[0].value; 151 return shortcutKey; 152 } 153 154 #selectAll() { 155 for (const { guid } of this.tabItems) { 156 this.selectedGuids.add(guid); 157 } 158 this.requestVirtualListUpdate(); 159 this.dispatchEvent( 160 new CustomEvent("update-selection", { 161 bubbles: true, 162 composed: true, 163 }) 164 ); 165 } 166 167 itemTemplate = (tabItem, i) => { 168 let tabIndex = -1; 169 if ((this.searchQuery || this.sortOption == "lastvisited") && i == 0) { 170 // Make the first row focusable if there is no header. 171 tabIndex = 0; 172 } else if (!this.searchQuery) { 173 tabIndex = 0; 174 } 175 return html` 176 <sidebar-tab-row 177 ?active=${i == this.activeIndex} 178 .canClose=${ifDefined(tabItem.canClose)} 179 .closedId=${ifDefined(tabItem.closedId)} 180 compact 181 .currentActiveElementId=${this.currentActiveElementId} 182 .closeRequested=${tabItem.closeRequested} 183 .containerObj=${tabItem.containerObj} 184 .fxaDeviceId=${ifDefined(tabItem.fxaDeviceId)} 185 .favicon=${tabItem.icon} 186 .guid=${tabItem.guid} 187 .hasPopup=${this.hasPopup} 188 .indicators=${tabItem.indicators} 189 .primaryL10nArgs=${ifDefined(tabItem.primaryL10nArgs)} 190 .primaryL10nId=${tabItem.primaryL10nId} 191 role="listitem" 192 .searchQuery=${ifDefined(this.searchQuery)} 193 .secondaryActionClass=${ifDefined( 194 this.secondaryActionClass ?? tabItem.secondaryActionClass 195 )} 196 .secondaryL10nArgs=${ifDefined(tabItem.secondaryL10nArgs)} 197 .secondaryL10nId=${tabItem.secondaryL10nId} 198 .selected=${this.selectedGuids.has(tabItem.guid)} 199 .sourceClosedId=${ifDefined(tabItem.sourceClosedId)} 200 .sourceWindowId=${ifDefined(tabItem.sourceWindowId)} 201 .tabElement=${ifDefined(tabItem.tabElement)} 202 tabindex=${tabIndex} 203 .title=${tabItem.title} 204 .url=${tabItem.url} 205 @keydown=${e => e.currentTarget.primaryActionHandler(e)} 206 ></sidebar-tab-row> 207 `; 208 }; 209 210 stylesheets() { 211 return [ 212 super.stylesheets(), 213 html`<link 214 rel="stylesheet" 215 href="chrome://browser/content/sidebar/sidebar-tab-list.css" 216 />`, 217 ]; 218 } 219 } 220 customElements.define("sidebar-tab-list", SidebarTabList); 221 222 export class SidebarTabRow extends FxviewTabRowBase { 223 static properties = { 224 containerObj: { type: Object }, 225 guid: { type: String }, 226 selected: { type: Boolean, reflect: true }, 227 indicators: { type: Array }, 228 }; 229 230 /** 231 * Fallback to the native implementation in sidebar. We want to focus the 232 * entire row instead of delegating it to link or hover buttons. 233 */ 234 focus() { 235 HTMLElement.prototype.focus.call(this); 236 } 237 238 #getContainerClasses() { 239 let containerClasses = ["fxview-tab-row-container-indicator", "icon"]; 240 if (this.containerObj) { 241 let { icon, color } = this.containerObj; 242 containerClasses.push(`identity-icon-${icon}`); 243 containerClasses.push(`identity-color-${color}`); 244 } 245 return containerClasses; 246 } 247 248 #containerIndicatorTemplate() { 249 let tabList = this.getRootNode().host; 250 let tabsToCheck = tabList.tabItems; 251 return html`${when( 252 tabsToCheck.some(tab => tab.containerObj), 253 () => html`<span class=${this.#getContainerClasses().join(" ")}></span>` 254 )}`; 255 } 256 257 secondaryButtonTemplate() { 258 return html`${when( 259 this.secondaryL10nId && this.secondaryActionClass, 260 () => 261 html`<moz-button 262 aria-haspopup=${ifDefined(this.hasPopup)} 263 class=${classMap({ 264 "fxview-tab-row-button": true, 265 [this.secondaryActionClass]: this.secondaryActionClass, 266 })} 267 data-l10n-args=${ifDefined(this.secondaryL10nArgs)} 268 data-l10n-id=${this.secondaryL10nId} 269 id="fxview-tab-row-secondary-button" 270 type="icon ghost" 271 @click=${this.secondaryActionHandler} 272 iconSrc=${this.getIconSrc(this.secondaryActionClass)} 273 ></moz-button>` 274 )}`; 275 } 276 277 render() { 278 return html` 279 ${this.stylesheets()} 280 ${when( 281 this.containerObj, 282 () => html` 283 <link 284 rel="stylesheet" 285 href="chrome://browser/content/usercontext/usercontext.css" 286 /> 287 ` 288 )} 289 <link 290 rel="stylesheet" 291 href="chrome://browser/content/sidebar/sidebar-tab-row.css" 292 /> 293 <a 294 class=${classMap({ 295 "fxview-tab-row-main": true, 296 "no-action-button-row": this.canClose === false, 297 muted: this.indicators?.includes("muted"), 298 attention: this.indicators?.includes("attention"), 299 soundplaying: this.indicators?.includes("soundplaying"), 300 "activemedia-blocked": this.indicators?.includes( 301 "activemedia-blocked" 302 ), 303 })} 304 ?disabled=${this.closeRequested} 305 data-l10n-args=${ifDefined(this.primaryL10nArgs)} 306 data-l10n-id=${ifDefined(this.primaryL10nId)} 307 href=${ifDefined(this.url)} 308 id="fxview-tab-row-main" 309 tabindex="-1" 310 title=${!this.primaryL10nId ? this.url : null} 311 @click=${this.primaryActionHandler} 312 @keydown=${this.primaryActionHandler} 313 > 314 ${this.faviconTemplate()} ${this.titleTemplate()} 315 </a> 316 ${this.secondaryButtonTemplate()} ${this.#containerIndicatorTemplate()} 317 `; 318 } 319 } 320 customElements.define("sidebar-tab-row", SidebarTabRow);