mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-07 18:04:46 +00:00
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:
parent
c207a3fc65
commit
eef24d2828
@ -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 [];
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user