Bug 1828673 - Add support for overview page in synced tabs. r=desktop-theme-reviewers,fxview-reviewers,jsudiaman,dao,sfoster,fluent-reviewers,flod

Differential Revision: https://phabricator.services.mozilla.com/D184414
This commit is contained in:
Mike Kaply 2023-08-03 18:46:32 +00:00
parent b4e65f0c36
commit ae0fa36393
6 changed files with 201 additions and 95 deletions

View file

@ -98,6 +98,9 @@
<div>
<view-recentlyclosed slot="recentlyclosed"></view-recentlyclosed>
</div>
<div>
<view-syncedtabs slot="syncedtabs"></view-syncedtabs>
</div>
</view-overview>
<view-history name="history"></view-history>
<view-opentabs name="opentabs"></view-opentabs>

View file

@ -6,6 +6,7 @@ import {
html,
ifDefined,
styleMap,
classMap,
when,
} from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
@ -213,35 +214,42 @@ export default class FxviewTabList extends MozLitElement {
role="list"
@keydown=${this.handleFocusElementInRow}
>
${tabItems.map(
(tabItem, i) =>
html`
<fxview-tab-row
exportparts="secondary-button"
?active=${i == activeIndex}
?compact=${this.compactRows}
.hasPopup=${hasPopup}
.currentActiveElementId=${currentActiveElementId}
.dateTimeFormat=${dateTimeFormat}
.favicon=${tabItem.icon}
.primaryL10nId=${tabItem.primaryL10nId}
.primaryL10nArgs=${ifDefined(tabItem.primaryL10nArgs)}
role="listitem"
.secondaryL10nId=${tabItem.secondaryL10nId}
.secondaryL10nArgs=${ifDefined(tabItem.secondaryL10nArgs)}
.closedId=${ifDefined(tabItem.closedId || tabItem.closedId)}
.tabElement=${ifDefined(tabItem.tabElement)}
.time=${(tabItem.time || tabItem.closedAt).toString().length ===
16
? (tabItem.time || tabItem.closedAt) / 1000
: tabItem.time || tabItem.closedAt}
.timeMsPref=${ifDefined(this.timeMsPref)}
.title=${tabItem.title}
.url=${tabItem.url}
>
</fxview-tab-row>
`
)}
${tabItems.map((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`
<fxview-tab-row
exportparts="secondary-button"
?active=${i == activeIndex}
?compact=${this.compactRows}
.hasPopup=${hasPopup}
.currentActiveElementId=${currentActiveElementId}
.dateTimeFormat=${dateTimeFormat}
.favicon=${tabItem.icon}
.primaryL10nId=${ifDefined(tabItem.primaryL10nId)}
.primaryL10nArgs=${ifDefined(tabItem.primaryL10nArgs)}
role="listitem"
.secondaryL10nId=${ifDefined(tabItem.secondaryL10nId)}
.secondaryL10nArgs=${ifDefined(tabItem.secondaryL10nArgs)}
.closedId=${ifDefined(tabItem.closedId || tabItem.closedId)}
.tabElement=${ifDefined(tabItem.tabElement)}
.time=${ifDefined(time)}
.timeMsPref=${ifDefined(this.timeMsPref)}
.title=${tabItem.title}
.url=${ifDefined(tabItem.url)}
>
</fxview-tab-row>
`;
})}
</div>
<slot name="menu"></slot>
`;
@ -336,7 +344,7 @@ export class FxviewTabRow extends MozLitElement {
dateFluentId(timestamp, dateTimeFormat, _nowThresholdMs = NOW_THRESHOLD_MS) {
if (!timestamp) {
return "";
return null;
}
if (dateTimeFormat === "relative") {
const elapsed = Date.now() - timestamp;
@ -447,14 +455,17 @@ export class FxviewTabRow extends MozLitElement {
/>
<link rel="stylesheet" href=${this.constructor.stylesheetUrl} />
<a
href=${this.url}
class="fxview-tab-row-main"
.href=${ifDefined(this.url)}
class=${classMap({
"fxview-tab-row-main": true,
"fxview-tab-row-header": !this.url,
})}
id="fxview-tab-row-main"
tabindex=${this.active &&
this.currentActiveElementId === "fxview-tab-row-main"
? "0"
: "-1"}
data-l10n-id=${this.primaryL10nId}
data-l10n-id=${ifDefined(this.primaryL10nId)}
data-l10n-args=${ifDefined(this.primaryL10nArgs)}
@click=${this.primaryActionHandler}
@keydown=${this.primaryActionHandler}
@ -492,9 +503,9 @@ export class FxviewTabRow extends MozLitElement {
class="fxview-tab-row-time"
id="fxview-tab-row-time"
?hidden=${this.compact || !timeString}
data-timestamp=${this.time}
data-timestamp=${ifDefined(this.time)}
data-l10n-id=${ifDefined(timeString)}
data-l10n-args=${timeArgs}
data-l10n-args=${ifDefined(timeArgs)}
>
</span>
</a>

View file

@ -51,6 +51,18 @@
background-color: var(--fxviewtabrow-element-background-active);
}
.fxview-tab-row-header {
margin-top: 8px;
cursor: inherit;
font-weight: 600;
}
.fxview-tab-row-header:hover,
.fxview-tab-row-header:hover:active {
color: inherit;
background-color: inherit;
}
@media (prefers-contrast) {
.fxview-tab-row-main,
.fxview-tab-row-main:hover,
@ -81,7 +93,7 @@
white-space: nowrap;
}
.fxview-tab-row-main:hover .fxview-tab-row-title {
.fxview-tab-row-main:hover:not(.fxview-tab-row-header) .fxview-tab-row-title {
text-decoration-line: underline;
}

View file

@ -15,7 +15,11 @@ const { TabsSetupFlowManager } = ChromeUtils.importESModule(
"resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
);
import { html, ifDefined } from "chrome://global/content/vendor/lit.all.mjs";
import {
html,
ifDefined,
when,
} from "chrome://global/content/vendor/lit.all.mjs";
import { ViewPage } from "./viewpage.mjs";
const SYNCED_TABS_CHANGED = "services.sync.tabs.changed";
@ -31,7 +35,7 @@ class SyncedTabsInView extends ViewPage {
this._id = Math.floor(Math.random() * 10e6);
this.currentSyncedTabs = [];
if (this.overview) {
this.maxTabsLength = 5;
this.maxTabsLength = 6; // 5 tabs plus the device row
} else {
// Setting maxTabsLength to -1 for no max
this.maxTabsLength = -1;
@ -218,6 +222,7 @@ class SyncedTabsInView extends ViewPage {
.descriptionLink=${ifDefined(descriptionLink)}
class="empty-state synced-tabs"
?isSelectedTab=${this.selectedTab}
?isInnerCard=${this.overview}
mainImageUrl="${ifDefined(mainImageUrl)}"
headerIconUrl="${ifDefined(headerIconUrl)}"
>
@ -284,15 +289,74 @@ class SyncedTabsInView extends ViewPage {
}
noDeviceTabsTemplate(deviceName, deviceType) {
if (this.overview) {
return html` ${this.deviceTemplate(deviceName, deviceType, [])}
<div
class="blackbox notabs"
data-l10n-id="firefoxview-syncedtabs-device-notabs"
></div>`;
}
return html`<card-container>
<h2 slot="header">
<div class="icon ${deviceType}" role="presentation"></div>
<span class="icon ${deviceType}" role="presentation"></span>
${deviceName}
</h2>
<div slot="main" class="blackbox notabs">No tabs open on this device</div>
<div
slot="main"
class="blackbox notabs"
data-l10n-id="firefoxview-syncedtabs-device-notabs"
></div>
</card-container>`;
}
deviceTemplate(deviceName, deviceType, tabs) {
let tabItems = this.getTabItems(tabs);
if (this.overview) {
/* Insert device at the beginning of the tabs array */
let icon;
switch (deviceType) {
case "phone":
icon = "chrome://browser/skin/device-phone.svg";
break;
case "desktop":
icon = "chrome://browser/skin/device-desktop.svg";
break;
case "tablet":
icon = "chrome://browser/skin/device-tablet.svg";
break;
}
tabItems.unshift({
icon,
title: deviceName,
});
}
return html`${when(
!this.overview,
() => html`<h2 slot="header">
<span class="icon ${deviceType}" role="presentation"></span>
${deviceName}
</h2>`
)}
<fxview-tab-list
slot="main"
class="syncedtabs"
hasPopup="menu"
.tabItems=${ifDefined(tabItems)}
maxTabsLength=${this.maxTabsLength}
@fxview-tab-list-primary-action=${this.onOpenLink}
@fxview-tab-list-secondary-action=${this.onContextMenu}
>
${when(
this.overview,
() => html`<h2 slot="header">
<span class="icon ${deviceType}" role="presentation"></span>
${deviceName}
</h2>`
)}
${this.panelListTemplate()}
</fxview-tab-list>`;
}
generateTabList() {
let renderArray = [];
let renderInfo = {};
@ -320,26 +384,25 @@ class SyncedTabsInView extends ViewPage {
for (let id in renderInfo) {
if (renderInfo[id].tabs.length) {
renderArray.push(html`<card-container>
<h2 slot="header">
<div
class="icon ${renderInfo[id].deviceType}"
role="presentation"
></div>
${renderInfo[id].name}
</h2>
<fxview-tab-list
slot="main"
class="syncedtabs"
hasPopup="menu"
.tabItems=${ifDefined(this.getTabItems(renderInfo[id].tabs))}
maxTabsLength=${this.maxTabsLength}
@fxview-tab-list-primary-action=${this.onOpenLink}
@fxview-tab-list-secondary-action=${this.onContextMenu}
>
${this.panelListTemplate()}
</fxview-tab-list>
</card-container>`);
if (this.overview) {
renderArray.push(
this.deviceTemplate(
renderInfo[id].name,
renderInfo[id].deviceType,
renderInfo[id].tabs
)
);
} else {
renderArray.push(
html`<card-container
>${this.deviceTemplate(
renderInfo[id].name,
renderInfo[id].deviceType,
renderInfo[id].tabs
)}</card-container
>`
);
}
} else {
renderArray.push(
this.noDeviceTabsTemplate(
@ -351,9 +414,35 @@ class SyncedTabsInView extends ViewPage {
}
return renderArray;
}
render() {
const stateIndex = this._currentSetupStateIndex;
generateCardContent() {
switch (this._currentSetupStateIndex) {
case 0 /* error-state */:
if (this.errorState) {
return this.generateMessageCard({ error: true });
}
break;
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*/:
return this.generateTabList();
}
return html``;
}
render() {
this.open =
!TabsSetupFlowManager.isTabSyncSetupComplete ||
Services.prefs.getBoolPref(UI_OPEN_STATE, true);
@ -376,34 +465,23 @@ class SyncedTabsInView extends ViewPage {
</div>`);
}
switch (stateIndex) {
case 0 /* error-state */:
if (this.errorState) {
renderArray.push(this.generateMessageCard({ error: true }));
}
break;
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
renderArray.push(
this.generateMessageCard({ error: true, errorState: "signed-out" })
);
} else {
renderArray.push(this.generateMessageCard({ action: "sign-in" }));
}
break;
case 2 /* connect-secondary-device*/:
renderArray.push(this.generateMessageCard({ action: "add-device" }));
break;
case 3 /* disabled-tab-sync */:
renderArray.push(
this.generateMessageCard({ action: "sync-tabs-disabled" })
);
break;
case 4 /* synced-tabs-loaded*/:
renderArray = renderArray.concat(this.generateTabList());
break;
if (this.overview) {
renderArray.push(
html`<card-container
preserveCollapseState
viewAllPage=${this._currentSetupStateIndex == 4 ? "syncedtabs" : null}
>
>
<h2
slot="header"
data-l10n-id="firefoxview-synced-tabs-header"
class="overview-header"
></h2>
<div slot="main">${this.generateCardContent()}</div>
</card-container>`
);
} else {
renderArray.push(this.generateCardContent());
}
return renderArray;
}

