Bug 1801204 - Update recently-closed-tabs.mjs to be template-driven r=sfoster,mstriemer

Differential Revision: https://phabricator.services.mozilla.com/D169010
This commit is contained in:
Kelly Cochrane 2023-02-24 21:32:25 +00:00
parent 70f184cd25
commit 976c5c450b
13 changed files with 302 additions and 354 deletions

View file

@ -628,18 +628,20 @@ details[open] > .page-section-header > .twisty {
} }
@media (prefers-contrast) { @media (prefers-contrast) {
span.closed-tab-li-main, .closed-tab-li-main,
button.closed-tab-li-dismiss { .closed-tab-li-dismiss {
color: ButtonText; color: ButtonText;
border-radius: 4px; border-radius: 4px;
border: 1px solid ButtonText; border: 1px solid ButtonText;
} }
} }
.closed-tab-li-main:hover { @media not (prefers-contrast) {
background-color: var(--fxview-element-background-hover); .closed-tab-li-main:hover {
color: var(--fxview-text-color-hover); background-color: var(--fxview-element-background-hover);
} color: var(--fxview-text-color-hover);
}
}
.closed-tab-li-main:hover .closed-tab-li-title { .closed-tab-li-main:hover .closed-tab-li-title {
text-decoration-line: underline; text-decoration-line: underline;
@ -694,32 +696,26 @@ details[open] > .page-section-header > .twisty {
fill: var(--in-content-button-text-color-hover); fill: var(--in-content-button-text-color-hover);
} }
.synced-tab-a, .tab-link,
.synced-tab-a:hover, .tab-link:hover,
.synced-tab-a:active, .tab-link:active,
.synced-tab-a:hover:active, .tab-link:hover:active,
.synced-tab-a:visited { .tab-link:visited {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
height: 100%;
} }
@media (prefers-contrast) { @media (prefers-contrast) {
.synced-tab-a { .tab-link {
border-color: FieldText; border-color: FieldText;
} }
.synced-tab-a, .tab-link,
.synced-tab-a:hover, .tab-link:hover,
.synced-tab-a:active, .tab-link:active,
.synced-tab-a:hover:active, .tab-link:hover:active,
.synced-tab-a:visited { .tab-link:visited {
color: LinkText; color: LinkText;
} }
.synced-tab-a:focus-visible {
box-shadow: none;
outline: var(--in-content-focus-outline);
outline-offset: var(--in-content-focus-outline-offset);
}
} }
.closed-tab-li-url, .closed-tab-li-url,
@ -796,7 +792,8 @@ details[open] > .page-section-header > .twisty {
grid-template-areas: grid-template-areas:
"favicon title title title" "favicon title title title"
"favicon domain domain domain" "favicon domain domain domain"
"favicon device device time" "favicon device device time";
height: 100%;
} }
.synced-tab-a:hover { .synced-tab-a:hover {

View file

@ -159,16 +159,7 @@
<h2 class="section-description" data-l10n-id="firefoxview-closed-tabs-description2"></h2> <h2 class="section-description" data-l10n-id="firefoxview-closed-tabs-description2"></h2>
</summary> </summary>
<div id="collapsible-tabs-container" id="recently-closed-tabs" role="region" aria-labelledby="recently-closed-tabs-header"> <div id="collapsible-tabs-container" id="recently-closed-tabs" role="region" aria-labelledby="recently-closed-tabs-header">
<recently-closed-tabs-list> <recently-closed-tabs-list></recently-closed-tabs-list>
<ol hidden="true" class="closed-tabs-list"></ol>
</recently-closed-tabs-list>
<div hidden="true" id="recently-closed-tabs-placeholder" class="placeholder-content">
<img id="recently-closed-empty-image" src="chrome://browser/content/recently-closed-empty.svg" role="presentation" alt=""/>
<div class="placeholder-text">
<h4 data-l10n-id="firefoxview-closed-tabs-placeholder-header" class="placeholder-header"></h4>
<p data-l10n-id="firefoxview-closed-tabs-placeholder-body" class="placeholder-body"></p>
</div>
</div>
</div> </div>
</details> </details>
</main> </main>

View file

@ -46,12 +46,8 @@ export function convertTimestamp(
} }
export function createFaviconElement(image, targetURI = "") { export function createFaviconElement(image, targetURI = "") {
const imageUrl = image
? lazy.PlacesUIUtils.getImageURL(image)
: `page-icon:${targetURI}`;
let favicon = document.createElement("div"); let favicon = document.createElement("div");
favicon.style.backgroundImage = `url('${getImageUrl(image, targetURI)}')`;
favicon.style.backgroundImage = `url('${imageUrl}')`;
favicon.classList.add("favicon"); favicon.classList.add("favicon");
return favicon; return favicon;
} }

View file

@ -10,11 +10,18 @@ ChromeUtils.defineESModuleGetters(lazy, {
import { import {
formatURIForDisplay, formatURIForDisplay,
convertTimestamp, convertTimestamp,
createFaviconElement, getImageUrl,
onToggleContainer, onToggleContainer,
NOW_THRESHOLD_MS, NOW_THRESHOLD_MS,
} from "./helpers.mjs"; } from "./helpers.mjs";
import {
html,
ifDefined,
styleMap,
} from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
const { XPCOMUtils } = ChromeUtils.importESModule( const { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs" "resource://gre/modules/XPCOMUtils.sys.mjs"
); );
@ -28,11 +35,12 @@ function getWindow() {
return window.browsingContext.embedderWindowGlobal.browsingContext.window; return window.browsingContext.embedderWindowGlobal.browsingContext.window;
} }
class RecentlyClosedTabsList extends HTMLElement { class RecentlyClosedTabsList extends MozLitElement {
constructor() { constructor() {
super(); super();
this.maxTabsLength = 25; this.maxTabsLength = 25;
this.closedTabsData = new Map(); this.recentlyClosedTabs = [];
this.lastFocusedIndex = -1;
// The recency timestamp update period is stored in a pref to allow tests to easily change it // The recency timestamp update period is stored in a pref to allow tests to easily change it
XPCOMUtils.defineLazyPreferenceGetter( XPCOMUtils.defineLazyPreferenceGetter(
@ -44,10 +52,15 @@ class RecentlyClosedTabsList extends HTMLElement {
); );
} }
get tabsList() { createRenderRoot() {
return this.querySelector("ol"); return this;
} }
static queries = {
tabsList: "ol",
timeElements: { all: "span.closed-tab-li-time" },
};
get fluentStrings() { get fluentStrings() {
if (!this._fluentStrings) { if (!this._fluentStrings) {
this._fluentStrings = new Localization(["browser/firefoxView.ftl"], true); this._fluentStrings = new Localization(["browser/firefoxView.ftl"], true);
@ -55,13 +68,8 @@ class RecentlyClosedTabsList extends HTMLElement {
return this._fluentStrings; return this._fluentStrings;
} }
get timeElements() {
return this.querySelectorAll("span.closed-tab-li-time");
}
connectedCallback() { connectedCallback() {
this.addEventListener("click", this); super.connectedCallback();
this.addEventListener("keydown", this);
this.intervalID = setInterval(() => this.updateTime(), lazy.timeMsPref); this.intervalID = setInterval(() => this.updateTime(), lazy.timeMsPref);
} }
@ -69,20 +77,6 @@ class RecentlyClosedTabsList extends HTMLElement {
clearInterval(this.intervalID); clearInterval(this.intervalID);
} }
handleEvent(event) {
if (
(event.type == "click" && !event.altKey) ||
(event.type == "keydown" && event.keyCode == KeyEvent.DOM_VK_RETURN) ||
(event.type == "keydown" && event.keyCode == KeyEvent.DOM_VK_SPACE)
) {
if (!event.target.classList.contains("closed-tab-li-dismiss")) {
this.openTabAndUpdate(event);
} else {
this.dismissTabAndUpdate(event);
}
}
}
updateTime() { updateTime() {
for (let timeEl of this.timeElements) { for (let timeEl of this.timeElements) {
timeEl.textContent = convertTimestamp( timeEl.textContent = convertTimestamp(
@ -105,44 +99,17 @@ class RecentlyClosedTabsList extends HTMLElement {
return value; return value;
} }
focusFirstItemOrHeader(dismissedIndex) {
// When a tab is removed from the list, the focus should
// remain on the list or the list header. This prevents context
// switching when navigating back to Firefox View.
let recentlyClosedList = [...this.tabsList.children];
if (recentlyClosedList.length) {
recentlyClosedList.forEach(element =>
element.setAttribute("tabindex", "-1")
);
let mainContent;
if (dismissedIndex) {
// Select the item above the one that was just dismissed
mainContent = recentlyClosedList[dismissedIndex - 1].querySelector(
".closed-tab-li-main"
);
} else {
mainContent = recentlyClosedList[0].querySelector(
".closed-tab-li-main"
);
}
mainContent.setAttribute("tabindex", "0");
mainContent.focus();
} else {
document.getElementById("recently-closed-tabs-header-section").focus();
}
}
openTabAndUpdate(event) { openTabAndUpdate(event) {
event.preventDefault(); event.preventDefault();
if (event.type == "click" && event.altKey) {
return;
}
const item = event.target.closest(".closed-tab-li"); const item = event.target.closest(".closed-tab-li");
// only used for telemetry // only used for telemetry
const position = [...this.tabsList.children].indexOf(item) + 1; const position = [...this.tabsList.children].indexOf(item) + 1;
const closedId = item.dataset.tabid; const closedId = item.dataset.tabid;
lazy.SessionStore.undoCloseById(closedId); lazy.SessionStore.undoCloseById(closedId);
this.tabsList.removeChild(item);
this.focusFirstItemOrHeader();
// record telemetry // record telemetry
let tabClosedAt = parseInt( let tabClosedAt = parseInt(
@ -174,11 +141,8 @@ class RecentlyClosedTabsList extends HTMLElement {
// Tab not found in recently closed list // Tab not found in recently closed list
return; return;
} }
this.tabsList.removeChild(item);
lazy.SessionStore.forgetClosedTab(getWindow(), closedTabIndex); lazy.SessionStore.forgetClosedTab(getWindow(), closedTabIndex);
this.focusFirstItemOrHeader(closedTabIndex);
// record telemetry // record telemetry
let tabClosedAt = parseInt( let tabClosedAt = parseInt(
item.querySelector(".closed-tab-li-time").dataset.timestamp item.querySelector(".closed-tab-li-time").dataset.timestamp
@ -197,138 +161,145 @@ class RecentlyClosedTabsList extends HTMLElement {
); );
} }
updateTabsList() { updateRecentlyClosedTabs() {
let newClosedTabs = lazy.SessionStore.getClosedTabData(getWindow()); let recentlyClosedTabsData = lazy.SessionStore.getClosedTabData(
newClosedTabs = newClosedTabs.slice(0, this.maxTabsLength); getWindow()
);
this.recentlyClosedTabs = recentlyClosedTabsData.slice(
0,
this.maxTabsLength
);
this.requestUpdate();
}
if (this.closedTabsData.size && !newClosedTabs.length) { render() {
// if a user purges history, clear the list let { recentlyClosedTabs } = this;
while (this.tabsList.lastElementChild) { let closedTabsContainer = document.getElementById(
this.tabsList.lastElementChild.remove(); "recently-closed-tabs-container"
} );
document
.getElementById("recently-closed-tabs-container") if (!recentlyClosedTabs.length) {
.togglePlaceholderVisibility(true); // Show empty message if no recently closed tabs
this.tabsList.hidden = true; closedTabsContainer.toggleContainerStyleForEmptyMsg(true);
this.closedTabsData = new Map(); return html`
return; ${this.emptyMessageTemplate()}
`;
} }
// First purge obsolete items out of the map so we don't leak them forever: closedTabsContainer.toggleContainerStyleForEmptyMsg(false);
for (let id of this.closedTabsData.keys()) {
if (!newClosedTabs.some(t => t.closedId == id)) {
this.closedTabsData.delete(id);
}
}
// Then work out which of the new closed tabs are additions and which update return html`
// existing items: <ol class="closed-tabs-list">
let tabsToAdd = []; ${recentlyClosedTabs.map((tab, i) =>
let tabsToUpdate = []; this.recentlyClosedTabTemplate(tab, !i)
for (let newTab of newClosedTabs) { )}
let oldTab = this.closedTabsData.get(newTab.closedId); </ol>
this.closedTabsData.set(newTab.closedId, newTab); `;
if (!oldTab) { }
tabsToAdd.push(newTab);
} else if (
this.getTabStateValue(oldTab, "url") !=
this.getTabStateValue(newTab, "url")
) {
tabsToUpdate.push(newTab);
}
}
// Remove existing tabs from tabsList if not in latest closedTabsData willUpdate() {
// which is necessary when using "Reopen Closed Tab" from the toolbar if (this.tabsList && this.tabsList.contains(document.activeElement)) {
// or when selecting "Forget this site" in History let activeLi = document.activeElement.closest(".closed-tab-li");
[...this.tabsList.children].forEach(existingTab => { this.lastFocusedIndex = [...this.tabsList.children].indexOf(activeLi);
if (!this.closedTabsData.get(parseInt(existingTab.dataset.tabid, 10))) { } else {
this.tabsList.removeChild(existingTab); this.lastFocusedIndex = -1;
}
});
// If there's nothing to add/update, return.
if (!tabsToAdd.length && !tabsToUpdate.length) {
return;
}
// Add new tabs.
for (let tab of tabsToAdd.reverse()) {
if (this.tabsList.children.length == this.maxTabsLength) {
this.tabsList.lastChild.remove();
}
let li = this.generateListItem(tab);
let mainContent = li.querySelector(".closed-tab-li-main");
// Only the first item in the list should be focusable
if (!this.tabsList.children.length) {
mainContent.setAttribute("tabindex", "0");
} else if (this.tabsList.children.length) {
mainContent.setAttribute("tabindex", "0");
this.tabsList.children[0].setAttribute("tabindex", "-1");
}
this.tabsList.prepend(li);
}
// Update any recently closed tabs that now have different URLs:
for (let tab of tabsToUpdate) {
let tabElement = this.querySelector(
`.closed-tab-li[data-tabid="${tab.closedId}"]`
);
let url = this.getTabStateValue(tab, "url");
this.updateURLForListItem(tabElement, url);
}
// Now unhide the list if necessary:
if (this.tabsList.hidden) {
this.tabsList.hidden = false;
document
.getElementById("recently-closed-tabs-container")
.togglePlaceholderVisibility(false);
} }
} }
generateListItem(tab) { updated() {
const li = document.createElement("li"); let focusRestored = false;
li.classList.add("closed-tab-li"); if (this.lastFocusedIndex >= 0) {
li.dataset.tabid = tab.closedId; if (this.tabsList && this.tabsList.children.length) {
let items = [...this.tabsList.children];
const title = document.createElement("span"); let newFocusIndex = Math.max(
title.textContent = `${tab.title}`; Math.min(items.length - 1, this.lastFocusedIndex - 1),
title.classList.add("closed-tab-li-title"); 0
);
const targetURI = this.getTabStateValue(tab, "url"); let newFocus = items[newFocusIndex];
const image = tab.image; if (newFocus) {
const favicon = createFaviconElement(image, targetURI); focusRestored = true;
newFocus.querySelector(".closed-tab-li-main").focus();
const urlElement = document.createElement("span"); }
urlElement.classList.add("closed-tab-li-url");
const time = document.createElement("span");
const convertedTime = convertTimestamp(tab.closedAt, this.fluentStrings);
time.textContent = convertedTime;
time.setAttribute("data-timestamp", tab.closedAt);
time.classList.add("closed-tab-li-time");
const mainContent = document.createElement("span");
mainContent.classList.add("closed-tab-li-main");
mainContent.setAttribute("role", "link");
mainContent.setAttribute("tabindex", 0);
mainContent.append(favicon, title, urlElement, time);
const dismissButton = document.createElement("button");
let tabTitle = tab.title ?? "";
document.l10n.setAttributes(
dismissButton,
"firefoxview-closed-tabs-dismiss-tab",
{
tabTitle,
} }
); if (!focusRestored) {
dismissButton.classList.add("closed-tab-li-dismiss"); document.getElementById("recently-closed-tabs-header-section").focus();
}
}
this.lastFocusedIndex = -1;
}
li.append(mainContent, dismissButton); emptyMessageTemplate() {
this.updateURLForListItem(li, targetURI); return html`
return li; <div
id="recently-closed-tabs-placeholder"
class="placeholder-content"
role="presentation"
>
<img
id="recently-closed-empty-image"
src="chrome://browser/content/recently-closed-empty.svg"
role="presentation"
alt=""
/>
<div class="placeholder-text">
<h4
data-l10n-id="firefoxview-closed-tabs-placeholder-header"
class="placeholder-header"
></h4>
<p
data-l10n-id="firefoxview-closed-tabs-placeholder-body"
class="placeholder-body"
></p>
</div>
</div>
`;
}
recentlyClosedTabTemplate(tab, primary) {
const targetURI = this.getTabStateValue(tab, "url");
const convertedTime = convertTimestamp(tab.closedAt, this.fluentStrings);
return html`
<li
class="closed-tab-li"
data-tabid=${tab.closedId}
data-targeturi=${targetURI}
tabindex=${ifDefined(primary ? null : "-1")}
>
<a
class="closed-tab-li-main tab-link"
tabindex="0"
href=${targetURI}
@click=${e => this.openTabAndUpdate(e)}
>
<div
class="favicon"
style=${styleMap({
backgroundImage: `url(${getImageUrl(tab.icon, targetURI)})`,
})}
></div>
<span class="closed-tab-li-title">
${tab.title}
</span>
<span
title=${targetURI}
class="closed-tab-li-url"
data-l10n-id="firefoxview-tabs-list-tab-button"
data-l10n-args=${JSON.stringify({ targetURI })}
>
${formatURIForDisplay(targetURI)}
</span>
<span class="closed-tab-li-time" data-timestamp=${tab.closedAt}>
${convertedTime}
</span>
</a>
<button
class="closed-tab-li-dismiss"
data-l10n-id="firefoxview-closed-tabs-dismiss-tab"
data-l10n-args=${JSON.stringify({ tabTitle: tab.title })}
@click=${e => this.dismissTabAndUpdate(e)}
></button>
</li>
`;
} }
// Update the URL for a new or previously-populated list item. // Update the URL for a new or previously-populated list item.
@ -413,8 +384,7 @@ class RecentlyClosedTabsContainer extends HTMLDetailsElement {
handleObservers(contentDocument) { handleObservers(contentDocument) {
if (contentDocument?.URL == "about:firefoxview") { if (contentDocument?.URL == "about:firefoxview") {
this.addObserversIfNeeded(); this.addObserversIfNeeded();
this.list.updateTabsList(); this.list.updateRecentlyClosedTabs();
this.maybeUpdateFocus();
} else { } else {
this.removeObserversIfNeeded(); this.removeObserversIfNeeded();
} }
@ -426,16 +396,12 @@ class RecentlyClosedTabsContainer extends HTMLDetailsElement {
(topic == SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH && (topic == SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH &&
subject.ownerGlobal == getWindow()) subject.ownerGlobal == getWindow())
) { ) {
this.list.updateTabsList(); this.list.updateRecentlyClosedTabs();
} }
} }
onLoad() { onLoad() {
if (this.getClosedTabCount() == 0) { this.list.updateRecentlyClosedTabs();
this.togglePlaceholderVisibility(true);
} else {
this.list.updateTabsList();
}
this.addObserversIfNeeded(); this.addObserversIfNeeded();
} }
@ -447,27 +413,7 @@ class RecentlyClosedTabsContainer extends HTMLDetailsElement {
} }
} }
/** toggleContainerStyleForEmptyMsg(visible) {
* Manages focus when returning to the Firefox View tab
*
* @memberof RecentlyClosedTabsContainer
*/
maybeUpdateFocus() {
// Check if focus is in the container element
if (this.contains(document.activeElement)) {
let listItems = this.list.querySelectorAll("li");
// More tabs may have been added to the list, so we'll refocus
// the first item in the list.
if (listItems.length) {
listItems[0].querySelector(".closed-tab-li-main").focus();
} else {
this.querySelector("summary").focus();
}
}
}
togglePlaceholderVisibility(visible) {
this.noTabsElement.toggleAttribute("hidden", !visible);
this.collapsibleContainer.classList.toggle("empty-container", visible); this.collapsibleContainer.classList.toggle("empty-container", visible);
} }

View file

@ -352,7 +352,7 @@ class TabPickupList extends HTMLElement {
li.classList.add("synced-tab-li"); li.classList.add("synced-tab-li");
const a = document.createElement("a"); const a = document.createElement("a");
a.classList.add("synced-tab-a"); a.classList.add("synced-tab-a", "tab-link");
a.target = "_blank"; a.target = "_blank";
if (index != 0) { if (index != 0) {
a.setAttribute("tabindex", "-1"); a.setAttribute("tabindex", "-1");

View file

@ -71,6 +71,10 @@ async function withFirefoxView(
// reset internal state so we aren't reacting to whatever state the last invocation left behind // reset internal state so we aren't reacting to whatever state the last invocation left behind
TabsSetupFlowManager.resetInternalState(); TabsSetupFlowManager.resetInternalState();
} }
// Setting this pref allows the test to run as expected with a keyboard on MacOS
await win.SpecialPowers.pushPrefEnv({
set: [["accessibility.tabfocus", 7]],
});
let tab = await openFirefoxViewTab(win); let tab = await openFirefoxViewTab(win);
let originalWindow = tab.ownerGlobal; let originalWindow = tab.ownerGlobal;
let result = await taskFn(tab.linkedBrowser); let result = await taskFn(tab.linkedBrowser);
@ -87,6 +91,7 @@ async function withFirefoxView(
"removeTab would have been called" "removeTab would have been called"
); );
} }
await win.SpecialPowers.popPrefEnv();
if (shouldCloseWin) { if (shouldCloseWin) {
await BrowserTestUtils.closeWindow(win); await BrowserTestUtils.closeWindow(win);
} }

View file

@ -245,7 +245,6 @@ add_task(async function test_add_ons_cant_unhide_fx_view() {
// Test navigation to first visible tab when the // Test navigation to first visible tab when the
// Firefox View button is present and active. // Firefox View button is present and active.
add_task(async function testFirstTabFocusableWhenFxViewOpen() { add_task(async function testFirstTabFocusableWhenFxViewOpen() {
await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
await withFirefoxView({}, async browser => { await withFirefoxView({}, async browser => {
let win = browser.ownerGlobal; let win = browser.ownerGlobal;
ok(win.FirefoxViewHandler.tab.selected, "Firefox View tab is selected"); ok(win.FirefoxViewHandler.tab.selected, "Firefox View tab is selected");
@ -266,7 +265,6 @@ add_task(async function testFirstTabFocusableWhenFxViewOpen() {
// Test that Firefox View tab is not multiselectable // Test that Firefox View tab is not multiselectable
add_task(async function testFxViewNotMultiselect() { add_task(async function testFxViewNotMultiselect() {
await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
await withFirefoxView({ win: window }, async browser => { await withFirefoxView({ win: window }, async browser => {
let win = browser.ownerGlobal; let win = browser.ownerGlobal;
Assert.ok( Assert.ok(

View file

@ -44,7 +44,7 @@ async function close_tab(tab) {
} }
async function dismiss_tab(tab, content) { async function dismiss_tab(tab, content) {
info(`Dismissing tab ${tab.dataset.targetURI}`); info(`Dismissing tab ${tab.dataset.targeturi}`);
const closedObjectsChanged = () => const closedObjectsChanged = () =>
TestUtils.topicObserved("sessionstore-closed-objects-changed"); TestUtils.topicObserved("sessionstore-closed-objects-changed");
let dismissButton = tab.querySelector(".closed-tab-li-dismiss"); let dismissButton = tab.querySelector(".closed-tab-li-dismiss");
@ -63,12 +63,15 @@ add_task(async function test_empty_list() {
"collapsible container should have correct styling when the list is empty" "collapsible container should have correct styling when the list is empty"
); );
testVisibility(browser, { Assert.ok(
expectedVisible: { document.getElementById("recently-closed-tabs-placeholder"),
"#recently-closed-tabs-placeholder": true, "The empty message is displayed."
"ol.closed-tabs-list": false, );
},
}); Assert.ok(
!document.querySelector("ol.closed-tabs-list"),
"The recently closed tabs list is not displayed."
);
const tab1 = await add_new_tab(URLs[0]); const tab1 = await add_new_tab(URLs[0]);
@ -85,12 +88,15 @@ add_task(async function test_empty_list() {
"collapsible container should have correct styling when the list is not empty" "collapsible container should have correct styling when the list is not empty"
); );
testVisibility(browser, { Assert.ok(
expectedVisible: { !document.getElementById("recently-closed-tabs-placeholder"),
"#recently-closed-tabs-placeholder": false, "The empty message is not displayed."
"ol.closed-tabs-list": true, );
},
}); Assert.ok(
document.querySelector("ol.closed-tabs-list"),
"The recently closed tabs list is displayed."
);
is( is(
document.querySelector("ol.closed-tabs-list").children.length, document.querySelector("ol.closed-tabs-list").children.length,
@ -146,7 +152,7 @@ add_task(async function test_list_ordering() {
ok( ok(
document document
.querySelector("ol.closed-tabs-list") .querySelector("ol.closed-tabs-list")
.firstChild.textContent.includes("mochi.test"), .children[0].textContent.includes("mochi.test"),
"first list item in recently-closed-tabs-list is in the correct order" "first list item in recently-closed-tabs-list is in the correct order"
); );
@ -158,9 +164,9 @@ add_task(async function test_list_ordering() {
); );
let ele = document.querySelector("ol.closed-tabs-list").firstElementChild; let ele = document.querySelector("ol.closed-tabs-list").firstElementChild;
let uri = ele.getAttribute("data-target-u-r-i"); let uri = ele.getAttribute("data-targeturi");
let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, uri); let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, uri);
ele.click(); ele.querySelector(".closed-tab-li-main").click();
await newTabPromise; await newTabPromise;
await TestUtils.waitForCondition( await TestUtils.waitForCondition(
@ -245,15 +251,18 @@ add_task(async function test_max_list_items() {
"collapsible container should have correct styling when the list is not empty" "collapsible container should have correct styling when the list is not empty"
); );
testVisibility(browser, { Assert.ok(
expectedVisible: { !document.getElementById("recently-closed-tabs-placeholder"),
"#recently-closed-tabs-placeholder": false, "The empty message is not displayed."
"ol.closed-tabs-list": true, );
},
}); Assert.ok(
document.querySelector("ol.closed-tabs-list"),
"The recently closed tabs list is displayed."
);
is( is(
document.querySelector("ol.closed-tabs-list").childNodes.length, document.querySelector("ol.closed-tabs-list").children.length,
mockMaxTabsLength, mockMaxTabsLength,
`recently-closed-tabs-list should have ${mockMaxTabsLength} list items` `recently-closed-tabs-list should have ${mockMaxTabsLength} list items`
); );
@ -265,9 +274,8 @@ add_task(async function test_max_list_items() {
const tab = await add_new_tab(URLs[3]); const tab = await add_new_tab(URLs[3]);
await close_tab(tab); await close_tab(tab);
await closedObjectsChanged; await closedObjectsChanged;
let firstListItem = document.querySelector("ol.closed-tabs-list") let firstListItem = document.querySelector("ol.closed-tabs-list")
.firstChild; .children[0];
await BrowserTestUtils.waitForMutationCondition( await BrowserTestUtils.waitForMutationCondition(
firstListItem, firstListItem,
{ characterData: true, childList: true, subtree: true }, { characterData: true, childList: true, subtree: true },
@ -279,7 +287,7 @@ add_task(async function test_max_list_items() {
); );
is( is(
document.querySelector("ol.closed-tabs-list").childNodes.length, document.querySelector("ol.closed-tabs-list").children.length,
mockMaxTabsLength, mockMaxTabsLength,
`recently-closed-tabs-list should still have ${mockMaxTabsLength} list items` `recently-closed-tabs-list should still have ${mockMaxTabsLength} list items`
); );
@ -310,6 +318,7 @@ add_task(async function test_time_updates_correctly() {
closedId: 0, closedId: 0,
closedAt: Date.now() - TAB_CLOSED_AGO_MS, closedAt: Date.now() - TAB_CLOSED_AGO_MS,
image: null, image: null,
title: "Example",
}, },
], ],
}, },
@ -329,9 +338,10 @@ add_task(async function test_time_updates_correctly() {
}, },
async browser => { async browser => {
const { document } = browser.contentWindow; const { document } = browser.contentWindow;
const numOfListItems = document.querySelector("ol.closed-tabs-list")
.children.length;
const lastListItem = document.querySelector("ol.closed-tabs-list") const lastListItem = document.querySelector("ol.closed-tabs-list")
.lastChild; .children[numOfListItems - 1];
const timeLabel = lastListItem.querySelector("span.closed-tab-li-time"); const timeLabel = lastListItem.querySelector("span.closed-tab-li-time");
let initialTimeText = timeLabel.textContent; let initialTimeText = timeLabel.textContent;
Assert.stringContains( Assert.stringContains(
@ -387,14 +397,12 @@ add_task(async function test_list_maintains_focus_when_restoring_tab() {
let gBrowser = browser.getTabBrowser(); let gBrowser = browser.getTabBrowser();
const { document } = browser.contentWindow; const { document } = browser.contentWindow;
const list = document.querySelectorAll(".closed-tab-li"); const list = document.querySelectorAll(".closed-tab-li");
let expectedFocusedElement = list[1].querySelector(".closed-tab-li-main");
list[0].querySelector(".closed-tab-li-main").focus(); list[0].querySelector(".closed-tab-li-main").focus();
EventUtils.synthesizeKey("KEY_Enter"); EventUtils.synthesizeKey("KEY_Enter");
let firefoxViewTab = gBrowser.tabs.find(tab => tab.label == "Firefox View"); let firefoxViewTab = gBrowser.tabs.find(tab => tab.label == "Firefox View");
await BrowserTestUtils.switchTab(gBrowser, firefoxViewTab); await BrowserTestUtils.switchTab(gBrowser, firefoxViewTab);
is( Assert.ok(
document.activeElement, document.activeElement.textContent.includes("mochitest index"),
expectedFocusedElement,
"Focus should be on the first item in the recently closed list" "Focus should be on the first item in the recently closed list"
); );
}); });
@ -414,7 +422,6 @@ add_task(async function test_list_maintains_focus_when_restoring_tab() {
); );
const list = document.querySelectorAll(".closed-tab-li"); const list = document.querySelectorAll(".closed-tab-li");
list[0].querySelector(".closed-tab-li-main").focus(); list[0].querySelector(".closed-tab-li-main").focus();
EventUtils.synthesizeKey("KEY_Enter"); EventUtils.synthesizeKey("KEY_Enter");
let firefoxViewTab = gBrowser.tabs.find(tab => tab.label == "Firefox View"); let firefoxViewTab = gBrowser.tabs.find(tab => tab.label == "Firefox View");
await BrowserTestUtils.switchTab(gBrowser, firefoxViewTab); await BrowserTestUtils.switchTab(gBrowser, firefoxViewTab);
@ -455,27 +462,25 @@ add_task(async function test_switch_before_closing() {
); );
BrowserTestUtils.loadURIString(newTab.linkedBrowser, FINAL_URL); BrowserTestUtils.loadURIString(newTab.linkedBrowser, FINAL_URL);
await loadPromise; await loadPromise;
// Close the added tab // Close the added tab
BrowserTestUtils.removeTab(newTab); BrowserTestUtils.removeTab(newTab);
const { document } = browser.contentWindow; const { document } = browser.contentWindow;
const tabsList = document.querySelector("ol.closed-tabs-list");
await BrowserTestUtils.waitForMutationCondition( await BrowserTestUtils.waitForMutationCondition(
tabsList, document.querySelector("recently-closed-tabs-list"),
{ childList: true }, { childList: true, subtree: true },
() => !!tabsList.children.length () => document.querySelector("ol.closed-tabs-list")
); );
const tabsList = document.querySelector("ol.closed-tabs-list");
info("A tab appeared in the list, ensure it has the right URL."); info("A tab appeared in the list, ensure it has the right URL.");
let urlBit = tabsList.firstElementChild.querySelector(".closed-tab-li-url"); let urlBit = tabsList.children[0].querySelector(".closed-tab-li-url");
await BrowserTestUtils.waitForMutationCondition( await BrowserTestUtils.waitForMutationCondition(
urlBit, urlBit,
{ characterData: true, attributeFilter: ["title"] }, { characterData: true, attributeFilter: ["title"] },
() => urlBit.textContent.includes(".com") () => urlBit.textContent.includes(".com")
); );
is( Assert.ok(
urlBit.textContent, urlBit.textContent.includes("example.com"),
"example.com",
"Item should end up with the correct URL." "Item should end up with the correct URL."
); );
}); });
@ -495,7 +500,7 @@ add_task(async function test_alt_click_no_launch() {
let gBrowser = browser.getTabBrowser(); let gBrowser = browser.getTabBrowser();
let originalTabsLength = gBrowser.tabs.length; let originalTabsLength = gBrowser.tabs.length;
await BrowserTestUtils.synthesizeMouseAtCenter( await BrowserTestUtils.synthesizeMouseAtCenter(
".closed-tab-li", ".closed-tab-li .closed-tab-li-main",
{ altKey: true }, { altKey: true },
browser browser
); );
@ -554,19 +559,6 @@ add_task(async function test_restore_recently_closed_tabs() {
await tabRestored; await tabRestored;
ok(true, "Tab was restored by using the Enter key"); ok(true, "Tab was restored by using the Enter key");
await EventUtils.synthesizeMouseAtCenter(
gBrowser.ownerDocument.getElementById("firefox-view-button"),
{ type: "mousedown" },
window
);
tabRestored = BrowserTestUtils.waitForNewTab(gBrowser, URLs[0]);
document.querySelector(".closed-tab-li .closed-tab-li-main").focus();
EventUtils.synthesizeKey(" ", {}, gBrowser.contentWindow);
await tabRestored;
ok(true, "Tab was restored by using the Space bar");
// clean up extra tabs // clean up extra tabs
while (gBrowser.tabs.length > 1) { while (gBrowser.tabs.length > 1) {
BrowserTestUtils.removeTab(gBrowser.tabs.at(-1)); BrowserTestUtils.removeTab(gBrowser.tabs.at(-1));
@ -618,7 +610,7 @@ add_task(async function test_reopen_recently_closed_tabs() {
); );
Assert.equal( Assert.equal(
tabsList.children[0].dataset.targetURI, tabsList.children[0].dataset.targeturi,
URLs[1], URLs[1],
`First recently closed item should be ${URLs[1]}` `First recently closed item should be ${URLs[1]}`
); );
@ -632,7 +624,7 @@ add_task(async function test_reopen_recently_closed_tabs() {
); );
Assert.equal( Assert.equal(
tabsList.children[0].dataset.targetURI, tabsList.children[0].dataset.targeturi,
URLs[2], URLs[2],
`First recently closed item should be ${URLs[2]}` `First recently closed item should be ${URLs[2]}`
); );
@ -646,7 +638,7 @@ add_task(async function test_reopen_recently_closed_tabs() {
); );
Assert.equal( Assert.equal(
tabsList.children[0].dataset.targetURI, tabsList.children[0].dataset.targeturi,
URLs[1], URLs[1],
`First recently closed item should be ${URLs[1]}` `First recently closed item should be ${URLs[1]}`
); );
@ -715,7 +707,7 @@ add_task(async function test_dismiss_tab() {
); );
Assert.equal( Assert.equal(
tabsList.children[0].dataset.targetURI, tabsList.children[0].dataset.targeturi,
URLs[1], URLs[1],
`First recently closed item should be ${URLs[1]}` `First recently closed item should be ${URLs[1]}`
); );
@ -750,7 +742,7 @@ add_task(async function test_dismiss_tab() {
); );
Assert.equal( Assert.equal(
tabsList.children[0].dataset.targetURI, tabsList.children[0].dataset.targeturi,
URLs[2], URLs[2],
`First recently closed item should be ${URLs[2]}` `First recently closed item should be ${URLs[2]}`
); );
@ -784,11 +776,14 @@ add_task(async function test_dismiss_tab() {
{ clear: true, process: "parent" } { clear: true, process: "parent" }
); );
testVisibility(browser, { Assert.ok(
expectedVisible: { document.getElementById("recently-closed-tabs-placeholder"),
"#recently-closed-tabs-placeholder": true, "The empty message is displayed."
"ol.closed-tabs-list": false, );
},
}); Assert.ok(
!document.querySelector("ol.closed-tabs-list"),
"The recently closed tabs list is not displayed."
);
}); });
}); });

View file

@ -73,7 +73,6 @@ add_task(async function test_keyboard_navigation() {
"isTabSyncSetupComplete" "isTabSyncSetupComplete"
); );
setupCompleteStub.returns(true); setupCompleteStub.returns(true);
await open_then_close(URLs[0]); await open_then_close(URLs[0]);
await withFirefoxView({ win: window }, async browser => { await withFirefoxView({ win: window }, async browser => {
@ -84,16 +83,19 @@ add_task(async function test_keyboard_navigation() {
); );
assertPreconditions(document, summary); assertPreconditions(document, summary);
tab(); tab();
ok( Assert.equal(
list[0].querySelector(".closed-tab-li-main").matches(":focus"), list[0].querySelector(".closed-tab-li-main"),
document.activeElement,
"The first link is focused" "The first link is focused"
); );
tab(true); tab(true);
ok( Assert.equal(
summary.matches(":focus"), summary,
document.activeElement,
"The container is focused when using shift+tab in the list" "The container is focused when using shift+tab in the list"
); );
}); });
@ -117,26 +119,30 @@ add_task(async function test_keyboard_navigation() {
tab(); tab();
ok( Assert.equal(
list[0].querySelector(".closed-tab-li-main").matches(":focus"), list[0].querySelector(".closed-tab-li-main"),
document.activeElement,
"The first link is focused" "The first link is focused"
); );
tab(); tab();
tab(); tab();
ok( Assert.equal(
list[1].querySelector(".closed-tab-li-main").matches(":focus"), list[1].querySelector(".closed-tab-li-main"),
document.activeElement,
"The second link is focused" "The second link is focused"
); );
tab(true); tab(true);
tab(true); tab(true);
ok( Assert.equal(
list[0].querySelector(".closed-tab-li-main").matches(":focus"), list[0].querySelector(".closed-tab-li-main"),
document.activeElement,
"The first link is focused again" "The first link is focused again"
); );
tab(true); tab(true);
ok( Assert.equal(
summary.matches(":focus"), summary,
document.activeElement,
"The container is focused when using shift+tab in the list" "The container is focused when using shift+tab in the list"
); );
}); });
@ -162,32 +168,37 @@ add_task(async function test_keyboard_navigation() {
tab(); tab();
ok( Assert.equal(
list[0].querySelector(".closed-tab-li-main").matches(":focus"), list[0].querySelector(".closed-tab-li-main"),
document.activeElement,
"The first link is focused" "The first link is focused"
); );
tab(); tab();
tab(); tab();
ok( Assert.equal(
list[1].querySelector(".closed-tab-li-main").matches(":focus"), list[1].querySelector(".closed-tab-li-main"),
document.activeElement,
"The second link is focused" "The second link is focused"
); );
tab(); tab();
tab(); tab();
ok( Assert.equal(
list[2].querySelector(".closed-tab-li-main").matches(":focus"), list[2].querySelector(".closed-tab-li-main"),
document.activeElement,
"The third link is focused" "The third link is focused"
); );
tab(true); tab(true);
tab(true); tab(true);
ok( Assert.equal(
list[1].querySelector(".closed-tab-li-main").matches(":focus"), list[1].querySelector(".closed-tab-li-main"),
document.activeElement,
"The second link is focused" "The second link is focused"
); );
tab(true); tab(true);
tab(true); tab(true);
ok( Assert.equal(
list[0].querySelector(".closed-tab-li-main").matches(":focus"), list[0].querySelector(".closed-tab-li-main"),
document.activeElement,
"The first link is focused" "The first link is focused"
); );
}); });
@ -218,7 +229,7 @@ add_task(async function test_dismiss_tab_keyboard() {
await dismiss_tab_keyboard(tabsList.children[0], document); await dismiss_tab_keyboard(tabsList.children[0], document);
Assert.equal( Assert.equal(
tabsList.children[0].dataset.targetURI, tabsList.children[0].dataset.targeturi,
URLs[1], URLs[1],
`First recently closed item should be ${URLs[1]}` `First recently closed item should be ${URLs[1]}`
); );
@ -232,7 +243,7 @@ add_task(async function test_dismiss_tab_keyboard() {
await dismiss_tab_keyboard(tabsList.children[0], document); await dismiss_tab_keyboard(tabsList.children[0], document);
Assert.equal( Assert.equal(
tabsList.children[0].dataset.targetURI, tabsList.children[0].dataset.targeturi,
URLs[0], URLs[0],
`First recently closed item should be ${URLs[0]}` `First recently closed item should be ${URLs[0]}`
); );
@ -245,11 +256,14 @@ add_task(async function test_dismiss_tab_keyboard() {
await dismiss_tab_keyboard(tabsList.children[0], document); await dismiss_tab_keyboard(tabsList.children[0], document);
testVisibility(browser, { Assert.ok(
expectedVisible: { document.getElementById("recently-closed-tabs-placeholder"),
"#recently-closed-tabs-placeholder": true, "The empty message is displayed."
"ol.closed-tabs-list": false, );
},
}); Assert.ok(
!document.querySelector("ol.closed-tabs-list"),
"The recently clsoed tabs list is not displayed."
);
}); });
}); });

View file

@ -498,7 +498,9 @@ add_task(async function test_mobile_promo_pref() {
}); });
// reset the dismissed pref, which should case the promo to get shown // reset the dismissed pref, which should case the promo to get shown
await SpecialPowers.popPrefEnv(); await SpecialPowers.pushPrefEnv({
set: [[MOBILE_PROMO_DISMISSED_PREF, false]],
});
await waitForElementVisible( await waitForElementVisible(
browser, browser,
"#tab-pickup-container > .promo-box", "#tab-pickup-container > .promo-box",
@ -760,6 +762,5 @@ add_task(async function test_close_device_connected_tab() {
// cleanup time // cleanup time
await tearDown(sandbox); await tearDown(sandbox);
await SpecialPowers.popPrefEnv();
await BrowserTestUtils.closeWindow(win); await BrowserTestUtils.closeWindow(win);
}); });

View file

@ -537,8 +537,6 @@ add_task(async function test_tabs_sync_on_user_page_reload() {
}); });
add_task(async function test_keyboard_navigation() { add_task(async function test_keyboard_navigation() {
// Setting this pref allows the test to run as expected on MacOS
await SpecialPowers.pushPrefEnv({ set: [["accessibility.tabfocus", 7]] });
TabsSetupFlowManager.resetInternalState(); TabsSetupFlowManager.resetInternalState();
const sandbox = setupRecentDeviceListMocks(); const sandbox = setupRecentDeviceListMocks();

View file

@ -42,6 +42,7 @@ add_task(async function test_closedId_order() {
}, },
closedId: 0, closedId: 0,
closedAt: Date.now() - 100, closedAt: Date.now() - 100,
title: "Example",
}, },
{ {
state: { state: {
@ -54,6 +55,7 @@ add_task(async function test_closedId_order() {
}, },
closedId: 1, closedId: 1,
closedAt: Date.now() - 50, closedAt: Date.now() - 50,
title: "about:mozilla",
}, },
{ {
state: { state: {
@ -66,6 +68,7 @@ add_task(async function test_closedId_order() {
}, },
closedId: 2, closedId: 2,
closedAt: Date.now(), closedAt: Date.now(),
title: "Example",
}, },
], ],
}, },

View file

@ -102,7 +102,11 @@ export class MozLitElement extends LitElement {
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
if (!this._l10nRootConnected && document.l10n) { if (
this.renderRoot == this.shadowRoot &&
!this._l10nRootConnected &&
document.l10n
) {
document.l10n.connectRoot(this.renderRoot); document.l10n.connectRoot(this.renderRoot);
this._l10nRootConnected = true; this._l10nRootConnected = true;
} }