/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {
classMap,
html,
ifDefined,
repeat,
styleMap,
when,
} from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
import { escapeRegExp } from "./helpers.mjs";
const NOW_THRESHOLD_MS = 91000;
const FXVIEW_ROW_HEIGHT_PX = 32;
const lazy = {};
let XPCOMUtils;
if (!window.IS_STORYBOOK) {
XPCOMUtils = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
).XPCOMUtils;
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"virtualListEnabledPref",
"browser.firefox-view.virtual-list.enabled"
);
ChromeUtils.defineLazyGetter(lazy, "relativeTimeFormat", () => {
return new Services.intl.RelativeTimeFormat(undefined, {
style: "narrow",
});
});
ChromeUtils.defineESModuleGetters(lazy, {
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
});
}
/**
* A list of clickable tab items
*
* @property {boolean} compactRows - Whether to hide the URL and date/time for each tab.
* @property {string} dateTimeFormat - Expected format for date and/or time
* @property {string} hasPopup - The aria-haspopup attribute for the secondary action, if required
* @property {number} maxTabsLength - The max number of tabs for the list
* @property {boolean} pinnedTabsGridView - Whether to show pinned tabs in a grid view
* @property {Array} tabItems - Items to show in the tab list
* @property {string} searchQuery - The query string to highlight, if provided.
* @property {string} secondaryActionClass - The class used to style the secondary action element
* @property {string} tertiaryActionClass - The class used to style the tertiary action element
*/
export default class FxviewTabList extends MozLitElement {
constructor() {
super();
window.MozXULElement.insertFTLIfNeeded("toolkit/branding/brandings.ftl");
window.MozXULElement.insertFTLIfNeeded("browser/fxviewTabList.ftl");
this.activeIndex = 0;
this.currentActiveElementId = "fxview-tab-row-main";
this.hasPopup = null;
this.dateTimeFormat = "relative";
this.maxTabsLength = 25;
this.tabItems = [];
this.pinnedTabs = [];
this.pinnedTabsGridView = false;
this.unpinnedTabs = [];
this.compactRows = false;
this.updatesPaused = true;
this.#register();
}
static properties = {
activeIndex: { type: Number },
compactRows: { type: Boolean },
currentActiveElementId: { type: String },
dateTimeFormat: { type: String },
hasPopup: { type: String },
maxTabsLength: { type: Number },
pinnedTabsGridView: { type: Boolean },
tabItems: { type: Array },
updatesPaused: { type: Boolean },
searchQuery: { type: String },
secondaryActionClass: { type: String },
tertiaryActionClass: { type: String },
};
static queries = {
rowEls: { all: "fxview-tab-row" },
rootVirtualListEl: "virtual-list",
};
willUpdate(changes) {
this.activeIndex = Math.min(
Math.max(this.activeIndex, 0),
this.tabItems.length - 1
);
if (changes.has("dateTimeFormat") || changes.has("updatesPaused")) {
this.clearIntervalTimer();
if (
!this.updatesPaused &&
this.dateTimeFormat == "relative" &&
!window.IS_STORYBOOK
) {
this.startIntervalTimer();
this.onIntervalUpdate();
}
}
// Move pinned tabs to the beginning of the list
if (this.pinnedTabsGridView) {
// Can set maxTabsLength to -1 to have no max
this.unpinnedTabs = this.tabItems.filter(
tab => !tab.indicators?.includes("pinned")
);
this.pinnedTabs = this.tabItems.filter(tab =>
tab.indicators?.includes("pinned")
);
if (this.maxTabsLength > 0) {
this.unpinnedTabs = this.unpinnedTabs.slice(0, this.maxTabsLength);
}
this.tabItems = [...this.pinnedTabs, ...this.unpinnedTabs];
} else if (this.maxTabsLength > 0) {
this.tabItems = this.tabItems.slice(0, this.maxTabsLength);
}
}
startIntervalTimer() {
this.clearIntervalTimer();
this.intervalID = setInterval(
() => this.onIntervalUpdate(),
this.timeMsPref
);
}
clearIntervalTimer() {
if (this.intervalID) {
clearInterval(this.intervalID);
delete this.intervalID;
}
}
#register() {
if (!window.IS_STORYBOOK) {
XPCOMUtils.defineLazyPreferenceGetter(
this,
"timeMsPref",
"browser.tabs.firefox-view.updateTimeMs",
NOW_THRESHOLD_MS,
(prefName, oldVal, newVal) => {
this.clearIntervalTimer();
if (!this.isConnected) {
return;
}
this.startIntervalTimer();
this.requestUpdate();
}
);
}
}
connectedCallback() {
super.connectedCallback();
if (
!this.updatesPaused &&
this.dateTimeFormat === "relative" &&
!window.IS_STORYBOOK
) {
this.startIntervalTimer();
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.clearIntervalTimer();
}
async getUpdateComplete() {
await super.getUpdateComplete();
await Promise.all(Array.from(this.rowEls).map(item => item.updateComplete));
}
onIntervalUpdate() {
this.requestUpdate();
Array.from(this.rowEls).forEach(fxviewTabRow =>
fxviewTabRow.requestUpdate()
);
}
/**
* Focuses the expected element (either the link or button) within fxview-tab-row
* The currently focused/active element ID within a row is stored in this.currentActiveElementId
*/
handleFocusElementInRow(e) {
let fxviewTabRow = e.target;
if (e.code == "ArrowUp") {
// Focus either the link or button of the previous row based on this.currentActiveElementId
e.preventDefault();
if (
(this.pinnedTabsGridView &&
this.activeIndex >= this.pinnedTabs.length) ||
!this.pinnedTabsGridView
) {
this.focusPrevRow();
}
} else if (e.code == "ArrowDown") {
// Focus either the link or button of the next row based on this.currentActiveElementId
e.preventDefault();
if (
this.pinnedTabsGridView &&
this.activeIndex < this.pinnedTabs.length
) {
this.focusIndex(this.pinnedTabs.length);
} else {
this.focusNextRow();
}
} else if (e.code == "ArrowRight") {
// Focus either the link or the button in the current row and
// set this.currentActiveElementId to that element's ID
e.preventDefault();
if (document.dir == "rtl") {
this.moveFocusLeft(fxviewTabRow);
} else {
this.moveFocusRight(fxviewTabRow);
}
} else if (e.code == "ArrowLeft") {
// Focus either the link or the button in the current row and
// set this.currentActiveElementId to that element's ID
e.preventDefault();
if (document.dir == "rtl") {
this.moveFocusRight(fxviewTabRow);
} else {
this.moveFocusLeft(fxviewTabRow);
}
}
}
moveFocusRight(fxviewTabRow) {
if (
this.pinnedTabsGridView &&
fxviewTabRow.indicators?.includes("pinned")
) {
this.focusNextRow();
} else if (
(fxviewTabRow.indicators?.includes("soundplaying") ||
fxviewTabRow.indicators?.includes("muted")) &&
this.currentActiveElementId === "fxview-tab-row-main"
) {
this.currentActiveElementId = fxviewTabRow.focusMediaButton();
} else if (
this.currentActiveElementId === "fxview-tab-row-media-button" ||
this.currentActiveElementId === "fxview-tab-row-main"
) {
this.currentActiveElementId = fxviewTabRow.focusSecondaryButton();
} else if (
fxviewTabRow.tertiaryButtonEl &&
this.currentActiveElementId === "fxview-tab-row-secondary-button"
) {
this.currentActiveElementId = fxviewTabRow.focusTertiaryButton();
}
}
moveFocusLeft(fxviewTabRow) {
if (
this.pinnedTabsGridView &&
(fxviewTabRow.indicators?.includes("pinned") ||
(this.currentActiveElementId === "fxview-tab-row-main" &&
this.activeIndex === this.pinnedTabs.length))
) {
this.focusPrevRow();
} else if (
this.currentActiveElementId === "fxview-tab-row-tertiary-button"
) {
this.currentActiveElementId = fxviewTabRow.focusSecondaryButton();
} else if (
(fxviewTabRow.indicators?.includes("soundplaying") ||
fxviewTabRow.indicators?.includes("muted")) &&
this.currentActiveElementId === "fxview-tab-row-secondary-button"
) {
this.currentActiveElementId = fxviewTabRow.focusMediaButton();
} else {
this.currentActiveElementId = fxviewTabRow.focusLink();
}
}
focusPrevRow() {
this.focusIndex(this.activeIndex - 1);
}
focusNextRow() {
this.focusIndex(this.activeIndex + 1);
}
async focusIndex(index) {
// Focus link or button of item
if (
((this.pinnedTabsGridView && index > this.pinnedTabs.length) ||
!this.pinnedTabsGridView) &&
lazy.virtualListEnabledPref
) {
let row = this.rootVirtualListEl.getItem(index - this.pinnedTabs.length);
if (!row) {
return;
}
let subList = this.rootVirtualListEl.getSubListForItem(
index - this.pinnedTabs.length
);
if (!subList) {
return;
}
this.activeIndex = index;
// In Bug 1866845, these manual updates to the sublists should be removed
// and scrollIntoView() should also be iterated on so that we aren't constantly
// moving the focused item to the center of the viewport
for (const sublist of Array.from(this.rootVirtualListEl.children)) {
await sublist.requestUpdate();
await sublist.updateComplete;
}
row.scrollIntoView({ block: "center" });
row.focus();
} else if (index >= 0 && index < this.rowEls?.length) {
this.rowEls[index].focus();
this.activeIndex = index;
}
}
shouldUpdate(changes) {
if (changes.has("updatesPaused")) {
if (this.updatesPaused) {
this.clearIntervalTimer();
}
}
return !this.updatesPaused;
}
itemTemplate = (tabItem, i) => {
let time;
if (tabItem.time || tabItem.closedAt) {
let stringTime = (tabItem.time || tabItem.closedAt).toString();
// Different APIs return time in different units, so we use
// the length to decide if it's milliseconds or nanoseconds.
if (stringTime.length === 16) {
time = (tabItem.time || tabItem.closedAt) / 1000;
} else {
time = tabItem.time || tabItem.closedAt;
}
}
return html`
`;
};
render() {
if (this.searchQuery && this.tabItems.length === 0) {
return this.#emptySearchResultsTemplate();
}
return html`
${when(
this.pinnedTabsGridView && this.pinnedTabs.length,
() => html`