summaryrefslogtreecommitdiffstats
path: root/browser/components/firefoxview
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:35:49 +0000
commitd8bbc7858622b6d9c278469aab701ca0b609cddf (patch)
treeeff41dc61d9f714852212739e6b3738b82a2af87 /browser/components/firefoxview
parentReleasing progress-linux version 125.0.3-1~progress7.99u1. (diff)
downloadfirefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz
firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--browser/components/firefoxview/HistoryController.mjs188
-rw-r--r--browser/components/firefoxview/OpenTabs.sys.mjs55
-rw-r--r--browser/components/firefoxview/SyncedTabsController.sys.mjs333
-rw-r--r--browser/components/firefoxview/card-container.css6
-rw-r--r--browser/components/firefoxview/card-container.mjs2
-rw-r--r--browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs17
-rw-r--r--browser/components/firefoxview/firefoxview.css19
-rw-r--r--browser/components/firefoxview/firefoxview.html12
-rw-r--r--browser/components/firefoxview/firefoxview.mjs16
-rw-r--r--browser/components/firefoxview/fxview-empty-state.css2
-rw-r--r--browser/components/firefoxview/fxview-tab-list.css22
-rw-r--r--browser/components/firefoxview/fxview-tab-list.mjs689
-rw-r--r--browser/components/firefoxview/fxview-tab-row.css178
-rw-r--r--browser/components/firefoxview/helpers.mjs17
-rw-r--r--browser/components/firefoxview/history.css13
-rw-r--r--browser/components/firefoxview/history.mjs224
-rw-r--r--browser/components/firefoxview/jar.mn4
-rw-r--r--browser/components/firefoxview/opentabs-tab-list.css32
-rw-r--r--browser/components/firefoxview/opentabs-tab-list.mjs593
-rw-r--r--browser/components/firefoxview/opentabs-tab-row.css119
-rw-r--r--browser/components/firefoxview/opentabs.mjs42
-rw-r--r--browser/components/firefoxview/recentlyclosed.mjs16
-rw-r--r--browser/components/firefoxview/syncedtabs.mjs387
-rw-r--r--browser/components/firefoxview/tests/browser/browser.toml8
-rw-r--r--browser/components/firefoxview/tests/browser/browser_firefoxview_dragDrop_pinned_tab.js102
-rw-r--r--browser/components/firefoxview/tests/browser/browser_firefoxview_paused.js99
-rw-r--r--browser/components/firefoxview/tests/browser/browser_firefoxview_search_telemetry.js5
-rw-r--r--browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js6
-rw-r--r--browser/components/firefoxview/tests/browser/browser_history_firefoxview.js15
-rw-r--r--browser/components/firefoxview/tests/browser/browser_opentabs_cards.js10
-rw-r--r--browser/components/firefoxview/tests/browser/browser_opentabs_firefoxview.js4
-rw-r--r--browser/components/firefoxview/tests/browser/browser_opentabs_recency.js350
-rw-r--r--browser/components/firefoxview/tests/browser/browser_opentabs_tab_indicators.js10
-rw-r--r--browser/components/firefoxview/tests/browser/browser_recentlyclosed_firefoxview.js12
-rw-r--r--browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js272
-rw-r--r--browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js7
-rw-r--r--browser/components/firefoxview/tests/browser/browser_tab_list_keyboard_navigation.js4
-rw-r--r--browser/components/firefoxview/tests/browser/browser_tab_on_close_warning.js2
-rw-r--r--browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html5
39 files changed, 2451 insertions, 1446 deletions
diff --git a/browser/components/firefoxview/HistoryController.mjs b/browser/components/firefoxview/HistoryController.mjs
new file mode 100644
index 0000000000..d2bda5cec2
--- /dev/null
+++ b/browser/components/firefoxview/HistoryController.mjs
@@ -0,0 +1,188 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ FirefoxViewPlacesQuery:
+ "resource:///modules/firefox-view-places-query.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+
+let XPCOMUtils = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+).XPCOMUtils;
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "maxRowsPref",
+ "browser.firefox-view.max-history-rows",
+ -1
+);
+
+const HISTORY_MAP_L10N_IDS = {
+ sidebar: {
+ "history-date-today": "sidebar-history-date-today",
+ "history-date-yesterday": "sidebar-history-date-yesterday",
+ "history-date-this-month": "sidebar-history-date-this-month",
+ "history-date-prev-month": "sidebar-history-date-prev-month",
+ },
+ firefoxview: {
+ "history-date-today": "firefoxview-history-date-today",
+ "history-date-yesterday": "firefoxview-history-date-yesterday",
+ "history-date-this-month": "firefoxview-history-date-this-month",
+ "history-date-prev-month": "firefoxview-history-date-prev-month",
+ },
+};
+
+export class HistoryController {
+ host;
+ allHistoryItems;
+ historyMapByDate;
+ historyMapBySite;
+ searchQuery;
+ searchResults;
+ sortOption;
+
+ constructor(host, options) {
+ this.allHistoryItems = new Map();
+ this.historyMapByDate = [];
+ this.historyMapBySite = [];
+ this.placesQuery = new lazy.FirefoxViewPlacesQuery();
+ this.searchQuery = "";
+ this.searchResults = null;
+ this.sortOption = "date";
+ this.searchResultsLimit = options?.searchResultsLimit || 300;
+ this.component = HISTORY_MAP_L10N_IDS?.[options?.component]
+ ? options?.component
+ : "firefoxview";
+ this.host = host;
+
+ host.addController(this);
+ }
+
+ async hostConnected() {
+ this.placesQuery.observeHistory(data => this.updateAllHistoryItems(data));
+ await this.updateHistoryData();
+ this.createHistoryMaps();
+ }
+
+ hostDisconnected() {
+ this.placesQuery.close();
+ }
+
+ deleteFromHistory() {
+ lazy.PlacesUtils.history.remove(this.host.triggerNode.url);
+ }
+
+ async onSearchQuery(e) {
+ this.searchQuery = e.detail.query;
+ await this.updateSearchResults();
+ this.host.requestUpdate();
+ }
+
+ async onChangeSortOption(e) {
+ this.sortOption = e.target.value;
+ await this.updateHistoryData();
+ await this.updateSearchResults();
+ this.host.requestUpdate();
+ }
+
+ async updateHistoryData() {
+ this.allHistoryItems = await this.placesQuery.getHistory({
+ daysOld: 60,
+ limit: lazy.maxRowsPref,
+ sortBy: this.sortOption,
+ });
+ }
+
+ async updateAllHistoryItems(allHistoryItems) {
+ if (allHistoryItems) {
+ this.allHistoryItems = allHistoryItems;
+ } else {
+ await this.updateHistoryData();
+ }
+ this.resetHistoryMaps();
+ this.host.requestUpdate();
+ await this.updateSearchResults();
+ }
+
+ async updateSearchResults() {
+ if (this.searchQuery) {
+ try {
+ this.searchResults = await this.placesQuery.searchHistory(
+ this.searchQuery,
+ this.searchResultsLimit
+ );
+ } catch (e) {
+ // Connection interrupted, ignore.
+ }
+ } else {
+ this.searchResults = null;
+ }
+ }
+
+ resetHistoryMaps() {
+ this.historyMapByDate = [];
+ this.historyMapBySite = [];
+ }
+
+ createHistoryMaps() {
+ if (!this.historyMapByDate.length) {
+ const {
+ visitsFromToday,
+ visitsFromYesterday,
+ visitsByDay,
+ visitsByMonth,
+ } = this.placesQuery;
+
+ // Add visits from today and yesterday.
+ if (visitsFromToday.length) {
+ this.historyMapByDate.push({
+ l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-today"],
+ items: visitsFromToday,
+ });
+ }
+ if (visitsFromYesterday.length) {
+ this.historyMapByDate.push({
+ l10nId:
+ HISTORY_MAP_L10N_IDS[this.component]["history-date-yesterday"],
+ items: visitsFromYesterday,
+ });
+ }
+
+ // Add visits from this month, grouped by day.
+ visitsByDay.forEach(visits => {
+ this.historyMapByDate.push({
+ l10nId:
+ HISTORY_MAP_L10N_IDS[this.component]["history-date-this-month"],
+ items: visits,
+ });
+ });
+
+ // Add visits from previous months, grouped by month.
+ visitsByMonth.forEach(visits => {
+ this.historyMapByDate.push({
+ l10nId:
+ HISTORY_MAP_L10N_IDS[this.component]["history-date-prev-month"],
+ items: visits,
+ });
+ });
+ } else if (
+ this.sortOption === "site" &&
+ !this.historyMapBySite.length &&
+ this.component === "firefoxview"
+ ) {
+ this.historyMapBySite = Array.from(
+ this.allHistoryItems.entries(),
+ ([domain, items]) => ({
+ domain,
+ items,
+ l10nId: domain ? null : "firefoxview-history-site-localhost",
+ })
+ ).sort((a, b) => a.domain.localeCompare(b.domain));
+ }
+ this.host.requestUpdate();
+ }
+}
diff --git a/browser/components/firefoxview/OpenTabs.sys.mjs b/browser/components/firefoxview/OpenTabs.sys.mjs
index 0771bf9e65..6d67ca44cc 100644
--- a/browser/components/firefoxview/OpenTabs.sys.mjs
+++ b/browser/components/firefoxview/OpenTabs.sys.mjs
@@ -33,6 +33,7 @@ const TAB_CHANGE_EVENTS = Object.freeze([
]);
const TAB_RECENCY_CHANGE_EVENTS = Object.freeze([
"activate",
+ "sizemodechange",
"TabAttrModified",
"TabClose",
"TabOpen",
@@ -75,6 +76,10 @@ class OpenTabsTarget extends EventTarget {
TabChange: new Set(),
TabRecencyChange: new Set(),
};
+ #sourceEventsByType = {
+ TabChange: new Set(),
+ TabRecencyChange: new Set(),
+ };
#dispatchChangesTask;
#started = false;
#watchedWindows = new Set();
@@ -143,7 +148,7 @@ class OpenTabsTarget extends EventTarget {
windowList.map(win => win.delayedStartupPromise)
).then(() => {
// re-filter the list as properties might have changed in the interim
- return windowList.filter(win => this.includeWindowFilter);
+ return windowList.filter(() => this.includeWindowFilter);
});
}
@@ -223,6 +228,9 @@ class OpenTabsTarget extends EventTarget {
for (let changedWindows of Object.values(this.#changedWindowsByType)) {
changedWindows.clear();
}
+ for (let sourceEvents of Object.values(this.#sourceEventsByType)) {
+ sourceEvents.clear();
+ }
this.#watchedWindows.clear();
this.#dispatchChangesTask?.disarm();
}
@@ -245,9 +253,16 @@ class OpenTabsTarget extends EventTarget {
tabContainer.addEventListener("TabUnpinned", this);
tabContainer.addEventListener("TabSelect", this);
win.addEventListener("activate", this);
+ win.addEventListener("sizemodechange", this);
- this.#scheduleEventDispatch("TabChange", {});
- this.#scheduleEventDispatch("TabRecencyChange", {});
+ this.#scheduleEventDispatch("TabChange", {
+ sourceWindowId: win.windowGlobalChild.innerWindowId,
+ sourceEvent: "watchWindow",
+ });
+ this.#scheduleEventDispatch("TabRecencyChange", {
+ sourceWindowId: win.windowGlobalChild.innerWindowId,
+ sourceEvent: "watchWindow",
+ });
}
/**
@@ -270,9 +285,16 @@ class OpenTabsTarget extends EventTarget {
tabContainer.removeEventListener("TabSelect", this);
tabContainer.removeEventListener("TabUnpinned", this);
win.removeEventListener("activate", this);
+ win.removeEventListener("sizemodechange", this);
- this.#scheduleEventDispatch("TabChange", {});
- this.#scheduleEventDispatch("TabRecencyChange", {});
+ this.#scheduleEventDispatch("TabChange", {
+ sourceWindowId: win.windowGlobalChild.innerWindowId,
+ sourceEvent: "unwatchWindow",
+ });
+ this.#scheduleEventDispatch("TabRecencyChange", {
+ sourceWindowId: win.windowGlobalChild.innerWindowId,
+ sourceEvent: "unwatchWindow",
+ });
}
}
@@ -281,11 +303,12 @@ class OpenTabsTarget extends EventTarget {
* Repeated calls within approx 16ms will be consolidated
* into one event dispatch.
*/
- #scheduleEventDispatch(eventType, { sourceWindowId } = {}) {
+ #scheduleEventDispatch(eventType, { sourceWindowId, sourceEvent } = {}) {
if (!this.haveListenersForEvent(eventType)) {
return;
}
+ this.#sourceEventsByType[eventType].add(sourceEvent);
this.#changedWindowsByType[eventType].add(sourceWindowId);
// Queue up an event dispatch - we use a deferred task to make this less noisy by
// consolidating multiple change events into one.
@@ -302,16 +325,18 @@ class OpenTabsTarget extends EventTarget {
for (let [eventType, changedWindowIds] of Object.entries(
this.#changedWindowsByType
)) {
+ let sourceEvents = this.#sourceEventsByType[eventType];
if (this.haveListenersForEvent(eventType) && changedWindowIds.size) {
- this.dispatchEvent(
- new CustomEvent(eventType, {
- detail: {
- windowIds: [...changedWindowIds],
- },
- })
- );
+ let changeEvent = new CustomEvent(eventType, {
+ detail: {
+ windowIds: [...changedWindowIds],
+ sourceEvents: [...sourceEvents],
+ },
+ });
+ this.dispatchEvent(changeEvent);
changedWindowIds.clear();
}
+ sourceEvents?.clear();
}
}
@@ -362,11 +387,13 @@ class OpenTabsTarget extends EventTarget {
if (TAB_RECENCY_CHANGE_EVENTS.includes(type)) {
this.#scheduleEventDispatch("TabRecencyChange", {
sourceWindowId: win.windowGlobalChild.innerWindowId,
+ sourceEvent: type,
});
}
if (TAB_CHANGE_EVENTS.includes(type)) {
this.#scheduleEventDispatch("TabChange", {
sourceWindowId: win.windowGlobalChild.innerWindowId,
+ sourceEvent: type,
});
}
}
@@ -377,7 +404,7 @@ const gExclusiveWindows = new (class {
constructor() {
Services.obs.addObserver(this, "domwindowclosed");
}
- observe(subject, topic, data) {
+ observe(subject) {
let win = subject;
let winTarget = this.perWindowInstances.get(win);
if (winTarget) {
diff --git a/browser/components/firefoxview/SyncedTabsController.sys.mjs b/browser/components/firefoxview/SyncedTabsController.sys.mjs
new file mode 100644
index 0000000000..6ab8249bfe
--- /dev/null
+++ b/browser/components/firefoxview/SyncedTabsController.sys.mjs
@@ -0,0 +1,333 @@
+/* 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/. */
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
+ SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
+});
+
+import { SyncedTabsErrorHandler } from "resource:///modules/firefox-view-synced-tabs-error-handler.sys.mjs";
+import { TabsSetupFlowManager } from "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs";
+import { searchTabList } from "chrome://browser/content/firefoxview/helpers.mjs";
+
+const SYNCED_TABS_CHANGED = "services.sync.tabs.changed";
+const TOPIC_SETUPSTATE_CHANGED = "firefox-view.setupstate.changed";
+
+/**
+ * The controller for synced tabs components.
+ *
+ * @implements {ReactiveController}
+ */
+export class SyncedTabsController {
+ /**
+ * @type {boolean}
+ */
+ contextMenu;
+ currentSetupStateIndex = -1;
+ currentSyncedTabs = [];
+ devices = [];
+ /**
+ * The current error state as determined by `SyncedTabsErrorHandler`.
+ *
+ * @type {number}
+ */
+ errorState = null;
+ /**
+ * Component associated with this controller.
+ *
+ * @type {ReactiveControllerHost}
+ */
+ host;
+ /**
+ * @type {Function}
+ */
+ pairDeviceCallback;
+ searchQuery = "";
+ /**
+ * @type {Function}
+ */
+ signupCallback;
+
+ /**
+ * Construct a new SyncedTabsController.
+ *
+ * @param {ReactiveControllerHost} host
+ * @param {object} options
+ * @param {boolean} [options.contextMenu]
+ * Whether synced tab items have a secondary context menu.
+ * @param {Function} [options.pairDeviceCallback]
+ * The function to call when the pair device window is opened.
+ * @param {Function} [options.signupCallback]
+ * The function to call when the signup window is opened.
+ */
+ constructor(host, { contextMenu, pairDeviceCallback, signupCallback } = {}) {
+ this.contextMenu = contextMenu;
+ this.pairDeviceCallback = pairDeviceCallback;
+ this.signupCallback = signupCallback;
+ this.observe = this.observe.bind(this);
+ this.host = host;
+ this.host.addController(this);
+ }
+
+ hostConnected() {
+ this.host.addEventListener("click", this);
+ }
+
+ hostDisconnected() {
+ this.host.removeEventListener("click", this);
+ }
+
+ addSyncObservers() {
+ Services.obs.addObserver(this.observe, SYNCED_TABS_CHANGED);
+ Services.obs.addObserver(this.observe, TOPIC_SETUPSTATE_CHANGED);
+ }
+
+ removeSyncObservers() {
+ Services.obs.removeObserver(this.observe, SYNCED_TABS_CHANGED);
+ Services.obs.removeObserver(this.observe, TOPIC_SETUPSTATE_CHANGED);
+ }
+
+ handleEvent(event) {
+ if (event.type == "click" && event.target.dataset.action) {
+ const { ErrorType } = SyncedTabsErrorHandler;
+ switch (event.target.dataset.action) {
+ case `${ErrorType.SYNC_ERROR}`:
+ case `${ErrorType.NETWORK_OFFLINE}`:
+ case `${ErrorType.PASSWORD_LOCKED}`: {
+ TabsSetupFlowManager.tryToClearError();
+ break;
+ }
+ case `${ErrorType.SIGNED_OUT}`:
+ case "sign-in": {
+ TabsSetupFlowManager.openFxASignup(event.target.ownerGlobal);
+ this.signupCallback?.();
+ break;
+ }
+ case "add-device": {
+ TabsSetupFlowManager.openFxAPairDevice(event.target.ownerGlobal);
+ this.pairDeviceCallback?.();
+ break;
+ }
+ case "sync-tabs-disabled": {
+ TabsSetupFlowManager.syncOpenTabs(event.target);
+ break;
+ }
+ case `${ErrorType.SYNC_DISCONNECTED}`: {
+ const win = event.target.ownerGlobal;
+ const { switchToTabHavingURI } =
+ win.docShell.chromeEventHandler.ownerGlobal;
+ switchToTabHavingURI(
+ "about:preferences?action=choose-what-to-sync#sync",
+ true,
+ {}
+ );
+ break;
+ }
+ }
+ }
+ }
+
+ async observe(_, topic, errorState) {
+ if (topic == TOPIC_SETUPSTATE_CHANGED) {
+ await this.updateStates(errorState);
+ }
+ if (topic == SYNCED_TABS_CHANGED) {
+ await this.getSyncedTabData();
+ }
+ }
+
+ async updateStates(errorState) {
+ let stateIndex = TabsSetupFlowManager.uiStateIndex;
+ errorState = errorState || SyncedTabsErrorHandler.getErrorType();
+
+ if (stateIndex == 4 && this.currentSetupStateIndex !== stateIndex) {
+ // trigger an initial request for the synced tabs list
+ await this.getSyncedTabData();
+ }
+
+ this.currentSetupStateIndex = stateIndex;
+ this.errorState = errorState;
+ this.host.requestUpdate();
+ }
+
+ actionMappings = {
+ "sign-in": {
+ header: "firefoxview-syncedtabs-signin-header",
+ description: "firefoxview-syncedtabs-signin-description",
+ buttonLabel: "firefoxview-syncedtabs-signin-primarybutton",
+ },
+ "add-device": {
+ header: "firefoxview-syncedtabs-adddevice-header",
+ description: "firefoxview-syncedtabs-adddevice-description",
+ buttonLabel: "firefoxview-syncedtabs-adddevice-primarybutton",
+ descriptionLink: {
+ name: "url",
+ url: "https://support.mozilla.org/kb/how-do-i-set-sync-my-computer#w_connect-additional-devices-to-sync",
+ },
+ },
+ "sync-tabs-disabled": {
+ header: "firefoxview-syncedtabs-synctabs-header",
+ description: "firefoxview-syncedtabs-synctabs-description",
+ buttonLabel: "firefoxview-tabpickup-synctabs-primarybutton",
+ },
+ loading: {
+ header: "firefoxview-syncedtabs-loading-header",
+ description: "firefoxview-syncedtabs-loading-description",
+ },
+ };
+
+ #getMessageCardForState({ error = false, action, errorState }) {
+ errorState = errorState || this.errorState;
+ let header,
+ description,
+ descriptionLink,
+ buttonLabel,
+ headerIconUrl,
+ mainImageUrl;
+ let descriptionArray;
+ if (error) {
+ let link;
+ ({ header, description, link, buttonLabel } =
+ SyncedTabsErrorHandler.getFluentStringsForErrorType(errorState));
+ action = `${errorState}`;
+ headerIconUrl = "chrome://global/skin/icons/info-filled.svg";
+ mainImageUrl =
+ "chrome://browser/content/firefoxview/synced-tabs-error.svg";
+ descriptionArray = [description];
+ if (errorState == "password-locked") {
+ descriptionLink = {};
+ // This is ugly, but we need to special case this link so we can
+ // coexist with the old view.
+ descriptionArray.push("firefoxview-syncedtab-password-locked-link");
+ descriptionLink.name = "syncedtab-password-locked-link";
+ descriptionLink.url = link.href;
+ }
+ } else {
+ header = this.actionMappings[action].header;
+ description = this.actionMappings[action].description;
+ buttonLabel = this.actionMappings[action].buttonLabel;
+ descriptionLink = this.actionMappings[action].descriptionLink;
+ mainImageUrl =
+ "chrome://browser/content/firefoxview/synced-tabs-error.svg";
+ descriptionArray = [description];
+ }
+ return {
+ action,
+ buttonLabel,
+ descriptionArray,
+ descriptionLink,
+ error,
+ header,
+ headerIconUrl,
+ mainImageUrl,
+ };
+ }
+
+ getRenderInfo() {
+ let renderInfo = {};
+ for (let tab of this.currentSyncedTabs) {
+ if (!(tab.client in renderInfo)) {
+ renderInfo[tab.client] = {
+ name: tab.device,
+ deviceType: tab.deviceType,
+ tabs: [],
+ };
+ }
+ renderInfo[tab.client].tabs.push(tab);
+ }
+
+ // Add devices without tabs
+ for (let device of this.devices) {
+ if (!(device.id in renderInfo)) {
+ renderInfo[device.id] = {
+ name: device.name,
+ deviceType: device.clientType,
+ tabs: [],
+ };
+ }
+ }
+
+ for (let id in renderInfo) {
+ renderInfo[id].tabItems = this.searchQuery
+ ? searchTabList(this.searchQuery, this.getTabItems(renderInfo[id].tabs))
+ : this.getTabItems(renderInfo[id].tabs);
+ }
+ return renderInfo;
+ }
+
+ getMessageCard() {
+ switch (this.currentSetupStateIndex) {
+ case 0 /* error-state */:
+ if (this.errorState) {
+ return this.#getMessageCardForState({ error: true });
+ }
+ return this.#getMessageCardForState({ action: "loading" });
+ case 1 /* not-signed-in */:
+ if (Services.prefs.prefHasUserValue("services.sync.lastversion")) {
+ // If this pref is set, the user has signed out of sync.
+ // This path is also taken if we are disconnected from sync. See bug 1784055
+ return this.#getMessageCardForState({
+ error: true,
+ errorState: "signed-out",
+ });
+ }
+ return this.#getMessageCardForState({ action: "sign-in" });
+ case 2 /* connect-secondary-device*/:
+ return this.#getMessageCardForState({ action: "add-device" });
+ case 3 /* disabled-tab-sync */:
+ return this.#getMessageCardForState({ action: "sync-tabs-disabled" });
+ case 4 /* synced-tabs-loaded*/:
+ // There seems to be an edge case where sync says everything worked
+ // fine but we have no devices.
+ if (!this.devices.length) {
+ return this.#getMessageCardForState({ action: "add-device" });
+ }
+ }
+ return null;
+ }
+
+ getTabItems(tabs) {
+ return tabs?.map(tab => ({
+ icon: tab.icon,
+ title: tab.title,
+ time: tab.lastUsed * 1000,
+ url: tab.url,
+ primaryL10nId: "firefoxview-tabs-list-tab-button",
+ primaryL10nArgs: JSON.stringify({ targetURI: tab.url }),
+ secondaryL10nId: this.contextMenu
+ ? "fxviewtabrow-options-menu-button"
+ : undefined,
+ secondaryL10nArgs: this.contextMenu
+ ? JSON.stringify({ tabTitle: tab.title })
+ : undefined,
+ }));
+ }
+
+ updateTabsList(syncedTabs) {
+ if (!syncedTabs.length) {
+ this.currentSyncedTabs = syncedTabs;
+ }
+
+ const tabsToRender = syncedTabs;
+
+ // Return early if new tabs are the same as previous ones
+ if (lazy.ObjectUtils.deepEqual(tabsToRender, this.currentSyncedTabs)) {
+ return;
+ }
+
+ this.currentSyncedTabs = tabsToRender;
+ this.host.requestUpdate();
+ }
+
+ async getSyncedTabData() {
+ this.devices = await lazy.SyncedTabs.getTabClients();
+ let tabs = await lazy.SyncedTabs.createRecentTabsList(this.devices, 50, {
+ removeAllDupes: false,
+ removeDeviceDupes: true,
+ });
+
+ this.updateTabsList(tabs);
+ }
+}
diff --git a/browser/components/firefoxview/card-container.css b/browser/components/firefoxview/card-container.css
index 953437bec1..0c6a81899b 100644
--- a/browser/components/firefoxview/card-container.css
+++ b/browser/components/firefoxview/card-container.css
@@ -14,9 +14,9 @@
}
}
-@media (prefers-contrast) {
+@media (forced-colors) or (prefers-contrast) {
.card-container {
- border: 1px solid CanvasText;
+ border: 1px solid var(--fxview-border);
}
}
@@ -83,7 +83,7 @@
background-color: var(--fxview-element-background-hover);
}
-@media (prefers-contrast) {
+@media (forced-colors) {
.chevron-icon {
border: 1px solid ButtonText;
color: ButtonText;
diff --git a/browser/components/firefoxview/card-container.mjs b/browser/components/firefoxview/card-container.mjs
index b58f42204a..1755d97555 100644
--- a/browser/components/firefoxview/card-container.mjs
+++ b/browser/components/firefoxview/card-container.mjs
@@ -118,7 +118,7 @@ class CardContainer extends MozLitElement {
}
updateTabLists() {
- let tabLists = this.querySelectorAll("fxview-tab-list");
+ let tabLists = this.querySelectorAll("fxview-tab-list, opentabs-tab-list");
if (tabLists) {
tabLists.forEach(tabList => {
tabList.updatesPaused = !this.visible || !this.isExpanded;
diff --git a/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs b/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs
index 4c43eea1b6..e1c999d89c 100644
--- a/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs
+++ b/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs
@@ -591,12 +591,6 @@ export const TabsSetupFlowManager = new (class {
);
this.didFxaTabOpen = true;
openTabInWindow(window, url, true);
- Services.telemetry.recordEvent(
- "firefoxview_next",
- "fxa_continue",
- "sync",
- null
- );
}
async openFxAPairDevice(window) {
@@ -605,18 +599,9 @@ export const TabsSetupFlowManager = new (class {
});
this.didFxaTabOpen = true;
openTabInWindow(window, url, true);
- Services.telemetry.recordEvent(
- "firefoxview_next",
- "fxa_mobile",
- "sync",
- null,
- {
- has_devices: this.secondaryDeviceConnected.toString(),
- }
- );
}
- syncOpenTabs(containerElem) {
+ syncOpenTabs() {
// Flip the pref on.
// The observer should trigger re-evaluating state and advance to next step
Services.prefs.setBoolPref(SYNC_TABS_PREF, true);
diff --git a/browser/components/firefoxview/firefoxview.css b/browser/components/firefoxview/firefoxview.css
index 6811ca54c4..a91c90c39e 100644
--- a/browser/components/firefoxview/firefoxview.css
+++ b/browser/components/firefoxview/firefoxview.css
@@ -31,6 +31,17 @@
--newtab-background-color: #F9F9FB;
--fxview-card-header-font-weight: 500;
+
+ /* Make the attention dot color match the browser UI on Linux, and on HCM
+ * with a lightweight theme. */
+ &[lwtheme] {
+ --attention-dot-color: light-dark(#2ac3a2, #54ffbd);
+ }
+ @media (-moz-platform: linux) {
+ &:not([lwtheme]) {
+ --attention-dot-color: AccentColor;
+ }
+ }
}
@media (prefers-color-scheme: dark) {
@@ -47,7 +58,7 @@
}
}
-@media (prefers-contrast) {
+@media (forced-colors) {
:root {
--fxview-element-background-hover: ButtonText;
--fxview-element-background-active: ButtonText;
@@ -59,6 +70,12 @@
}
}
+@media (prefers-contrast) {
+ :root {
+ --fxview-border: var(--border-color);
+ }
+}
+
@media (max-width: 52rem) {
:root {
--fxview-sidebar-width: 82px;
diff --git a/browser/components/firefoxview/firefoxview.html b/browser/components/firefoxview/firefoxview.html
index 6fa0f59a8f..5bffb5a1d8 100644
--- a/browser/components/firefoxview/firefoxview.html
+++ b/browser/components/firefoxview/firefoxview.html
@@ -72,6 +72,7 @@
>
</moz-page-nav-button>
<moz-page-nav-button
+ class="sync-ui-item"
view="syncedtabs"
data-l10n-id="firefoxview-synced-tabs-nav"
iconSrc="chrome://browser/content/firefoxview/view-syncedtabs.svg"
@@ -95,7 +96,10 @@
<view-recentlyclosed slot="recentlyclosed"></view-recentlyclosed>
</div>
<div>
- <view-syncedtabs slot="syncedtabs"></view-syncedtabs>
+ <view-syncedtabs
+ class="sync-ui-item"
+ slot="syncedtabs"
+ ></view-syncedtabs>
</div>
</view-recentbrowsing>
<view-history name="history" type="page"></view-history>
@@ -104,7 +108,11 @@
name="recentlyclosed"
type="page"
></view-recentlyclosed>
- <view-syncedtabs name="syncedtabs" type="page"></view-syncedtabs>
+ <view-syncedtabs
+ class="sync-ui-item"
+ name="syncedtabs"
+ type="page"
+ ></view-syncedtabs>
</named-deck>
</div>
</main>
diff --git a/browser/components/firefoxview/firefoxview.mjs b/browser/components/firefoxview/firefoxview.mjs
index 3e61482cc0..e31536bc8b 100644
--- a/browser/components/firefoxview/firefoxview.mjs
+++ b/browser/components/firefoxview/firefoxview.mjs
@@ -80,6 +80,16 @@ async function updateSearchKeyboardShortcut() {
searchKeyboardShortcut = key.toLocaleLowerCase();
}
+function updateSyncVisibility() {
+ const syncEnabled = Services.prefs.getBoolPref(
+ "identity.fxaccounts.enabled",
+ false
+ );
+ for (const el of document.querySelectorAll(".sync-ui-item")) {
+ el.hidden = !syncEnabled;
+ }
+}
+
window.addEventListener("DOMContentLoaded", async () => {
recordEnteredTelemetry();
@@ -106,6 +116,7 @@ window.addEventListener("DOMContentLoaded", async () => {
onViewsDeckViewChange();
await updateSearchTextboxSize();
await updateSearchKeyboardShortcut();
+ updateSyncVisibility();
if (Cu.isInAutomation) {
Services.obs.notifyObservers(null, "firefoxview-entered");
@@ -150,12 +161,17 @@ window.addEventListener(
document.body.textContent = "";
topChromeWindow.removeEventListener("command", onCommand);
Services.obs.removeObserver(onLocalesChanged, "intl:app-locales-changed");
+ Services.prefs.removeObserver(
+ "identity.fxaccounts.enabled",
+ updateSyncVisibility
+ );
},
{ once: true }
);
topChromeWindow.addEventListener("command", onCommand);
Services.obs.addObserver(onLocalesChanged, "intl:app-locales-changed");
+Services.prefs.addObserver("identity.fxaccounts.enabled", updateSyncVisibility);
function onCommand(e) {
if (document.hidden || !e.target.closest("#contentAreaContextMenu")) {
diff --git a/browser/components/firefoxview/fxview-empty-state.css b/browser/components/firefoxview/fxview-empty-state.css
index 80b4099e6a..8c0d08c1f8 100644
--- a/browser/components/firefoxview/fxview-empty-state.css
+++ b/browser/components/firefoxview/fxview-empty-state.css
@@ -93,7 +93,7 @@
img.greyscale {
filter: grayscale(100%);
- @media not (prefers-contrast) {
+ @media not (forced-colors) {
opacity: 0.5;
}
}
diff --git a/browser/components/firefoxview/fxview-tab-list.css b/browser/components/firefoxview/fxview-tab-list.css
index 5a4bff023a..f0881d8ce8 100644
--- a/browser/components/firefoxview/fxview-tab-list.css
+++ b/browser/components/firefoxview/fxview-tab-list.css
@@ -9,35 +9,21 @@
.fxview-tab-list {
display: grid;
- grid-template-columns: min-content 3fr min-content 2fr 1fr 1fr min-content min-content;
+ grid-template-columns: min-content 3fr 2fr 1fr 1fr min-content;
gap: var(--space-xsmall);
- &.pinned {
- display: flex;
- flex-wrap: wrap;
-
- > virtual-list {
- display: block;
- }
-
- > fxview-tab-row {
- display: block;
- margin-block-end: var(--space-xsmall);
- }
- }
-
:host([compactRows]) & {
- grid-template-columns: min-content 1fr min-content min-content min-content;
+ grid-template-columns: min-content 1fr min-content min-content;
}
}
virtual-list {
display: grid;
- grid-column: span 9;
+ grid-column: span 7;
grid-template-columns: subgrid;
.top-padding,
.bottom-padding {
- grid-column: span 9;
+ grid-column: span 7;
}
}
diff --git a/browser/components/firefoxview/fxview-tab-list.mjs b/browser/components/firefoxview/fxview-tab-list.mjs
index 978ab79724..57181e3bea 100644
--- a/browser/components/firefoxview/fxview-tab-list.mjs
+++ b/browser/components/firefoxview/fxview-tab-list.mjs
@@ -12,6 +12,8 @@ import {
} from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
import { escapeRegExp } from "./helpers.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-button.mjs";
const NOW_THRESHOLD_MS = 91000;
const FXVIEW_ROW_HEIGHT_PX = 32;
@@ -45,13 +47,13 @@ if (!window.IS_STORYBOOK) {
* @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} searchInProgress - Whether a search has been initiated.
* @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 {
+export class FxviewTabListBase extends MozLitElement {
constructor() {
super();
window.MozXULElement.insertFTLIfNeeded("toolkit/branding/brandings.ftl");
@@ -62,10 +64,8 @@ export default class FxviewTabList extends MozLitElement {
this.dateTimeFormat = "relative";
this.maxTabsLength = 25;
this.tabItems = [];
- this.pinnedTabs = [];
- this.pinnedTabsGridView = false;
- this.unpinnedTabs = [];
this.compactRows = false;
+ this.searchInProgress = false;
this.updatesPaused = true;
this.#register();
}
@@ -77,16 +77,18 @@ export default class FxviewTabList extends MozLitElement {
dateTimeFormat: { type: String },
hasPopup: { type: String },
maxTabsLength: { type: Number },
- pinnedTabsGridView: { type: Boolean },
tabItems: { type: Array },
updatesPaused: { type: Boolean },
searchQuery: { type: String },
+ searchInProgress: { type: Boolean },
secondaryActionClass: { type: String },
tertiaryActionClass: { type: String },
};
static queries = {
- rowEls: { all: "fxview-tab-row" },
+ rowEls: {
+ all: "fxview-tab-row",
+ },
rootVirtualListEl: "virtual-list",
};
@@ -108,20 +110,7 @@ export default class FxviewTabList extends MozLitElement {
}
}
- // 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) {
+ if (this.maxTabsLength > 0) {
this.tabItems = this.tabItems.slice(0, this.maxTabsLength);
}
}
@@ -148,7 +137,7 @@ export default class FxviewTabList extends MozLitElement {
"timeMsPref",
"browser.tabs.firefox-view.updateTimeMs",
NOW_THRESHOLD_MS,
- (prefName, oldVal, newVal) => {
+ () => {
this.clearIntervalTimer();
if (!this.isConnected) {
return;
@@ -197,93 +186,32 @@ export default class FxviewTabList extends MozLitElement {
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();
- }
+ 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();
- }
+ 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);
+ fxviewTabRow.moveFocusLeft();
} else {
- this.moveFocusRight(fxviewTabRow);
+ fxviewTabRow.moveFocusRight();
}
} 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);
+ fxviewTabRow.moveFocusRight();
} else {
- this.moveFocusLeft(fxviewTabRow);
+ fxviewTabRow.moveFocusLeft();
}
}
}
- 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);
}
@@ -294,18 +222,12 @@ export default class FxviewTabList extends MozLitElement {
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 (lazy.virtualListEnabledPref) {
+ let row = this.rootVirtualListEl.getItem(index);
if (!row) {
return;
}
- let subList = this.rootVirtualListEl.getSubListForItem(
- index - this.pinnedTabs.length
- );
+ let subList = this.rootVirtualListEl.getSubListForItem(index);
if (!subList) {
return;
}
@@ -347,27 +269,15 @@ export default class FxviewTabList extends MozLitElement {
time = tabItem.time || tabItem.closedAt;
}
}
+
return html`
<fxview-tab-row
- exportparts="secondary-button"
- class=${classMap({
- pinned:
- this.pinnedTabsGridView && tabItem.indicators?.includes("pinned"),
- })}
?active=${i == this.activeIndex}
?compact=${this.compactRows}
- .hasPopup=${this.hasPopup}
- .containerObj=${ifDefined(tabItem.containerObj)}
.currentActiveElementId=${this.currentActiveElementId}
- .dateTimeFormat=${this.dateTimeFormat}
.favicon=${tabItem.icon}
- .indicators=${ifDefined(tabItem.indicators)}
- .pinnedTabsGridView=${ifDefined(this.pinnedTabsGridView)}
.primaryL10nId=${tabItem.primaryL10nId}
.primaryL10nArgs=${ifDefined(tabItem.primaryL10nArgs)}
- role=${this.pinnedTabsGridView && tabItem.indicators?.includes("pinned")
- ? "none"
- : "listitem"}
.secondaryL10nId=${tabItem.secondaryL10nId}
.secondaryL10nArgs=${ifDefined(tabItem.secondaryL10nArgs)}
.tertiaryL10nId=${ifDefined(tabItem.tertiaryL10nId)}
@@ -377,41 +287,36 @@ export default class FxviewTabList extends MozLitElement {
.sourceClosedId=${ifDefined(tabItem.sourceClosedId)}
.sourceWindowId=${ifDefined(tabItem.sourceWindowId)}
.closedId=${ifDefined(tabItem.closedId || tabItem.closedId)}
- .searchQuery=${ifDefined(this.searchQuery)}
+ role="listitem"
.tabElement=${ifDefined(tabItem.tabElement)}
.time=${ifDefined(time)}
- .timeMsPref=${ifDefined(this.timeMsPref)}
.title=${tabItem.title}
.url=${tabItem.url}
+ .searchQuery=${ifDefined(this.searchQuery)}
+ .timeMsPref=${ifDefined(this.timeMsPref)}
+ .hasPopup=${this.hasPopup}
+ .dateTimeFormat=${this.dateTimeFormat}
></fxview-tab-row>
`;
};
+ stylesheets() {
+ return html`<link
+ rel="stylesheet"
+ href="chrome://browser/content/firefoxview/fxview-tab-list.css"
+ />`;
+ }
+
render() {
- if (this.searchQuery && this.tabItems.length === 0) {
- return this.#emptySearchResultsTemplate();
+ if (
+ this.searchQuery &&
+ this.tabItems.length === 0 &&
+ !this.searchInProgress
+ ) {
+ return this.emptySearchResultsTemplate();
}
return html`
- <link
- rel="stylesheet"
- href="chrome://browser/content/firefoxview/fxview-tab-list.css"
- />
- ${when(
- this.pinnedTabsGridView && this.pinnedTabs.length,
- () => html`
- <div
- id="fxview-tab-list"
- class="fxview-tab-list pinned"
- data-l10n-id="firefoxview-pinned-tabs"
- role="tablist"
- @keydown=${this.handleFocusElementInRow}
- >
- ${this.pinnedTabs.map((tabItem, i) =>
- this.itemTemplate(tabItem, i)
- )}
- </div>
- `
- )}
+ ${this.stylesheets()}
<div
id="fxview-tab-list"
class="fxview-tab-list"
@@ -424,28 +329,21 @@ export default class FxviewTabList extends MozLitElement {
() => html`
<virtual-list
.activeIndex=${this.activeIndex}
- .pinnedTabsIndexOffset=${this.pinnedTabsGridView
- ? this.pinnedTabs.length
- : 0}
- .items=${this.pinnedTabsGridView
- ? this.unpinnedTabs
- : this.tabItems}
+ .items=${this.tabItems}
.template=${this.itemTemplate}
></virtual-list>
- `
- )}
- ${when(
- !lazy.virtualListEnabledPref,
- () => html`
- ${this.tabItems.map((tabItem, i) => this.itemTemplate(tabItem, i))}
- `
+ `,
+ () =>
+ html`${this.tabItems.map((tabItem, i) =>
+ this.itemTemplate(tabItem, i)
+ )}`
)}
</div>
<slot name="menu"></slot>
`;
}
- #emptySearchResultsTemplate() {
+ emptySearchResultsTemplate() {
return html` <fxview-empty-state
class="search-results"
headerLabel="firefoxview-search-results-empty"
@@ -455,23 +353,20 @@ export default class FxviewTabList extends MozLitElement {
</fxview-empty-state>`;
}
}
-customElements.define("fxview-tab-list", FxviewTabList);
+customElements.define("fxview-tab-list", FxviewTabListBase);
/**
* A tab item that displays favicon, title, url, and time of last access
*
* @property {boolean} active - Should current item have focus on keydown
* @property {boolean} compact - Whether to hide the URL and date/time for this tab.
- * @property {object} containerObj - Info about an open tab's container if within one
* @property {string} currentActiveElementId - ID of currently focused element within each tab item
* @property {string} dateTimeFormat - Expected format for date and/or time
* @property {string} hasPopup - The aria-haspopup attribute for the secondary action, if required
- * @property {string} indicators - An array of tab indicators if any are present
* @property {number} closedId - The tab ID for when the tab item was closed.
* @property {number} sourceClosedId - The closedId of the closed window its from if applicable
* @property {number} sourceWindowId - The sessionstore id of the window its from if applicable
* @property {string} favicon - The favicon for the tab item.
- * @property {boolean} pinnedTabsGridView - Whether the show pinned tabs in a grid view
* @property {string} primaryL10nId - The l10n id used for the primary action element
* @property {string} primaryL10nArgs - The l10n args used for the primary action element
* @property {string} secondaryL10nId - The l10n id used for the secondary action button
@@ -487,23 +382,14 @@ customElements.define("fxview-tab-list", FxviewTabList);
* @property {number} timeMsPref - The frequency in milliseconds of updates to relative time
* @property {string} searchQuery - The query string to highlight, if provided.
*/
-export class FxviewTabRow extends MozLitElement {
- constructor() {
- super();
- this.active = false;
- this.currentActiveElementId = "fxview-tab-row-main";
- }
-
+export class FxviewTabRowBase extends MozLitElement {
static properties = {
active: { type: Boolean },
compact: { type: Boolean },
- containerObj: { type: Object },
currentActiveElementId: { type: String },
dateTimeFormat: { type: String },
favicon: { type: String },
hasPopup: { type: String },
- indicators: { type: Array },
- pinnedTabsGridView: { type: Boolean },
primaryL10nId: { type: String },
primaryL10nArgs: { type: String },
secondaryL10nId: { type: String },
@@ -523,12 +409,16 @@ export class FxviewTabRow extends MozLitElement {
searchQuery: { type: String },
};
+ constructor() {
+ super();
+ this.active = false;
+ this.currentActiveElementId = "fxview-tab-row-main";
+ }
+
static queries = {
mainEl: "#fxview-tab-row-main",
secondaryButtonEl: "#fxview-tab-row-secondary-button:not([hidden])",
tertiaryButtonEl: "#fxview-tab-row-tertiary-button",
- mediaButtonEl: "#fxview-tab-row-media-button",
- pinnedTabButtonEl: "button#fxview-tab-row-main",
};
get currentFocusable() {
@@ -539,50 +429,45 @@ export class FxviewTabRow extends MozLitElement {
return focusItem;
}
- connectedCallback() {
- super.connectedCallback();
- this.addEventListener("keydown", this.handleKeydown);
- }
-
- disconnectedCallback() {
- super.disconnectedCallback();
- this.removeEventListener("keydown", this.handleKeydown);
- }
-
- handleKeydown(e) {
- if (
- this.active &&
- this.pinnedTabsGridView &&
- this.indicators?.includes("pinned") &&
- e.key === "m" &&
- e.ctrlKey
- ) {
- this.muteOrUnmuteTab();
- }
- }
-
focus() {
this.currentFocusable.focus();
}
focusSecondaryButton() {
+ let tabList = this.getRootNode().host;
this.secondaryButtonEl.focus();
- return this.secondaryButtonEl.id;
+ tabList.currentActiveElementId = this.secondaryButtonEl.id;
}
focusTertiaryButton() {
+ let tabList = this.getRootNode().host;
this.tertiaryButtonEl.focus();
- return this.tertiaryButtonEl.id;
- }
-
- focusMediaButton() {
- this.mediaButtonEl.focus();
- return this.mediaButtonEl.id;
+ tabList.currentActiveElementId = this.tertiaryButtonEl.id;
}
focusLink() {
+ let tabList = this.getRootNode().host;
this.mainEl.focus();
- return this.mainEl.id;
+ tabList.currentActiveElementId = this.mainEl.id;
+ }
+
+ moveFocusRight() {
+ if (this.currentActiveElementId === "fxview-tab-row-main") {
+ this.focusSecondaryButton();
+ } else if (
+ this.tertiaryButtonEl &&
+ this.currentActiveElementId === "fxview-tab-row-secondary-button"
+ ) {
+ this.focusTertiaryButton();
+ }
+ }
+
+ moveFocusLeft() {
+ if (this.currentActiveElementId === "fxview-tab-row-tertiary-button") {
+ this.focusSecondaryButton();
+ } else {
+ this.focusLink();
+ }
}
dateFluentArgs(timestamp, dateTimeFormat) {
@@ -652,16 +537,6 @@ export class FxviewTabRow extends MozLitElement {
return icon;
}
- getContainerClasses() {
- let containerClasses = ["fxview-tab-row-container-indicator", "icon"];
- if (this.containerObj) {
- let { icon, color } = this.containerObj;
- containerClasses.push(`identity-icon-${icon}`);
- containerClasses.push(`identity-color-${color}`);
- }
- return containerClasses;
- }
-
primaryActionHandler(event) {
if (
(event.type == "click" && !event.altKey) ||
@@ -683,9 +558,6 @@ export class FxviewTabRow extends MozLitElement {
secondaryActionHandler(event) {
if (
- (this.pinnedTabsGridView &&
- this.indicators?.includes("pinned") &&
- event.type == "contextmenu") ||
(event.type == "click" && event.detail && !event.altKey) ||
// detail=0 is from keyboard
(event.type == "click" && !event.detail)
@@ -718,92 +590,80 @@ export class FxviewTabRow extends MozLitElement {
}
}
- muteOrUnmuteTab(e) {
- e?.preventDefault();
- // If the tab has no sound playing, the mute/unmute button will be removed when toggled.
- // We should move the focus to the right in that case. This does not apply to pinned tabs
- // on the Open Tabs page.
- let shouldMoveFocus =
- (!this.pinnedTabsGridView ||
- (!this.indicators.includes("pinned") && this.pinnedTabsGridView)) &&
- this.mediaButtonEl &&
- !this.indicators.includes("soundplaying") &&
- this.currentActiveElementId === "fxview-tab-row-media-button";
-
- // detail=0 is from keyboard
- if (e?.type == "click" && !e?.detail && shouldMoveFocus) {
- let tabList = this.getRootNode().host;
- if (document.dir == "rtl") {
- tabList.moveFocusLeft(this);
- } else {
- tabList.moveFocusRight(this);
- }
+ /**
+ * Find all matches of query within the given string, and compute the result
+ * to be rendered.
+ *
+ * @param {string} query
+ * @param {string} string
+ */
+ highlightSearchMatches(query, string) {
+ const fragments = [];
+ const regex = RegExp(escapeRegExp(query), "dgi");
+ let prevIndexEnd = 0;
+ let result;
+ while ((result = regex.exec(string)) !== null) {
+ const [indexStart, indexEnd] = result.indices[0];
+ fragments.push(string.substring(prevIndexEnd, indexStart));
+ fragments.push(
+ html`<strong>${string.substring(indexStart, indexEnd)}</strong>`
+ );
+ prevIndexEnd = regex.lastIndex;
}
- this.tabElement.toggleMuteAudio();
+ fragments.push(string.substring(prevIndexEnd));
+ return fragments;
+ }
+
+ stylesheets() {
+ return html`<link
+ rel="stylesheet"
+ href="chrome://browser/content/firefoxview/fxview-tab-row.css"
+ />`;
}
- #faviconTemplate() {
+ faviconTemplate() {
return html`<span
- class="${classMap({
- "fxview-tab-row-favicon-wrapper": true,
- pinned: this.indicators?.includes("pinned"),
- pinnedOnNewTab: this.indicators?.includes("pinnedOnNewTab"),
- attention: this.indicators?.includes("attention"),
- bookmark: this.indicators?.includes("bookmark"),
- })}"
+ class="fxview-tab-row-favicon icon"
+ id="fxview-tab-row-favicon"
+ style=${styleMap({
+ backgroundImage: `url(${this.getImageUrl(this.favicon, this.url)})`,
+ })}
+ ></span>`;
+ }
+
+ titleTemplate() {
+ const title = this.title;
+ return html`<span
+ class="fxview-tab-row-title text-truncated-ellipsis"
+ id="fxview-tab-row-title"
+ dir="auto"
>
- <span
- class="fxview-tab-row-favicon icon"
- id="fxview-tab-row-favicon"
- style=${styleMap({
- backgroundImage: `url(${this.getImageUrl(this.favicon, this.url)})`,
- })}
- ></span>
${when(
- this.pinnedTabsGridView &&
- this.indicators?.includes("pinned") &&
- (this.indicators?.includes("muted") ||
- this.indicators?.includes("soundplaying")),
- () => html`
- <button
- class="fxview-tab-row-pinned-media-button ghost-button icon-button"
- id="fxview-tab-row-media-button"
- tabindex="-1"
- data-l10n-id=${this.indicators?.includes("muted")
- ? "fxviewtabrow-unmute-tab-button-no-context"
- : "fxviewtabrow-mute-tab-button-no-context"}
- muted=${this.indicators?.includes("muted")}
- soundplaying=${this.indicators?.includes("soundplaying") &&
- !this.indicators?.includes("muted")}
- @click=${this.muteOrUnmuteTab}
- ></button>
- `
+ this.searchQuery,
+ () => this.highlightSearchMatches(this.searchQuery, title),
+ () => title
)}
</span>`;
}
- #pinnedTabItemTemplate() {
- return html` <button
- class="fxview-tab-row-main ghost-button semi-transparent"
- id="fxview-tab-row-main"
- aria-haspopup=${ifDefined(this.hasPopup)}
- data-l10n-id=${ifDefined(this.primaryL10nId)}
- data-l10n-args=${ifDefined(this.primaryL10nArgs)}
- tabindex=${this.active &&
- this.currentActiveElementId === "fxview-tab-row-main"
- ? "0"
- : "-1"}
- role="tab"
- @click=${this.primaryActionHandler}
- @keydown=${this.primaryActionHandler}
- @contextmenu=${this.secondaryActionHandler}
+ urlTemplate() {
+ return html`<span
+ class="fxview-tab-row-url text-truncated-ellipsis"
+ id="fxview-tab-row-url"
>
- ${this.#faviconTemplate()}
- </button>`;
+ ${when(
+ this.searchQuery,
+ () =>
+ this.highlightSearchMatches(
+ this.searchQuery,
+ this.formatURIForDisplay(this.url)
+ ),
+ () => this.formatURIForDisplay(this.url)
+ )}
+ </span>`;
}
- #unpinnedTabItemTemplate() {
- const title = this.title;
+ dateTemplate() {
const relativeString = this.relativeTime(
this.time,
this.dateTimeFormat,
@@ -815,11 +675,81 @@ export class FxviewTabRow extends MozLitElement {
!window.IS_STORYBOOK ? this.timeMsPref : NOW_THRESHOLD_MS
);
const dateArgs = this.dateFluentArgs(this.time, this.dateTimeFormat);
+ return html`<span class="fxview-tab-row-date" id="fxview-tab-row-date">
+ <span
+ ?hidden=${relativeString || !dateString}
+ data-l10n-id=${ifDefined(dateString)}
+ data-l10n-args=${ifDefined(dateArgs)}
+ ></span>
+ <span ?hidden=${!relativeString}>${relativeString}</span>
+ </span>`;
+ }
+
+ timeTemplate() {
const timeString = this.timeFluentId(this.dateTimeFormat);
const time = this.time;
const timeArgs = JSON.stringify({ time });
+ return html`<span
+ class="fxview-tab-row-time"
+ id="fxview-tab-row-time"
+ ?hidden=${!timeString}
+ data-timestamp=${ifDefined(this.time)}
+ data-l10n-id=${ifDefined(timeString)}
+ data-l10n-args=${ifDefined(timeArgs)}
+ >
+ </span>`;
+ }
- return html`<a
+ secondaryButtonTemplate() {
+ return html`${when(
+ this.secondaryL10nId && this.secondaryActionHandler,
+ () => html`<moz-button
+ type="icon ghost"
+ class=${classMap({
+ "fxview-tab-row-button": true,
+ [this.secondaryActionClass]: this.secondaryActionClass,
+ })}
+ id="fxview-tab-row-secondary-button"
+ data-l10n-id=${this.secondaryL10nId}
+ data-l10n-args=${ifDefined(this.secondaryL10nArgs)}
+ aria-haspopup=${ifDefined(this.hasPopup)}
+ @click=${this.secondaryActionHandler}
+ tabindex="${this.active &&
+ this.currentActiveElementId === "fxview-tab-row-secondary-button"
+ ? "0"
+ : "-1"}"
+ ></moz-button>`
+ )}`;
+ }
+
+ tertiaryButtonTemplate() {
+ return html`${when(
+ this.tertiaryL10nId && this.tertiaryActionHandler,
+ () => html`<moz-button
+ type="icon ghost"
+ class=${classMap({
+ "fxview-tab-row-button": true,
+ [this.tertiaryActionClass]: this.tertiaryActionClass,
+ })}
+ id="fxview-tab-row-tertiary-button"
+ data-l10n-id=${this.tertiaryL10nId}
+ data-l10n-args=${ifDefined(this.tertiaryL10nArgs)}
+ aria-haspopup=${ifDefined(this.hasPopup)}
+ @click=${this.tertiaryActionHandler}
+ tabindex="${this.active &&
+ this.currentActiveElementId === "fxview-tab-row-tertiary-button"
+ ? "0"
+ : "-1"}"
+ ></moz-button>`
+ )}`;
+ }
+}
+
+export class FxviewTabRow extends FxviewTabRowBase {
+ render() {
+ return html`
+ ${this.stylesheets()}
+ <a
href=${ifDefined(this.url)}
class="fxview-tab-row-main"
id="fxview-tab-row-main"
@@ -833,176 +763,16 @@ export class FxviewTabRow extends MozLitElement {
@keydown=${this.primaryActionHandler}
title=${!this.primaryL10nId ? this.url : null}
>
- ${this.#faviconTemplate()}
- <span
- class="fxview-tab-row-title text-truncated-ellipsis"
- id="fxview-tab-row-title"
- dir="auto"
- >
- ${when(
- this.searchQuery,
- () => this.#highlightSearchMatches(this.searchQuery, title),
- () => title
- )}
- </span>
- <span class=${this.getContainerClasses().join(" ")}></span>
- <span
- class="fxview-tab-row-url text-truncated-ellipsis"
- id="fxview-tab-row-url"
- ?hidden=${this.compact}
- >
- ${when(
- this.searchQuery,
- () =>
- this.#highlightSearchMatches(
- this.searchQuery,
- this.formatURIForDisplay(this.url)
- ),
- () => this.formatURIForDisplay(this.url)
- )}
- </span>
- <span
- class="fxview-tab-row-date"
- id="fxview-tab-row-date"
- ?hidden=${this.compact}
- >
- <span
- ?hidden=${relativeString || !dateString}
- data-l10n-id=${ifDefined(dateString)}
- data-l10n-args=${ifDefined(dateArgs)}
- ></span>
- <span ?hidden=${!relativeString}>${relativeString}</span>
- </span>
- <span
- class="fxview-tab-row-time"
- id="fxview-tab-row-time"
- ?hidden=${this.compact || !timeString}
- data-timestamp=${ifDefined(this.time)}
- data-l10n-id=${ifDefined(timeString)}
- data-l10n-args=${ifDefined(timeArgs)}
- >
- </span>
+ ${this.faviconTemplate()} ${this.titleTemplate()}
+ ${when(
+ !this.compact,
+ () => html`${this.urlTemplate()} ${this.dateTemplate()}
+ ${this.timeTemplate()}`
+ )}
</a>
- ${when(
- this.indicators?.includes("soundplaying") ||
- this.indicators?.includes("muted"),
- () => html`<button
- class=fxview-tab-row-button ghost-button icon-button semi-transparent"
- id="fxview-tab-row-media-button"
- data-l10n-id=${
- this.indicators?.includes("muted")
- ? "fxviewtabrow-unmute-tab-button-no-context"
- : "fxviewtabrow-mute-tab-button-no-context"
- }
- muted=${this.indicators?.includes("muted")}
- soundplaying=${
- this.indicators?.includes("soundplaying") &&
- !this.indicators?.includes("muted")
- }
- @click=${this.muteOrUnmuteTab}
- tabindex="${
- this.active &&
- this.currentActiveElementId === "fxview-tab-row-media-button"
- ? "0"
- : "-1"
- }"
- ></button>`,
- () => html`<span></span>`
- )}
- ${when(
- this.secondaryL10nId && this.secondaryActionHandler,
- () => html`<button
- class=${classMap({
- "fxview-tab-row-button": true,
- "ghost-button": true,
- "icon-button": true,
- "semi-transparent": true,
- [this.secondaryActionClass]: this.secondaryActionClass,
- })}
- id="fxview-tab-row-secondary-button"
- data-l10n-id=${this.secondaryL10nId}
- data-l10n-args=${ifDefined(this.secondaryL10nArgs)}
- aria-haspopup=${ifDefined(this.hasPopup)}
- @click=${this.secondaryActionHandler}
- tabindex="${this.active &&
- this.currentActiveElementId === "fxview-tab-row-secondary-button"
- ? "0"
- : "-1"}"
- ></button>`
- )}
- ${when(
- this.tertiaryL10nId && this.tertiaryActionHandler,
- () => html`<button
- class=${classMap({
- "fxview-tab-row-button": true,
- "ghost-button": true,
- "icon-button": true,
- "semi-transparent": true,
- [this.tertiaryActionClass]: this.tertiaryActionClass,
- })}
- id="fxview-tab-row-tertiary-button"
- data-l10n-id=${this.tertiaryL10nId}
- data-l10n-args=${ifDefined(this.tertiaryL10nArgs)}
- aria-haspopup=${ifDefined(this.hasPopup)}
- @click=${this.tertiaryActionHandler}
- tabindex="${this.active &&
- this.currentActiveElementId === "fxview-tab-row-tertiary-button"
- ? "0"
- : "-1"}"
- ></button>`
- )}`;
- }
-
- render() {
- return html`
- ${when(
- this.containerObj,
- () => html`
- <link
- rel="stylesheet"
- href="chrome://browser/content/usercontext/usercontext.css"
- />
- `
- )}
- <link
- rel="stylesheet"
- href="chrome://global/skin/in-content/common.css"
- />
- <link
- rel="stylesheet"
- href="chrome://browser/content/firefoxview/fxview-tab-row.css"
- />
- ${when(
- this.pinnedTabsGridView && this.indicators?.includes("pinned"),
- this.#pinnedTabItemTemplate.bind(this),
- this.#unpinnedTabItemTemplate.bind(this)
- )}
+ ${this.secondaryButtonTemplate()} ${this.tertiaryButtonTemplate()}
`;
}
-
- /**
- * Find all matches of query within the given string, and compute the result
- * to be rendered.
- *
- * @param {string} query
- * @param {string} string
- */
- #highlightSearchMatches(query, string) {
- const fragments = [];
- const regex = RegExp(escapeRegExp(query), "dgi");
- let prevIndexEnd = 0;
- let result;
- while ((result = regex.exec(string)) !== null) {
- const [indexStart, indexEnd] = result.indices[0];
- fragments.push(string.substring(prevIndexEnd, indexStart));
- fragments.push(
- html`<strong>${string.substring(indexStart, indexEnd)}</strong>`
- );
- prevIndexEnd = regex.lastIndex;
- }
- fragments.push(string.substring(prevIndexEnd));
- return fragments;
- }
}
customElements.define("fxview-tab-row", FxviewTabRow);
@@ -1040,10 +810,16 @@ export class VirtualList extends MozLitElement {
this.isSubList = false;
this.isVisible = false;
this.intersectionObserver = new IntersectionObserver(
- ([entry]) => (this.isVisible = entry.isIntersecting),
+ ([entry]) => {
+ this.isVisible = entry.isIntersecting;
+ },
{ root: this.ownerDocument }
);
- this.resizeObserver = new ResizeObserver(([entry]) => {
+ this.selfResizeObserver = new ResizeObserver(() => {
+ // Trigger the intersection observer once the tab rows have rendered
+ this.triggerIntersectionObserver();
+ });
+ this.childResizeObserver = new ResizeObserver(([entry]) => {
if (entry.contentRect?.height > 0) {
// Update properties on top-level virtual-list
this.parentElement.itemHeightEstimate = entry.contentRect.height;
@@ -1058,7 +834,8 @@ export class VirtualList extends MozLitElement {
disconnectedCallback() {
super.disconnectedCallback();
this.intersectionObserver.disconnect();
- this.resizeObserver.disconnect();
+ this.childResizeObserver.disconnect();
+ this.selfResizeObserver.disconnect();
}
triggerIntersectionObserver() {
@@ -1090,7 +867,6 @@ export class VirtualList extends MozLitElement {
this.items.slice(i, i + this.maxRenderCountEstimate)
);
}
- this.triggerIntersectionObserver();
}
}
@@ -1103,13 +879,17 @@ export class VirtualList extends MozLitElement {
firstUpdated() {
this.intersectionObserver.observe(this);
+ this.selfResizeObserver.observe(this);
if (this.isSubList && this.children[0]) {
- this.resizeObserver.observe(this.children[0]);
+ this.childResizeObserver.observe(this.children[0]);
}
}
updated(changedProperties) {
this.updateListHeight(changedProperties);
+ if (changedProperties.has("items") && !this.isSubList) {
+ this.triggerIntersectionObserver();
+ }
}
updateListHeight(changedProperties) {
@@ -1157,5 +937,4 @@ export class VirtualList extends MozLitElement {
return "";
}
}
-
customElements.define("virtual-list", VirtualList);
diff --git a/browser/components/firefoxview/fxview-tab-row.css b/browser/components/firefoxview/fxview-tab-row.css
index 219d7e8aa2..c1c8f967a7 100644
--- a/browser/components/firefoxview/fxview-tab-row.css
+++ b/browser/components/firefoxview/fxview-tab-row.css
@@ -2,9 +2,11 @@
* 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 url("chrome://global/skin/design-system/text-and-typography.css");
+
:host {
- --fxviewtabrow-element-background-hover: color-mix(in srgb, currentColor 14%, transparent);
- --fxviewtabrow-element-background-active: color-mix(in srgb, currentColor 21%, transparent);
+ --fxviewtabrow-element-background-hover: var(--button-background-color-ghost-hover);
+ --fxviewtabrow-element-background-active: var(--button-background-color-ghost-active);
display: grid;
grid-template-columns: subgrid;
grid-column: span 9;
@@ -12,7 +14,7 @@
border-radius: 4px;
}
-@media (prefers-contrast) {
+@media (forced-colors) {
:host {
--fxviewtabrow-element-background-hover: ButtonText;
--fxviewtabrow-element-background-active: ButtonText;
@@ -32,115 +34,42 @@
cursor: pointer;
text-decoration: none;
- :host(.pinned) & {
- padding: var(--space-small);
- min-width: unset;
- margin: 0;
+ :host([compact]) & {
+ grid-template-columns: min-content auto;
}
}
.fxview-tab-row-main,
.fxview-tab-row-main:visited,
-.fxview-tab-row-main:hover:active,
-.fxview-tab-row-button {
+.fxview-tab-row-main:hover:active {
color: inherit;
}
-.fxview-tab-row-main:hover,
-.fxview-tab-row-button.ghost-button.icon-button:enabled:hover {
+.fxview-tab-row-main:hover {
background-color: var(--fxviewtabrow-element-background-hover);
color: var(--fxviewtabrow-text-color-hover);
-
- & .fxview-tab-row-favicon-wrapper .fxview-tab-row-favicon::after {
- stroke: var(--fxview-indicator-stroke-color-hover);
- }
}
-.fxview-tab-row-main:hover:active,
-.fxview-tab-row-button.ghost-button.icon-button:enabled:hover:active {
+.fxview-tab-row-main:hover:active {
background-color: var(--fxviewtabrow-element-background-active);
}
-@media (prefers-contrast) {
- a.fxview-tab-row-main,
- a.fxview-tab-row-main:hover,
- a.fxview-tab-row-main:active {
+@media (forced-colors) {
+ .fxview-tab-row-main,
+ .fxview-tab-row-main:hover,
+ .fxview-tab-row-main:active {
background-color: transparent;
border: 1px solid LinkText;
color: LinkText;
}
- a.fxview-tab-row-main:visited,
- a.fxview-tab-row-main:visited:hover {
+ .fxview-tab-row-main:visited,
+ .fxview-tab-row-main:visited:hover {
border: 1px solid VisitedText;
color: VisitedText;
}
}
-.fxview-tab-row-favicon-wrapper {
- height: 16px;
- position: relative;
-
- .fxview-tab-row-favicon::after,
- .fxview-tab-row-button::after,
- &.pinned .fxview-tab-row-pinned-media-button {
- display: block;
- content: "";
- background-size: 12px;
- background-position: center;
- background-repeat: no-repeat;
- position: relative;
- height: 12px;
- width: 12px;
- -moz-context-properties: fill, stroke;
- fill: currentColor;
- stroke: var(--fxview-background-color-secondary);
- }
-
- &:is(.pinnedOnNewTab, .bookmark):not(.attention) .fxview-tab-row-favicon::after {
- inset-block-start: 9px;
- inset-inline-end: -6px;
- }
-
- &.pinnedOnNewTab .fxview-tab-row-favicon::after,
- &.pinnedOnNewTab .fxview-tab-row-button::after {
- background-image: url("chrome://browser/skin/pin-12.svg");
- }
-
- &.bookmark .fxview-tab-row-favicon::after,
- &.bookmark .fxview-tab-row-button::after {
- background-image: url("chrome://browser/skin/bookmark-12.svg");
- fill: var(--fxview-primary-action-background);
- }
-
- &.attention .fxview-tab-row-favicon::after,
- &.attention .fxview-tab-row-button::after {
- background-image: radial-gradient(circle, light-dark(rgb(42, 195, 162), rgb(84, 255, 189)), light-dark(rgb(42, 195, 162), rgb(84, 255, 189)) 2px, transparent 2px);
- height: 4px;
- width: 100%;
- inset-block-start: 20px;
- }
-
- &.pinned .fxview-tab-row-pinned-media-button {
- inset-block-start: -10px;
- inset-inline-end: -10px;
- border-radius: 100%;
- background-color: var(--fxview-background-color-secondary);
- padding: 6px;
- min-width: 0;
- min-height: 0;
- position: absolute;
-
- &[muted="true"] {
- background-image: url("chrome://global/skin/media/audio-muted.svg");
- }
-
- &[soundplaying="true"] {
- background-image: url("chrome://global/skin/media/audio.svg");
- }
- }
-}
-
.fxview-tab-row-favicon {
background-size: cover;
-moz-context-properties: fill;
@@ -155,15 +84,6 @@
text-align: match-parent;
}
-.fxview-tab-row-container-indicator {
- height: 16px;
- width: 16px;
- background-image: var(--identity-icon);
- background-size: cover;
- -moz-context-properties: fill;
- fill: var(--identity-icon-color);
-}
-
.fxview-tab-row-url {
color: var(--text-color-deemphasized);
text-decoration-line: underline;
@@ -182,62 +102,22 @@
font-weight: 400;
}
-.fxview-tab-row-button {
- margin: 0;
- cursor: pointer;
- min-width: 0;
- background-color: transparent;
-
- &[muted="true"],
- &[soundplaying="true"] {
- background-size: 16px;
- background-repeat: no-repeat;
- background-position: center;
- -moz-context-properties: fill;
- fill: currentColor;
- }
-
- &[muted="true"] {
- background-image: url("chrome://global/skin/media/audio-muted.svg");
- }
-
- &[soundplaying="true"] {
- background-image: url("chrome://global/skin/media/audio.svg");
- }
-
- &.dismiss-button {
- background-image: url("chrome://global/skin/icons/close.svg");
- }
-
- &.options-button {
- background-image: url("chrome://global/skin/icons/more.svg");
- }
+.fxview-tab-row-button::part(button) {
+ color: var(--fxview-text-primary-color)
}
-@media (prefers-contrast) {
- .fxview-tab-row-button,
- button.fxview-tab-row-main {
- border: 1px solid ButtonText;
- color: ButtonText;
- }
+.fxview-tab-row-button[muted="true"]::part(button) {
+ background-image: url("chrome://global/skin/media/audio-muted.svg");
+}
- .fxview-tab-row-button.ghost-button.icon-button:enabled:hover,
- button.fxview-tab-row-main:enabled:hover {
- border: 1px solid SelectedItem;
- color: SelectedItem;
- }
+.fxview-tab-row-button[soundplaying="true"]::part(button) {
+ background-image: url("chrome://global/skin/media/audio.svg");
+}
- .fxview-tab-row-button.ghost-button.icon-button:enabled:active,
- button.fxview-tab-row-main:enabled:active {
- color: SelectedItem;
- }
+.fxview-tab-row-button.dismiss-button::part(button) {
+ background-image: url("chrome://global/skin/icons/close.svg");
+}
- .fxview-tab-row-button.ghost-button.icon-button:enabled,
- .fxview-tab-row-button.ghost-button.icon-button:enabled:hover,
- .fxview-tab-row-button.ghost-button.icon-button:enabled:active
- button.fxview-tab-row-main:enabled,
- button.fxview-tab-row-main:enabled:hover,
- button.fxview-tab-row-main:enabled:active {
- background-color: ButtonFace;
- }
+.fxview-tab-row-button.options-button::part(button) {
+ background-image: url("chrome://global/skin/icons/more.svg");
}
diff --git a/browser/components/firefoxview/helpers.mjs b/browser/components/firefoxview/helpers.mjs
index 3cb308a587..b206deef18 100644
--- a/browser/components/firefoxview/helpers.mjs
+++ b/browser/components/firefoxview/helpers.mjs
@@ -173,3 +173,20 @@ export function escapeHtmlEntities(text) {
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
+
+export function navigateToLink(e) {
+ let currentWindow =
+ e.target.ownerGlobal.browsingContext.embedderWindowGlobal.browsingContext
+ .window;
+ if (currentWindow.openTrustedLinkIn) {
+ let where = lazy.BrowserUtils.whereToOpenLink(
+ e.detail.originalEvent,
+ false,
+ true
+ );
+ if (where == "current") {
+ where = "tab";
+ }
+ currentWindow.openTrustedLinkIn(e.originalTarget.url, where);
+ }
+}
diff --git a/browser/components/firefoxview/history.css b/browser/components/firefoxview/history.css
index dd2786a8c7..a10291ddb5 100644
--- a/browser/components/firefoxview/history.css
+++ b/browser/components/firefoxview/history.css
@@ -51,19 +51,8 @@
cursor: pointer;
}
-.import-history-banner .close {
+moz-button.close::part(button) {
background-image: url("chrome://global/skin/icons/close-12.svg");
- background-repeat: no-repeat;
- background-position: center center;
- -moz-context-properties: fill;
- fill: currentColor;
- min-width: auto;
- min-height: auto;
- width: 24px;
- height: 24px;
- margin: 0;
- padding: 0;
- flex-shrink: 0;
}
dialog {
diff --git a/browser/components/firefoxview/history.mjs b/browser/components/firefoxview/history.mjs
index 1fe028449b..478422d49b 100644
--- a/browser/components/firefoxview/history.mjs
+++ b/browser/components/firefoxview/history.mjs
@@ -7,18 +7,21 @@ import {
ifDefined,
when,
} from "chrome://global/content/vendor/lit.all.mjs";
-import { escapeHtmlEntities, isSearchEnabled } from "./helpers.mjs";
+import {
+ escapeHtmlEntities,
+ isSearchEnabled,
+ navigateToLink,
+} from "./helpers.mjs";
import { ViewPage } from "./viewpage.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://browser/content/migration/migration-wizard.mjs";
+import { HistoryController } from "./HistoryController.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-button.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
- BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
- FirefoxViewPlacesQuery:
- "resource:///modules/firefox-view-places-query.sys.mjs",
- PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs",
});
@@ -26,13 +29,6 @@ let XPCOMUtils = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
).XPCOMUtils;
-XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
- "maxRowsPref",
- "browser.firefox-view.max-history-rows",
- -1
-);
-
const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart";
const HAS_IMPORTED_HISTORY_PREF = "browser.migrate.interactions.history";
const IMPORT_HISTORY_DISMISSED_PREF =
@@ -44,35 +40,30 @@ class HistoryInView extends ViewPage {
constructor() {
super();
this._started = false;
- this.allHistoryItems = new Map();
- this.historyMapByDate = [];
- this.historyMapBySite = [];
// Setting maxTabsLength to -1 for no max
this.maxTabsLength = -1;
- this.placesQuery = new lazy.FirefoxViewPlacesQuery();
- this.searchQuery = "";
- this.searchResults = null;
- this.sortOption = "date";
this.profileAge = 8;
this.fullyUpdated = false;
this.cumulativeSearches = 0;
}
+ controller = new HistoryController(this, {
+ searchResultsLimit: SEARCH_RESULTS_LIMIT,
+ });
+
start() {
if (this._started) {
return;
}
this._started = true;
- this.#updateAllHistoryItems();
- this.placesQuery.observeHistory(data => this.#updateAllHistoryItems(data));
+ this.controller.updateAllHistoryItems();
this.toggleVisibilityInCardContainer();
}
async connectedCallback() {
super.connectedCallback();
- await this.updateHistoryData();
XPCOMUtils.defineLazyPreferenceGetter(
this,
"importHistoryDismissedPref",
@@ -91,6 +82,7 @@ class HistoryInView extends ViewPage {
this.requestUpdate();
}
);
+
if (!this.importHistoryDismissedPref && !this.hasImportedHistoryPrefs) {
let profileAccessor = await lazy.ProfileAge();
let profileCreateTime = await profileAccessor.created;
@@ -106,7 +98,6 @@ class HistoryInView extends ViewPage {
return;
}
this._started = false;
- this.placesQuery.close();
this.toggleVisibilityInCardContainer();
}
@@ -120,32 +111,6 @@ class HistoryInView extends ViewPage {
);
}
- async #updateAllHistoryItems(allHistoryItems) {
- if (allHistoryItems) {
- this.allHistoryItems = allHistoryItems;
- } else {
- await this.updateHistoryData();
- }
- this.resetHistoryMaps();
- this.lists.forEach(list => list.requestUpdate());
- await this.#updateSearchResults();
- }
-
- async #updateSearchResults() {
- if (this.searchQuery) {
- try {
- this.searchResults = await this.placesQuery.searchHistory(
- this.searchQuery,
- SEARCH_RESULTS_LIMIT
- );
- } catch (e) {
- // Connection interrupted, ignore.
- }
- } else {
- this.searchResults = null;
- }
- }
-
viewVisibleCallback() {
this.start();
}
@@ -166,14 +131,8 @@ class HistoryInView extends ViewPage {
};
static properties = {
- ...ViewPage.properties,
- allHistoryItems: { type: Map },
- historyMapByDate: { type: Array },
- historyMapBySite: { type: Array },
// Making profileAge a reactive property for testing
profileAge: { type: Number },
- searchResults: { type: Array },
- sortOption: { type: String },
};
async getUpdateComplete() {
@@ -181,70 +140,8 @@ class HistoryInView extends ViewPage {
await Promise.all(Array.from(this.cards).map(card => card.updateComplete));
}
- async updateHistoryData() {
- this.allHistoryItems = await this.placesQuery.getHistory({
- daysOld: 60,
- limit: lazy.maxRowsPref,
- sortBy: this.sortOption,
- });
- }
-
- resetHistoryMaps() {
- this.historyMapByDate = [];
- this.historyMapBySite = [];
- }
-
- createHistoryMaps() {
- if (this.sortOption === "date" && !this.historyMapByDate.length) {
- const {
- visitsFromToday,
- visitsFromYesterday,
- visitsByDay,
- visitsByMonth,
- } = this.placesQuery;
-
- // Add visits from today and yesterday.
- if (visitsFromToday.length) {
- this.historyMapByDate.push({
- l10nId: "firefoxview-history-date-today",
- items: visitsFromToday,
- });
- }
- if (visitsFromYesterday.length) {
- this.historyMapByDate.push({
- l10nId: "firefoxview-history-date-yesterday",
- items: visitsFromYesterday,
- });
- }
-
- // Add visits from this month, grouped by day.
- visitsByDay.forEach(visits => {
- this.historyMapByDate.push({
- l10nId: "firefoxview-history-date-this-month",
- items: visits,
- });
- });
-
- // Add visits from previous months, grouped by month.
- visitsByMonth.forEach(visits => {
- this.historyMapByDate.push({
- l10nId: "firefoxview-history-date-prev-month",
- items: visits,
- });
- });
- } else if (this.sortOption === "site" && !this.historyMapBySite.length) {
- this.historyMapBySite = Array.from(
- this.allHistoryItems.entries(),
- ([domain, items]) => ({
- domain,
- items,
- l10nId: domain ? null : "firefoxview-history-site-localhost",
- })
- ).sort((a, b) => a.domain.localeCompare(b.domain));
- }
- }
-
onPrimaryAction(e) {
+ navigateToLink(e);
// Record telemetry
Services.telemetry.recordEvent(
"firefoxview_next",
@@ -254,26 +151,13 @@ class HistoryInView extends ViewPage {
{}
);
- if (this.searchQuery) {
+ if (this.controller.searchQuery) {
const searchesHistogram = Services.telemetry.getKeyedHistogramById(
"FIREFOX_VIEW_CUMULATIVE_SEARCHES"
);
searchesHistogram.add("history", this.cumulativeSearches);
this.cumulativeSearches = 0;
}
-
- let currentWindow = this.getWindow();
- if (currentWindow.openTrustedLinkIn) {
- let where = lazy.BrowserUtils.whereToOpenLink(
- e.detail.originalEvent,
- false,
- true
- );
- if (where == "current") {
- where = "tab";
- }
- currentWindow.openTrustedLinkIn(e.originalTarget.url, where);
- }
}
onSecondaryAction(e) {
@@ -282,24 +166,29 @@ class HistoryInView extends ViewPage {
}
deleteFromHistory(e) {
- lazy.PlacesUtils.history.remove(this.triggerNode.url);
+ this.controller.deleteFromHistory();
this.recordContextMenuTelemetry("delete-from-history", e);
}
async onChangeSortOption(e) {
- this.sortOption = e.target.value;
+ await this.controller.onChangeSortOption(e);
Services.telemetry.recordEvent(
"firefoxview_next",
"sort_history",
"tabs",
null,
{
- sort_type: this.sortOption,
- search_start: this.searchQuery ? "true" : "false",
+ sort_type: this.controller.sortOption,
+ search_start: this.controller.searchQuery ? "true" : "false",
}
);
- await this.updateHistoryData();
- await this.#updateSearchResults();
+ }
+
+ async onSearchQuery(e) {
+ await this.controller.onSearchQuery(e);
+ this.cumulativeSearches = this.controller.searchQuery
+ ? this.cumulativeSearches + 1
+ : 0;
}
showAllHistory() {
@@ -396,9 +285,9 @@ class HistoryInView extends ViewPage {
* The template to use for cards-container.
*/
get cardsTemplate() {
- if (this.searchResults) {
+ if (this.controller.searchResults) {
return this.#searchResultsTemplate();
- } else if (this.allHistoryItems.size) {
+ } else if (this.controller.allHistoryItems.size) {
return this.#historyCardsTemplate();
}
return this.#emptyMessageTemplate();
@@ -406,8 +295,11 @@ class HistoryInView extends ViewPage {
#historyCardsTemplate() {
let cardsTemplate = [];
- if (this.sortOption === "date" && this.historyMapByDate.length) {
- this.historyMapByDate.forEach(historyItem => {
+ if (
+ this.controller.sortOption === "date" &&
+ this.controller.historyMapByDate.length
+ ) {
+ this.controller.historyMapByDate.forEach(historyItem => {
if (historyItem.items.length) {
let dateArg = JSON.stringify({ date: historyItem.items[0].time });
cardsTemplate.push(html`<card-container>
@@ -424,7 +316,7 @@ class HistoryInView extends ViewPage {
: "time"}
hasPopup="menu"
maxTabsLength=${this.maxTabsLength}
- .tabItems=${historyItem.items}
+ .tabItems=${[...historyItem.items]}
@fxview-tab-list-primary-action=${this.onPrimaryAction}
@fxview-tab-list-secondary-action=${this.onSecondaryAction}
>
@@ -433,8 +325,8 @@ class HistoryInView extends ViewPage {
</card-container>`);
}
});
- } else if (this.historyMapBySite.length) {
- this.historyMapBySite.forEach(historyItem => {
+ } else if (this.controller.historyMapBySite.length) {
+ this.controller.historyMapBySite.forEach(historyItem => {
if (historyItem.items.length) {
cardsTemplate.push(html`<card-container>
<h3 slot="header" data-l10n-id="${ifDefined(historyItem.l10nId)}">
@@ -446,7 +338,7 @@ class HistoryInView extends ViewPage {
dateTimeFormat="dateTime"
hasPopup="menu"
maxTabsLength=${this.maxTabsLength}
- .tabItems=${historyItem.items}
+ .tabItems=${[...historyItem.items]}
@fxview-tab-list-primary-action=${this.onPrimaryAction}
@fxview-tab-list-secondary-action=${this.onSecondaryAction}
>
@@ -504,17 +396,17 @@ class HistoryInView extends ViewPage {
slot="header"
data-l10n-id="firefoxview-search-results-header"
data-l10n-args=${JSON.stringify({
- query: escapeHtmlEntities(this.searchQuery),
+ query: escapeHtmlEntities(this.controller.searchQuery),
})}
></h3>
${when(
- this.searchResults.length,
+ this.controller.searchResults.length,
() =>
html`<h3
slot="secondary-header"
data-l10n-id="firefoxview-search-results-count"
data-l10n-args="${JSON.stringify({
- count: this.searchResults.length,
+ count: this.controller.searchResults.length,
})}"
></h3>`
)}
@@ -524,10 +416,11 @@ class HistoryInView extends ViewPage {
dateTimeFormat="dateTime"
hasPopup="menu"
maxTabsLength="-1"
- .searchQuery=${this.searchQuery}
- .tabItems=${this.searchResults}
+ .searchQuery=${this.controller.searchQuery}
+ .tabItems=${this.controller.searchResults}
@fxview-tab-list-primary-action=${this.onPrimaryAction}
@fxview-tab-list-secondary-action=${this.onSecondaryAction}
+ .searchInProgress=${this.controller.placesQuery.searchInProgress}
>
${this.panelListTemplate()}
</fxview-tab-list>
@@ -569,7 +462,7 @@ class HistoryInView extends ViewPage {
id="sort-by-date"
name="history-sort-option"
value="date"
- ?checked=${this.sortOption === "date"}
+ ?checked=${this.controller.sortOption === "date"}
@click=${this.onChangeSortOption}
/>
<label
@@ -583,7 +476,7 @@ class HistoryInView extends ViewPage {
id="sort-by-site"
name="history-sort-option"
value="site"
- ?checked=${this.sortOption === "site"}
+ ?checked=${this.controller.sortOption === "site"}
@click=${this.onChangeSortOption}
/>
<label
@@ -612,11 +505,12 @@ class HistoryInView extends ViewPage {
data-l10n-id="firefoxview-choose-browser-button"
@click=${this.openMigrationWizard}
></button>
- <button
- class="close ghost-button"
+ <moz-button
+ class="close"
+ type="icon ghost"
data-l10n-id="firefoxview-import-history-close-button"
@click=${this.dismissImportHistory}
- ></button>
+ ></moz-button>
</div>
</div>
</card-container>
@@ -624,32 +518,24 @@ class HistoryInView extends ViewPage {
</div>
<div
class="show-all-history-footer"
- ?hidden=${!this.allHistoryItems.size}
+ ?hidden=${!this.controller.allHistoryItems.size}
>
<button
class="show-all-history-button"
data-l10n-id="firefoxview-show-all-history"
@click=${this.showAllHistory}
- ?hidden=${this.searchResults}
+ ?hidden=${this.controller.searchResults}
></button>
</div>
`;
}
- async onSearchQuery(e) {
- this.searchQuery = e.detail.query;
- this.cumulativeSearches = this.searchQuery
- ? this.cumulativeSearches + 1
- : 0;
- this.#updateSearchResults();
- }
-
- willUpdate(changedProperties) {
+ willUpdate() {
this.fullyUpdated = false;
- if (this.allHistoryItems.size && !changedProperties.has("sortOption")) {
+ if (this.controller.allHistoryItems.size) {
// onChangeSortOption() will update history data once it has been fetched
// from the API.
- this.createHistoryMaps();
+ this.controller.createHistoryMaps();
}
}
}
diff --git a/browser/components/firefoxview/jar.mn b/browser/components/firefoxview/jar.mn
index 1e5cc3e690..8bf3597aa5 100644
--- a/browser/components/firefoxview/jar.mn
+++ b/browser/components/firefoxview/jar.mn
@@ -9,6 +9,7 @@ browser.jar:
content/browser/firefoxview/firefoxview.mjs
content/browser/firefoxview/history.css
content/browser/firefoxview/history.mjs
+ content/browser/firefoxview/HistoryController.mjs
content/browser/firefoxview/opentabs.mjs
content/browser/firefoxview/view-opentabs.css
content/browser/firefoxview/syncedtabs.mjs
@@ -23,6 +24,9 @@ browser.jar:
content/browser/firefoxview/fxview-tab-list.css
content/browser/firefoxview/fxview-tab-list.mjs
content/browser/firefoxview/fxview-tab-row.css
+ content/browser/firefoxview/opentabs-tab-list.css
+ content/browser/firefoxview/opentabs-tab-list.mjs
+ content/browser/firefoxview/opentabs-tab-row.css
content/browser/firefoxview/recentlyclosed.mjs
content/browser/firefoxview/viewpage.mjs
content/browser/firefoxview/history-empty.svg (content/history-empty.svg)
diff --git a/browser/components/firefoxview/opentabs-tab-list.css b/browser/components/firefoxview/opentabs-tab-list.css
new file mode 100644
index 0000000000..9245a0fada
--- /dev/null
+++ b/browser/components/firefoxview/opentabs-tab-list.css
@@ -0,0 +1,32 @@
+/* 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/. */
+
+.fxview-tab-list {
+ &.pinned {
+ display: flex;
+ flex-wrap: wrap;
+
+ > virtual-list {
+ display: block;
+ }
+
+ > opentabs-tab-row {
+ display: block;
+ margin-block-end: var(--space-xsmall);
+ }
+ }
+
+ &.hasContainerTab {
+ grid-template-columns: min-content 3fr min-content 2fr 1fr 1fr min-content min-content;
+ }
+}
+
+virtual-list {
+ grid-column: span 9;
+
+ .top-padding,
+ .bottom-padding {
+ grid-column: span 9;
+ }
+}
diff --git a/browser/components/firefoxview/opentabs-tab-list.mjs b/browser/components/firefoxview/opentabs-tab-list.mjs
new file mode 100644
index 0000000000..4b6d6b3c86
--- /dev/null
+++ b/browser/components/firefoxview/opentabs-tab-list.mjs
@@ -0,0 +1,593 @@
+/* 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,
+ styleMap,
+ when,
+} from "chrome://global/content/vendor/lit.all.mjs";
+import {
+ FxviewTabListBase,
+ FxviewTabRowBase,
+} from "chrome://browser/content/firefoxview/fxview-tab-list.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://global/content/elements/moz-button.mjs";
+
+const lazy = {};
+let XPCOMUtils;
+
+XPCOMUtils = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+).XPCOMUtils;
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "virtualListEnabledPref",
+ "browser.firefox-view.virtual-list.enabled"
+);
+
+/**
+ * A list of clickable tab items
+ *
+ * @property {boolean} pinnedTabsGridView - Whether to show pinned tabs in a grid view
+ */
+
+export class OpenTabsTabList extends FxviewTabListBase {
+ constructor() {
+ super();
+ this.pinnedTabsGridView = false;
+ this.pinnedTabs = [];
+ this.unpinnedTabs = [];
+ }
+
+ static properties = {
+ pinnedTabsGridView: { type: Boolean },
+ };
+
+ static queries = {
+ ...FxviewTabListBase.queries,
+ rowEls: {
+ all: "opentabs-tab-row",
+ },
+ };
+
+ 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") {
+ 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);
+ }
+ }
+
+ /**
+ * 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") {
+ fxviewTabRow.moveFocusLeft();
+ } else {
+ fxviewTabRow.moveFocusRight();
+ }
+ } 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") {
+ fxviewTabRow.moveFocusRight();
+ } else {
+ fxviewTabRow.moveFocusLeft();
+ }
+ }
+ }
+
+ 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;
+ }
+ }
+
+ #getTabListWrapperClasses() {
+ let wrapperClasses = ["fxview-tab-list"];
+ let tabsToCheck = this.pinnedTabsGridView
+ ? this.unpinnedTabs
+ : this.tabItems;
+ if (tabsToCheck.some(tab => tab.containerObj)) {
+ wrapperClasses.push(`hasContainerTab`);
+ }
+ return wrapperClasses;
+ }
+
+ 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`<opentabs-tab-row
+ ?active=${i == this.activeIndex}
+ class=${classMap({
+ pinned:
+ this.pinnedTabsGridView && tabItem.indicators?.includes("pinned"),
+ })}
+ .currentActiveElementId=${this.currentActiveElementId}
+ .favicon=${tabItem.icon}
+ .compact=${this.compactRows}
+ .containerObj=${ifDefined(tabItem.containerObj)}
+ .indicators=${tabItem.indicators}
+ .pinnedTabsGridView=${ifDefined(this.pinnedTabsGridView)}
+ .primaryL10nId=${tabItem.primaryL10nId}
+ .primaryL10nArgs=${ifDefined(tabItem.primaryL10nArgs)}
+ .secondaryL10nId=${tabItem.secondaryL10nId}
+ .secondaryL10nArgs=${ifDefined(tabItem.secondaryL10nArgs)}
+ .tertiaryL10nId=${ifDefined(tabItem.tertiaryL10nId)}
+ .tertiaryL10nArgs=${ifDefined(tabItem.tertiaryL10nArgs)}
+ .secondaryActionClass=${this.secondaryActionClass}
+ .tertiaryActionClass=${ifDefined(this.tertiaryActionClass)}
+ .sourceClosedId=${ifDefined(tabItem.sourceClosedId)}
+ .sourceWindowId=${ifDefined(tabItem.sourceWindowId)}
+ .closedId=${ifDefined(tabItem.closedId || tabItem.closedId)}
+ role=${tabItem.pinned && this.pinnedTabsGridView ? "tab" : "listitem"}
+ .tabElement=${ifDefined(tabItem.tabElement)}
+ .time=${ifDefined(time)}
+ .title=${tabItem.title}
+ .url=${tabItem.url}
+ .searchQuery=${ifDefined(this.searchQuery)}
+ .timeMsPref=${ifDefined(this.timeMsPref)}
+ .hasPopup=${this.hasPopup}
+ .dateTimeFormat=${this.dateTimeFormat}
+ ></opentabs-tab-row>`;
+ };
+
+ render() {
+ if (this.searchQuery && this.tabItems.length === 0) {
+ return this.emptySearchResultsTemplate();
+ }
+ return html`
+ ${this.stylesheets()}
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/firefoxview/opentabs-tab-list.css"
+ />
+ ${when(
+ this.pinnedTabsGridView && this.pinnedTabs.length,
+ () => html`
+ <div
+ id="fxview-tab-list"
+ class="fxview-tab-list pinned"
+ data-l10n-id="firefoxview-pinned-tabs"
+ role="tablist"
+ @keydown=${this.handleFocusElementInRow}
+ >
+ ${this.pinnedTabs.map((tabItem, i) =>
+ this.customItemTemplate
+ ? this.customItemTemplate(tabItem, i)
+ : this.itemTemplate(tabItem, i)
+ )}
+ </div>
+ `
+ )}
+ <div
+ id="fxview-tab-list"
+ class=${this.#getTabListWrapperClasses().join(" ")}
+ data-l10n-id="firefoxview-tabs"
+ role="list"
+ @keydown=${this.handleFocusElementInRow}
+ >
+ ${when(
+ lazy.virtualListEnabledPref,
+ () => html`
+ <virtual-list
+ .activeIndex=${this.activeIndex}
+ .pinnedTabsIndexOffset=${this.pinnedTabsGridView
+ ? this.pinnedTabs.length
+ : 0}
+ .items=${this.pinnedTabsGridView
+ ? this.unpinnedTabs
+ : this.tabItems}
+ .template=${this.itemTemplate}
+ ></virtual-list>
+ `,
+ () =>
+ html`${this.tabItems.map((tabItem, i) =>
+ this.itemTemplate(tabItem, i)
+ )}`
+ )}
+ </div>
+ <slot name="menu"></slot>
+ `;
+ }
+}
+customElements.define("opentabs-tab-list", OpenTabsTabList);
+
+/**
+ * A tab item that displays favicon, title, url, and time of last access
+ *
+ * @property {object} containerObj - Info about an open tab's container if within one
+ * @property {string} indicators - An array of tab indicators if any are present
+ * @property {boolean} pinnedTabsGridView - Whether the show pinned tabs in a grid view
+ */
+
+export class OpenTabsTabRow extends FxviewTabRowBase {
+ constructor() {
+ super();
+ this.indicators = [];
+ this.pinnedTabsGridView = false;
+ }
+
+ static properties = {
+ ...FxviewTabRowBase.properties,
+ containerObj: { type: Object },
+ indicators: { type: Array },
+ pinnedTabsGridView: { type: Boolean },
+ };
+
+ static queries = {
+ ...FxviewTabRowBase.queries,
+ mediaButtonEl: "#fxview-tab-row-media-button",
+ pinnedTabButtonEl: "moz-button#fxview-tab-row-main",
+ };
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.addEventListener("keydown", this.handleKeydown);
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this.removeEventListener("keydown", this.handleKeydown);
+ }
+
+ handleKeydown(e) {
+ if (
+ this.active &&
+ this.pinnedTabsGridView &&
+ this.indicators?.includes("pinned") &&
+ e.key === "m" &&
+ e.ctrlKey
+ ) {
+ this.muteOrUnmuteTab();
+ }
+ }
+
+ moveFocusRight() {
+ let tabList = this.getRootNode().host;
+ if (this.pinnedTabsGridView && this.indicators?.includes("pinned")) {
+ tabList.focusNextRow();
+ } else if (
+ (this.indicators?.includes("soundplaying") ||
+ this.indicators?.includes("muted")) &&
+ this.currentActiveElementId === "fxview-tab-row-main"
+ ) {
+ this.focusMediaButton();
+ } else if (
+ this.currentActiveElementId === "fxview-tab-row-media-button" ||
+ this.currentActiveElementId === "fxview-tab-row-main"
+ ) {
+ this.focusSecondaryButton();
+ } else if (
+ this.tertiaryButtonEl &&
+ this.currentActiveElementId === "fxview-tab-row-secondary-button"
+ ) {
+ this.focusTertiaryButton();
+ }
+ }
+
+ moveFocusLeft() {
+ let tabList = this.getRootNode().host;
+ if (
+ this.pinnedTabsGridView &&
+ (this.indicators?.includes("pinned") ||
+ (tabList.currentActiveElementId === "fxview-tab-row-main" &&
+ tabList.activeIndex === tabList.pinnedTabs.length))
+ ) {
+ tabList.focusPrevRow();
+ } else if (
+ tabList.currentActiveElementId === "fxview-tab-row-tertiary-button"
+ ) {
+ this.focusSecondaryButton();
+ } else if (
+ (this.indicators?.includes("soundplaying") ||
+ this.indicators?.includes("muted")) &&
+ tabList.currentActiveElementId === "fxview-tab-row-secondary-button"
+ ) {
+ this.focusMediaButton();
+ } else {
+ this.focusLink();
+ }
+ }
+
+ focusMediaButton() {
+ let tabList = this.getRootNode().host;
+ this.mediaButtonEl.focus();
+ tabList.currentActiveElementId = this.mediaButtonEl.id;
+ }
+
+ #secondaryActionHandler(event) {
+ if (
+ (this.pinnedTabsGridView &&
+ this.indicators?.includes("pinned") &&
+ event.type == "contextmenu") ||
+ (event.type == "click" && event.detail && !event.altKey) ||
+ // detail=0 is from keyboard
+ (event.type == "click" && !event.detail)
+ ) {
+ event.preventDefault();
+ this.dispatchEvent(
+ new CustomEvent("fxview-tab-list-secondary-action", {
+ bubbles: true,
+ composed: true,
+ detail: { originalEvent: event, item: this },
+ })
+ );
+ }
+ }
+
+ #faviconTemplate() {
+ return html`<span
+ class="${classMap({
+ "fxview-tab-row-favicon-wrapper": true,
+ pinned: this.indicators?.includes("pinned"),
+ pinnedOnNewTab: this.indicators?.includes("pinnedOnNewTab"),
+ attention: this.indicators?.includes("attention"),
+ bookmark: this.indicators?.includes("bookmark"),
+ })}"
+ >
+ <span
+ class="fxview-tab-row-favicon icon"
+ id="fxview-tab-row-favicon"
+ style=${styleMap({
+ backgroundImage: `url(${this.getImageUrl(this.favicon, this.url)})`,
+ })}
+ ></span>
+ ${when(
+ this.pinnedTabsGridView &&
+ this.indicators?.includes("pinned") &&
+ (this.indicators?.includes("muted") ||
+ this.indicators?.includes("soundplaying")),
+ () => html`
+ <button
+ class="fxview-tab-row-pinned-media-button"
+ id="fxview-tab-row-media-button"
+ tabindex="-1"
+ data-l10n-id=${this.indicators?.includes("muted")
+ ? "fxviewtabrow-unmute-tab-button-no-context"
+ : "fxviewtabrow-mute-tab-button-no-context"}
+ muted=${this.indicators?.includes("muted")}
+ soundplaying=${this.indicators?.includes("soundplaying") &&
+ !this.indicators?.includes("muted")}
+ @click=${this.muteOrUnmuteTab}
+ ></button>
+ `
+ )}
+ </span>`;
+ }
+
+ #getContainerClasses() {
+ let containerClasses = ["fxview-tab-row-container-indicator", "icon"];
+ if (this.containerObj) {
+ let { icon, color } = this.containerObj;
+ containerClasses.push(`identity-icon-${icon}`);
+ containerClasses.push(`identity-color-${color}`);
+ }
+ return containerClasses;
+ }
+
+ muteOrUnmuteTab(e) {
+ e?.preventDefault();
+ // If the tab has no sound playing, the mute/unmute button will be removed when toggled.
+ // We should move the focus to the right in that case. This does not apply to pinned tabs
+ // on the Open Tabs page.
+ let shouldMoveFocus =
+ (!this.pinnedTabsGridView ||
+ (!this.indicators.includes("pinned") && this.pinnedTabsGridView)) &&
+ this.mediaButtonEl &&
+ !this.indicators.includes("soundplaying") &&
+ this.currentActiveElementId === "fxview-tab-row-media-button";
+
+ // detail=0 is from keyboard
+ if (e?.type == "click" && !e?.detail && shouldMoveFocus) {
+ if (document.dir == "rtl") {
+ this.moveFocusLeft();
+ } else {
+ this.moveFocusRight();
+ }
+ }
+ this.tabElement.toggleMuteAudio();
+ }
+
+ #mediaButtonTemplate() {
+ return html`${when(
+ this.indicators?.includes("soundplaying") ||
+ this.indicators?.includes("muted"),
+ () => html`<moz-button
+ type="icon ghost"
+ class="fxview-tab-row-button"
+ id="fxview-tab-row-media-button"
+ data-l10n-id=${this.indicators?.includes("muted")
+ ? "fxviewtabrow-unmute-tab-button-no-context"
+ : "fxviewtabrow-mute-tab-button-no-context"}
+ muted=${this.indicators?.includes("muted")}
+ soundplaying=${this.indicators?.includes("soundplaying") &&
+ !this.indicators?.includes("muted")}
+ @click=${this.muteOrUnmuteTab}
+ tabindex="${this.active &&
+ this.currentActiveElementId === "fxview-tab-row-media-button"
+ ? "0"
+ : "-1"}"
+ ></moz-button>`,
+ () => html`<span></span>`
+ )}`;
+ }
+
+ #containerIndicatorTemplate() {
+ let tabList = this.getRootNode().host;
+ let tabsToCheck = tabList.pinnedTabsGridView
+ ? tabList.unpinnedTabs
+ : tabList.tabItems;
+ return html`${when(
+ tabsToCheck.some(tab => tab.containerObj),
+ () => html`<span class=${this.#getContainerClasses().join(" ")}></span>`
+ )}`;
+ }
+
+ #pinnedTabItemTemplate() {
+ return html`
+ <moz-button
+ type="icon ghost"
+ id="fxview-tab-row-main"
+ aria-haspopup=${ifDefined(this.hasPopup)}
+ data-l10n-id=${ifDefined(this.primaryL10nId)}
+ data-l10n-args=${ifDefined(this.primaryL10nArgs)}
+ tabindex=${this.active &&
+ this.currentActiveElementId === "fxview-tab-row-main"
+ ? "0"
+ : "-1"}
+ role="tab"
+ @click=${this.primaryActionHandler}
+ @keydown=${this.primaryActionHandler}
+ @contextmenu=${this.#secondaryActionHandler}
+ >
+ ${this.#faviconTemplate()}
+ </moz-button>
+ `;
+ }
+
+ #unpinnedTabItemTemplate() {
+ return html`<a
+ href=${ifDefined(this.url)}
+ class="fxview-tab-row-main"
+ id="fxview-tab-row-main"
+ tabindex=${this.active &&
+ this.currentActiveElementId === "fxview-tab-row-main"
+ ? "0"
+ : "-1"}
+ data-l10n-id=${ifDefined(this.primaryL10nId)}
+ data-l10n-args=${ifDefined(this.primaryL10nArgs)}
+ @click=${this.primaryActionHandler}
+ @keydown=${this.primaryActionHandler}
+ title=${!this.primaryL10nId ? this.url : null}
+ >
+ ${this.#faviconTemplate()} ${this.titleTemplate()}
+ ${when(
+ !this.compact,
+ () => html`${this.#containerIndicatorTemplate()} ${this.urlTemplate()}
+ ${this.dateTemplate()} ${this.timeTemplate()}`
+ )}
+ </a>
+ ${this.#mediaButtonTemplate()} ${this.secondaryButtonTemplate()}
+ ${this.tertiaryButtonTemplate()}`;
+ }
+
+ render() {
+ return html`
+ ${this.stylesheets()}
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/firefoxview/opentabs-tab-row.css"
+ />
+ ${when(
+ this.containerObj,
+ () => html`
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/usercontext/usercontext.css"
+ />
+ `
+ )}
+ ${when(
+ this.pinnedTabsGridView && this.indicators?.includes("pinned"),
+ this.#pinnedTabItemTemplate.bind(this),
+ this.#unpinnedTabItemTemplate.bind(this)
+ )}
+ `;
+ }
+}
+customElements.define("opentabs-tab-row", OpenTabsTabRow);
diff --git a/browser/components/firefoxview/opentabs-tab-row.css b/browser/components/firefoxview/opentabs-tab-row.css
new file mode 100644
index 0000000000..e5c00884b3
--- /dev/null
+++ b/browser/components/firefoxview/opentabs-tab-row.css
@@ -0,0 +1,119 @@
+/* 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/. */
+
+.fxview-tab-row-favicon-wrapper {
+ height: 16px;
+ position: relative;
+ display: block;
+
+ .fxview-tab-row-favicon::after,
+ .fxview-tab-row-button::after,
+ &.pinned .fxview-tab-row-pinned-media-button {
+ display: block;
+ content: "";
+ background-size: 12px;
+ background-position: center;
+ background-repeat: no-repeat;
+ position: relative;
+ height: 12px;
+ width: 12px;
+ -moz-context-properties: fill, stroke;
+ fill: currentColor;
+ stroke: var(--fxview-background-color-secondary);
+ }
+
+ &:is(.pinnedOnNewTab, .bookmark):not(.attention) .fxview-tab-row-favicon::after {
+ inset-block-start: 9px;
+ inset-inline-end: -6px;
+ }
+
+ &.pinnedOnNewTab .fxview-tab-row-favicon::after,
+ &.pinnedOnNewTab .fxview-tab-row-button::after {
+ background-image: url("chrome://browser/skin/pin-12.svg");
+ }
+
+ &.bookmark .fxview-tab-row-favicon::after,
+ &.bookmark .fxview-tab-row-button::after {
+ background-image: url("chrome://browser/skin/bookmark-12.svg");
+ fill: var(--fxview-primary-action-background);
+ }
+
+ &.attention .fxview-tab-row-favicon::after,
+ &.attention .fxview-tab-row-button::after {
+ background-image: radial-gradient(circle, var(--attention-dot-color), var(--attention-dot-color) 2px, transparent 2px);
+ height: 4px;
+ width: 100%;
+ inset-block-start: 20px;
+ }
+
+ &.pinned .fxview-tab-row-pinned-media-button {
+ inset-block-start: -5px;
+ inset-inline-end: 1px;
+ border: var(--button-border);
+ border-radius: 100%;
+ background-color: var(--fxview-background-color-secondary);
+ padding: 6px;
+ min-width: 0;
+ min-height: 0;
+ position: absolute;
+
+ &[muted="true"] {
+ background-image: url("chrome://global/skin/media/audio-muted.svg");
+ }
+
+ &[soundplaying="true"] {
+ background-image: url("chrome://global/skin/media/audio.svg");
+ }
+
+ &:active,
+ &:hover:active {
+ background-color: var(--button-background-color-active);
+ }
+ }
+}
+
+.fxview-tab-row-container-indicator {
+ height: 16px;
+ width: 16px;
+ background-image: var(--identity-icon);
+ background-size: cover;
+ -moz-context-properties: fill;
+ fill: var(--identity-icon-color);
+}
+
+.fxview-tab-row-main {
+ :host(.pinned) & {
+ padding: var(--space-small);
+ min-width: unset;
+ margin: 0;
+ }
+}
+
+button.fxview-tab-row-main:hover {
+ & .fxview-tab-row-favicon-wrapper .fxview-tab-row-favicon::after {
+ stroke: var(--fxview-indicator-stroke-color-hover);
+ }
+}
+
+@media (prefers-contrast) {
+ button.fxview-tab-row-main {
+ border: 1px solid ButtonText;
+ color: ButtonText;
+ }
+
+ button.fxview-tab-row-main:enabled:hover {
+ border: 1px solid SelectedItem;
+ color: SelectedItem;
+ }
+
+ button.fxview-tab-row-main:enabled:active {
+ color: SelectedItem;
+ }
+
+ button.fxview-tab-row-main:enabled,
+ button.fxview-tab-row-main:enabled:hover,
+ button.fxview-tab-row-main:enabled:active {
+ background-color: ButtonFace;
+ }
+}
diff --git a/browser/components/firefoxview/opentabs.mjs b/browser/components/firefoxview/opentabs.mjs
index 8d7723e931..fb84553e26 100644
--- a/browser/components/firefoxview/opentabs.mjs
+++ b/browser/components/firefoxview/opentabs.mjs
@@ -17,6 +17,8 @@ import {
MAX_TABS_FOR_RECENT_BROWSING,
} from "./helpers.mjs";
import { ViewPage, ViewPageContent } from "./viewpage.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/firefoxview/opentabs-tab-list.mjs";
const lazy = {};
@@ -36,6 +38,9 @@ ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
).getFxAccountsSingleton();
});
+const TOPIC_DEVICESTATE_CHANGED = "firefox-view.devicestate.changed";
+const TOPIC_DEVICELIST_UPDATED = "fxaccounts:devicelist_updated";
+
/**
* A collection of open tabs grouped by window.
*
@@ -339,7 +344,7 @@ class OpenTabsInView extends ViewPage {
></view-opentabs-card>`;
}
- handleEvent({ detail, target, type }) {
+ handleEvent({ detail, type }) {
if (this.recentBrowsing && type === "fxview-search-textbox-query") {
this.onSearchQuery({ detail });
return;
@@ -424,7 +429,7 @@ class OpenTabsInViewCard extends ViewPageContent {
static queries = {
cardEl: "card-container",
tabContextMenu: "view-opentabs-contextmenu",
- tabList: "fxview-tab-list",
+ tabList: "opentabs-tab-list",
};
openContextMenu(e) {
@@ -565,7 +570,7 @@ class OpenTabsInViewCard extends ViewPageContent {
() => html`<h3 slot="header">${this.title}</h3>`
)}
<div class="fxview-tab-list-container" slot="main">
- <fxview-tab-list
+ <opentabs-tab-list
.hasPopup=${"menu"}
?compactRows=${this.classList.contains("width-limited")}
@fxview-tab-list-primary-action=${this.onTabListRowClick}
@@ -579,7 +584,7 @@ class OpenTabsInViewCard extends ViewPageContent {
.searchQuery=${this.searchQuery}
.pinnedTabsGridView=${!this.recentBrowsing}
><view-opentabs-contextmenu slot="menu"></view-opentabs-contextmenu>
- </fxview-tab-list>
+ </opentabs-tab-list>
</div>
${when(
this.recentBrowsing,
@@ -659,7 +664,7 @@ customElements.define("view-opentabs-card", OpenTabsInViewCard);
class OpenTabsContextMenu extends MozLitElement {
static properties = {
devices: { type: Array },
- triggerNode: { type: Object },
+ triggerNode: { hasChanged: () => true, type: Object },
};
static queries = {
@@ -669,6 +674,7 @@ class OpenTabsContextMenu extends MozLitElement {
constructor() {
super();
this.triggerNode = null;
+ this.boundObserve = (...args) => this.observe(...args);
this.devices = [];
}
@@ -680,6 +686,28 @@ class OpenTabsContextMenu extends MozLitElement {
return this.ownerDocument.querySelector("view-opentabs");
}
+ connectedCallback() {
+ super.connectedCallback();
+ this.fetchDevicesPromise = this.fetchDevices();
+ Services.obs.addObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED);
+ Services.obs.addObserver(this.boundObserve, TOPIC_DEVICESTATE_CHANGED);
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ Services.obs.removeObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED);
+ Services.obs.removeObserver(this.boundObserve, TOPIC_DEVICESTATE_CHANGED);
+ }
+
+ observe(_subject, topic, _data) {
+ if (
+ topic == TOPIC_DEVICELIST_UPDATED ||
+ topic == TOPIC_DEVICESTATE_CHANGED
+ ) {
+ this.fetchDevicesPromise = this.fetchDevices();
+ }
+ }
+
async fetchDevices() {
const currentWindow = this.ownerViewPage.getWindow();
if (currentWindow?.gSync) {
@@ -699,7 +727,7 @@ class OpenTabsContextMenu extends MozLitElement {
return;
}
this.triggerNode = triggerNode;
- await this.fetchDevices();
+ await this.fetchDevicesPromise;
await this.getUpdateComplete();
this.panelList.toggle(originalEvent);
}
@@ -1022,7 +1050,7 @@ function getTabListItems(tabs, isRecentBrowsing) {
? JSON.stringify({ tabTitle: tab.label })
: null,
tabElement: tab,
- time: tab.lastAccessed,
+ time: tab.lastSeenActive,
title: tab.label,
url,
};
diff --git a/browser/components/firefoxview/recentlyclosed.mjs b/browser/components/firefoxview/recentlyclosed.mjs
index 83c323256c..7efd8d09f2 100644
--- a/browser/components/firefoxview/recentlyclosed.mjs
+++ b/browser/components/firefoxview/recentlyclosed.mjs
@@ -65,7 +65,7 @@ class RecentlyClosedTabsInView extends ViewPage {
tabList: "fxview-tab-list",
};
- observe(subject, topic, data) {
+ observe(subject, topic) {
if (
topic == SS_NOTIFY_CLOSED_OBJECTS_CHANGED ||
(topic == SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH &&
@@ -249,13 +249,22 @@ class RecentlyClosedTabsInView extends ViewPage {
onDismissTab(e) {
const closedId = parseInt(e.originalTarget.closedId, 10);
const sourceClosedId = parseInt(e.originalTarget.sourceClosedId, 10);
- const sourceWindowId = e.originalTarget.souceWindowId;
- if (sourceWindowId || !isNaN(sourceClosedId)) {
+ const sourceWindowId = e.originalTarget.sourceWindowId;
+ if (!isNaN(sourceClosedId)) {
+ // the sourceClosedId is an identifier for a now-closed window the tab
+ // was closed in.
lazy.SessionStore.forgetClosedTabById(closedId, {
sourceClosedId,
+ });
+ } else if (sourceWindowId) {
+ // the sourceWindowId is an identifier for a currently-open window the tab
+ // was closed in.
+ lazy.SessionStore.forgetClosedTabById(closedId, {
sourceWindowId,
});
} else {
+ // without either identifier, SessionStore will need to walk its window collections
+ // to find the close tab with matching closedId
lazy.SessionStore.forgetClosedTabById(closedId);
}
@@ -387,7 +396,6 @@ class RecentlyClosedTabsInView extends ViewPage {
() =>
html`
<fxview-tab-list
- class="with-dismiss-button"
slot="main"
.maxTabsLength=${!this.recentBrowsing || this.showAll
? -1
diff --git a/browser/components/firefoxview/syncedtabs.mjs b/browser/components/firefoxview/syncedtabs.mjs
index d64da45a30..1c65650c10 100644
--- a/browser/components/firefoxview/syncedtabs.mjs
+++ b/browser/components/firefoxview/syncedtabs.mjs
@@ -4,13 +4,9 @@
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
- BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
- SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
+ SyncedTabsController: "resource:///modules/SyncedTabsController.sys.mjs",
});
-const { SyncedTabsErrorHandler } = ChromeUtils.importESModule(
- "resource:///modules/firefox-view-synced-tabs-error-handler.sys.mjs"
-);
const { TabsSetupFlowManager } = ChromeUtils.importESModule(
"resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
);
@@ -24,43 +20,52 @@ import { ViewPage } from "./viewpage.mjs";
import {
escapeHtmlEntities,
isSearchEnabled,
- searchTabList,
MAX_TABS_FOR_RECENT_BROWSING,
+ navigateToLink,
} from "./helpers.mjs";
-const SYNCED_TABS_CHANGED = "services.sync.tabs.changed";
-const TOPIC_SETUPSTATE_CHANGED = "firefox-view.setupstate.changed";
const UI_OPEN_STATE = "browser.tabs.firefox-view.ui-state.tab-pickup.open";
class SyncedTabsInView extends ViewPage {
+ controller = new lazy.SyncedTabsController(this, {
+ contextMenu: true,
+ pairDeviceCallback: () =>
+ Services.telemetry.recordEvent(
+ "firefoxview_next",
+ "fxa_mobile",
+ "sync",
+ null,
+ {
+ has_devices: TabsSetupFlowManager.secondaryDeviceConnected.toString(),
+ }
+ ),
+ signupCallback: () =>
+ Services.telemetry.recordEvent(
+ "firefoxview_next",
+ "fxa_continue",
+ "sync",
+ null
+ ),
+ });
+
constructor() {
super();
this._started = false;
- this.boundObserve = (...args) => this.observe(...args);
- this._currentSetupStateIndex = -1;
- this.errorState = null;
this._id = Math.floor(Math.random() * 10e6);
- this.currentSyncedTabs = [];
if (this.recentBrowsing) {
this.maxTabsLength = MAX_TABS_FOR_RECENT_BROWSING;
} else {
// Setting maxTabsLength to -1 for no max
this.maxTabsLength = -1;
}
- this.devices = [];
this.fullyUpdated = false;
- this.searchQuery = "";
this.showAll = false;
this.cumulativeSearches = 0;
+ this.onSearchQuery = this.onSearchQuery.bind(this);
}
static properties = {
...ViewPage.properties,
- errorState: { type: Number },
- currentSyncedTabs: { type: Array },
- _currentSetupStateIndex: { type: Number },
- devices: { type: Array },
- searchQuery: { type: String },
showAll: { type: Boolean },
cumulativeSearches: { type: Number },
};
@@ -72,26 +77,19 @@ class SyncedTabsInView extends ViewPage {
tabLists: { all: "fxview-tab-list" },
};
- connectedCallback() {
- super.connectedCallback();
- this.addEventListener("click", this);
- }
-
start() {
if (this._started) {
return;
}
this._started = true;
- Services.obs.addObserver(this.boundObserve, TOPIC_SETUPSTATE_CHANGED);
- Services.obs.addObserver(this.boundObserve, SYNCED_TABS_CHANGED);
-
- this.updateStates();
+ this.controller.addSyncObservers();
+ this.controller.updateStates();
this.onVisibilityChange();
if (this.recentBrowsing) {
this.recentBrowsingElement.addEventListener(
"fxview-search-textbox-query",
- this
+ this.onSearchQuery
);
}
}
@@ -103,75 +101,21 @@ class SyncedTabsInView extends ViewPage {
this._started = false;
TabsSetupFlowManager.updateViewVisibility(this._id, "unloaded");
this.onVisibilityChange();
-
- Services.obs.removeObserver(this.boundObserve, TOPIC_SETUPSTATE_CHANGED);
- Services.obs.removeObserver(this.boundObserve, SYNCED_TABS_CHANGED);
+ this.controller.removeSyncObservers();
if (this.recentBrowsing) {
this.recentBrowsingElement.removeEventListener(
"fxview-search-textbox-query",
- this
+ this.onSearchQuery
);
}
}
- willUpdate(changedProperties) {
- if (changedProperties.has("searchQuery")) {
- this.cumulativeSearches = this.searchQuery
- ? this.cumulativeSearches + 1
- : 0;
- }
- }
-
disconnectedCallback() {
super.disconnectedCallback();
this.stop();
}
- handleEvent(event) {
- if (event.type == "click" && event.target.dataset.action) {
- const { ErrorType } = SyncedTabsErrorHandler;
- switch (event.target.dataset.action) {
- case `${ErrorType.SYNC_ERROR}`:
- case `${ErrorType.NETWORK_OFFLINE}`:
- case `${ErrorType.PASSWORD_LOCKED}`: {
- TabsSetupFlowManager.tryToClearError();
- break;
- }
- case `${ErrorType.SIGNED_OUT}`:
- case "sign-in": {
- TabsSetupFlowManager.openFxASignup(event.target.ownerGlobal);
- break;
- }
- case "add-device": {
- TabsSetupFlowManager.openFxAPairDevice(event.target.ownerGlobal);
- break;
- }
- case "sync-tabs-disabled": {
- TabsSetupFlowManager.syncOpenTabs(event.target);
- break;
- }
- case `${ErrorType.SYNC_DISCONNECTED}`: {
- const win = event.target.ownerGlobal;
- const { switchToTabHavingURI } =
- win.docShell.chromeEventHandler.ownerGlobal;
- switchToTabHavingURI(
- "about:preferences?action=choose-what-to-sync#sync",
- true,
- {}
- );
- break;
- }
- }
- }
- if (event.type == "change") {
- TabsSetupFlowManager.syncOpenTabs(event.target);
- }
- if (this.recentBrowsing && event.type === "fxview-search-textbox-query") {
- this.onSearchQuery(event);
- }
- }
-
viewVisibleCallback() {
this.start();
}
@@ -196,90 +140,16 @@ class SyncedTabsInView extends ViewPage {
this.toggleVisibilityInCardContainer();
}
- async observe(subject, topic, errorState) {
- if (topic == TOPIC_SETUPSTATE_CHANGED) {
- this.updateStates(errorState);
- }
- if (topic == SYNCED_TABS_CHANGED) {
- this.getSyncedTabData();
- }
- }
-
- updateStates(errorState) {
- let stateIndex = TabsSetupFlowManager.uiStateIndex;
- errorState = errorState || SyncedTabsErrorHandler.getErrorType();
-
- if (stateIndex == 4 && this._currentSetupStateIndex !== stateIndex) {
- // trigger an initial request for the synced tabs list
- this.getSyncedTabData();
- }
-
- this._currentSetupStateIndex = stateIndex;
- this.errorState = errorState;
- }
-
- actionMappings = {
- "sign-in": {
- header: "firefoxview-syncedtabs-signin-header",
- description: "firefoxview-syncedtabs-signin-description",
- buttonLabel: "firefoxview-syncedtabs-signin-primarybutton",
- },
- "add-device": {
- header: "firefoxview-syncedtabs-adddevice-header",
- description: "firefoxview-syncedtabs-adddevice-description",
- buttonLabel: "firefoxview-syncedtabs-adddevice-primarybutton",
- descriptionLink: {
- name: "url",
- url: "https://support.mozilla.org/kb/how-do-i-set-sync-my-computer#w_connect-additional-devices-to-sync",
- },
- },
- "sync-tabs-disabled": {
- header: "firefoxview-syncedtabs-synctabs-header",
- description: "firefoxview-syncedtabs-synctabs-description",
- buttonLabel: "firefoxview-tabpickup-synctabs-primarybutton",
- },
- loading: {
- header: "firefoxview-syncedtabs-loading-header",
- description: "firefoxview-syncedtabs-loading-description",
- },
- };
-
- generateMessageCard({ error = false, action, errorState }) {
- errorState = errorState || this.errorState;
- let header,
- description,
- descriptionLink,
- buttonLabel,
- headerIconUrl,
- mainImageUrl;
- let descriptionArray;
- if (error) {
- let link;
- ({ header, description, link, buttonLabel } =
- SyncedTabsErrorHandler.getFluentStringsForErrorType(errorState));
- action = `${errorState}`;
- headerIconUrl = "chrome://global/skin/icons/info-filled.svg";
- mainImageUrl =
- "chrome://browser/content/firefoxview/synced-tabs-error.svg";
- descriptionArray = [description];
- if (errorState == "password-locked") {
- descriptionLink = {};
- // This is ugly, but we need to special case this link so we can
- // coexist with the old view.
- descriptionArray.push("firefoxview-syncedtab-password-locked-link");
- descriptionLink.name = "syncedtab-password-locked-link";
- descriptionLink.url = link.href;
- }
- } else {
- header = this.actionMappings[action].header;
- description = this.actionMappings[action].description;
- buttonLabel = this.actionMappings[action].buttonLabel;
- descriptionLink = this.actionMappings[action].descriptionLink;
- mainImageUrl =
- "chrome://browser/content/firefoxview/synced-tabs-error.svg";
- descriptionArray = [description];
- }
-
+ generateMessageCard({
+ action,
+ buttonLabel,
+ descriptionArray,
+ descriptionLink,
+ error,
+ header,
+ headerIconUrl,
+ mainImageUrl,
+ }) {
return html`
<fxview-empty-state
headerLabel=${header}
@@ -299,7 +169,7 @@ class SyncedTabsInView extends ViewPage {
?hidden=${!buttonLabel}
data-l10n-id="${ifDefined(buttonLabel)}"
data-action="${action}"
- @click=${this.handleEvent}
+ @click=${e => this.controller.handleEvent(e)}
aria-details="empty-container"
></button>
</fxview-empty-state>
@@ -307,28 +177,19 @@ class SyncedTabsInView extends ViewPage {
}
onOpenLink(event) {
- let currentWindow = this.getWindow();
- if (currentWindow.openTrustedLinkIn) {
- let where = lazy.BrowserUtils.whereToOpenLink(
- event.detail.originalEvent,
- false,
- true
- );
- if (where == "current") {
- where = "tab";
+ navigateToLink(event);
+
+ Services.telemetry.recordEvent(
+ "firefoxview_next",
+ "synced_tabs",
+ "tabs",
+ null,
+ {
+ page: this.recentBrowsing ? "recentbrowsing" : "syncedtabs",
}
- currentWindow.openTrustedLinkIn(event.originalTarget.url, where);
- Services.telemetry.recordEvent(
- "firefoxview_next",
- "synced_tabs",
- "tabs",
- null,
- {
- page: this.recentBrowsing ? "recentbrowsing" : "syncedtabs",
- }
- );
- }
- if (this.searchQuery) {
+ );
+
+ if (this.controller.searchQuery) {
const searchesHistogram = Services.telemetry.getKeyedHistogramById(
"FIREFOX_VIEW_CUMULATIVE_SEARCHES"
);
@@ -384,7 +245,7 @@ class SyncedTabsInView extends ViewPage {
class="blackbox notabs search-results-empty"
data-l10n-id="firefoxview-search-results-empty"
data-l10n-args=${JSON.stringify({
- query: escapeHtmlEntities(this.searchQuery),
+ query: escapeHtmlEntities(this.controller.searchQuery),
})}
></div>
`,
@@ -405,7 +266,8 @@ class SyncedTabsInView extends ViewPage {
}
onSearchQuery(e) {
- this.searchQuery = e.detail.query;
+ this.controller.searchQuery = e.detail.query;
+ this.cumulativeSearches = e.detail.query ? this.cumulativeSearches + 1 : 0;
this.showAll = false;
}
@@ -422,7 +284,7 @@ class SyncedTabsInView extends ViewPage {
secondaryActionClass="options-button"
hasPopup="menu"
.tabItems=${ifDefined(tabItems)}
- .searchQuery=${this.searchQuery}
+ .searchQuery=${this.controller.searchQuery}
maxTabsLength=${this.showAll ? -1 : this.maxTabsLength}
@fxview-tab-list-primary-action=${this.onOpenLink}
@fxview-tab-list-secondary-action=${this.onContextMenu}
@@ -434,33 +296,9 @@ class SyncedTabsInView extends ViewPage {
generateTabList() {
let renderArray = [];
- let renderInfo = {};
- for (let tab of this.currentSyncedTabs) {
- if (!(tab.client in renderInfo)) {
- renderInfo[tab.client] = {
- name: tab.device,
- deviceType: tab.deviceType,
- tabs: [],
- };
- }
- renderInfo[tab.client].tabs.push(tab);
- }
-
- // Add devices without tabs
- for (let device of this.devices) {
- if (!(device.id in renderInfo)) {
- renderInfo[device.id] = {
- name: device.name,
- deviceType: device.clientType,
- tabs: [],
- };
- }
- }
-
+ let renderInfo = this.controller.getRenderInfo();
for (let id in renderInfo) {
- let tabItems = this.searchQuery
- ? searchTabList(this.searchQuery, this.getTabItems(renderInfo[id].tabs))
- : this.getTabItems(renderInfo[id].tabs);
+ let tabItems = renderInfo[id].tabItems;
if (tabItems.length) {
const template = this.recentBrowsing
? this.deviceTemplate(
@@ -509,7 +347,7 @@ class SyncedTabsInView extends ViewPage {
isShowAllLinkVisible(tabItems) {
return (
this.recentBrowsing &&
- this.searchQuery &&
+ this.controller.searchQuery &&
tabItems.length > this.maxTabsLength &&
!this.showAll
);
@@ -536,35 +374,10 @@ class SyncedTabsInView extends ViewPage {
}
generateCardContent() {
- switch (this._currentSetupStateIndex) {
- case 0 /* error-state */:
- if (this.errorState) {
- return this.generateMessageCard({ error: true });
- }
- return this.generateMessageCard({ action: "loading" });
- case 1 /* not-signed-in */:
- if (Services.prefs.prefHasUserValue("services.sync.lastversion")) {
- // If this pref is set, the user has signed out of sync.
- // This path is also taken if we are disconnected from sync. See bug 1784055
- return this.generateMessageCard({
- error: true,
- errorState: "signed-out",
- });
- }
- return this.generateMessageCard({ action: "sign-in" });
- case 2 /* connect-secondary-device*/:
- return this.generateMessageCard({ action: "add-device" });
- case 3 /* disabled-tab-sync */:
- return this.generateMessageCard({ action: "sync-tabs-disabled" });
- case 4 /* synced-tabs-loaded*/:
- // There seems to be an edge case where sync says everything worked
- // fine but we have no devices.
- if (!this.devices.length) {
- return this.generateMessageCard({ action: "add-device" });
- }
- return this.generateTabList();
- }
- return html``;
+ const cardProperties = this.controller.getMessageCard();
+ return cardProperties
+ ? this.generateMessageCard(cardProperties)
+ : this.generateTabList();
}
render() {
@@ -589,7 +402,7 @@ class SyncedTabsInView extends ViewPage {
data-l10n-id="firefoxview-synced-tabs-header"
></h2>
${when(
- isSearchEnabled() || this._currentSetupStateIndex === 4,
+ isSearchEnabled() || this.controller.currentSetupStateIndex === 4,
() => html`<div class="syncedtabs-header">
${when(
isSearchEnabled(),
@@ -606,12 +419,12 @@ class SyncedTabsInView extends ViewPage {
</div>`
)}
${when(
- this._currentSetupStateIndex === 4,
+ this.controller.currentSetupStateIndex === 4,
() => html`
<button
class="small-button"
data-action="add-device"
- @click=${this.handleEvent}
+ @click=${e => this.controller.handleEvent(e)}
>
<img
class="icon"
@@ -635,9 +448,9 @@ class SyncedTabsInView extends ViewPage {
html`<card-container
preserveCollapseState
shortPageName="syncedtabs"
- ?showViewAll=${this._currentSetupStateIndex == 4 &&
- this.currentSyncedTabs.length}
- ?isEmptyState=${!this.currentSyncedTabs.length}
+ ?showViewAll=${this.controller.currentSetupStateIndex == 4 &&
+ this.controller.currentSyncedTabs.length}
+ ?isEmptyState=${!this.controller.currentSyncedTabs.length}
>
>
<h3
@@ -656,71 +469,9 @@ class SyncedTabsInView extends ViewPage {
return renderArray;
}
- async onReload() {
- await TabsSetupFlowManager.syncOnPageReload();
- }
-
- getTabItems(tabs) {
- tabs = tabs || this.tabs;
- return tabs?.map(tab => ({
- icon: tab.icon,
- title: tab.title,
- time: tab.lastUsed * 1000,
- url: tab.url,
- primaryL10nId: "firefoxview-tabs-list-tab-button",
- primaryL10nArgs: JSON.stringify({ targetURI: tab.url }),
- secondaryL10nId: "fxviewtabrow-options-menu-button",
- secondaryL10nArgs: JSON.stringify({ tabTitle: tab.title }),
- }));
- }
-
- updateTabsList(syncedTabs) {
- if (!syncedTabs.length) {
- this.currentSyncedTabs = syncedTabs;
- this.sendTabTelemetry(0);
- }
-
- const tabsToRender = syncedTabs;
-
- // Return early if new tabs are the same as previous ones
- if (
- JSON.stringify(tabsToRender) == JSON.stringify(this.currentSyncedTabs)
- ) {
- return;
- }
-
- this.currentSyncedTabs = tabsToRender;
- // Record the full tab count
- this.sendTabTelemetry(syncedTabs.length);
- }
-
- async getSyncedTabData() {
- this.devices = await lazy.SyncedTabs.getTabClients();
- let tabs = await lazy.SyncedTabs.createRecentTabsList(this.devices, 50, {
- removeAllDupes: false,
- removeDeviceDupes: true,
- });
-
- this.updateTabsList(tabs);
- }
-
updated() {
this.fullyUpdated = true;
this.toggleVisibilityInCardContainer();
}
-
- sendTabTelemetry(numTabs) {
- /*
- Services.telemetry.recordEvent(
- "firefoxview_next",
- "synced_tabs",
- "tabs",
- null,
- {
- count: numTabs.toString(),
- }
- );
-*/
- }
}
customElements.define("view-syncedtabs", SyncedTabsInView);
diff --git a/browser/components/firefoxview/tests/browser/browser.toml b/browser/components/firefoxview/tests/browser/browser.toml
index 9f9c1c0176..db8b2ea25c 100644
--- a/browser/components/firefoxview/tests/browser/browser.toml
+++ b/browser/components/firefoxview/tests/browser/browser.toml
@@ -27,6 +27,8 @@ skip-if = ["true"] # Bug 1869605 and # Bug 1870296
["browser_firefoxview.js"]
+["browser_firefoxview_dragDrop_pinned_tab.js"]
+
["browser_firefoxview_general_telemetry.js"]
["browser_firefoxview_navigation.js"]
@@ -51,17 +53,15 @@ skip-if = ["true"] # Bug 1851453
["browser_opentabs_firefoxview.js"]
["browser_opentabs_more.js"]
-fail-if = ["a11y_checks"] # Bugs 1858041, 1854625, and 1872174 clicked Show all link is not accessible because it is "hidden" when clicked
skip-if = ["verify"] # Bug 1886017
["browser_opentabs_pinned_tabs.js"]
["browser_opentabs_recency.js"]
skip-if = [
- "os == 'win'",
- "os == 'mac' && verify",
+ "os == 'mac'",
"os == 'linux'"
-] # macos times out, see bug 1857293, skipped for windows, see bug 1858460, Bug 1875877 - frequent fails on linux.
+] # macos times out, see bug 1857293, Bug 1875877 - frequent fails on linux.
["browser_opentabs_search.js"]
fail-if = ["a11y_checks"] # Bug 1850591 clicked moz-page-nav-button button is not focusable
diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_dragDrop_pinned_tab.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_dragDrop_pinned_tab.js
new file mode 100644
index 0000000000..dd30d53030
--- /dev/null
+++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_dragDrop_pinned_tab.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function dragAndDrop(
+ tab1,
+ tab2,
+ initialWindow = window,
+ destWindow = window,
+ afterTab = true,
+ context
+) {
+ let rect = tab2.getBoundingClientRect();
+ let event = {
+ ctrlKey: false,
+ altKey: false,
+ clientX: rect.left + rect.width / 2 + 10 * (afterTab ? 1 : -1),
+ clientY: rect.top + rect.height / 2,
+ };
+
+ if (destWindow != initialWindow) {
+ // Make sure that both tab1 and tab2 are visible
+ initialWindow.focus();
+ initialWindow.moveTo(rect.left, rect.top + rect.height * 3);
+ }
+
+ EventUtils.synthesizeDrop(
+ tab1,
+ tab2,
+ null,
+ "move",
+ initialWindow,
+ destWindow,
+ event
+ );
+
+ // Ensure dnd suppression is cleared.
+ EventUtils.synthesizeMouseAtCenter(tab2, { type: "mouseup" }, context);
+}
+
+add_task(async function () {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, URLs[0]);
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, URLs[1]);
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ let win1 = browser.ownerGlobal;
+ await navigateToViewAndWait(document, "opentabs");
+
+ let openTabs = document.querySelector("view-opentabs[name=opentabs]");
+ await openTabs.updateComplete;
+ await TestUtils.waitForCondition(
+ () => openTabs.viewCards[0].tabList.rowEls.length
+ );
+ await openTabs.openTabsTarget.readyWindowsPromise;
+ let card = openTabs.viewCards[0];
+ let tabRows = card.tabList.rowEls;
+ let tabChangeRaised;
+
+ // Pin first two tabs
+ for (var i = 0; i < 2; i++) {
+ tabChangeRaised = BrowserTestUtils.waitForEvent(
+ NonPrivateTabs,
+ "TabChange"
+ );
+ let currentTabEl = tabRows[i];
+ let currentTab = currentTabEl.tabElement;
+ info(`Pinning tab ${i + 1} with label: ${currentTab.label}`);
+ win1.gBrowser.pinTab(currentTab);
+ await tabChangeRaised;
+ await openTabs.updateComplete;
+ tabRows = card.tabList.rowEls;
+ currentTabEl = tabRows[i];
+
+ await TestUtils.waitForCondition(
+ () => currentTabEl.indicators.includes("pinned"),
+ `Tab ${i + 1} is pinned.`
+ );
+ }
+
+ info(`First two tabs are pinned.`);
+
+ let win2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ await openTabs.updateComplete;
+ await TestUtils.waitForCondition(
+ () => openTabs.viewCards.length === 2,
+ "Two windows are shown for Open Tabs in in Fx View."
+ );
+
+ let pinnedTab = win1.gBrowser.visibleTabs[0];
+ let newWindowTab = win2.gBrowser.visibleTabs[0];
+
+ dragAndDrop(newWindowTab, pinnedTab, win2, win1, true, content);
+
+ await switchToFxViewTab();
+ await openTabs.updateComplete;
+ await TestUtils.waitForCondition(
+ () => openTabs.viewCards.length === 1,
+ "One window is shown for Open Tabs in in Fx View."
+ );
+ });
+ cleanupTabs();
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_paused.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_paused.js
index e61b48b472..52dfce962d 100644
--- a/browser/components/firefoxview/tests/browser/browser_firefoxview_paused.js
+++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_paused.js
@@ -191,42 +191,6 @@ async function checkFxRenderCalls(browser, elements, selectedView) {
sandbox.restore();
}
-function dragAndDrop(
- tab1,
- tab2,
- initialWindow = window,
- destWindow = window,
- afterTab = true,
- context
-) {
- let rect = tab2.getBoundingClientRect();
- let event = {
- ctrlKey: false,
- altKey: false,
- clientX: rect.left + rect.width / 2 + 10 * (afterTab ? 1 : -1),
- clientY: rect.top + rect.height / 2,
- };
-
- if (destWindow != initialWindow) {
- // Make sure that both tab1 and tab2 are visible
- initialWindow.focus();
- initialWindow.moveTo(rect.left, rect.top + rect.height * 3);
- }
-
- EventUtils.synthesizeDrop(
- tab1,
- tab2,
- null,
- "move",
- initialWindow,
- destWindow,
- event
- );
-
- // Ensure dnd suppression is cleared.
- EventUtils.synthesizeMouseAtCenter(tab2, { type: "mouseup" }, context);
-}
-
add_task(async function test_recentbrowsing() {
await setupOpenAndClosedTabs();
@@ -438,66 +402,3 @@ add_task(async function test_recentlyclosed() {
});
await BrowserTestUtils.removeTab(TestTabs.tab2);
});
-
-add_task(async function test_drag_drop_pinned_tab() {
- await setupOpenAndClosedTabs();
- await withFirefoxView({}, async browser => {
- const { document } = browser.contentWindow;
- let win1 = browser.ownerGlobal;
- await navigateToViewAndWait(document, "opentabs");
-
- let openTabs = document.querySelector("view-opentabs[name=opentabs]");
- await openTabs.updateComplete;
- await TestUtils.waitForCondition(
- () => openTabs.viewCards[0].tabList.rowEls.length
- );
- await openTabs.openTabsTarget.readyWindowsPromise;
- let card = openTabs.viewCards[0];
- let tabRows = card.tabList.rowEls;
- let tabChangeRaised;
-
- // Pin first two tabs
- for (var i = 0; i < 2; i++) {
- tabChangeRaised = BrowserTestUtils.waitForEvent(
- NonPrivateTabs,
- "TabChange"
- );
- let currentTabEl = tabRows[i];
- let currentTab = currentTabEl.tabElement;
- info(`Pinning tab ${i + 1} with label: ${currentTab.label}`);
- win1.gBrowser.pinTab(currentTab);
- await tabChangeRaised;
- await openTabs.updateComplete;
- tabRows = card.tabList.rowEls;
- currentTabEl = tabRows[i];
-
- await TestUtils.waitForCondition(
- () => currentTabEl.indicators.includes("pinned"),
- `Tab ${i + 1} is pinned.`
- );
- }
-
- info(`First two tabs are pinned.`);
-
- let win2 = await BrowserTestUtils.openNewBrowserWindow();
-
- await openTabs.updateComplete;
- await TestUtils.waitForCondition(
- () => openTabs.viewCards.length === 2,
- "Two windows are shown for Open Tabs in in Fx View."
- );
-
- let pinnedTab = win1.gBrowser.visibleTabs[0];
- let newWindowTab = win2.gBrowser.visibleTabs[0];
-
- dragAndDrop(newWindowTab, pinnedTab, win2, win1, true, content);
-
- await switchToFxViewTab();
- await openTabs.updateComplete;
- await TestUtils.waitForCondition(
- () => openTabs.viewCards.length === 1,
- "One window is shown for Open Tabs in in Fx View."
- );
- });
- cleanupTabs();
-});
diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_search_telemetry.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_search_telemetry.js
index c76a11d3ad..e1aa58ae49 100644
--- a/browser/components/firefoxview/tests/browser/browser_firefoxview_search_telemetry.js
+++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_search_telemetry.js
@@ -537,7 +537,7 @@ add_task(async function test_cumulative_searches_history_telemetry() {
() =>
history.fullyUpdated &&
history?.lists[0].rowEls?.length === 1 &&
- history?.searchQuery,
+ history?.controller?.searchQuery,
"Expected search results are not shown yet."
);
@@ -605,7 +605,8 @@ add_task(async function test_cumulative_searches_syncedtabs_telemetry() {
);
await TestUtils.waitForCondition(
() =>
- syncedTabs.tabLists[0].rowEls.length === 1 && syncedTabs?.searchQuery,
+ syncedTabs.tabLists[0].rowEls.length === 1 &&
+ syncedTabs.controller.searchQuery,
"Expected search results are not shown yet."
);
diff --git a/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js b/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js
index 037729ea7d..b556649d52 100644
--- a/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js
+++ b/browser/components/firefoxview/tests/browser/browser_firefoxview_tab.js
@@ -78,7 +78,7 @@ add_task(async function aria_attributes() {
"true",
'Firefox View button should have `aria-pressed="true"` upon selecting it'
);
- win.BrowserOpenTab();
+ win.BrowserCommands.openTab();
is(
win.FirefoxViewHandler.button.getAttribute("aria-pressed"),
"false",
@@ -118,8 +118,8 @@ add_task(async function homepage_new_tab() {
win.gBrowser.tabContainer,
"TabOpen"
);
- win.BrowserHome();
- info("Waiting for BrowserHome() to open a new tab");
+ win.BrowserCommands.home();
+ info("Waiting for BrowserCommands.home() to open a new tab");
await newTabOpened;
assertFirefoxViewTab(win);
ok(
diff --git a/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js
index c4c096acff..847ce4d9fd 100644
--- a/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js
+++ b/browser/components/firefoxview/tests/browser/browser_history_firefoxview.js
@@ -58,14 +58,14 @@ function isElInViewport(element) {
async function historyComponentReady(historyComponent, expectedHistoryItems) {
await TestUtils.waitForCondition(
() =>
- [...historyComponent.allHistoryItems.values()].reduce(
+ [...historyComponent.controller.allHistoryItems.values()].reduce(
(acc, { length }) => acc + length,
0
) === expectedHistoryItems,
"History component ready"
);
- let expected = historyComponent.historyMapByDate.length;
+ let expected = historyComponent.controller.historyMapByDate.length;
let actual = historyComponent.cards.length;
is(expected, actual, `Total number of cards should be ${expected}`);
@@ -242,7 +242,8 @@ add_task(async function test_list_ordering() {
await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
await sortHistoryTelemetry(sortHistoryEvent);
- let expectedNumOfCards = historyComponent.historyMapBySite.length;
+ let expectedNumOfCards =
+ historyComponent.controller.historyMapBySite.length;
info(`Total number of cards should be ${expectedNumOfCards}`);
await BrowserTestUtils.waitForMutationCondition(
@@ -345,7 +346,7 @@ add_task(async function test_empty_states() {
"Import history banner is shown"
);
let importHistoryCloseButton =
- historyComponent.cards[0].querySelector("button.close");
+ historyComponent.cards[0].querySelector("moz-button.close");
importHistoryCloseButton.click();
await TestUtils.waitForCondition(() => historyComponent.fullyUpdated);
ok(
@@ -484,7 +485,7 @@ add_task(async function test_search_history() {
{ childList: true, subtree: true },
() =>
historyComponent.cards.length ===
- historyComponent.historyMapByDate.length
+ historyComponent.controller.historyMapByDate.length
);
searchTextbox.blur();
@@ -513,7 +514,7 @@ add_task(async function test_search_history() {
{ childList: true, subtree: true },
() =>
historyComponent.cards.length ===
- historyComponent.historyMapByDate.length
+ historyComponent.controller.historyMapByDate.length
);
});
});
@@ -528,7 +529,7 @@ add_task(async function test_persist_collapse_card_after_view_change() {
historyComponent.profileAge = 8;
await TestUtils.waitForCondition(
() =>
- [...historyComponent.allHistoryItems.values()].reduce(
+ [...historyComponent.controller.allHistoryItems.values()].reduce(
(acc, { length }) => acc + length,
0
) === 4
diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_cards.js b/browser/components/firefoxview/tests/browser/browser_opentabs_cards.js
index d4de3ae5a9..5fdcf89d70 100644
--- a/browser/components/firefoxview/tests/browser/browser_opentabs_cards.js
+++ b/browser/components/firefoxview/tests/browser/browser_opentabs_cards.js
@@ -203,13 +203,15 @@ add_task(async function open_tab_new_window() {
const cards = getOpenTabsCards(openTabs);
const originalWinRows = await getTabRowsForCard(cards[1]);
const [row] = originalWinRows;
+
+ // We hide date/time and URL columns in tab rows when there are multiple window cards for spacial reasons
ok(
- row.shadowRoot.getElementById("fxview-tab-row-url").hidden,
- "The URL is hidden, since we have two windows."
+ !row.shadowRoot.getElementById("fxview-tab-row-url"),
+ "The URL span element isn't found within the tab row as expected, since we have two open windows."
);
ok(
- row.shadowRoot.getElementById("fxview-tab-row-date").hidden,
- "The date is hidden, since we have two windows."
+ !row.shadowRoot.getElementById("fxview-tab-row-date"),
+ "The date span element isn't found within the tab row as expected, since we have two open windows."
);
info("Select a tab from the original window.");
tabChangeRaised = BrowserTestUtils.waitForEvent(
diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_opentabs_firefoxview.js
index 955c2363d7..2c415e7aa2 100644
--- a/browser/components/firefoxview/tests/browser/browser_opentabs_firefoxview.js
+++ b/browser/components/firefoxview/tests/browser/browser_opentabs_firefoxview.js
@@ -131,7 +131,7 @@ async function moreMenuSetup() {
}
add_task(async function test_close_open_tab() {
- await withFirefoxView({}, async browser => {
+ await withFirefoxView({}, async () => {
const [cards, rows] = await moreMenuSetup();
const firstTab = rows[0];
const tertiaryButtonEl = firstTab.tertiaryButtonEl;
@@ -321,7 +321,7 @@ add_task(async function test_send_device_submenu() {
.stub(gSync, "getSendTabTargets")
.callsFake(() => fxaDevicesWithCommands);
- await withFirefoxView({}, async browser => {
+ await withFirefoxView({}, async () => {
// TEST_URL1 is our only tab, left over from previous test
Assert.deepEqual(
getVisibleTabURLs(),
diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_recency.js b/browser/components/firefoxview/tests/browser/browser_opentabs_recency.js
index ee3f9981e1..fc10ef2eb0 100644
--- a/browser/components/firefoxview/tests/browser/browser_opentabs_recency.js
+++ b/browser/components/firefoxview/tests/browser/browser_opentabs_recency.js
@@ -2,23 +2,30 @@
* http://creativecommons.org/publicdomain/zero/1.0/ */
/*
- This test checks the recent-browsing view of open tabs in about:firefoxview next
+ This test checks that the recent-browsing view of open tabs in about:firefoxview
presents the correct tab data in the correct order.
*/
+SimpleTest.requestCompleteLog();
+
+const { ObjectUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/ObjectUtils.sys.mjs"
+);
+let origBrowserState;
const tabURL1 = "data:,Tab1";
const tabURL2 = "data:,Tab2";
const tabURL3 = "data:,Tab3";
const tabURL4 = "data:,Tab4";
-let gInitialTab;
-let gInitialTabURL;
-
add_setup(function () {
- gInitialTab = gBrowser.selectedTab;
- gInitialTabURL = tabUrl(gInitialTab);
+ origBrowserState = SessionStore.getBrowserState();
});
+async function cleanup() {
+ await switchToWindow(window);
+ await SessionStoreTestUtils.promiseBrowserState(origBrowserState);
+}
+
function tabUrl(tab) {
return tab.linkedBrowser.currentURI?.spec;
}
@@ -37,6 +44,12 @@ async function minimizeWindow(win) {
ok(win.document.hidden, "Top level window should be hidden");
}
+function getAllSelectedTabURLs() {
+ return BrowserWindowTracker.orderedWindows.map(win =>
+ tabUrl(win.gBrowser.selectedTab)
+ );
+}
+
async function restoreWindow(win) {
ok(win.document.hidden, "Top level window should be hidden");
let promiseSizeModeChange = BrowserTestUtils.waitForEvent(
@@ -93,86 +106,91 @@ async function restoreWindow(win) {
ok(!win.document.hidden, "Top level window should be visible");
}
-async function prepareOpenTabs(urls, win = window) {
- const reusableTabURLs = ["about:newtab", "about:blank"];
- const gBrowser = win.gBrowser;
-
- for (let url of urls) {
- if (
- gBrowser.visibleTabs.length == 1 &&
- reusableTabURLs.includes(gBrowser.selectedBrowser.currentURI.spec)
- ) {
- // we'll load into this tab rather than opening a new one
- info(
- `Loading ${url} into blank tab: ${gBrowser.selectedBrowser.currentURI.spec}`
- );
- BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url);
- await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, null, url);
- } else {
- info(`Loading ${url} into new tab`);
- await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
- }
- await new Promise(res => win.requestAnimationFrame(res));
+async function prepareOpenWindowsAndTabs(windowsData) {
+ // windowsData selected tab URL should be unique so we can map tab URL to window
+ const browserState = {
+ windows: windowsData.map((winData, index) => {
+ const tabs = winData.tabs.map(url => ({
+ entries: [{ url, triggeringPrincipal_base64 }],
+ }));
+ return {
+ tabs,
+ selected: winData.selectedIndex + 1,
+ zIndex: index + 1,
+ };
+ }),
+ };
+ await SessionStoreTestUtils.promiseBrowserState(browserState);
+ await NonPrivateTabs.readyWindowsPromise;
+ const selectedTabURLOrder = browserState.windows.map(winData => {
+ return winData.tabs[winData.selected - 1].entries[0].url;
+ });
+ const windowByTabURL = new Map();
+ for (let win of BrowserWindowTracker.orderedWindows) {
+ windowByTabURL.set(tabUrl(win.gBrowser.selectedTab), win);
}
- Assert.equal(
- gBrowser.visibleTabs.length,
- urls.length,
- `Prepared ${urls.length} tabs as expected`
- );
- Assert.equal(
- tabUrl(gBrowser.selectedTab),
- urls[urls.length - 1],
- "The selectedTab is the last of the URLs given as expected"
+ is(
+ windowByTabURL.size,
+ windowsData.length,
+ "The tab URL to window mapping includes an entry for each window"
);
-}
-
-async function cleanup(...windowsToClose) {
- await Promise.all(
- windowsToClose.map(win => BrowserTestUtils.closeWindow(win))
+ info(
+ `After promiseBrowserState, selected tab order is: ${Array.from(
+ windowByTabURL.keys()
+ )}`
);
- while (gBrowser.visibleTabs.length > 1) {
- await SessionStoreTestUtils.closeTab(gBrowser.tabs.at(-1));
- }
- if (gBrowser.selectedBrowser.currentURI.spec !== gInitialTabURL) {
- BrowserTestUtils.startLoadingURIString(
- gBrowser.selectedBrowser,
- gInitialTabURL
- );
- await BrowserTestUtils.browserLoaded(
- gBrowser.selectedBrowser,
- null,
- gInitialTabURL
- );
+ // Make any corrections to the window order by selecting each in reverse order
+ for (let url of selectedTabURLOrder.toReversed()) {
+ await switchToWindow(windowByTabURL.get(url));
}
+ // Verify windows are in the expected order
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ selectedTabURLOrder,
+ "The windows and their selected tabs are in the expected order"
+ );
+ Assert.deepEqual(
+ BrowserWindowTracker.orderedWindows.map(win =>
+ win.gBrowser.visibleTabs.map(tab => tabUrl(tab))
+ ),
+ windowsData.map(winData => winData.tabs),
+ "We opened all the tabs in each window"
+ );
}
-function getOpenTabsComponent(browser) {
+function getRecentOpenTabsComponent(browser) {
return browser.contentDocument.querySelector(
"view-recentbrowsing view-opentabs"
);
}
-async function checkTabList(browser, expected) {
- const tabsView = getOpenTabsComponent(browser);
+async function checkRecentTabList(browser, expected) {
+ const tabsView = getRecentOpenTabsComponent(browser);
const [openTabsCard] = getOpenTabsCards(tabsView);
await openTabsCard.updateComplete;
const tabListRows = await getTabRowsForCard(openTabsCard);
Assert.ok(tabListRows, "Found the tab list element");
let actual = Array.from(tabListRows).map(row => row.url);
- Assert.deepEqual(
- actual,
- expected,
- "Tab list has items with URLs in the expected order"
+ await BrowserTestUtils.waitForCondition(
+ () => ObjectUtils.deepEqual(actual, expected),
+ "Waiting for tab list to hvae items with URLs in the expected order"
);
}
add_task(async function test_single_window_tabs() {
- await prepareOpenTabs([tabURL1, tabURL2]);
+ const testData = [
+ {
+ tabs: [tabURL1, tabURL2],
+ selectedIndex: 1, // the 2nd tab should be selected
+ },
+ ];
+ await prepareOpenWindowsAndTabs(testData);
+
await openFirefoxViewTab(window).then(async viewTab => {
const browser = viewTab.linkedBrowser;
- await checkTabList(browser, [tabURL2, tabURL1]);
+ await checkRecentTabList(browser, [tabURL2, tabURL1]);
// switch to the first tab
let promiseHidden = BrowserTestUtils.waitForEvent(
@@ -192,25 +210,62 @@ add_task(async function test_single_window_tabs() {
// and check the results in the open tabs section of Recent Browsing
await openFirefoxViewTab(window).then(async viewTab => {
const browser = viewTab.linkedBrowser;
- await checkTabList(browser, [tabURL1, tabURL2]);
+ await checkRecentTabList(browser, [tabURL1, tabURL2]);
});
await cleanup();
});
add_task(async function test_multiple_window_tabs() {
const fxViewURL = getFirefoxViewURL();
- const win1 = window;
+ const testData = [
+ {
+ // this window should be active after restore
+ tabs: [tabURL1, tabURL2],
+ selectedIndex: 0, // tabURL1 should be selected
+ },
+ {
+ tabs: [tabURL3, tabURL4],
+ selectedIndex: 0, // tabURL3 should be selected
+ },
+ ];
+ await prepareOpenWindowsAndTabs(testData);
+
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL1, tabURL3],
+ "The windows and their selected tabs are in the expected order"
+ );
let tabChangeRaised;
- await prepareOpenTabs([tabURL1, tabURL2]);
- const win2 = await BrowserTestUtils.openNewBrowserWindow();
- await prepareOpenTabs([tabURL3, tabURL4], win2);
+ const [win1, win2] = BrowserWindowTracker.orderedWindows;
+
+ info(`Switch to window 1's 2nd tab: ${tabUrl(win1.gBrowser.visibleTabs[1])}`);
+ await BrowserTestUtils.switchTab(gBrowser, win1.gBrowser.visibleTabs[1]);
+ await switchToWindow(win2);
+
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL3, tabURL2],
+ `Window 2 has selected the ${tabURL3} tab, window 1 has ${tabURL2}`
+ );
+ info(`Switch to window 2's 2nd tab: ${tabUrl(win2.gBrowser.visibleTabs[1])}`);
+ tabChangeRaised = BrowserTestUtils.waitForEvent(
+ NonPrivateTabs,
+ "TabRecencyChange"
+ );
+ await BrowserTestUtils.switchTab(win2.gBrowser, win2.gBrowser.visibleTabs[1]);
+ await tabChangeRaised;
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL4, tabURL2],
+ `window 2 has selected the ${tabURL4} tab, ${tabURL2} remains selected in window 1`
+ );
// to avoid confusing the results by activating different windows,
// check fxview in the current window - which is win2
info("Switching to fxview tab in win2");
await openFirefoxViewTab(win2).then(async viewTab => {
const browser = viewTab.linkedBrowser;
- await checkTabList(browser, [tabURL4, tabURL3, tabURL2, tabURL1]);
+ await checkRecentTabList(browser, [tabURL4, tabURL3, tabURL2, tabURL1]);
Assert.equal(
tabUrl(win2.gBrowser.selectedTab),
@@ -218,7 +273,7 @@ add_task(async function test_multiple_window_tabs() {
`The selected tab in window 2 is ${fxViewURL}`
);
- info("Switching to first tab (tab3) in win2");
+ info("Switching to first tab in win2");
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
@@ -231,20 +286,20 @@ add_task(async function test_multiple_window_tabs() {
win2.gBrowser,
win2.gBrowser.visibleTabs[0]
);
- Assert.equal(
- tabUrl(win2.gBrowser.selectedTab),
- tabURL3,
- `The selected tab in window 2 is ${tabURL3}`
- );
await tabChangeRaised;
await promiseHidden;
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL3, tabURL2],
+ `window 2 has switched to ${tabURL3}, ${tabURL2} remains selected in window 1`
+ );
});
info("Opening fxview in win2 to confirm tab3 is most recent");
await openFirefoxViewTab(win2).then(async viewTab => {
const browser = viewTab.linkedBrowser;
info("Check result of selecting 1ist tab in window 2");
- await checkTabList(browser, [tabURL3, tabURL4, tabURL2, tabURL1]);
+ await checkRecentTabList(browser, [tabURL3, tabURL4, tabURL2, tabURL1]);
});
info("Focusing win1, where tab2 should be selected");
@@ -254,10 +309,10 @@ add_task(async function test_multiple_window_tabs() {
);
await switchToWindow(win1);
await tabChangeRaised;
- Assert.equal(
- tabUrl(win1.gBrowser.selectedTab),
- tabURL2,
- `The selected tab in window 1 is ${tabURL2}`
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL2, fxViewURL],
+ `The selected tab in window 1 is ${tabURL2}, ${fxViewURL} remains selected in window 2`
);
info("Opening fxview in win1 to confirm tab2 is most recent");
@@ -266,7 +321,7 @@ add_task(async function test_multiple_window_tabs() {
info(
"In fxview, check result of activating window 1, where tab 2 is selected"
);
- await checkTabList(browser, [tabURL2, tabURL3, tabURL4, tabURL1]);
+ await checkRecentTabList(browser, [tabURL2, tabURL3, tabURL4, tabURL1]);
let promiseHidden = BrowserTestUtils.waitForEvent(
browser.contentDocument,
@@ -284,45 +339,50 @@ add_task(async function test_multiple_window_tabs() {
await promiseHidden;
await tabChangeRaised;
});
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL1, fxViewURL],
+ `The selected tab in window 1 is ${tabURL1}, ${fxViewURL} remains selected in window 2`
+ );
// check result in the fxview in the 1st window
info("Opening fxview in win1 to confirm tab1 is most recent");
await openFirefoxViewTab(win1).then(async viewTab => {
const browser = viewTab.linkedBrowser;
info("Check result of selecting 1st tab in win1");
- await checkTabList(browser, [tabURL1, tabURL2, tabURL3, tabURL4]);
+ await checkRecentTabList(browser, [tabURL1, tabURL2, tabURL3, tabURL4]);
});
- await cleanup(win2);
+ await cleanup();
});
add_task(async function test_windows_activation() {
- const win1 = window;
- await prepareOpenTabs([tabURL1], win1);
- let fxViewTab;
- let tabChangeRaised;
- info("switch to firefox-view and leave it selected");
- await openFirefoxViewTab(win1).then(tab => (fxViewTab = tab));
+ // use Session restore to batch-open windows and tabs
+ const testData = [
+ {
+ // this window should be active after restore
+ tabs: [tabURL1],
+ selectedIndex: 0, // tabURL1 should be selected
+ },
+ {
+ tabs: [tabURL2],
+ selectedIndex: 0, // tabURL2 should be selected
+ },
+ {
+ tabs: [tabURL3],
+ selectedIndex: 0, // tabURL3 should be selected
+ },
+ ];
+ await prepareOpenWindowsAndTabs(testData);
- const win2 = await BrowserTestUtils.openNewBrowserWindow();
- await switchToWindow(win2);
- await prepareOpenTabs([tabURL2], win2);
-
- const win3 = await BrowserTestUtils.openNewBrowserWindow();
- await switchToWindow(win3);
- await prepareOpenTabs([tabURL3], win3);
-
- tabChangeRaised = BrowserTestUtils.waitForEvent(
- NonPrivateTabs,
- "TabRecencyChange"
- );
- info("Switching back to win 1");
- await switchToWindow(win1);
- info("Waiting for tabChangeRaised to resolve");
- await tabChangeRaised;
+ let tabChangeRaised;
+ const [win1, win2] = BrowserWindowTracker.orderedWindows;
- const browser = fxViewTab.linkedBrowser;
- await checkTabList(browser, [tabURL3, tabURL2, tabURL1]);
+ info("switch to firefox-view and leave it selected");
+ await openFirefoxViewTab(win1).then(async viewTab => {
+ const browser = viewTab.linkedBrowser;
+ await checkRecentTabList(browser, [tabURL1, tabURL2, tabURL3]);
+ });
info("switch to win2 and confirm its selected tab becomes most recent");
tabChangeRaised = BrowserTestUtils.waitForEvent(
@@ -331,24 +391,52 @@ add_task(async function test_windows_activation() {
);
await switchToWindow(win2);
await tabChangeRaised;
- await checkTabList(browser, [tabURL2, tabURL3, tabURL1]);
- await cleanup(win2, win3);
+ await openFirefoxViewTab(win1).then(async viewTab => {
+ await checkRecentTabList(viewTab.linkedBrowser, [
+ tabURL2,
+ tabURL1,
+ tabURL3,
+ ]);
+ });
+ await cleanup();
});
add_task(async function test_minimize_restore_windows() {
- const win1 = window;
- let tabChangeRaised;
- await prepareOpenTabs([tabURL1, tabURL2]);
- const win2 = await BrowserTestUtils.openNewBrowserWindow();
- await prepareOpenTabs([tabURL3, tabURL4], win2);
- await NonPrivateTabs.readyWindowsPromise;
+ const fxViewURL = getFirefoxViewURL();
+ const testData = [
+ {
+ // this window should be active after restore
+ tabs: [tabURL1, tabURL2],
+ selectedIndex: 1, // tabURL2 should be selected
+ },
+ {
+ tabs: [tabURL3, tabURL4],
+ selectedIndex: 0, // tabURL3 should be selected
+ },
+ ];
+ await prepareOpenWindowsAndTabs(testData);
+ const [win1, win2] = BrowserWindowTracker.orderedWindows;
+
+ // switch to the last (tabURL4) tab in window 2
+ await switchToWindow(win2);
+ let tabChangeRaised = BrowserTestUtils.waitForEvent(
+ NonPrivateTabs,
+ "TabRecencyChange"
+ );
+ await BrowserTestUtils.switchTab(win2.gBrowser, win2.gBrowser.visibleTabs[1]);
+ await tabChangeRaised;
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL4, tabURL2],
+ "The windows and their selected tabs are in the expected order"
+ );
// to avoid confusing the results by activating different windows,
// check fxview in the current window - which is win2
info("Opening fxview in win2 to confirm tab4 is most recent");
await openFirefoxViewTab(win2).then(async viewTab => {
const browser = viewTab.linkedBrowser;
- await checkTabList(browser, [tabURL4, tabURL3, tabURL2, tabURL1]);
+ await checkRecentTabList(browser, [tabURL4, tabURL3, tabURL2, tabURL1]);
let promiseHidden = BrowserTestUtils.waitForEvent(
browser.contentDocument,
@@ -366,6 +454,11 @@ add_task(async function test_minimize_restore_windows() {
await promiseHidden;
await tabChangeRaised;
});
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL3, tabURL2],
+ `Window 2 has ${tabURL3} selected, window 1 remains at ${tabURL2}`
+ );
// then minimize the window, focusing the 1st window
info("Minimizing win2, leaving tab 3 selected");
@@ -378,32 +471,41 @@ add_task(async function test_minimize_restore_windows() {
await switchToWindow(win1);
await tabChangeRaised;
- Assert.equal(
- tabUrl(win1.gBrowser.selectedTab),
- tabURL2,
- `The selected tab in window 1 is ${tabURL2}`
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL2, tabURL3],
+ `Window 1 has ${tabURL2} selected, window 2 remains at ${tabURL3}`
);
info("Opening fxview in win1 to confirm tab2 is most recent");
await openFirefoxViewTab(win1).then(async viewTab => {
const browser = viewTab.linkedBrowser;
- await checkTabList(browser, [tabURL2, tabURL3, tabURL4, tabURL1]);
+ await checkRecentTabList(browser, [tabURL2, tabURL3, tabURL4, tabURL1]);
info(
"Restoring win2 and focusing it - which should make its selected tab most recent"
);
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
- "TabRecencyChange"
+ "TabRecencyChange",
+ false,
+ event => event.detail.sourceEvents?.includes("activate")
);
await restoreWindow(win2);
await switchToWindow(win2);
+ // make sure we wait for the activate event from OpenTabs.
await tabChangeRaised;
+ Assert.deepEqual(
+ getAllSelectedTabURLs(),
+ [tabURL3, fxViewURL],
+ `Window 2 was restored and has ${tabURL3} selected, window 1 remains at ${fxViewURL}`
+ );
+
info(
"Checking tab order in fxview in win1, to confirm tab3 is most recent"
);
- await checkTabList(browser, [tabURL3, tabURL2, tabURL4, tabURL1]);
+ await checkRecentTabList(browser, [tabURL3, tabURL2, tabURL4, tabURL1]);
});
-
- await cleanup(win2);
+ info("test done, waiting for cleanup");
+ await cleanup();
});
diff --git a/browser/components/firefoxview/tests/browser/browser_opentabs_tab_indicators.js b/browser/components/firefoxview/tests/browser/browser_opentabs_tab_indicators.js
index 78fab976ed..4403a8e36a 100644
--- a/browser/components/firefoxview/tests/browser/browser_opentabs_tab_indicators.js
+++ b/browser/components/firefoxview/tests/browser/browser_opentabs_tab_indicators.js
@@ -94,12 +94,16 @@ add_task(async function test_container_indicator() {
await TestUtils.waitForCondition(
() =>
Array.from(openTabs.viewCards[0].tabList.rowEls).some(rowEl => {
- containerTabElem = rowEl;
- return rowEl.containerObj;
+ let hasContainerObj;
+ if (rowEl.containerObj?.icon) {
+ containerTabElem = rowEl;
+ hasContainerObj = rowEl.containerObj;
+ }
+
+ return hasContainerObj;
}),
"The container tab element isn't marked in Fx View."
);
-
ok(
containerTabElem.shadowRoot
.querySelector(".fxview-tab-row-container-indicator")
diff --git a/browser/components/firefoxview/tests/browser/browser_recentlyclosed_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_recentlyclosed_firefoxview.js
index fcfcf20562..85879667bb 100644
--- a/browser/components/firefoxview/tests/browser/browser_recentlyclosed_firefoxview.js
+++ b/browser/components/firefoxview/tests/browser/browser_recentlyclosed_firefoxview.js
@@ -372,6 +372,12 @@ add_task(async function test_dismiss_tab() {
info("calling dismiss_tab on the top, most-recently closed tab");
let closedTabItem = listItems[0];
+ // the most recently closed tab was in window 3 which got closed
+ // so we expect a sourceClosedId on the item element
+ ok(
+ !isNaN(closedTabItem.sourceClosedId),
+ "Item has a sourceClosedId property"
+ );
// dismiss the first tab and verify the list is correctly updated
await dismiss_tab(closedTabItem);
@@ -390,6 +396,12 @@ add_task(async function test_dismiss_tab() {
// dismiss the last tab and verify the list is correctly updated
closedTabItem = listItems[listItems.length - 1];
+ ok(
+ isNaN(closedTabItem.sourceClosedId),
+ "Item does not have a sourceClosedId property"
+ );
+ ok(closedTabItem.sourceWindowId, "Item has a sourceWindowId property");
+
await dismiss_tab(closedTabItem);
await listElem.getUpdateComplete;
diff --git a/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js
index 86e4d9cdee..a644b39fc6 100644
--- a/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js
+++ b/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js
@@ -69,19 +69,23 @@ add_task(async function test_network_offline() {
"view-syncedtabs:not([slot=syncedtabs])"
);
await TestUtils.waitForCondition(() => syncedTabsComponent.fullyUpdated);
- await BrowserTestUtils.waitForMutationCondition(
- syncedTabsComponent.shadowRoot.querySelector(".cards-container"),
- { childList: true },
- () => syncedTabsComponent.shadowRoot.innerHTML.includes("network-offline")
+ await TestUtils.waitForCondition(
+ () =>
+ syncedTabsComponent.emptyState.shadowRoot.textContent.includes(
+ "Check your internet connection"
+ ),
+ "The expected network offline error message is displayed."
);
- let emptyState =
- syncedTabsComponent.shadowRoot.querySelector("fxview-empty-state");
ok(
- emptyState.getAttribute("headerlabel").includes("network-offline"),
+ syncedTabsComponent.emptyState
+ .getAttribute("headerlabel")
+ .includes("network-offline"),
"Network offline message is shown"
);
- emptyState.querySelector("button[data-action='network-offline']").click();
+ syncedTabsComponent.emptyState
+ .querySelector("button[data-action='network-offline']")
+ .click();
await BrowserTestUtils.waitForCondition(
() => TabsSetupFlowManager.tryToClearError.calledOnce
@@ -92,10 +96,10 @@ add_task(async function test_network_offline() {
"TabsSetupFlowManager.tryToClearError() was called once"
);
- emptyState =
- syncedTabsComponent.shadowRoot.querySelector("fxview-empty-state");
ok(
- emptyState.getAttribute("headerlabel").includes("network-offline"),
+ syncedTabsComponent.emptyState
+ .getAttribute("headerlabel")
+ .includes("network-offline"),
"Network offline message is still shown"
);
@@ -121,16 +125,18 @@ add_task(async function test_sync_error() {
"view-syncedtabs:not([slot=syncedtabs])"
);
await TestUtils.waitForCondition(() => syncedTabsComponent.fullyUpdated);
- await BrowserTestUtils.waitForMutationCondition(
- syncedTabsComponent.shadowRoot.querySelector(".cards-container"),
- { childList: true },
- () => syncedTabsComponent.shadowRoot.innerHTML.includes("sync-error")
+ await TestUtils.waitForCondition(
+ () =>
+ syncedTabsComponent.emptyState.shadowRoot.textContent.includes(
+ "having trouble syncing"
+ ),
+ "Sync error message is shown."
);
- let emptyState =
- syncedTabsComponent.shadowRoot.querySelector("fxview-empty-state");
ok(
- emptyState.getAttribute("headerlabel").includes("sync-error"),
+ syncedTabsComponent.emptyState
+ .getAttribute("headerlabel")
+ .includes("sync-error"),
"Correct message should show when there's a sync service error"
);
@@ -139,3 +145,233 @@ add_task(async function test_sync_error() {
});
await tearDown(sandbox);
});
+
+add_task(async function test_sync_disabled_by_policy() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["identity.fxaccounts.enabled", false]],
+ });
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ const recentBrowsingSyncedTabs = document.querySelector(
+ "view-syncedtabs[slot=syncedtabs]"
+ );
+ const syncedtabsPageNavButton = document.querySelector(
+ "moz-page-nav-button[view='syncedtabs']"
+ );
+
+ ok(
+ BrowserTestUtils.isHidden(recentBrowsingSyncedTabs),
+ "Synced tabs should not be visible from recent browsing."
+ );
+ ok(
+ BrowserTestUtils.isHidden(syncedtabsPageNavButton),
+ "Synced tabs nav button should not be visible."
+ );
+
+ document.location.assign(`${getFirefoxViewURL()}#syncedtabs`);
+ await TestUtils.waitForTick();
+ is(
+ document.querySelector("moz-page-nav").currentView,
+ "recentbrowsing",
+ "Should not be able to navigate to synced tabs."
+ );
+ });
+ await tearDown();
+});
+
+add_task(async function test_sync_error_signed_out() {
+ // sync error should not show if user is not signed in
+ let sandbox = await setupWithDesktopDevices(UIState.STATUS_NOT_CONFIGURED);
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ await navigateToViewAndWait(document, "syncedtabs");
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ Services.obs.notifyObservers(null, "weave:service:sync:error");
+
+ let syncedTabsComponent = document.querySelector(
+ "view-syncedtabs:not([slot=syncedtabs])"
+ );
+ await TestUtils.waitForCondition(
+ () => syncedTabsComponent.fullyUpdated,
+ "The synced tabs component has finished updating."
+ );
+ await TestUtils.waitForCondition(
+ () =>
+ syncedTabsComponent.emptyState.shadowRoot.textContent.includes(
+ "sign in to your account"
+ ),
+ "Sign in header is shown."
+ );
+
+ ok(
+ syncedTabsComponent.emptyState
+ .getAttribute("headerlabel")
+ .includes("signin-header"),
+ "Sign in message is shown"
+ );
+ });
+ await tearDown(sandbox);
+});
+
+add_task(async function test_sync_disconnected_error() {
+ // it's possible for fxa to be enabled but sync not enabled.
+ const sandbox = setupSyncFxAMocks({
+ state: UIState.STATUS_SIGNED_IN,
+ syncEnabled: false,
+ });
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ await navigateToViewAndWait(document, "syncedtabs");
+
+ // triggered when user disconnects sync in about:preferences
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ let syncedTabsComponent = document.querySelector(
+ "view-syncedtabs:not([slot=syncedtabs])"
+ );
+ info("Waiting for the synced tabs error step to be visible");
+ await TestUtils.waitForCondition(
+ () => syncedTabsComponent.fullyUpdated,
+ "The synced tabs component has finished updating."
+ );
+ await TestUtils.waitForCondition(
+ () =>
+ syncedTabsComponent.emptyState.shadowRoot.textContent.includes(
+ "allow syncing"
+ ),
+ "The expected synced tabs empty state header is shown."
+ );
+
+ info(
+ "Waiting for a mutation condition to ensure the right syncing error message"
+ );
+ ok(
+ syncedTabsComponent.emptyState
+ .getAttribute("headerlabel")
+ .includes("sync-disconnected-header"),
+ "Correct message should show when sync's been disconnected error"
+ );
+
+ let preferencesTabPromise = BrowserTestUtils.waitForNewTab(
+ browser.getTabBrowser(),
+ "about:preferences?action=choose-what-to-sync#sync",
+ true
+ );
+ let emptyStateButton = syncedTabsComponent.emptyState.querySelector(
+ "button[data-action='sync-disconnected']"
+ );
+ EventUtils.synthesizeMouseAtCenter(emptyStateButton, {}, content);
+ let preferencesTab = await preferencesTabPromise;
+ await BrowserTestUtils.removeTab(preferencesTab);
+ });
+ await tearDown(sandbox);
+});
+
+add_task(async function test_password_change_disconnect_error() {
+ // When the user changes their password on another device, we get into a state
+ // where the user is signed out but sync is still enabled.
+ const sandbox = setupSyncFxAMocks({
+ state: UIState.STATUS_LOGIN_FAILED,
+ syncEnabled: true,
+ });
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ await navigateToViewAndWait(document, "syncedtabs");
+
+ // triggered by the user changing fxa password on another device
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ let syncedTabsComponent = document.querySelector(
+ "view-syncedtabs:not([slot=syncedtabs])"
+ );
+ await TestUtils.waitForCondition(
+ () => syncedTabsComponent.fullyUpdated,
+ "The synced tabs component has finished updating."
+ );
+ await TestUtils.waitForCondition(
+ () =>
+ syncedTabsComponent.emptyState.shadowRoot.textContent.includes(
+ "sign in to your account"
+ ),
+ "The expected synced tabs empty state header is shown."
+ );
+
+ ok(
+ syncedTabsComponent.emptyState
+ .getAttribute("headerlabel")
+ .includes("signin-header"),
+ "Sign in message is shown"
+ );
+ });
+ await tearDown(sandbox);
+});
+
+add_task(async function test_multiple_errors() {
+ let sandbox = await setupWithDesktopDevices();
+ await withFirefoxView({}, async browser => {
+ const { document } = browser.contentWindow;
+ await navigateToViewAndWait(document, "syncedtabs");
+ // Simulate conditions in which both the locked password and sync error
+ // messages could be shown
+ LoginTestUtils.primaryPassword.enable();
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+ Services.obs.notifyObservers(null, "weave:service:sync:error");
+
+ let syncedTabsComponent = document.querySelector(
+ "view-syncedtabs:not([slot=syncedtabs])"
+ );
+ await TestUtils.waitForCondition(
+ () => syncedTabsComponent.fullyUpdated,
+ "The synced tabs component has finished updating."
+ );
+ info("Waiting for the primary password error message to be shown");
+ await TestUtils.waitForCondition(
+ () =>
+ syncedTabsComponent.emptyState.shadowRoot.textContent.includes(
+ "enter the Primary Password"
+ ),
+ "The expected synced tabs empty state header is shown."
+ );
+
+ ok(
+ syncedTabsComponent.emptyState
+ .getAttribute("headerlabel")
+ .includes("password-locked-header"),
+ "Password locked message is shown"
+ );
+
+ const errorLink = syncedTabsComponent.emptyState.shadowRoot.querySelector(
+ "a[data-l10n-name=syncedtab-password-locked-link]"
+ );
+ ok(
+ errorLink && BrowserTestUtils.isVisible(errorLink),
+ "Error link is visible"
+ );
+
+ // Clear the primary password error message
+ LoginTestUtils.primaryPassword.disable();
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
+ info("Waiting for the sync error message to be shown");
+ await TestUtils.waitForCondition(
+ () => syncedTabsComponent.fullyUpdated,
+ "The synced tabs component has finished updating."
+ );
+ await TestUtils.waitForCondition(
+ () =>
+ syncedTabsComponent.emptyState.shadowRoot.textContent.includes(
+ "having trouble syncing"
+ ),
+ "The expected synced tabs empty state header is shown."
+ );
+
+ ok(
+ errorLink && BrowserTestUtils.isHidden(errorLink),
+ "Error link is now hidden"
+ );
+
+ // Clear the sync error
+ Services.obs.notifyObservers(null, "weave:service:sync:finish");
+ });
+ await tearDown(sandbox);
+});
diff --git a/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js
index 11f135cd52..1bf387f578 100644
--- a/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js
+++ b/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js
@@ -276,9 +276,12 @@ add_task(async function test_tabs() {
});
await withFirefoxView({ openNewWindow: true }, async browser => {
+ // Notify observers while in recent browsing. Once synced tabs is selected,
+ // it should have the updated data.
+ Services.obs.notifyObservers(null, UIState.ON_UPDATE);
+
const { document } = browser.contentWindow;
await navigateToViewAndWait(document, "syncedtabs");
- Services.obs.notifyObservers(null, UIState.ON_UPDATE);
let syncedTabsComponent = document.querySelector(
"view-syncedtabs:not([slot=syncedtabs])"
@@ -309,7 +312,7 @@ add_task(async function test_tabs() {
);
ok(tabRow1[1].shadowRoot.textContent.includes, "Sandboxes - Sinon.JS");
is(tabRow1.length, 2, "Correct number of rows are displayed.");
- let tabRow2 = tabLists[1].shadowRoot.querySelectorAll("fxview-tab-row");
+ let tabRow2 = tabLists[1].rowEls;
is(tabRow2.length, 2, "Correct number of rows are dispayed.");
ok(tabRow1[0].shadowRoot.textContent.includes, "The Guardian");
ok(tabRow1[1].shadowRoot.textContent.includes, "The Times");
diff --git a/browser/components/firefoxview/tests/browser/browser_tab_list_keyboard_navigation.js b/browser/components/firefoxview/tests/browser/browser_tab_list_keyboard_navigation.js
index d83c1056e0..270c3b6809 100644
--- a/browser/components/firefoxview/tests/browser/browser_tab_list_keyboard_navigation.js
+++ b/browser/components/firefoxview/tests/browser/browser_tab_list_keyboard_navigation.js
@@ -93,7 +93,7 @@ add_task(async function test_focus_moves_after_unmute() {
);
// Unmute using keyboard
- card.tabList.currentActiveElementId = mutedTab.focusMediaButton();
+ mutedTab.focusMediaButton();
isActiveElement(mutedTab.mediaButtonEl);
info("The media button has focus.");
@@ -124,7 +124,7 @@ add_task(async function test_focus_moves_after_unmute() {
);
mutedTab = card.tabList.rowEls[0];
- card.tabList.currentActiveElementId = mutedTab.focusLink();
+ mutedTab.focusLink();
isActiveElement(mutedTab.mainEl);
info("The 'main' element has focus.");
diff --git a/browser/components/firefoxview/tests/browser/browser_tab_on_close_warning.js b/browser/components/firefoxview/tests/browser/browser_tab_on_close_warning.js
index 9980980c29..a63a55163a 100644
--- a/browser/components/firefoxview/tests/browser/browser_tab_on_close_warning.js
+++ b/browser/components/firefoxview/tests/browser/browser_tab_on_close_warning.js
@@ -31,7 +31,7 @@ add_task(
info("Opening Firefox View tab...");
await openFirefoxViewTab(win);
info("Trigger warnAboutClosingWindow()");
- win.BrowserTryToCloseWindow();
+ win.BrowserCommands.tryToCloseWindow();
await BrowserTestUtils.closeWindow(win);
ok(!dialogObserver.wasOpened, "Dialog was not opened");
dialogObserver.cleanup();
diff --git a/browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html b/browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html
index e48f776592..52ddc277c7 100644
--- a/browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html
+++ b/browser/components/firefoxview/tests/chrome/test_fxview_tab_list.html
@@ -11,11 +11,6 @@
<script type="module" src="chrome://browser/content/firefoxview/fxview-tab-list.mjs"></script>
</head>
<body>
- <style>
- fxview-tab-list.history::part(secondary-button) {
- background-image: url("chrome://global/skin/icons/more.svg");
- }
- </style>
<p id="display"></p>
<div id="content" style="max-width: 750px">
<fxview-tab-list class="history" .dateTimeFormat="relative" .hasPopup="menu">