/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {
html,
ifDefined,
styleMap,
} from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
const NOW_THRESHOLD_MS = 91000;
const lazy = {};
let XPCOMUtils;
if (!window.IS_STORYBOOK) {
XPCOMUtils = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
).XPCOMUtils;
XPCOMUtils.defineLazyGetter(lazy, "relativeTimeFormat", () => {
return new Services.intl.RelativeTimeFormat(undefined, {
style: "narrow",
});
});
ChromeUtils.defineESModuleGetters(lazy, {
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs",
});
}
/**
* A list of clickable tab items
*
* @property {string} dateTimeFormat - Expected format for date and/or time
* @property {string} hasPopup - The aria-haspopup attribute for the secondary action, if required
* @property {number} maxTabsLength - The max number of tabs for the list
* @property {Array} tabItems - Items to show in the tab list
*/
export default class FxviewTabList extends MozLitElement {
constructor() {
super();
window.MozXULElement.insertFTLIfNeeded("toolkit/branding/brandings.ftl");
window.MozXULElement.insertFTLIfNeeded("browser/fxviewTabList.ftl");
this.activeIndex = 0;
this.currentActiveElementId = "fxview-tab-row-main";
this.hasPopup = null;
this.dateTimeFormat = "relative";
this.maxTabsLength = 25;
this.tabItems = [];
this.#register();
}
static properties = {
activeIndex: { type: Number },
currentActiveElementId: { type: String },
dateTimeFormat: { type: String },
hasPopup: { type: String },
maxTabsLength: { type: Number },
tabItems: { type: Array },
};
static queries = {
rowEls: { all: "fxview-tab-row" },
};
willUpdate(changes) {
this.activeIndex = Math.min(
Math.max(this.activeIndex, 0),
this.tabItems.length - 1
);
if (changes.has("dateTimeFormat")) {
if (this.dateTimeFormat == "relative" && !window.IS_STORYBOOK) {
this.intervalID = setInterval(
() => this.onIntervalUpdate(),
this.timeMsPref
);
} else {
clearInterval(this.intervalID);
}
}
}
#register() {
if (!window.IS_STORYBOOK) {
XPCOMUtils.defineLazyPreferenceGetter(
this,
"timeMsPref",
"browser.tabs.firefox-view.updateTimeMs",
NOW_THRESHOLD_MS,
(prefName, oldVal, newVal) => {
if (!this.isConnected) {
return;
}
clearInterval(this.intervalID);
this.intervalID = setInterval(() => {
this.onIntervalUpdate();
}, newVal);
this.requestUpdate();
}
);
}
}
connectedCallback() {
super.connectedCallback();
if (this.dateTimeFormat === "relative" && !window.IS_STORYBOOK) {
this.intervalID = setInterval(
() => this.onIntervalUpdate(),
this.timeMsPref
);
}
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.intervalID) {
clearInterval(this.intervalID);
}
}
async getUpdateComplete() {
await super.getUpdateComplete();
await Promise.all(Array.from(this.rowEls).map(item => item.updateComplete));
}
onIntervalUpdate() {
this.requestUpdate();
Array.from(this.rowEls).forEach(fxviewTabRow =>
fxviewTabRow.requestUpdate()
);
}
/**
* Focuses the expected element (either the link or button) within fxview-tab-row
* The currently focused/active element ID within a row is stored in this.currentActiveElementId
*/
handleFocusElementInRow(e) {
let fxviewTabRow = e.target;
if (e.code == "ArrowUp") {
// Focus either the link or button of the previous row based on this.currentActiveElementId
e.preventDefault();
this.focusPrevRow();
} else if (e.code == "ArrowDown") {
// Focus either the link or button of the next row based on this.currentActiveElementId
e.preventDefault();
this.focusNextRow();
} else if (e.code == "ArrowRight") {
// Focus either the link or the button in the current row and
// set this.currentActiveElementId to that element's ID
e.preventDefault();
if (document.dir == "rtl") {
this.currentActiveElementId = fxviewTabRow.focusLink();
} else {
this.currentActiveElementId = fxviewTabRow.focusButton();
}
} else if (e.code == "ArrowLeft") {
// Focus either the link or the button in the current row and
// set this.currentActiveElementId to that element's ID
e.preventDefault();
if (document.dir == "rtl") {
this.currentActiveElementId = fxviewTabRow.focusButton();
} else {
this.currentActiveElementId = fxviewTabRow.focusLink();
}
}
}
focusPrevRow() {
// Focus link or button of item above
let previousIndex = this.activeIndex - 1;
if (previousIndex >= 0) {
this.rowEls[previousIndex].focus();
this.activeIndex = previousIndex;
}
}
focusNextRow() {
// Focus link or button of item below
let nextIndex = this.activeIndex + 1;
if (nextIndex < this.rowEls.length) {
this.rowEls[nextIndex].focus();
this.activeIndex = nextIndex;
}
}
// Use a relative URL in storybook to get faster reloads on style changes.
static stylesheetUrl = window.IS_STORYBOOK
? "./fxview-tab-list.css"
: "chrome://browser/content/firefoxview/fxview-tab-list.css";
render() {
this.tabItems = this.tabItems.slice(0, this.maxTabsLength);
const {
activeIndex,
currentActiveElementId,
dateTimeFormat,
hasPopup,
tabItems,
} = this;
return html`
${tabItems.map(
(tabItem, i) =>
html`
`
)}
`;
}
}
customElements.define("fxview-tab-list", FxviewTabList);
/**
* A tab item that displays favicon, title, url, and time of last access
*
* @property {boolean} active - Should current item have focus on keydown
* @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 {number} tabid - The tab ID for when the tab item.
* @property {string} favicon - The favicon for the tab item.
* @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 {number} time - The timestamp for when the tab was last accessed.
* @property {string} title - The title for the tab item.
* @property {string} url - The url for the tab item.
* @property {number} timeMsPref - The frequency in milliseconds of updates to relative time
*/
export class FxviewTabRow extends MozLitElement {
constructor() {
super();
this.active = false;
this.currentActiveElementId = "fxview-tab-row-main";
}
static properties = {
active: { type: Boolean },
currentActiveElementId: { type: String },
dateTimeFormat: { type: String },
favicon: { type: String },
hasPopup: { type: String },
primaryL10nId: { type: String },
primaryL10nArgs: { type: String },
secondaryL10nId: { type: String },
secondaryL10nArgs: { type: String },
tabid: { type: Number },
time: { type: Number },
title: { type: String },
timeMsPref: { type: Number },
url: { type: String },
};
static queries = {
mainEl: ".fxview-tab-row-main",
buttonEl: ".fxview-tab-row-button:not([hidden])",
};
get currentFocusable() {
return this.renderRoot.getElementById(this.currentActiveElementId);
}
connectedCallback() {
super.connectedCallback();
}
focus() {
this.currentFocusable.focus();
}
focusButton() {
this.buttonEl.focus();
return this.buttonEl.id;
}
focusLink() {
this.mainEl.focus();
return this.mainEl.id;
}
// Use a relative URL in storybook to get faster reloads on style changes.
static stylesheetUrl = window.IS_STORYBOOK
? "./fxview-tab-row.css"
: "chrome://browser/content/firefoxview/fxview-tab-row.css";
dateFluentArgs(timestamp, dateTimeFormat) {
if (dateTimeFormat === "date" || dateTimeFormat === "dateTime") {
return JSON.stringify({ date: timestamp });
}
return null;
}
dateFluentId(timestamp, dateTimeFormat, _nowThresholdMs = NOW_THRESHOLD_MS) {
if (!timestamp) {
return "";
}
if (dateTimeFormat === "relative") {
const elapsed = Date.now() - timestamp;
if (elapsed <= _nowThresholdMs || !lazy.relativeTimeFormat) {
// Use a different string for very recent timestamps
return "fxviewtabrow-just-now-timestamp";
}
return null;
} else if (dateTimeFormat === "date" || dateTimeFormat === "dateTime") {
return "fxviewtabrow-date";
}
return null;
}
relativeTime(timestamp, dateTimeFormat, _nowThresholdMs = NOW_THRESHOLD_MS) {
if (dateTimeFormat === "relative") {
const elapsed = Date.now() - timestamp;
if (elapsed > _nowThresholdMs && lazy.relativeTimeFormat) {
return lazy.relativeTimeFormat.formatBestUnit(new Date(timestamp));
}
}
return null;
}
timeFluentId(dateTimeFormat) {
if (dateTimeFormat === "time" || dateTimeFormat === "dateTime") {
return "fxviewtabrow-time";
}
return null;
}
formatURIForDisplay(uriString) {
return !window.IS_STORYBOOK
? lazy.BrowserUtils.formatURIStringForDisplay(uriString)
: uriString;
}
getImageUrl(icon, targetURI) {
if (!window.IS_STORYBOOK) {
return icon
? lazy.PlacesUIUtils.getImageURL(icon)
: `page-icon:${targetURI}`;
}
return `chrome://global/skin/icons/defaultFavicon.svg`;
}
primaryActionHandler(event) {
if (
(event.type == "click" && !event.altKey) ||
(event.type == "keydown" && event.code == "Enter") ||
(event.type == "keydown" && event.code == "Space")
) {
event.preventDefault();
if (!window.IS_STORYBOOK) {
this.dispatchEvent(
new CustomEvent("fxview-tab-list-primary-action", {
bubbles: true,
composed: true,
detail: { originalEvent: event, item: this },
})
);
}
}
}
secondaryActionHandler(event) {
if (
(event.type == "click" && event.detail && !event.altKey) ||
// detail=0 is from keyboard
(event.type == "click" && !event.detail)
) {
event.preventDefault();
this.dispatchEvent(
new CustomEvent("fxview-tab-list-secondary-action", {
bubbles: true,
composed: true,
detail: { originalEvent: event, item: this },
})
);
}
}
render() {
const title = this.title;
const relativeString = this.relativeTime(
this.time,
this.dateTimeFormat,
!window.IS_STORYBOOK ? this.timeMsPref : NOW_THRESHOLD_MS
);
const dateString = this.dateFluentId(
this.time,
this.dateTimeFormat,
!window.IS_STORYBOOK ? this.timeMsPref : NOW_THRESHOLD_MS
);
const dateArgs = this.dateFluentArgs(this.time, this.dateTimeFormat);
const timeString = this.timeFluentId(this.dateTimeFormat);
const time = this.time;
const timeArgs = JSON.stringify({ time });
return html`
${title}
${this.formatURIForDisplay(this.url)}
${relativeString}
`;
}
}
customElements.define("fxview-tab-row", FxviewTabRow);