summaryrefslogtreecommitdiffstats
path: root/browser/components/firefoxview/tab-pickup-list.mjs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /browser/components/firefoxview/tab-pickup-list.mjs
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/firefoxview/tab-pickup-list.mjs')
-rw-r--r--browser/components/firefoxview/tab-pickup-list.mjs348
1 files changed, 348 insertions, 0 deletions
diff --git a/browser/components/firefoxview/tab-pickup-list.mjs b/browser/components/firefoxview/tab-pickup-list.mjs
new file mode 100644
index 0000000000..6cc5a61ff6
--- /dev/null
+++ b/browser/components/firefoxview/tab-pickup-list.mjs
@@ -0,0 +1,348 @@
+/* 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/. */
+
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "SyncedTabs",
+ "resource://services-sync/SyncedTabs.jsm"
+);
+
+import {
+ formatURIForDisplay,
+ convertTimestamp,
+ createFaviconElement,
+ NOW_THRESHOLD_MS,
+} from "./helpers.mjs";
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const SYNCED_TABS_CHANGED = "services.sync.tabs.changed";
+
+class TabPickupList extends HTMLElement {
+ constructor() {
+ super();
+ this.maxTabsLength = 3;
+ this.boundObserve = (...args) => {
+ this.getSyncedTabData(...args);
+ };
+
+ // The recency timestamp update period is stored in a pref to allow tests to easily change it
+ XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "timeMsPref",
+ "browser.tabs.firefox-view.updateTimeMs",
+ NOW_THRESHOLD_MS,
+ () => this.updateTime()
+ );
+ }
+
+ get tabsList() {
+ return this.querySelector("ol");
+ }
+
+ get fluentStrings() {
+ if (!this._fluentStrings) {
+ this._fluentStrings = new Localization(["browser/firefoxView.ftl"], true);
+ }
+ return this._fluentStrings;
+ }
+
+ get timeElements() {
+ return this.querySelectorAll("span.synced-tab-li-time");
+ }
+
+ connectedCallback() {
+ this.placeholderContainer = document.getElementById(
+ "synced-tabs-placeholder"
+ );
+ this.tabPickupContainer = document.getElementById(
+ "tabpickup-tabs-container"
+ );
+
+ this.addEventListener("click", this);
+ Services.obs.addObserver(this.boundObserve, SYNCED_TABS_CHANGED);
+
+ // inform ancestor elements our getSyncedTabData method is available to fetch data
+ this.dispatchEvent(new CustomEvent("list-ready", { bubbles: true }));
+ }
+
+ handleEvent(event) {
+ if (
+ event.type == "click" ||
+ (event.type == "keydown" && event.keyCode == KeyEvent.DOM_VK_RETURN)
+ ) {
+ const item = event.target.closest(".synced-tab-li");
+ let index = [...this.tabsList.children].indexOf(item);
+ let deviceType = item.dataset.deviceType;
+ Services.telemetry.recordEvent(
+ "firefoxview",
+ "tab_pickup",
+ "tabs",
+ null,
+ {
+ position: (++index).toString(),
+ deviceType,
+ }
+ );
+ }
+ if (event.type == "keydown") {
+ switch (event.key) {
+ case "ArrowRight": {
+ event.preventDefault();
+ this.moveFocusToSecondElement();
+ break;
+ }
+ case "ArrowLeft": {
+ event.preventDefault();
+ this.moveFocusToFirstElement();
+ break;
+ }
+ case "ArrowDown": {
+ event.preventDefault();
+ this.moveFocusToNextElement();
+ break;
+ }
+ case "ArrowUp": {
+ event.preventDefault();
+ this.moveFocusToPreviousElement();
+ break;
+ }
+ case "Tab": {
+ this.resetFocus(event);
+ }
+ }
+ }
+ }
+
+ /**
+ * Handles removing and setting tabindex on elements
+ * while moving focus to the next element
+ *
+ * @param {HTMLElement} currentElement currently focused element
+ * @param {HTMLElement} nextElement element that should receive focus next
+ * @memberof TabPickupList
+ * @private
+ */
+ #manageTabIndexAndFocus(currentElement, nextElement) {
+ currentElement.setAttribute("tabindex", "-1");
+ nextElement.removeAttribute("tabindex");
+ nextElement.focus();
+ }
+
+ moveFocusToFirstElement() {
+ let selectableElements = Array.from(this.tabsList.querySelectorAll("a"));
+ let firstElement = selectableElements[0];
+ let selectedElement = this.tabsList.querySelector("a:not([tabindex]");
+ this.#manageTabIndexAndFocus(selectedElement, firstElement);
+ }
+
+ moveFocusToSecondElement() {
+ let selectableElements = Array.from(this.tabsList.querySelectorAll("a"));
+ let secondElement = selectableElements[1];
+ if (secondElement) {
+ let selectedElement = this.tabsList.querySelector("a:not([tabindex]");
+ this.#manageTabIndexAndFocus(selectedElement, secondElement);
+ }
+ }
+
+ moveFocusToNextElement() {
+ let selectableElements = Array.from(this.tabsList.querySelectorAll("a"));
+ let selectedElement = this.tabsList.querySelector("a:not([tabindex]");
+ let nextElement =
+ selectableElements.findIndex(elem => elem == selectedElement) + 1;
+ if (nextElement < selectableElements.length) {
+ this.#manageTabIndexAndFocus(
+ selectedElement,
+ selectableElements[nextElement]
+ );
+ }
+ }
+ moveFocusToPreviousElement() {
+ let selectableElements = Array.from(this.tabsList.querySelectorAll("a"));
+ let selectedElement = this.tabsList.querySelector("a:not([tabindex]");
+ let previousElement =
+ selectableElements.findIndex(elem => elem == selectedElement) - 1;
+ if (previousElement >= 0) {
+ this.#manageTabIndexAndFocus(
+ selectedElement,
+ selectableElements[previousElement]
+ );
+ }
+ }
+ resetFocus(e) {
+ let selectableElements = Array.from(this.tabsList.querySelectorAll("a"));
+ let selectedElement = this.tabsList.querySelector("a:not([tabindex]");
+ selectedElement.setAttribute("tabindex", "-1");
+ selectableElements[0].removeAttribute("tabindex");
+ if (e.shiftKey) {
+ e.preventDefault();
+ document
+ .getElementById("tab-pickup-container")
+ .querySelector("summary")
+ .focus();
+ }
+ }
+
+ cleanup() {
+ Services.obs.removeObserver(this.boundObserve, SYNCED_TABS_CHANGED);
+ clearInterval(this.intervalID);
+ }
+
+ updateTime() {
+ for (let timeEl of this.timeElements) {
+ timeEl.textContent = convertTimestamp(
+ parseInt(timeEl.getAttribute("data-timestamp")),
+ this.fluentStrings,
+ lazy.timeMsPref
+ );
+ }
+ }
+
+ togglePlaceholderVisibility(visible) {
+ this.placeholderContainer.toggleAttribute("hidden", !visible);
+ this.placeholderContainer.classList.toggle("empty-container", visible);
+ }
+
+ async getSyncedTabData() {
+ let tabs = await lazy.SyncedTabs.getRecentTabs(50);
+
+ this.updateTabsList(tabs);
+ }
+
+ updateTabsList(syncedTabs) {
+ // don't do anything while the loading state is active
+
+ while (this.tabsList.firstChild) {
+ this.tabsList.firstChild.remove();
+ }
+
+ if (!syncedTabs.length) {
+ this.sendTabTelemetry(0);
+ this.togglePlaceholderVisibility(true);
+ this.tabsList.hidden = true;
+ return;
+ }
+
+ for (let i = 0; i < this.maxTabsLength; i++) {
+ let li = null;
+ if (!syncedTabs[i]) {
+ li = this.generatePlaceholder();
+ } else {
+ li = this.generateListItem(syncedTabs[i], i);
+ }
+ this.tabsList.append(li);
+ }
+
+ if (this.tabsList.hidden) {
+ this.tabsList.hidden = false;
+ this.togglePlaceholderVisibility(false);
+
+ if (!this.intervalID) {
+ this.intervalID = setInterval(() => this.updateTime(), lazy.timeMsPref);
+ }
+ }
+
+ this.sendTabTelemetry(syncedTabs.length);
+ }
+
+ generatePlaceholder() {
+ const li = document.createElement("li");
+ li.classList.add("synced-tab-li-placeholder");
+ li.setAttribute("role", "presentation");
+
+ const favicon = document.createElement("span");
+ favicon.classList.add("li-placeholder-favicon");
+
+ const title = document.createElement("span");
+ title.classList.add("li-placeholder-title");
+
+ const domain = document.createElement("span");
+ domain.classList.add("li-placeholder-domain");
+
+ li.append(favicon, title, domain);
+ return li;
+ }
+
+ generateListItem(tab, index) {
+ const li = document.createElement("li");
+ li.classList.add("synced-tab-li");
+ li.dataset.deviceType = tab.deviceType;
+
+ const targetURI = tab.url;
+ const a = document.createElement("a");
+ a.classList.add("synced-tab-a");
+ a.href = targetURI;
+ a.target = "_blank";
+ if (index != 0) {
+ a.setAttribute("tabindex", "-1");
+ }
+ a.addEventListener("keydown", this);
+
+ const title = document.createElement("span");
+ title.textContent = tab.title;
+ title.classList.add("synced-tab-li-title");
+
+ const favicon = createFaviconElement(tab.icon, targetURI);
+
+ const lastUsedMs = tab.lastUsed * 1000;
+ const time = document.createElement("span");
+ time.textContent = convertTimestamp(lastUsedMs, this.fluentStrings);
+ time.classList.add("synced-tab-li-time");
+ time.setAttribute("data-timestamp", lastUsedMs);
+
+ const url = document.createElement("span");
+ const device = document.createElement("span");
+ const deviceIcon = document.createElement("div");
+ deviceIcon.classList.add("icon", tab.deviceType);
+ deviceIcon.setAttribute("role", "presentation");
+
+ const deviceText = tab.device;
+ device.textContent = deviceText;
+ device.prepend(deviceIcon);
+ device.title = deviceText;
+
+ url.textContent = formatURIForDisplay(tab.url);
+ url.title = tab.url;
+ url.classList.add("synced-tab-li-url");
+ document.l10n.setAttributes(url, "firefoxview-tabs-list-tab-button", {
+ targetURI,
+ });
+ device.classList.add("synced-tab-li-device");
+
+ // the first list item is different from the second and third
+ if (index == 0) {
+ const badge = this.createBadge();
+ a.append(favicon, badge, title, url, device, time);
+ } else {
+ a.append(favicon, title, url, device, time);
+ }
+
+ li.append(a);
+ return li;
+ }
+
+ createBadge() {
+ const badge = document.createElement("div");
+ const dot = document.createElement("span");
+ const badgeText = document.createElement("span");
+
+ badgeText.setAttribute("data-l10n-id", "firefoxview-pickup-tabs-badge");
+ badgeText.classList.add("badge-text");
+ badge.classList.add("last-active-badge");
+ dot.classList.add("dot");
+ badge.append(dot, badgeText);
+ return badge;
+ }
+
+ sendTabTelemetry(numTabs) {
+ Services.telemetry.recordEvent("firefoxview", "synced_tabs", "tabs", null, {
+ count: numTabs.toString(),
+ });
+ }
+}
+
+customElements.define("tab-pickup-list", TabPickupList);