/* 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);