View file

@ -74,17 +74,17 @@ You'll need to pass along some of the following properties:
* `hasPopup` (**Optional**): The optional aria-haspopup attribute for the secondary action, if required
* `maxTabsLength` (**Optional**): The max number of tabs you want to display in the tabs list. The default value will be `25` if no max value is given. You may use any negative number such as `-1` to indicate no max.
* `tabItems` (**Required**): An array of tab data such as History nodes, Bookmark nodes, Synced Tabs, etc.
* The component is expecting to receive the following properties within each `tabItems` object (you may need to do some normalizing for this):
* The component is expecting to receive the following properties within each `tabItems` object (you may need to do some normalizing for this). If you just pass a title and an icon, it creates a header row that is not clickable.
* `icon` (**Required**) - The location string for the favicon. Will fallback to default favicon if none is found.
* `primaryL10nId` (**Required**) - The l10n id to be used for the primary action element. This fluent string should ONLY define a `.title` attribute to describe the link element in each row.
* `primaryL10nId` (**Optional**) - The l10n id to be used for the primary action element. This fluent string should ONLY define a `.title` attribute to describe the link element in each row.
* `primaryL10nArgs` (**Optional**) - The l10n args you can optionally pass for the primary action element
* `secondaryL10nId` (**Optional**) - The l10n id to be used for the secondary action button. This fluent string should ONLY define a `.title` attribute to describe the secondary button in each row.
* `secondaryL10nArgs` (**Optional**) - The l10n args you can optionally pass for the secondary action button
* `tabElement` (**Optional**) - The MozTabbrowserTab element for the tab item.
* `tabid` (**Optional**) - Optional property expected for Recently Closed tab data
* `time` (**Required**) - The time in milliseconds for expected last interaction with the tab (Ex: `lastUsed` for SyncedTabs tabs, `closedAt` for RecentlyClosed tabs, etc.)
* `time` (**Optional**) - The time in milliseconds for expected last interaction with the tab (Ex: `lastUsed` for SyncedTabs tabs, `closedAt` for RecentlyClosed tabs, etc.)
* `title` (**Required**) - The title for the tab
* `url` (**Required**) - The full URL for the tab
* `url` (**Optional**) - The full URL for the tab
### Notes

View file

@ -230,4 +230,6 @@ firefoxview-recentlyclosed-empty-header = Closed a tab too soon?
firefoxview-recentlyclosed-empty-description = Here youll find the tabs you recently closed, so you can reopen any of them quickly.
firefoxview-recentlyclosed-empty-description-two = To find tabs from longer ago, view your <a data-l10n-name="history-url">browsing history</a>.
##
## This message is displayed below the name of another connected device when it doesn't have any open tabs.
firefoxview-syncedtabs-device-notabs = No tabs open on this device