Bug 1857005 - Add indicators to open tabs in Fx View r=desktop-theme-reviewers,fxview-reviewers,fluent-reviewers,sfoster,bolsson

Differential Revision: https://phabricator.services.mozilla.com/D199119
This commit is contained in:
Kelly Cochrane
2024-01-30 19:52:31 +00:00
parent c5ac190354
commit b3dfa7a67a
8 changed files with 483 additions and 31 deletions

View File

@@ -15,7 +15,14 @@ ChromeUtils.defineESModuleGetters(lazy, {
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});
const TAB_ATTRS_TO_WATCH = Object.freeze(["image", "label"]);
const TAB_ATTRS_TO_WATCH = Object.freeze([
"attention",
"image",
"label",
"muted",
"soundplaying",
"titlechanged",
]);
const TAB_CHANGE_EVENTS = Object.freeze([
"TabAttrModified",
"TabClose",
@@ -30,6 +37,7 @@ const TAB_RECENCY_CHANGE_EVENTS = Object.freeze([
"TabClose",
"TabOpen",
"TabSelect",
"TabAttrModified",
]);
// Debounce tab/tab recency changes and dispatch max once per frame at 60fps

View File

@@ -4,21 +4,21 @@
.fxview-tab-list {
display: grid;
grid-template-columns: min-content 3fr 2fr 1fr 1fr min-content;
grid-template-columns: min-content 3fr min-content 2fr 1fr 1fr min-content min-content;
gap: 6px;
}
:host([compactRows]) .fxview-tab-list {
grid-template-columns: min-content 1fr min-content;
grid-template-columns: min-content 1fr min-content min-content min-content;
}
virtual-list {
display: grid;
grid-column: span 7;
grid-column: span 9;
grid-template-columns: subgrid;
.top-padding,
.bottom-padding {
grid-column: span 7;
grid-column: span 9;
}
}

View File

@@ -3,6 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {
classMap,
html,
ifDefined,
repeat,
@@ -185,7 +186,19 @@ export default class FxviewTabList extends MozLitElement {
// set this.currentActiveElementId to that element's ID
e.preventDefault();
if (document.dir == "rtl") {
this.currentActiveElementId = fxviewTabRow.focusLink();
if (
(fxviewTabRow.soundPlaying || fxviewTabRow.muted) &&
this.currentActiveElementId === "fxview-tab-row-secondary-button"
) {
this.currentActiveElementId = fxviewTabRow.focusMediaButton();
} else {
this.currentActiveElementId = fxviewTabRow.focusLink();
}
} else if (
(fxviewTabRow.soundPlaying || fxviewTabRow.muted) &&
this.currentActiveElementId === "fxview-tab-row-main"
) {
this.currentActiveElementId = fxviewTabRow.focusMediaButton();
} else {
this.currentActiveElementId = fxviewTabRow.focusButton();
}
@@ -194,7 +207,19 @@ export default class FxviewTabList extends MozLitElement {
// set this.currentActiveElementId to that element's ID
e.preventDefault();
if (document.dir == "rtl") {
this.currentActiveElementId = fxviewTabRow.focusButton();
if (
(fxviewTabRow.soundPlaying || fxviewTabRow.muted) &&
this.currentActiveElementId === "fxview-tab-row-main"
) {
this.currentActiveElementId = fxviewTabRow.focusMediaButton();
} else {
this.currentActiveElementId = fxviewTabRow.focusButton();
}
} else if (
(fxviewTabRow.soundPlaying || fxviewTabRow.muted) &&
this.currentActiveElementId === "fxview-tab-row-secondary-button"
) {
this.currentActiveElementId = fxviewTabRow.focusMediaButton();
} else {
this.currentActiveElementId = fxviewTabRow.focusLink();
}
@@ -264,14 +289,20 @@ export default class FxviewTabList extends MozLitElement {
?active=${i == this.activeIndex}
?compact=${this.compactRows}
.hasPopup=${this.hasPopup}
.containerObj=${tabItem.containerObj}
.currentActiveElementId=${this.currentActiveElementId}
.dateTimeFormat=${this.dateTimeFormat}
.favicon=${tabItem.icon}
.isBookmark=${ifDefined(tabItem.isBookmark)}
.muted=${ifDefined(tabItem.muted)}
.pinned=${ifDefined(tabItem.pinned)}
.primaryL10nId=${tabItem.primaryL10nId}
.primaryL10nArgs=${ifDefined(tabItem.primaryL10nArgs)}
role="listitem"
.secondaryL10nId=${tabItem.secondaryL10nId}
.secondaryL10nArgs=${ifDefined(tabItem.secondaryL10nArgs)}
.attention=${ifDefined(tabItem.attention)}
.soundPlaying=${ifDefined(tabItem.soundPlaying)}
.sourceClosedId=${ifDefined(tabItem.sourceClosedId)}
.sourceWindowId=${ifDefined(tabItem.sourceWindowId)}
.closedId=${ifDefined(tabItem.closedId || tabItem.closedId)}
@@ -280,6 +311,7 @@ export default class FxviewTabList extends MozLitElement {
.time=${ifDefined(time)}
.timeMsPref=${ifDefined(this.timeMsPref)}
.title=${tabItem.title}
.titleChanged=${ifDefined(tabItem.titleChanged)}
.url=${tabItem.url}
></fxview-tab-row>
`;
@@ -338,20 +370,27 @@ customElements.define("fxview-tab-list", FxviewTabList);
*
* @property {boolean} active - Should current item have focus on keydown
* @property {boolean} compact - Whether to hide the URL and date/time for this tab.
* @property {object} containerObj - Info about an open tab's container if within one
* @property {string} currentActiveElementId - ID of currently focused element within each tab item
* @property {string} dateTimeFormat - Expected format for date and/or time
* @property {string} hasPopup - The aria-haspopup attribute for the secondary action, if required
* @property {boolean} isBookmark - Whether an open tab is bookmarked
* @property {number} closedId - The tab ID for when the tab item was closed.
* @property {number} sourceClosedId - The closedId of the closed window its from if applicable
* @property {number} sourceWindowId - The sessionstore id of the window its from if applicable
* @property {string} favicon - The favicon for the tab item.
* @property {boolean} muted - Whether an open tab is muted
* @property {boolean} pinned - Whether an open tab is pinned
* @property {string} primaryL10nId - The l10n id used for the primary action element
* @property {string} primaryL10nArgs - The l10n args used for the primary action element
* @property {string} secondaryL10nId - The l10n id used for the secondary action button
* @property {string} secondaryL10nArgs - The l10n args used for the secondary action element
* @property {boolean} attention - Whether to show a notification dot
* @property {boolean} soundPlaying - Whether an open tab has soundPlaying
* @property {object} tabElement - The MozTabbrowserTab element for the tab item.
* @property {number} time - The timestamp for when the tab was last accessed.
* @property {string} title - The title for the tab item.
* @property {boolean} titleChanged - Whether the title has changed for an open tab
* @property {string} url - The url for the tab item.
* @property {number} timeMsPref - The frequency in milliseconds of updates to relative time
* @property {string} searchQuery - The query string to highlight, if provided.
@@ -366,20 +405,27 @@ export class FxviewTabRow extends MozLitElement {
static properties = {
active: { type: Boolean },
compact: { type: Boolean },
containerObj: { type: Object },
currentActiveElementId: { type: String },
dateTimeFormat: { type: String },
favicon: { type: String },
hasPopup: { type: String },
isBookmark: { type: Boolean },
muted: { type: Boolean },
pinned: { type: Boolean },
primaryL10nId: { type: String },
primaryL10nArgs: { type: String },
secondaryL10nId: { type: String },
secondaryL10nArgs: { type: String },
soundPlaying: { type: Boolean },
closedId: { type: Number },
sourceClosedId: { type: Number },
sourceWindowId: { type: String },
tabElement: { type: Object },
time: { type: Number },
title: { type: String },
titleChanged: { type: Boolean },
attention: { type: Boolean },
timeMsPref: { type: Number },
url: { type: String },
searchQuery: { type: String },
@@ -387,11 +433,16 @@ export class FxviewTabRow extends MozLitElement {
static queries = {
mainEl: ".fxview-tab-row-main",
buttonEl: ".fxview-tab-row-button:not([hidden])",
buttonEl: "#fxview-tab-row-secondary-button:not([hidden])",
mediaButtonEl: "#fxview-tab-row-media-button",
};
get currentFocusable() {
return this.renderRoot.getElementById(this.currentActiveElementId);
let focusItem = this.renderRoot.getElementById(this.currentActiveElementId);
if (!focusItem) {
focusItem = this.renderRoot.getElementById("fxview-tab-row-main");
}
return focusItem;
}
focus() {
@@ -403,6 +454,11 @@ export class FxviewTabRow extends MozLitElement {
return this.buttonEl.id;
}
focusMediaButton() {
this.mediaButtonEl.focus();
return this.mediaButtonEl.id;
}
focusLink() {
this.mainEl.focus();
return this.mainEl.id;
@@ -475,6 +531,16 @@ export class FxviewTabRow extends MozLitElement {
return icon;
}
getContainerClasses() {
let containerClasses = ["fxview-tab-row-container-indicator", "icon"];
if (this.containerObj) {
let { icon, color } = this.containerObj;
containerClasses.push(`identity-icon-${icon}`);
containerClasses.push(`identity-color-${color}`);
}
return containerClasses;
}
primaryActionHandler(event) {
if (
(event.type == "click" && !event.altKey) ||
@@ -511,6 +577,11 @@ export class FxviewTabRow extends MozLitElement {
}
}
muteOrUnmuteTab() {
this.tabElement.toggleMuteAudio();
this.muted = !this.muted;
}
render() {
const title = this.title;
const relativeString = this.relativeTime(
@@ -528,6 +599,15 @@ export class FxviewTabRow extends MozLitElement {
const time = this.time;
const timeArgs = JSON.stringify({ time });
return html`
${when(
this.containerObj,
() => html`
<link
rel="stylesheet"
href="chrome://browser/content/usercontext/usercontext.css"
/>
`
)}
<link
rel="stylesheet"
href="chrome://global/skin/in-content/common.css"
@@ -550,12 +630,27 @@ export class FxviewTabRow extends MozLitElement {
@keydown=${this.primaryActionHandler}
>
<span
class="fxview-tab-row-favicon icon"
id="fxview-tab-row-favicon"
style=${styleMap({
backgroundImage: `url(${this.getImageUrl(this.favicon, this.url)})`,
})}
></span>
class="${classMap({
"fxview-tab-row-favicon-wrapper": true,
bookmark: this.isBookmark && !this.attention,
notification: this.pinned
? this.attention || this.titleChanged
: this.attention,
soundplaying: this.soundPlaying && !this.muted && this.pinned,
muted: this.muted && this.pinned,
})}"
>
<span
class="fxview-tab-row-favicon icon"
id="fxview-tab-row-favicon"
style=${styleMap({
backgroundImage: `url(${this.getImageUrl(
this.favicon,
this.url
)})`,
})}
></span>
</span>
<span
class="fxview-tab-row-title text-truncated-ellipsis"
id="fxview-tab-row-title"
@@ -567,6 +662,7 @@ export class FxviewTabRow extends MozLitElement {
() => title
)}
</span>
<span class=${this.getContainerClasses().join(" ")}></span>
<span
class="fxview-tab-row-url text-truncated-ellipsis"
id="fxview-tab-row-url"
@@ -604,6 +700,29 @@ export class FxviewTabRow extends MozLitElement {
>
</span>
</a>
${when(
(this.soundPlaying || this.muted) && !this.pinned,
() => html`<button
class=fxview-tab-row-button ghost-button icon-button semi-transparent"
id="fxview-tab-row-media-button"
data-l10n-id=${
this.muted
? "fxviewtabrow-unmute-tab-button"
: "fxviewtabrow-mute-tab-button"
}
data-l10n-args=${JSON.stringify({ tabTitle: title })}
muted=${ifDefined(this.muted)}
soundplaying=${this.soundPlaying && !this.muted}
@click=${this.muteOrUnmuteTab}
tabindex="${
this.active &&
this.currentActiveElementId === "fxview-tab-row-media-button"
? "0"
: "-1"
}"
></button>`,
() => html`<span></span>`
)}
${when(
this.secondaryL10nId && this.secondaryActionHandler,
() => html`<button

View File

@@ -7,7 +7,7 @@
--fxviewtabrow-element-background-active: color-mix(in srgb, currentColor 21%, transparent);
display: grid;
grid-template-columns: subgrid;
grid-column: span 6;
grid-column: span 9;
align-items: stretch;
border-radius: 4px;
}
@@ -23,7 +23,7 @@
.fxview-tab-row-main {
display: grid;
grid-template-columns: subgrid;
grid-column: span 5;
grid-column: span 6;
gap: 16px;
border-radius: 4px;
align-items: center;
@@ -66,6 +66,56 @@
}
}
.fxview-tab-row-favicon-wrapper {
height: 16px;
.fxview-tab-row-favicon::after {
display: block;
content: "";
background-size: 12px;
background-position: center;
background-repeat: no-repeat;
position: absolute;
height: 12px;
width: 12px;
-moz-context-properties: fill, stroke;
fill: currentColor;
stroke: var(--fxview-background-color-secondary);
}
&.bookmark .fxview-tab-row-favicon::after {
background-image: url("chrome://browser/skin/bookmark-12.svg");
inset-block-start: 9px;
inset-inline-end: -6px;
fill: var(--fxview-primary-action-background);
}
&.notification .fxview-tab-row-favicon::after {
background-image: radial-gradient(circle, light-dark(rgb(42, 195, 162), rgb(84, 255, 189)), light-dark(rgb(42, 195, 162), rgb(84, 255, 189)) 2px, transparent 2px);
height: 4px;
width: 100%;
inset-block-start: 20px;
}
&.soundplaying .fxview-tab-row-favicon::after {
background-image: url("chrome://global/skin/media/audio.svg");
inset-block-start: -5px;
inset-inline-end: -7px;
border-radius: 100%;
background-color: var(--fxview-background-color-secondary);
padding: 2px;
}
&.muted .fxview-tab-row-favicon::after {
background-image: url("chrome://global/skin/media/audio-muted.svg");
inset-block-start: -5px;
inset-inline-end: -7px;
border-radius: 100%;
background-color: var(--fxview-background-color-secondary);
padding: 2px;
}
}
.fxview-tab-row-favicon {
background-size: cover;
-moz-context-properties: fill;
@@ -80,6 +130,15 @@
text-align: match-parent;
}
.fxview-tab-row-container-indicator {
height: 16px;
width: 16px;
background-image: var(--identity-icon);
background-size: cover;
-moz-context-properties: fill;
fill: var(--identity-icon-color);
}
.fxview-tab-row-url {
color: var(--text-color-deemphasized);
text-decoration-line: underline;
@@ -101,6 +160,25 @@
.fxview-tab-row-button {
margin: 0;
cursor: pointer;
min-width: 0;
background-color: transparent;
&[muted="true"],
&[soundplaying="true"] {
background-size: 16px;
background-repeat: no-repeat;
background-position: center;
-moz-context-properties: fill;
fill: currentColor;
}
&[muted="true"] {
background-image: url("chrome://global/skin/media/audio-muted.svg");
}
&[soundplaying="true"] {
background-image: url("chrome://global/skin/media/audio.svg");
}
}
@media (prefers-contrast) {

View File

@@ -21,6 +21,8 @@ import { ViewPage, ViewPageContent } from "./viewpage.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ContextualIdentityService:
"resource://gre/modules/ContextualIdentityService.sys.mjs",
NonPrivateTabs: "resource:///modules/OpenTabs.sys.mjs",
getTabsTargetForWindow: "resource:///modules/OpenTabs.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
@@ -476,6 +478,7 @@ class OpenTabsInViewCard extends ViewPageContent {
.maxTabsLength=${this.getMaxTabsLength()}
.tabItems=${this.searchResults || getTabListItems(this.tabs)}
.searchQuery=${this.searchQuery}
.showTabIndicators=${true}
><view-opentabs-contextmenu slot="menu"></view-opentabs-contextmenu>
</fxview-tab-list>
</div>
@@ -715,6 +718,24 @@ class OpenTabsContextMenu extends MozLitElement {
}
customElements.define("view-opentabs-contextmenu", OpenTabsContextMenu);
/**
* Checks if a given tab is within a container (contextual identity)
*
* @param {MozTabbrowserTab[]} tab
* Tab to fetch container info on.
* @returns {object[]}
* Container object.
*/
function getContainerObj(tab) {
let userContextId = tab.getAttribute("usercontextid");
let containerObj = null;
if (userContextId) {
containerObj =
lazy.ContextualIdentityService.getPublicIdentityFromId(userContextId);
}
return containerObj;
}
/**
* Convert a list of tabs into the format expected by the fxview-tab-list
* component.
@@ -725,19 +746,27 @@ customElements.define("view-opentabs-contextmenu", OpenTabsContextMenu);
* Formatted objects.
*/
function getTabListItems(tabs) {
return tabs
?.filter(tab => !tab.closing && !tab.hidden && !tab.pinned)
.map(tab => ({
icon: tab.getAttribute("image"),
primaryL10nId: "firefoxview-opentabs-tab-row",
primaryL10nArgs: JSON.stringify({
url: tab.linkedBrowser?.currentURI?.spec,
}),
secondaryL10nId: "fxviewtabrow-options-menu-button",
secondaryL10nArgs: JSON.stringify({ tabTitle: tab.label }),
tabElement: tab,
time: tab.lastAccessed,
title: tab.label,
let filtered = tabs?.filter(
tab => !tab.closing && !tab.hidden && !tab.pinned
);
return filtered.map(tab => ({
attention: tab.hasAttribute("attention"),
containerObj: getContainerObj(tab),
icon: tab.getAttribute("image"),
muted: tab.hasAttribute("muted"),
pinned: tab.pinned,
primaryL10nId: "firefoxview-opentabs-tab-row",
primaryL10nArgs: JSON.stringify({
url: tab.linkedBrowser?.currentURI?.spec,
}));
}),
secondaryL10nId: "fxviewtabrow-options-menu-button",
secondaryL10nArgs: JSON.stringify({ tabTitle: tab.label }),
soundPlaying: tab.hasAttribute("soundplaying"),
tabElement: tab,
time: tab.lastAccessed,
title: tab.label,
titleChanged: tab.hasAttribute("titlechanged"),
url: tab.linkedBrowser?.currentURI?.spec,
}));
}

View File

@@ -63,6 +63,7 @@ skip-if = [
"os == 'mac' && verify"
] # macos times out, see bug 1857293, skipped for windows, see bug 1858460
["browser_opentabs_tab_indicators.js"]
["browser_recentlyclosed_firefoxview.js"]
fail-if = ["a11y_checks"] # Bug 1854625 clicked button.fxview-tab-row-secondary-button and a.fxview-tab-row-main may not be focusable

View File

@@ -0,0 +1,207 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const { NonPrivateTabs } = ChromeUtils.importESModule(
"resource:///modules/OpenTabs.sys.mjs"
);
let pageWithAlert =
// eslint-disable-next-line @microsoft/sdl/no-insecure-url
"http://example.com/browser/browser/base/content/test/tabPrompts/openPromptOffTimeout.html";
let pageWithSound =
"http://mochi.test:8888/browser/dom/base/test/file_audioLoop.html";
function cleanup() {
// Cleanup
while (gBrowser.tabs.length > 1) {
BrowserTestUtils.removeTab(gBrowser.tabs[0]);
}
}
add_task(async function test_notification_dot_indicator() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
let win = browser.ownerGlobal;
await navigateToCategoryAndWait(document, "opentabs");
// load page that opens prompt when page is hidden
let openedTab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
pageWithAlert,
true
);
let openedTabGotAttentionPromise = BrowserTestUtils.waitForAttribute(
"attention",
openedTab
);
let tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
);
await switchToFxViewTab();
let openTabs = document.querySelector("view-opentabs[name=opentabs]");
await openedTabGotAttentionPromise;
await tabChangeRaised;
await openTabs.updateComplete;
await TestUtils.waitForCondition(
() => openTabs.viewCards[0].tabList.rowEls[1].attention,
"The opened tab doesn't have the attention property, so no notification dot is shown."
);
info("The newly opened tab has a notification dot.");
// Switch back to other tab to close prompt before cleanup
await BrowserTestUtils.switchTab(gBrowser, openedTab);
EventUtils.synthesizeKey("KEY_Enter", {}, win);
cleanup();
});
});
add_task(async function test_container_indicator() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
let win = browser.ownerGlobal;
// Load a page in a container tab
let userContextId = 1;
let containerTab = BrowserTestUtils.addTab(win.gBrowser, URLs[0], {
userContextId,
});
await BrowserTestUtils.browserLoaded(
containerTab.linkedBrowser,
false,
URLs[0]
);
await navigateToCategoryAndWait(document, "opentabs");
let openTabs = document.querySelector("view-opentabs[name=opentabs]");
await TestUtils.waitForCondition(
() =>
containerTab.getAttribute("usercontextid") === userContextId.toString(),
"The container tab doesn't have the usercontextid attribute."
);
await openTabs.updateComplete;
await TestUtils.waitForCondition(
() => openTabs.viewCards[0].tabList?.rowEls.length,
"The tab list hasn't rendered."
);
info("openTabs component has finished updating.");
let containerTabElem = openTabs.viewCards[0].tabList.rowEls[1];
await TestUtils.waitForCondition(
() => containerTabElem.containerObj,
"The container tab element isn't marked in Fx View."
);
ok(
containerTabElem.shadowRoot
.querySelector(".fxview-tab-row-container-indicator")
.classList.contains("identity-color-blue"),
"The container color is blue."
);
info("The newly opened tab is marked as a container tab.");
cleanup();
});
});
add_task(async function test_sound_playing_muted_indicator() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
await navigateToCategoryAndWait(document, "opentabs");
// Load a page in a container tab
let soundTab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
pageWithSound,
true
);
let tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
);
await switchToFxViewTab();
let openTabs = document.querySelector("view-opentabs[name=opentabs]");
await TestUtils.waitForCondition(() =>
soundTab.hasAttribute("soundplaying")
);
await tabChangeRaised;
await openTabs.updateComplete;
await TestUtils.waitForCondition(
() => openTabs.viewCards[0].tabList?.rowEls.length,
"The tab list hasn't rendered."
);
let soundPlayingTabElem = openTabs.viewCards[0].tabList.rowEls[1];
await TestUtils.waitForCondition(() => soundPlayingTabElem.soundPlaying);
ok(
soundPlayingTabElem.mediaButtonEl,
"The tab has the mute button showing."
);
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
);
// Mute the tab
EventUtils.synthesizeMouseAtCenter(
soundPlayingTabElem.mediaButtonEl,
{},
content
);
await TestUtils.waitForCondition(
() => soundTab.hasAttribute("soundplaying"),
"The tab doesn't have the soundplaying attribute."
);
await tabChangeRaised;
await openTabs.updateComplete;
await TestUtils.waitForCondition(() => soundPlayingTabElem.muted);
ok(
soundPlayingTabElem.mediaButtonEl,
"The tab has the unmute button showing."
);
// Mute and unmute the tab and make sure the element in Fx View updates
soundTab.toggleMuteAudio();
await tabChangeRaised;
await openTabs.updateComplete;
await TestUtils.waitForCondition(() => soundPlayingTabElem.soundPlaying);
ok(
soundPlayingTabElem.mediaButtonEl,
"The tab has the mute button showing."
);
soundTab.toggleMuteAudio();
await tabChangeRaised;
await openTabs.updateComplete;
await TestUtils.waitForCondition(() => soundPlayingTabElem.muted);
ok(
soundPlayingTabElem.mediaButtonEl,
"The tab has the unmute button showing."
);
cleanup();
});
});