diff options
Diffstat (limited to 'browser/components/firefoxview')
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, """) .replace(/'/g, "'"); } + +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"> |