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> <div>
<view-recentlyclosed slot="recentlyclosed"></view-recentlyclosed> <view-recentlyclosed slot="recentlyclosed"></view-recentlyclosed>
</div> </div>
<div>
<view-syncedtabs slot="syncedtabs"></view-syncedtabs>
</div>
</view-overview> </view-overview>
<view-history name="history"></view-history> <view-history name="history"></view-history>
<view-opentabs name="opentabs"></view-opentabs> <view-opentabs name="opentabs"></view-opentabs>

View file

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

View file

@ -51,6 +51,18 @@
background-color: var(--fxviewtabrow-element-background-active); 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) { @media (prefers-contrast) {
.fxview-tab-row-main, .fxview-tab-row-main,
.fxview-tab-row-main:hover, .fxview-tab-row-main:hover,
@ -81,7 +93,7 @@
white-space: nowrap; 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; text-decoration-line: underline;
} }

View file

@ -15,7 +15,11 @@ const { TabsSetupFlowManager } = ChromeUtils.importESModule(
"resource:///modules/firefox-view-tabs-setup-manager.sys.mjs" "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"; import { ViewPage } from "./viewpage.mjs";
const SYNCED_TABS_CHANGED = "services.sync.tabs.changed"; const SYNCED_TABS_CHANGED = "services.sync.tabs.changed";
@ -31,7 +35,7 @@ class SyncedTabsInView extends ViewPage {
this._id = Math.floor(Math.random() * 10e6); this._id = Math.floor(Math.random() * 10e6);
this.currentSyncedTabs = []; this.currentSyncedTabs = [];
if (this.overview) { if (this.overview) {
this.maxTabsLength = 5; this.maxTabsLength = 6; // 5 tabs plus the device row
} else { } else {
// Setting maxTabsLength to -1 for no max // Setting maxTabsLength to -1 for no max
this.maxTabsLength = -1; this.maxTabsLength = -1;
@ -218,6 +222,7 @@ class SyncedTabsInView extends ViewPage {
.descriptionLink=${ifDefined(descriptionLink)} .descriptionLink=${ifDefined(descriptionLink)}
class="empty-state synced-tabs" class="empty-state synced-tabs"
?isSelectedTab=${this.selectedTab} ?isSelectedTab=${this.selectedTab}
?isInnerCard=${this.overview}
mainImageUrl="${ifDefined(mainImageUrl)}" mainImageUrl="${ifDefined(mainImageUrl)}"
headerIconUrl="${ifDefined(headerIconUrl)}" headerIconUrl="${ifDefined(headerIconUrl)}"
> >
@ -284,15 +289,74 @@ class SyncedTabsInView extends ViewPage {
} }
noDeviceTabsTemplate(deviceName, deviceType) { 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> return html`<card-container>
<h2 slot="header"> <h2 slot="header">
<div class="icon ${deviceType}" role="presentation"></div> <span class="icon ${deviceType}" role="presentation"></span>
${deviceName} ${deviceName}
</h2> </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>`; </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() { generateTabList() {
let renderArray = []; let renderArray = [];
let renderInfo = {}; let renderInfo = {};
@ -320,26 +384,25 @@ class SyncedTabsInView extends ViewPage {
for (let id in renderInfo) { for (let id in renderInfo) {
if (renderInfo[id].tabs.length) { if (renderInfo[id].tabs.length) {
renderArray.push(html`<card-container> if (this.overview) {
<h2 slot="header"> renderArray.push(
<div this.deviceTemplate(
class="icon ${renderInfo[id].deviceType}" renderInfo[id].name,
role="presentation" renderInfo[id].deviceType,
></div> renderInfo[id].tabs
${renderInfo[id].name} )
</h2> );
<fxview-tab-list } else {
slot="main" renderArray.push(
class="syncedtabs" html`<card-container
hasPopup="menu" >${this.deviceTemplate(
.tabItems=${ifDefined(this.getTabItems(renderInfo[id].tabs))} renderInfo[id].name,
maxTabsLength=${this.maxTabsLength} renderInfo[id].deviceType,
@fxview-tab-list-primary-action=${this.onOpenLink} renderInfo[id].tabs
@fxview-tab-list-secondary-action=${this.onContextMenu} )}</card-container
> >`
${this.panelListTemplate()} );
</fxview-tab-list> }
</card-container>`);
} else { } else {
renderArray.push( renderArray.push(
this.noDeviceTabsTemplate( this.noDeviceTabsTemplate(
@ -351,9 +414,35 @@ class SyncedTabsInView extends ViewPage {
} }
return renderArray; 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 = this.open =
!TabsSetupFlowManager.isTabSyncSetupComplete || !TabsSetupFlowManager.isTabSyncSetupComplete ||
Services.prefs.getBoolPref(UI_OPEN_STATE, true); Services.prefs.getBoolPref(UI_OPEN_STATE, true);
@ -376,34 +465,23 @@ class SyncedTabsInView extends ViewPage {
</div>`); </div>`);
} }
switch (stateIndex) { if (this.overview) {
case 0 /* error-state */: renderArray.push(
if (this.errorState) { html`<card-container
renderArray.push(this.generateMessageCard({ error: true })); preserveCollapseState
} viewAllPage=${this._currentSetupStateIndex == 4 ? "syncedtabs" : null}
break; >
case 1 /* not-signed-in */: >
if (Services.prefs.prefHasUserValue("services.sync.lastversion")) { <h2
// If this pref is set, the user has signed out of sync. slot="header"
// This path is also taken if we are disconnected from sync. See bug 1784055 data-l10n-id="firefoxview-synced-tabs-header"
renderArray.push( class="overview-header"
this.generateMessageCard({ error: true, errorState: "signed-out" }) ></h2>
); <div slot="main">${this.generateCardContent()}</div>
} else { </card-container>`
renderArray.push(this.generateMessageCard({ action: "sign-in" })); );
} } else {
break; renderArray.push(this.generateCardContent());
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;
} }
return renderArray; 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 * `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. * `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. * `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. * `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 * `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. * `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 * `secondaryL10nArgs` (**Optional**) - The l10n args you can optionally pass for the secondary action button
* `tabElement` (**Optional**) - The MozTabbrowserTab element for the tab item. * `tabElement` (**Optional**) - The MozTabbrowserTab element for the tab item.
* `tabid` (**Optional**) - Optional property expected for Recently Closed tab data * `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 * `title` (**Required**) - The title for the tab
* `url` (**Required**) - The full URL for the tab * `url` (**Optional**) - The full URL for the tab
### Notes ### 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 = 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>. 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