Bug 1855817 - Add the ability to sort tabs by recency in the (view all) Open tabs section of Firefox View r=fluent-reviewers,fxview-reviewers,sclements

Differential Revision: https://phabricator.services.mozilla.com/D200464
This commit is contained in:
Jonathan Sudiaman 2024-02-07 11:16:40 +00:00
parent c207a3fc65
commit eef24d2828
7 changed files with 193 additions and 59 deletions

View File

@ -315,12 +315,16 @@ class OpenTabsTarget extends EventTarget {
/*
* @param {Window} win
* @param {boolean} sortByRecency
* @returns {Array<Tab>}
* The list of visible tabs for the browser window
*/
getTabsForWindow(win) {
getTabsForWindow(win, sortByRecency = false) {
if (this.currentWindows.includes(win)) {
return [...win.gBrowser.visibleTabs];
const { visibleTabs } = win.gBrowser;
return sortByRecency
? visibleTabs.toSorted(lastSeenActiveSort)
: [...visibleTabs];
}
return [];
}

View File

@ -39,15 +39,21 @@ ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
*
* @property {Array<Window>} windows
* A list of windows with the same privateness
* @property {string} sortOption
* The sorting order of open tabs:
* - "recency": Sorted by recent activity. (For recent browsing, this is the only option.)
* - "tabStripOrder": Match the order in which they appear on the tab strip.
*/
class OpenTabsInView extends ViewPage {
static properties = {
...ViewPage.properties,
windows: { type: Array },
searchQuery: { type: String },
sortOption: { type: String },
};
static queries = {
viewCards: { all: "view-opentabs-card" },
optionsContainer: ".open-tabs-options",
searchTextbox: "fxview-search-textbox",
};
@ -66,6 +72,12 @@ class OpenTabsInView extends ViewPage {
this.openTabsTarget = lazy.NonPrivateTabs;
}
this.searchQuery = "";
this.sortOption = this.recentBrowsing
? "recency"
: Services.prefs.getStringPref(
"browser.tabs.firefox-view.ui-state.opentabs.sort-option",
"recency"
);
}
start() {
@ -73,12 +85,7 @@ class OpenTabsInView extends ViewPage {
return;
}
this._started = true;
if (this.recentBrowsing) {
this.openTabsTarget.addEventListener("TabRecencyChange", this);
} else {
this.openTabsTarget.addEventListener("TabChange", this);
}
this.#setupTabChangeListener();
// To resolve the race between this component wanting to render all the windows'
// tabs, while those windows are still potentially opening, flip this property
@ -144,6 +151,16 @@ class OpenTabsInView extends ViewPage {
this.stop();
}
#setupTabChangeListener() {
if (this.sortOption === "recency") {
this.openTabsTarget.addEventListener("TabRecencyChange", this);
this.openTabsTarget.removeEventListener("TabChange", this);
} else {
this.openTabsTarget.removeEventListener("TabRecencyChange", this);
this.openTabsTarget.addEventListener("TabChange", this);
}
}
render() {
if (this.recentBrowsing) {
return this.getRecentBrowsingTemplate();
@ -152,7 +169,10 @@ class OpenTabsInView extends ViewPage {
let index = 1;
const otherWindows = [];
this.windows.forEach(win => {
const tabs = this.openTabsTarget.getTabsForWindow(win);
const tabs = this.openTabsTarget.getTabsForWindow(
win,
this.sortOption === "recency"
);
if (win === this.currentWindow) {
currentWindowIndex = index++;
currentWindowTabs = tabs;
@ -187,18 +207,50 @@ class OpenTabsInView extends ViewPage {
class="page-header heading-large"
data-l10n-id="firefoxview-opentabs-header"
></h2>
${when(
isSearchEnabled(),
() => html`<div>
<fxview-search-textbox
data-l10n-id="firefoxview-search-text-box-opentabs"
data-l10n-attrs="placeholder"
@fxview-search-textbox-query=${this.onSearchQuery}
.size=${this.searchTextboxSize}
pageName=${this.recentBrowsing ? "recentbrowsing" : "opentabs"}
></fxview-search-textbox>
</div>`
)}
<div class="open-tabs-options">
${when(
isSearchEnabled(),
() => html`<div>
<fxview-search-textbox
data-l10n-id="firefoxview-search-text-box-opentabs"
data-l10n-attrs="placeholder"
@fxview-search-textbox-query=${this.onSearchQuery}
.size=${this.searchTextboxSize}
pageName=${this.recentBrowsing ? "recentbrowsing" : "opentabs"}
></fxview-search-textbox>
</div>`
)}
<div class="open-tabs-sort-wrapper">
<div class="open-tabs-sort-option">
<input
type="radio"
id="sort-by-recency"
name="open-tabs-sort-option"
value="recency"
?checked=${this.sortOption === "recency"}
@click=${this.onChangeSortOption}
/>
<label
for="sort-by-recency"
data-l10n-id="firefoxview-sort-open-tabs-by-recency-label"
></label>
</div>
<div class="open-tabs-sort-option">
<input
type="radio"
id="sort-by-order"
name="open-tabs-sort-option"
value="tabStripOrder"
?checked=${this.sortOption === "tabStripOrder"}
@click=${this.onChangeSortOption}
/>
<label
for="sort-by-order"
data-l10n-id="firefoxview-sort-open-tabs-by-order-label"
></label>
</div>
</div>
</div>
</div>
<div
card-count=${cardCount}
@ -244,6 +296,17 @@ class OpenTabsInView extends ViewPage {
this.searchQuery = e.detail.query;
}
onChangeSortOption(e) {
this.sortOption = e.target.value;
this.#setupTabChangeListener();
if (!this.recentBrowsing) {
Services.prefs.setStringPref(
"browser.tabs.firefox-view.ui-state.opentabs.sort-option",
this.sortOption
);
}
}
/**
* Render a template for the 'Recent browsing' page, which shows a shorter list of
* open tabs in the current window.

View File

@ -63,6 +63,29 @@ async function getRowsForCard(card) {
return card.tabList.rowEls;
}
/**
* Verify that there are the expected number of cards, and that each card has
* the expected URLs in order.
*
* @param {tabbrowser} browser
* The browser to verify in.
* @param {string[][]} expected
* The expected URLs for each card.
*/
async function checkTabLists(browser, expected) {
const cards = getCards(browser);
is(cards.length, expected.length, `There are ${expected.length} windows.`);
for (let i = 0; i < cards.length; i++) {
const tabItems = await getRowsForCard(cards[i]);
const actual = Array.from(tabItems).map(({ url }) => url);
Assert.deepEqual(
actual,
expected[i],
"Tab list has items with URLs in the expected order"
);
}
}
add_task(async function open_tab_same_window() {
await openFirefoxViewTab(window).then(async viewTab => {
const browser = viewTab.linkedBrowser;
@ -71,15 +94,7 @@ add_task(async function open_tab_same_window() {
await openTabs.openTabsTarget.readyWindowsPromise;
await openTabs.updateComplete;
const cards = getCards(browser);
is(cards.length, 1, "There is one window.");
let tabItems = await getRowsForCard(cards[0]);
is(tabItems.length, 1, "There is one items.");
is(
tabItems[0].url,
gBrowser.visibleTabs[0].linkedBrowser.currentURI.spec,
"The first item represents the first visible tab"
);
await checkTabLists(browser, [[gInitialTabURL]]);
let promiseHidden = BrowserTestUtils.waitForEvent(
browser.contentDocument,
"visibilitychange"
@ -98,19 +113,17 @@ add_task(async function open_tab_same_window() {
await openFirefoxViewTab(window).then(async viewTab => {
const browser = viewTab.linkedBrowser;
const openTabs = getOpenTabsComponent(browser);
setSortOption(openTabs, "tabStripOrder");
await openTabs.openTabsTarget.readyWindowsPromise;
await openTabs.updateComplete;
const cards = getCards(browser);
is(cards.length, 1, "There is one window.");
let tabItems = await getRowsForCard(cards[0]);
is(tabItems.length, 2, "There are two items.");
is(tabItems[1].url, TEST_URL, "The newly opened tab appears last.");
await checkTabLists(browser, [[gInitialTabURL, TEST_URL]]);
let promiseHidden = BrowserTestUtils.waitForEvent(
browser.contentDocument,
"visibilitychange"
);
const cards = getCards(browser);
const tabItems = await getRowsForCard(cards[0]);
tabItems[0].mainEl.click();
await promiseHidden;
});
@ -141,8 +154,6 @@ add_task(async function open_tab_same_window() {
await openFirefoxViewTab(window).then(async viewTab => {
const browser = viewTab.linkedBrowser;
const cards = getCards(browser);
let tabItems;
let tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
@ -152,14 +163,7 @@ add_task(async function open_tab_same_window() {
gBrowser.moveTabTo(newTab, 0);
await tabChangeRaised;
await BrowserTestUtils.waitForMutationCondition(
cards[0].shadowRoot,
{ childList: true, subtree: true },
async () => {
tabItems = await getRowsForCard(cards[0]);
return tabItems[0].url === TEST_URL;
}
);
await checkTabLists(browser, [[TEST_URL, gInitialTabURL]]);
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
@ -167,11 +171,8 @@ add_task(async function open_tab_same_window() {
await BrowserTestUtils.removeTab(newTab);
await tabChangeRaised;
await checkTabLists(browser, [[gInitialTabURL]]);
const [card] = getCards(browser);
await TestUtils.waitForCondition(
async () => (await getRowsForCard(card)).length === 1,
"There is one tab left after closing the new one."
);
const [row] = await getRowsForCard(card);
ok(
!row.shadowRoot.getElementById("fxview-tab-row-url").hidden,
@ -196,20 +197,16 @@ add_task(async function open_tab_new_window() {
const browser = viewTab.linkedBrowser;
await navigateToOpenTabs(browser);
const openTabs = getOpenTabsComponent(browser);
setSortOption(openTabs, "tabStripOrder");
await openTabs.openTabsTarget.readyWindowsPromise;
await openTabs.updateComplete;
await checkTabLists(browser, [
[gInitialTabURL, TEST_URL],
[gInitialTabURL],
]);
const cards = getCards(browser);
is(cards.length, 2, "There are two windows.");
const newWinRows = await getRowsForCard(cards[0]);
const originalWinRows = await getRowsForCard(cards[1]);
is(
originalWinRows.length,
1,
"There is one tab item in the original window."
);
is(newWinRows.length, 2, "There are two tab items in the new window.");
is(newWinRows[1].url, TEST_URL, "The new tab item appears last.");
const [row] = originalWinRows;
ok(
row.shadowRoot.getElementById("fxview-tab-row-url").hidden,
@ -274,11 +271,51 @@ add_task(async function open_tab_new_private_window() {
await cleanup();
});
add_task(async function open_tab_new_window_sort_by_recency() {
info("Open new tabs in a new window.");
const newWindow = await BrowserTestUtils.openNewBrowserWindow();
const tabs = [
newWindow.gBrowser.selectedTab,
await BrowserTestUtils.openNewForegroundTab(newWindow.gBrowser, URLs[0]),
await BrowserTestUtils.openNewForegroundTab(newWindow.gBrowser, URLs[1]),
];
info("Open Firefox View in the original window.");
await openFirefoxViewTab(window).then(async ({ linkedBrowser }) => {
await navigateToOpenTabs(linkedBrowser);
const openTabs = getOpenTabsComponent(linkedBrowser);
setSortOption(openTabs, "recency");
await openTabs.openTabsTarget.readyWindowsPromise;
await openTabs.updateComplete;
await checkTabLists(linkedBrowser, [
[gInitialTabURL],
[URLs[1], URLs[0], gInitialTabURL],
]);
info("Select tabs in the new window to trigger recency changes.");
await SimpleTest.promiseFocus(newWindow);
await BrowserTestUtils.switchTab(newWindow.gBrowser, tabs[1]);
await BrowserTestUtils.switchTab(newWindow.gBrowser, tabs[0]);
await SimpleTest.promiseFocus(window);
await TestUtils.waitForCondition(async () => {
const [, secondCard] = getCards(linkedBrowser);
const tabItems = await getRowsForCard(secondCard);
return tabItems[0].url === gInitialTabURL;
});
await checkTabLists(linkedBrowser, [
[gInitialTabURL],
[gInitialTabURL, URLs[0], URLs[1]],
]);
});
await cleanup();
});
add_task(async function styling_for_multiple_windows() {
await openFirefoxViewTab(window).then(async viewTab => {
const browser = viewTab.linkedBrowser;
await navigateToOpenTabs(browser);
const openTabs = getOpenTabsComponent(browser);
setSortOption(openTabs, "tabStripOrder");
await openTabs.openTabsTarget.readyWindowsPromise;
await openTabs.updateComplete;

View File

@ -125,6 +125,7 @@ async function moreMenuSetup() {
await navigateToCategoryAndWait(document, "opentabs");
let openTabs = document.querySelector("view-opentabs[name=opentabs]");
setSortOption(openTabs, "tabStripOrder");
await openTabs.openTabsTarget.readyWindowsPromise;
info("waiting for openTabs' first card rows");

View File

@ -649,6 +649,14 @@ async function telemetryEvent(eventDetails) {
);
}
function setSortOption(component, value) {
info(`Sort by ${value}.`);
const el = component.optionsContainer.querySelector(
`input[value='${value}']`
);
EventUtils.synthesizeMouseAtCenter(el, {}, el.ownerGlobal);
}
function getOpenTabsCards(openTabs) {
return openTabs.shadowRoot.querySelectorAll("view-opentabs-card");
}

View File

@ -23,3 +23,22 @@
grid-template-columns: repeat(2, 1fr);
}
}
.open-tabs-options, .open-tabs-sort-wrapper {
display: flex;
gap: 24px;
}
.open-tabs-options {
flex-wrap: wrap;
}
.open-tabs-sort-option {
display: flex;
align-items: center;
gap: 8px;
& label {
white-space: nowrap;
}
}

View File

@ -224,6 +224,8 @@ firefoxview-search-results-empty = No results for “{ $query }”
firefoxview-sort-history-by-date-label = Sort by date
firefoxview-sort-history-by-site-label = Sort by site
firefoxview-sort-open-tabs-by-recency-label = Sort by recent activity
firefoxview-sort-open-tabs-by-order-label = Sort by tab order
# Variables:
# $url (string) - URL that will be opened in the new tab