tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

UrlbarView.sys.mjs (138768B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      7 
      8 /**
      9 * @import {ProvidersManager} from "UrlbarProvidersManager.sys.mjs"
     10 */
     11 
     12 const lazy = {};
     13 
     14 ChromeUtils.defineESModuleGetters(lazy, {
     15  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
     16  ContextualIdentityService:
     17    "resource://gre/modules/ContextualIdentityService.sys.mjs",
     18  L10nCache: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
     19  ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
     20  UrlbarProviderOpenTabs:
     21    "moz-src:///browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs",
     22  UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
     23  UrlbarProviderQuickSuggest:
     24    "moz-src:///browser/components/urlbar/UrlbarProviderQuickSuggest.sys.mjs",
     25  UrlbarProviderTopSites:
     26    "moz-src:///browser/components/urlbar/UrlbarProviderTopSites.sys.mjs",
     27  UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs",
     28  UrlbarSearchOneOffs:
     29    "moz-src:///browser/components/urlbar/UrlbarSearchOneOffs.sys.mjs",
     30  UrlbarTokenizer:
     31    "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs",
     32  UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
     33 });
     34 
     35 XPCOMUtils.defineLazyServiceGetter(
     36  lazy,
     37  "styleSheetService",
     38  "@mozilla.org/content/style-sheet-service;1",
     39  Ci.nsIStyleSheetService
     40 );
     41 
     42 // Query selector for selectable elements in results.
     43 const SELECTABLE_ELEMENT_SELECTOR = "[role=button], [selectable], a";
     44 const KEYBOARD_SELECTABLE_ELEMENT_SELECTOR =
     45  "[role=button]:not([keyboard-inaccessible]), [selectable], a";
     46 
     47 const RESULT_MENU_COMMANDS = {
     48  DISMISS: "dismiss",
     49  HELP: "help",
     50  MANAGE: "manage",
     51 };
     52 
     53 const getBoundsWithoutFlushing = element =>
     54  element.ownerGlobal.windowUtils.getBoundsWithoutFlushing(element);
     55 
     56 // Used to get a unique id to use for row elements, it wraps at 9999, that
     57 // should be plenty for our needs.
     58 let gUniqueIdSerial = 1;
     59 function getUniqueId(prefix) {
     60  return prefix + (gUniqueIdSerial++ % 9999);
     61 }
     62 
     63 /**
     64 * Receives and displays address bar autocomplete results.
     65 */
     66 export class UrlbarView {
     67  // Stale rows are removed on a timer with this timeout.
     68  static removeStaleRowsTimeout = 400;
     69 
     70  /**
     71   * @param {UrlbarInput} input
     72   *   The UrlbarInput instance belonging to this UrlbarView instance.
     73   */
     74  constructor(input) {
     75    this.input = input;
     76    this.panel = input.panel;
     77    this.controller = input.controller;
     78    this.document = this.panel.ownerDocument;
     79    this.window = this.document.defaultView;
     80 
     81    this.#rows = this.panel.querySelector(".urlbarView-results");
     82    this.resultMenu = this.panel.querySelector(".urlbarView-result-menu");
     83    this.#resultMenuCommands = new WeakMap();
     84 
     85    this.#rows.addEventListener("mousedown", this);
     86 
     87    // For the horizontal fade-out effect, set the overflow attribute on result
     88    // rows when they overflow.
     89    this.#rows.addEventListener("overflow", this);
     90    this.#rows.addEventListener("underflow", this);
     91 
     92    this.resultMenu.addEventListener("command", this);
     93    this.resultMenu.addEventListener("popupshowing", this);
     94 
     95    // `noresults` is used to style the one-offs without their usual top border
     96    // when no results are present.
     97    this.panel.setAttribute("noresults", "true");
     98 
     99    this.controller.setView(this);
    100    this.controller.addListener(this);
    101    // This is used by autoOpen to avoid flickering results when reopening
    102    // previously abandoned searches.
    103    this.queryContextCache = new QueryContextCache(5);
    104 
    105    // We cache l10n strings to avoid Fluent's async lookup.
    106    this.#l10nCache = new lazy.L10nCache(this.document.l10n);
    107 
    108    for (let viewTemplate of UrlbarView.dynamicViewTemplatesByName.values()) {
    109      if (viewTemplate.stylesheet) {
    110        addDynamicStylesheet(this.window, viewTemplate.stylesheet);
    111      }
    112    }
    113  }
    114 
    115  get oneOffSearchButtons() {
    116    if (this.input.sapName != "urlbar") {
    117      return null;
    118    }
    119    if (!this.#oneOffSearchButtons) {
    120      this.#oneOffSearchButtons = new lazy.UrlbarSearchOneOffs(this);
    121      this.#oneOffSearchButtons.addEventListener(
    122        "SelectedOneOffButtonChanged",
    123        this
    124      );
    125    }
    126    return this.#oneOffSearchButtons;
    127  }
    128 
    129  /**
    130   * Whether the panel is open.
    131   *
    132   * @returns {boolean}
    133   */
    134  get isOpen() {
    135    return this.input.hasAttribute("open");
    136  }
    137 
    138  get allowEmptySelection() {
    139    let { heuristicResult } = this.#queryContext || {};
    140    return !heuristicResult || !this.#shouldShowHeuristic(heuristicResult);
    141  }
    142 
    143  get selectedRowIndex() {
    144    if (!this.isOpen) {
    145      return -1;
    146    }
    147 
    148    let selectedRow = this.#getSelectedRow();
    149 
    150    if (!selectedRow) {
    151      return -1;
    152    }
    153 
    154    return selectedRow.result.rowIndex;
    155  }
    156 
    157  set selectedRowIndex(val) {
    158    if (!this.isOpen) {
    159      throw new Error(
    160        "UrlbarView: Cannot select an item if the view isn't open."
    161      );
    162    }
    163 
    164    if (val < 0) {
    165      this.#selectElement(null);
    166      return;
    167    }
    168 
    169    let items = Array.from(this.#rows.children).filter(r =>
    170      this.#isElementVisible(r)
    171    );
    172    if (val >= items.length) {
    173      throw new Error(`UrlbarView: Index ${val} is out of bounds.`);
    174    }
    175 
    176    // Select the first selectable element inside the row. If it doesn't
    177    // contain a selectable element, clear the selection.
    178    let row = items[val];
    179    let element = this.#getNextSelectableElement(row);
    180    if (this.#getRowFromElement(element) != row) {
    181      element = null;
    182    }
    183 
    184    this.#selectElement(element);
    185  }
    186 
    187  get selectedElementIndex() {
    188    if (!this.isOpen || !this.#selectedElement) {
    189      return -1;
    190    }
    191 
    192    return this.#selectedElement.elementIndex;
    193  }
    194 
    195  /**
    196   * @returns {UrlbarResult}
    197   *   The currently selected result.
    198   */
    199  get selectedResult() {
    200    if (!this.isOpen) {
    201      return null;
    202    }
    203 
    204    return this.#getSelectedRow()?.result;
    205  }
    206 
    207  /**
    208   * @returns {HTMLElement}
    209   *   The currently selected element.
    210   */
    211  get selectedElement() {
    212    if (!this.isOpen) {
    213      return null;
    214    }
    215 
    216    return this.#selectedElement;
    217  }
    218 
    219  /**
    220   * @returns {ProvidersManager}
    221   */
    222  get #providersManager() {
    223    return this.controller.manager;
    224  }
    225 
    226  /**
    227   * @returns {boolean}
    228   *   Whether the SPACE key should activate the selected element (if any)
    229   *   instead of adding to the input value.
    230   */
    231  shouldSpaceActivateSelectedElement() {
    232    // We want SPACE to activate buttons only.
    233    if (this.selectedElement?.getAttribute("role") != "button") {
    234      return false;
    235    }
    236    // Make sure the input field is empty, otherwise the user might want to add
    237    // a space to the current search string. As it stands, selecting a button
    238    // should always clear the input field, so this is just an extra safeguard.
    239    if (this.input.value) {
    240      return false;
    241    }
    242    return true;
    243  }
    244 
    245  /**
    246   * Clears selection, regardless of view status.
    247   */
    248  clearSelection() {
    249    this.#selectElement(null, { updateInput: false });
    250  }
    251 
    252  /**
    253   * @returns {number}
    254   *   The number of visible results in the view.  Note that this may be larger
    255   *   than the number of results in the current query context since the view
    256   *   may be showing stale results.
    257   */
    258  get visibleRowCount() {
    259    let sum = 0;
    260    for (let row of this.#rows.children) {
    261      sum += Number(this.#isElementVisible(row));
    262    }
    263    return sum;
    264  }
    265 
    266  /**
    267   * Returns the result of the row containing the given element, or the result
    268   * of the element if it itself is a row.
    269   *
    270   * @param {Element} element
    271   *   An element in the view.
    272   * @returns {UrlbarResult}
    273   *   The result of the element's row.
    274   */
    275  getResultFromElement(element) {
    276    return element?.classList.contains("urlbarView-result-menuitem")
    277      ? this.#resultMenuResult
    278      : this.#getRowFromElement(element)?.result;
    279  }
    280 
    281  /**
    282   * @param {number} index
    283   *   The index from which to fetch the result.
    284   * @returns {UrlbarResult}
    285   *   The result at `index`. Null if the view is closed or if there are no
    286   *   results.
    287   */
    288  getResultAtIndex(index) {
    289    if (
    290      !this.isOpen ||
    291      !this.#rows.children.length ||
    292      index >= this.#rows.children.length
    293    ) {
    294      return null;
    295    }
    296 
    297    return this.#rows.children[index].result;
    298  }
    299 
    300  /**
    301   * @param {UrlbarResult} result A result.
    302   * @returns {boolean} True if the given result is selected.
    303   */
    304  resultIsSelected(result) {
    305    if (this.selectedRowIndex < 0) {
    306      return false;
    307    }
    308 
    309    return result.rowIndex == this.selectedRowIndex;
    310  }
    311 
    312  /**
    313   * Moves the view selection forward or backward.
    314   *
    315   * @param {number} amount
    316   *   The number of steps to move.
    317   * @param {object} options Options object
    318   * @param {boolean} [options.reverse]
    319   *   Set to true to select the previous item. By default the next item
    320   *   will be selected.
    321   * @param {boolean} [options.userPressedTab]
    322   *   Set to true if the user pressed Tab to select a result. Default false.
    323   */
    324  selectBy(amount, { reverse = false, userPressedTab = false } = {}) {
    325    if (!this.isOpen) {
    326      throw new Error(
    327        "UrlbarView: Cannot select an item if the view isn't open."
    328      );
    329    }
    330 
    331    // Freeze results as the user is interacting with them, unless we are
    332    // deferring events while waiting for critical results.
    333    if (!this.input.eventBufferer.isDeferringEvents) {
    334      this.controller.cancelQuery();
    335    }
    336 
    337    if (!userPressedTab) {
    338      let { selectedRowIndex } = this;
    339      let end = this.visibleRowCount - 1;
    340      if (selectedRowIndex == -1) {
    341        this.selectedRowIndex = reverse ? end : 0;
    342        return;
    343      }
    344      let endReached = selectedRowIndex == (reverse ? 0 : end);
    345      if (endReached) {
    346        if (this.allowEmptySelection) {
    347          this.#selectElement(null);
    348        } else {
    349          this.selectedRowIndex = reverse ? end : 0;
    350        }
    351        return;
    352      }
    353 
    354      let index = Math.min(end, selectedRowIndex + amount * (reverse ? -1 : 1));
    355      // When navigating with arrow keys we skip rows that contain
    356      // global actions.
    357      if (
    358        this.#rows.children[index]?.result.providerName ==
    359          "UrlbarProviderGlobalActions" &&
    360        this.#rows.children.length > 2
    361      ) {
    362        index = index + (reverse ? -1 : 1);
    363      }
    364      this.selectedRowIndex = Math.max(0, index);
    365      return;
    366    }
    367 
    368    // Tab key handling below.
    369 
    370    // Do not set aria-activedescendant if the user is moving to a
    371    // tab-to-search result with the Tab key. If
    372    // accessibility.tabToSearch.announceResults is set, the tab-to-search
    373    // result was announced to the user as they typed. We don't set
    374    // aria-activedescendant so the user doesn't think they have to press
    375    // Enter to enter search mode. See bug 1647929.
    376    const isSkippableTabToSearchAnnounce = selectedElt => {
    377      let result = this.getResultFromElement(selectedElt);
    378      let skipAnnouncement =
    379        result?.providerName == "UrlbarProviderTabToSearch" &&
    380        !this.#announceTabToSearchOnSelection &&
    381        lazy.UrlbarPrefs.get("accessibility.tabToSearch.announceResults");
    382      if (skipAnnouncement) {
    383        // Once we skip setting aria-activedescendant once, we should not skip
    384        // it again if the user returns to that result.
    385        this.#announceTabToSearchOnSelection = true;
    386      }
    387      return skipAnnouncement;
    388    };
    389 
    390    let selectedElement = this.#selectedElement;
    391 
    392    // We cache the first and last rows since they will not change while
    393    // selectBy is running.
    394    let firstSelectableElement = this.getFirstSelectableElement();
    395    // getLastSelectableElement will not return an element that is over
    396    // maxResults and thus may be hidden and not selectable.
    397    let lastSelectableElement = this.getLastSelectableElement();
    398 
    399    if (!selectedElement) {
    400      selectedElement = reverse
    401        ? lastSelectableElement
    402        : firstSelectableElement;
    403      this.#selectElement(selectedElement, {
    404        setAccessibleFocus: !isSkippableTabToSearchAnnounce(selectedElement),
    405      });
    406      return;
    407    }
    408    let endReached = reverse
    409      ? selectedElement == firstSelectableElement
    410      : selectedElement == lastSelectableElement;
    411    if (endReached) {
    412      if (this.allowEmptySelection) {
    413        selectedElement = null;
    414      } else {
    415        selectedElement = reverse
    416          ? lastSelectableElement
    417          : firstSelectableElement;
    418      }
    419      this.#selectElement(selectedElement, {
    420        setAccessibleFocus: !isSkippableTabToSearchAnnounce(selectedElement),
    421      });
    422      return;
    423    }
    424 
    425    while (amount-- > 0) {
    426      let next = reverse
    427        ? this.#getPreviousSelectableElement(selectedElement)
    428        : this.#getNextSelectableElement(selectedElement);
    429      if (!next) {
    430        break;
    431      }
    432      selectedElement = next;
    433    }
    434    this.#selectElement(selectedElement, {
    435      setAccessibleFocus: !isSkippableTabToSearchAnnounce(selectedElement),
    436    });
    437  }
    438 
    439  async acknowledgeFeedback(result) {
    440    let row = this.#rows.children[result.rowIndex];
    441    if (!row) {
    442      return;
    443    }
    444 
    445    let l10n = { id: "urlbar-feedback-acknowledgment" };
    446    await this.#l10nCache.ensure(l10n);
    447    if (row.result != result) {
    448      return;
    449    }
    450 
    451    let { value } = this.#l10nCache.get(l10n);
    452    row.setAttribute("feedback-acknowledgment", value);
    453    this.window.A11yUtils.announce({
    454      raw: value,
    455      source: row._content.closest("[role=option]"),
    456    });
    457  }
    458 
    459  /**
    460   * Replaces the given result's row with a dismissal-acknowledgment tip.
    461   *
    462   * @param {UrlbarResult} result
    463   *   The result that was dismissed.
    464   * @param {object} titleL10n
    465   *   The localization object shown as dismissed feedback.
    466   */
    467  #acknowledgeDismissal(result, titleL10n) {
    468    let row = this.#rows.children[result.rowIndex];
    469    if (!row || row.result != result) {
    470      return;
    471    }
    472 
    473    // The row is no longer selectable. It's necessary to clear the selection
    474    // before replacing the row because replacement will likely create a new
    475    // `urlbarView-row-inner`, which will interfere with the ability of
    476    // `#selectElement()` to clear the old selection after replacement, below.
    477    let isSelected = this.#getSelectedRow() == row;
    478    if (isSelected) {
    479      this.#selectElement(null, { updateInput: false });
    480    }
    481    this.#setRowSelectable(row, false);
    482 
    483    // Replace the row with a dismissal acknowledgment tip.
    484    let tip = new lazy.UrlbarResult({
    485      type: lazy.UrlbarUtils.RESULT_TYPE.TIP,
    486      source: lazy.UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
    487      payload: {
    488        type: "dismissalAcknowledgment",
    489        titleL10n,
    490        buttons: [{ l10n: { id: "urlbar-search-tips-confirm-short" } }],
    491        icon: "chrome://branding/content/icon32.png",
    492      },
    493      rowLabel: !result.hideRowLabel && this.#rowLabel(row),
    494      hideRowLabel: result.hideRowLabel,
    495      richSuggestionIconSize: 32,
    496    });
    497    this.#updateRow(row, tip);
    498    this.#updateIndices();
    499 
    500    // If the row was selected, move the selection to the tip button.
    501    if (isSelected) {
    502      this.#selectElement(this.#getNextSelectableElement(row), {
    503        updateInput: false,
    504      });
    505    }
    506  }
    507 
    508  removeAccessibleFocus() {
    509    this.#setAccessibleFocus(null);
    510  }
    511 
    512  clear() {
    513    this.#rows.textContent = "";
    514    this.panel.setAttribute("noresults", "true");
    515    this.clearSelection();
    516    this.visibleResults = [];
    517  }
    518 
    519  /**
    520   * Closes the view, cancelling the query if necessary.
    521   *
    522   * @param {object} options Options object
    523   * @param {boolean} [options.elementPicked]
    524   *   True if the view is being closed because a result was picked.
    525   * @param {boolean} [options.showFocusBorder]
    526   *   True if the Urlbar focus border should be shown after the view is closed.
    527   */
    528  close({ elementPicked = false, showFocusBorder = true } = {}) {
    529    const isShowingZeroPrefix =
    530      this.#queryContext && !this.#queryContext.searchString;
    531    this.controller.cancelQuery();
    532    // We do not show the focus border when an element is picked because we'd
    533    // flash it just before the input is blurred. The focus border is removed
    534    // in UrlbarInput._on_blur.
    535    if (!elementPicked && showFocusBorder) {
    536      this.input.removeAttribute("suppress-focus-border");
    537    }
    538 
    539    if (!this.isOpen) {
    540      return;
    541    }
    542 
    543    this.#inputWidthOnLastClose = getBoundsWithoutFlushing(this.input).width;
    544 
    545    // We exit search mode preview on close since the result previewing it is
    546    // implicitly unselected.
    547    if (this.input.searchMode?.isPreview) {
    548      this.input.searchMode = null;
    549      this.window.gBrowser.userTypedValue = null;
    550    }
    551 
    552    this.resultMenu.hidePopup();
    553    this.removeAccessibleFocus();
    554    this.input.inputField.setAttribute("aria-expanded", "false");
    555    this.#openPanelInstance = null;
    556    this.#previousTabToSearchEngine = null;
    557 
    558    this.input.removeAttribute("open");
    559    this.input.endLayoutExtend();
    560 
    561    // Search Tips can open the view without the Urlbar being focused. If the
    562    // tip is ignored (e.g. the page content is clicked or the window loses
    563    // focus) we should discard the telemetry event created when the view was
    564    // opened.
    565    if (!this.input.focused && !elementPicked) {
    566      this.controller.engagementEvent.discard();
    567      this.controller.engagementEvent.record(null, {});
    568    }
    569 
    570    this.window.removeEventListener("resize", this);
    571    this.window.removeEventListener("blur", this);
    572 
    573    this.controller.notify(this.controller.NOTIFICATIONS.VIEW_CLOSE);
    574 
    575    // Revoke icon blob URLs that were created while the view was open.
    576    if (this.#blobUrlsByResultUrl) {
    577      for (let blobUrl of this.#blobUrlsByResultUrl.values()) {
    578        URL.revokeObjectURL(blobUrl);
    579      }
    580      this.#blobUrlsByResultUrl.clear();
    581    }
    582 
    583    if (isShowingZeroPrefix) {
    584      if (elementPicked) {
    585        Glean.urlbarZeroprefix.engagement.add(1);
    586      } else {
    587        Glean.urlbarZeroprefix.abandonment.add(1);
    588      }
    589    }
    590  }
    591 
    592  /**
    593   * This can be used to open the view automatically as a consequence of
    594   * specific user actions. For Top Sites searches (without a search string)
    595   * the view is opened only for mouse or keyboard interactions.
    596   * If the user abandoned a search (there is a search string) the view is
    597   * reopened, and we try to use cached results to reduce flickering, then a new
    598   * query is started to refresh results.
    599   *
    600   * @param {object} options Options object
    601   * @param {Event} options.event The event associated with the call to autoOpen.
    602   * @param {boolean} [options.suppressFocusBorder] If true, we hide the focus border
    603   *        when the panel is opened. This is true by default to avoid flashing
    604   *        the border when the unfocused address bar is clicked.
    605   * @returns {boolean} Whether the view was opened.
    606   */
    607  autoOpen({ event, suppressFocusBorder = true }) {
    608    if (this.#pickSearchTipIfPresent(event)) {
    609      return false;
    610    }
    611 
    612    if (!event) {
    613      return false;
    614    }
    615 
    616    let queryOptions = { event };
    617    if (
    618      !this.input.value ||
    619      this.input.getAttribute("pageproxystate") == "valid"
    620    ) {
    621      if (!this.isOpen && ["mousedown", "command"].includes(event.type)) {
    622        // Try to reuse the cached top-sites context. If it's not cached, then
    623        // there will be a gap of time between when the input is focused and
    624        // when the view opens that can be perceived as flicker.
    625        if (!this.input.searchMode && this.queryContextCache.topSitesContext) {
    626          this.onQueryResults(this.queryContextCache.topSitesContext);
    627        }
    628        this.input.startQuery(queryOptions);
    629        if (suppressFocusBorder) {
    630          this.input.toggleAttribute("suppress-focus-border", true);
    631        }
    632        return true;
    633      }
    634      return false;
    635    }
    636 
    637    // Reopen abandoned searches only if the input is focused.
    638    if (!this.input.focused) {
    639      return false;
    640    }
    641 
    642    // Tab switch is the only case where we requery if the view is open, because
    643    // switching tabs doesn't necessarily close the view.
    644    if (this.isOpen && event.type != "tabswitch") {
    645      return false;
    646    }
    647 
    648    // We can reuse the current rows as they are if the input value and width
    649    // haven't changed since the view was closed. The width check is related to
    650    // row overflow: If we reuse the current rows, overflow and underflow events
    651    // won't fire even if the view's width has changed and there are rows that
    652    // do actually overflow or underflow. That means previously overflowed rows
    653    // may unnecessarily show the overflow gradient, for example.
    654    if (
    655      this.#rows.firstElementChild &&
    656      this.#queryContext.searchString == this.input.value &&
    657      this.#inputWidthOnLastClose == getBoundsWithoutFlushing(this.input).width
    658    ) {
    659      // We can reuse the current rows.
    660      queryOptions.allowAutofill = this.#queryContext.allowAutofill;
    661    } else {
    662      // To reduce flickering, try to reuse a cached UrlbarQueryContext. The
    663      // overflow problem is addressed in this case because `onQueryResults()`
    664      // starts the regular view-update process, during which the overflow state
    665      // is reset on all rows.
    666      let cachedQueryContext = this.queryContextCache.get(this.input.value);
    667      if (cachedQueryContext) {
    668        this.onQueryResults(cachedQueryContext);
    669      }
    670    }
    671 
    672    // Disable autofill when search terms persist, as users are likely refining
    673    // their search rather than navigating to a website matching the search
    674    // term. If they do want to navigate directly, users can modify their
    675    // search, which resets persistence and re-enables autofill.
    676    let state = this.input.getBrowserState(
    677      this.window.gBrowser.selectedBrowser
    678    );
    679    if (state.persist?.shouldPersist) {
    680      queryOptions.allowAutofill = false;
    681    }
    682 
    683    this.controller.engagementEvent.discard();
    684    queryOptions.searchString = this.input.value;
    685    queryOptions.autofillIgnoresSelection = true;
    686    queryOptions.event.interactionType = "returned";
    687 
    688    // Opening the panel now will show the rows from the previous query, so to
    689    // avoid flicker, open it only if the search string hasn't changed. Also
    690    // check for a tip to avoid search tip flicker (bug 1812261). If we don't
    691    // open the panel here, we'll open it when the view receives results from
    692    // the new query.
    693    if (
    694      this.#queryContext?.results?.length &&
    695      this.#queryContext.searchString == this.input.value &&
    696      this.#queryContext.results[0].type != lazy.UrlbarUtils.RESULT_TYPE.TIP
    697    ) {
    698      this.#openPanel();
    699    }
    700 
    701    // If we had cached results, this will just refresh them, avoiding results
    702    // flicker, otherwise there may be some noise.
    703    this.input.startQuery(queryOptions);
    704    if (suppressFocusBorder) {
    705      this.input.toggleAttribute("suppress-focus-border", true);
    706    }
    707    return true;
    708  }
    709 
    710  // UrlbarController listener methods.
    711  onQueryStarted(queryContext) {
    712    this.#queryWasCancelled = false;
    713    this.#queryUpdatedResults = false;
    714    this.#openPanelInstance = null;
    715    if (!queryContext.searchString) {
    716      this.#previousTabToSearchEngine = null;
    717    }
    718    this.#startRemoveStaleRowsTimer();
    719 
    720    // Cache l10n strings so they're available when we update the view as
    721    // results arrive. This is a no-op for strings that are already cached.
    722    // `#cacheL10nStrings` is async but we don't await it because doing so would
    723    // require view updates to be async. Instead we just opportunistically cache
    724    // and if there's a cache miss we fall back to `l10n.setAttributes`.
    725    this.#cacheL10nStrings();
    726  }
    727 
    728  onQueryCancelled() {
    729    this.#queryWasCancelled = true;
    730    this.#cancelRemoveStaleRowsTimer();
    731  }
    732 
    733  onQueryFinished(queryContext) {
    734    this.#cancelRemoveStaleRowsTimer();
    735    if (this.#queryWasCancelled) {
    736      return;
    737    }
    738 
    739    // At this point the query finished successfully. If it returned some
    740    // results, remove stale rows. Otherwise remove all rows.
    741    if (this.#queryUpdatedResults) {
    742      this.#removeStaleRows();
    743    } else {
    744      this.clear();
    745    }
    746 
    747    // Now that the view has finished updating for this query, record the exposure.
    748    if (!queryContext.searchString) {
    749      Glean.urlbarZeroprefix.exposure.add(1);
    750    }
    751 
    752    // If the query returned results, we're done.
    753    if (this.#queryUpdatedResults) {
    754      return;
    755    }
    756 
    757    // If search mode isn't active, close the view.
    758    if (!this.input.searchMode) {
    759      this.close();
    760      return;
    761    }
    762 
    763    // Search mode is active.  If the one-offs should be shown, make sure they
    764    // are enabled and show the view.
    765    let openPanelInstance = (this.#openPanelInstance = {});
    766    this.oneOffSearchButtons?.willHide().then(willHide => {
    767      if (!willHide && openPanelInstance == this.#openPanelInstance) {
    768        this.oneOffSearchButtons.enable(true);
    769        this.#openPanel();
    770      }
    771    });
    772  }
    773 
    774  onQueryResults(queryContext) {
    775    this.queryContextCache.put(queryContext);
    776    this.#queryContext = queryContext;
    777 
    778    if (!this.isOpen) {
    779      this.clear();
    780    }
    781 
    782    // Set the actionmode atttribute if we are in actions search mode.
    783    // We do this before updating the result rows so that there is no flicker
    784    // after the actions are initially displayed.
    785    if (
    786      this.input.searchMode?.source == lazy.UrlbarUtils.RESULT_SOURCE.ACTIONS
    787    ) {
    788      this.#rows.toggleAttribute("actionmode", true);
    789    }
    790 
    791    this.#queryUpdatedResults = true;
    792    this.#updateResults();
    793 
    794    let firstResult = queryContext.results[0];
    795 
    796    if (queryContext.lastResultCount == 0) {
    797      // Clear the selection when we get a new set of results.
    798      this.#selectElement(null, {
    799        updateInput: false,
    800      });
    801 
    802      // Show the one-off search buttons unless any of the following are true:
    803      //  * The first result is a search tip
    804      //  * The search string is empty
    805      //  * The search string starts with an `@` or a search restriction
    806      //    character
    807      this.oneOffSearchButtons?.enable(
    808        (firstResult.providerName != "UrlbarProviderSearchTips" ||
    809          queryContext.trimmedSearchString) &&
    810          queryContext.trimmedSearchString[0] != "@" &&
    811          (queryContext.trimmedSearchString[0] !=
    812            lazy.UrlbarTokenizer.RESTRICT.SEARCH ||
    813            queryContext.trimmedSearchString.length != 1)
    814      );
    815    }
    816 
    817    if (!this.#selectedElement && !this.oneOffSearchButtons?.selectedButton) {
    818      if (firstResult.heuristic) {
    819        // Select the heuristic result.  The heuristic may not be the first
    820        // result added, which is why we do this check here when each result is
    821        // added and not above.
    822        if (this.#shouldShowHeuristic(firstResult)) {
    823          this.#selectElement(this.getFirstSelectableElement(), {
    824            updateInput: false,
    825            setAccessibleFocus:
    826              this.controller._userSelectionBehavior == "arrow",
    827          });
    828        } else {
    829          this.input.setResultForCurrentValue(firstResult);
    830        }
    831      } else if (
    832        firstResult.payload.providesSearchMode &&
    833        queryContext.trimmedSearchString != "@"
    834      ) {
    835        // Filtered keyword offer results can be in the first position but not
    836        // be heuristic results. We do this so the user can press Tab to select
    837        // them, resembling tab-to-search. In that case, the input value is
    838        // still associated with the first result.
    839        this.input.setResultForCurrentValue(firstResult);
    840      }
    841    }
    842 
    843    // Announce tab-to-search results to screen readers as the user types.
    844    // Check to make sure we don't announce the same engine multiple times in
    845    // a row.
    846    let secondResult = queryContext.results[1];
    847    if (
    848      secondResult?.providerName == "UrlbarProviderTabToSearch" &&
    849      lazy.UrlbarPrefs.get("accessibility.tabToSearch.announceResults") &&
    850      this.#previousTabToSearchEngine != secondResult.payload.engine
    851    ) {
    852      let engine = secondResult.payload.engine;
    853      this.window.A11yUtils.announce({
    854        id: secondResult.payload.isGeneralPurposeEngine
    855          ? "urlbar-result-action-before-tabtosearch-web"
    856          : "urlbar-result-action-before-tabtosearch-other",
    857        args: { engine },
    858      });
    859      this.#previousTabToSearchEngine = engine;
    860      // Do not set aria-activedescendant when the user tabs to the result
    861      // because we already announced it.
    862      this.#announceTabToSearchOnSelection = false;
    863    }
    864 
    865    // If we update the selected element, a new unique ID is generated for it.
    866    // We need to ensure that aria-activedescendant reflects this new ID.
    867    if (this.#selectedElement && !this.oneOffSearchButtons?.selectedButton) {
    868      let aadID = this.input.inputField.getAttribute("aria-activedescendant");
    869      if (aadID && !this.document.getElementById(aadID)) {
    870        this.#setAccessibleFocus(this.#selectedElement);
    871      }
    872    }
    873 
    874    this.#openPanel();
    875 
    876    if (firstResult.heuristic) {
    877      // The heuristic result may be a search alias result, so apply formatting
    878      // if necessary.  Conversely, the heuristic result of the previous query
    879      // may have been an alias, so remove formatting if necessary.
    880      this.input.formatValue();
    881    }
    882 
    883    if (queryContext.deferUserSelectionProviders.size) {
    884      // DeferUserSelectionProviders block user selection until the result is
    885      // shown, so it's the view's duty to remove them.
    886      // Doing it sooner, like when the results are added by the provider,
    887      // would not suffice because there's still a delay before those results
    888      // reach the view.
    889      queryContext.results.forEach(r => {
    890        queryContext.deferUserSelectionProviders.delete(r.providerName);
    891      });
    892    }
    893  }
    894 
    895  /**
    896   * Handles removing a result from the view when it is removed from the query,
    897   * and attempts to select the new result on the same row.
    898   *
    899   * This assumes that the result rows are in index order.
    900   *
    901   * @param {number} index The index of the result that has been removed.
    902   */
    903  onQueryResultRemoved(index) {
    904    let rowToRemove = this.#rows.children[index];
    905 
    906    let { result } = rowToRemove;
    907    if (result.acknowledgeDismissalL10n) {
    908      // Replace the result's row with a dismissal acknowledgment tip.
    909      this.#acknowledgeDismissal(result, result.acknowledgeDismissalL10n);
    910      return;
    911    }
    912 
    913    let updateSelection = rowToRemove == this.#getSelectedRow();
    914    rowToRemove.remove();
    915    this.#updateIndices();
    916 
    917    if (!updateSelection) {
    918      return;
    919    }
    920    // Select the row at the same index, if possible.
    921    let newSelectionIndex = index;
    922    if (index >= this.#queryContext.results.length) {
    923      newSelectionIndex = this.#queryContext.results.length - 1;
    924    }
    925    if (newSelectionIndex >= 0) {
    926      this.selectedRowIndex = newSelectionIndex;
    927    }
    928  }
    929 
    930  openResultMenu(result, anchor) {
    931    this.#resultMenuResult = result;
    932 
    933    let event = new CustomEvent("ResultMenuTriggered", {
    934      detail: { target: anchor },
    935    });
    936 
    937    if (AppConstants.platform == "macosx") {
    938      // `openPopup(anchor)` doesn't use a native context menu, which is very
    939      // noticeable on Mac. Use `openPopup()` with x and y coords instead. See
    940      // bug 1831760 and bug 1710459.
    941      let rect = getBoundsWithoutFlushing(anchor);
    942      rect = this.window.windowUtils.toScreenRectInCSSUnits(
    943        rect.x,
    944        rect.y,
    945        rect.width,
    946        rect.height
    947      );
    948      this.resultMenu.openPopup(null, {
    949        x: rect.x,
    950        y: rect.y + rect.height,
    951        triggerEvent: event,
    952      });
    953    } else {
    954      this.resultMenu.openPopup(anchor, {
    955        position: "bottomright topright",
    956        triggerEvent: event,
    957      });
    958    }
    959 
    960    anchor.toggleAttribute("open", true);
    961    let listener = event => {
    962      if (event.target == this.resultMenu) {
    963        anchor.removeAttribute("open");
    964        this.resultMenu.removeEventListener("popuphidden", listener);
    965      }
    966    };
    967    this.resultMenu.addEventListener("popuphidden", listener);
    968  }
    969 
    970  /**
    971   * Clears the result menu commands cache, removing the cached commands for all
    972   * results. This is useful when the commands for one or more results change
    973   * while the results remain in the view.
    974   */
    975  invalidateResultMenuCommands() {
    976    this.#resultMenuCommands = new WeakMap();
    977  }
    978 
    979  /**
    980   * Passes DOM events for the view to the on_<event type> methods.
    981   *
    982   * @param {Event} event
    983   *   DOM event from the <view>.
    984   */
    985  handleEvent(event) {
    986    let methodName = "on_" + event.type;
    987    if (methodName in this) {
    988      this[methodName](event);
    989    } else {
    990      throw new Error("Unrecognized UrlbarView event: " + event.type);
    991    }
    992  }
    993 
    994  static dynamicViewTemplatesByName = new Map();
    995 
    996  /**
    997   * Registers the view template for a dynamic result type.  A view template is
    998   * a plain object that describes the DOM subtree for a dynamic result type.
    999   * When a dynamic result is shown in the urlbar view, its type's view template
   1000   * is used to construct the part of the view that represents the result.
   1001   *
   1002   * The specified view template will be available to the urlbars in all current
   1003   * and future browser windows until it is unregistered.  A given dynamic
   1004   * result type has at most one view template.  If this method is called for a
   1005   * dynamic result type more than once, the view template in the last call
   1006   * overrides those in previous calls.
   1007   *
   1008   * @param {string} name
   1009   *   The view template will be registered for the dynamic result type with
   1010   *   this name.
   1011   * @param {object} viewTemplate
   1012   *   This object describes the DOM subtree for the given dynamic result type.
   1013   *   It should be a tree-like nested structure with each object in the nesting
   1014   *   representing a DOM element to be created.  This tree-like structure is
   1015   *   achieved using the `children` property described below.  Each object in
   1016   *   the structure may include the following properties:
   1017   *
   1018   *   {string} tag
   1019   *     The tag name of the object.  It is required for all objects in the
   1020   *     structure except the root object and declares the kind of element that
   1021   *     will be created for the object: span, div, img, etc.
   1022   *   {string} [name]
   1023   *     The name of the object. This value is required if you need to update
   1024   *     the object's DOM element at query time. It's also helpful but not
   1025   *     required if you need to style the element. When defined, it serves two
   1026   *     important functions:
   1027   *
   1028   *     (1) The element created for the object will automatically have a class
   1029   *         named `urlbarView-dynamic-${dynamicType}-${name}`, where
   1030   *         `dynamicType` is the name of the dynamic result type.  The element
   1031   *         will also automatically have an attribute "name" whose value is
   1032   *         this name.  The class and attribute allow the element to be styled
   1033   *         in CSS.
   1034   *     (2) The name is used when updating the view.  See
   1035   *         UrlbarProvider.getViewUpdate().
   1036   *
   1037   *     Names must be unique within a view template, but they don't need to be
   1038   *     globally unique.  i.e., two different view templates can use the same
   1039   *     names, and other DOM elements can use the same names in their IDs and
   1040   *     classes.  The name also suffixes the dynamic element's ID: an element
   1041   *     with name `data` will get the ID `urlbarView-row-{unique number}-data`.
   1042   *     If there is no name provided for the root element, the root element
   1043   *     will not get an ID.
   1044   *   {object} [attributes]
   1045   *     An optional mapping from attribute names to values.  For each
   1046   *     name-value pair, an attribute is added to the element created for the
   1047   *     object. The `id` attribute is reserved and cannot be set by the
   1048   *     provider. Element IDs are passed back to the provider in getViewUpdate
   1049   *     if they are needed.
   1050   *   {array} [children]
   1051   *     An optional list of children.  Each item in the array must be an object
   1052   *     as described here.  For each item, a child element as described by the
   1053   *     item is created and added to the element created for the parent object.
   1054   *   {array} [classList]
   1055   *     An optional list of classes.  Each class will be added to the element
   1056   *     created for the object by calling element.classList.add().
   1057   *   {boolean} [overflowable]
   1058   *     If true, the element's overflow status will be tracked in order to
   1059   *     fade it out when needed.
   1060   *   {string} [stylesheet]
   1061   *     An optional stylesheet URL.  This property is valid only on the root
   1062   *     object in the structure.  The stylesheet will be loaded in all browser
   1063   *     windows so that the dynamic result type view may be styled.
   1064   */
   1065  static addDynamicViewTemplate(name, viewTemplate) {
   1066    this.dynamicViewTemplatesByName.set(name, viewTemplate);
   1067    if (viewTemplate.stylesheet) {
   1068      for (let window of lazy.BrowserWindowTracker.orderedWindows) {
   1069        addDynamicStylesheet(window, viewTemplate.stylesheet);
   1070      }
   1071    }
   1072  }
   1073 
   1074  /**
   1075   * Unregisters the view template for a dynamic result type.
   1076   *
   1077   * @param {string} name
   1078   *   The view template will be unregistered for the dynamic result type with
   1079   *   this name.
   1080   */
   1081  static removeDynamicViewTemplate(name) {
   1082    let viewTemplate = this.dynamicViewTemplatesByName.get(name);
   1083    if (!viewTemplate) {
   1084      return;
   1085    }
   1086    this.dynamicViewTemplatesByName.delete(name);
   1087    if (viewTemplate.stylesheet) {
   1088      for (let window of lazy.BrowserWindowTracker.orderedWindows) {
   1089        removeDynamicStylesheet(window, viewTemplate.stylesheet);
   1090      }
   1091    }
   1092  }
   1093 
   1094  // Private properties and methods below.
   1095  #announceTabToSearchOnSelection;
   1096  #blobUrlsByResultUrl = null;
   1097  #inputWidthOnLastClose = 0;
   1098  #l10nCache;
   1099  #mousedownSelectedElement;
   1100  #openPanelInstance;
   1101  #oneOffSearchButtons;
   1102  #previousTabToSearchEngine;
   1103  #queryContext;
   1104  #queryUpdatedResults;
   1105  #queryWasCancelled;
   1106  #removeStaleRowsTimer;
   1107  #resultMenuResult;
   1108  #resultMenuCommands;
   1109  #rows;
   1110  #rawSelectedElement;
   1111 
   1112  /**
   1113   * #rawSelectedElement may be disconnected from the DOM (e.g. it was remove()d)
   1114   * but we want a connected #selectedElement usually. We don't use a WeakRef
   1115   * because it would depend too much on GC timing.
   1116   *
   1117   * @returns {HTMLElement} the selected element.
   1118   */
   1119  get #selectedElement() {
   1120    return this.#rawSelectedElement?.isConnected
   1121      ? this.#rawSelectedElement
   1122      : null;
   1123  }
   1124 
   1125  #createElement(name) {
   1126    return this.document.createElementNS("http://www.w3.org/1999/xhtml", name);
   1127  }
   1128 
   1129  #openPanel() {
   1130    if (this.isOpen) {
   1131      return;
   1132    }
   1133    this.controller.userSelectionBehavior = "none";
   1134 
   1135    this.panel.removeAttribute("action-override");
   1136 
   1137    this.#enableOrDisableRowWrap();
   1138 
   1139    this.input.inputField.setAttribute("aria-expanded", "true");
   1140 
   1141    this.input.toggleAttribute("suppress-focus-border", true);
   1142    this.input.toggleAttribute("open", true);
   1143    this.input.startLayoutExtend();
   1144 
   1145    this.window.addEventListener("resize", this);
   1146    this.window.addEventListener("blur", this);
   1147 
   1148    this.controller.notify(this.controller.NOTIFICATIONS.VIEW_OPEN);
   1149 
   1150    if (lazy.UrlbarPrefs.get("closeOtherPanelsOnOpen")) {
   1151      this.window.docShell.treeOwner
   1152        .QueryInterface(Ci.nsIInterfaceRequestor)
   1153        .getInterface(Ci.nsIAppWindow)
   1154        .rollupAllPopups();
   1155    }
   1156  }
   1157 
   1158  #shouldShowHeuristic(result) {
   1159    if (!result?.heuristic) {
   1160      throw new Error("A heuristic result must be given");
   1161    }
   1162    return (
   1163      !lazy.UrlbarPrefs.get("experimental.hideHeuristic") ||
   1164      result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP
   1165    );
   1166  }
   1167 
   1168  /**
   1169   * Whether a result is a search suggestion.
   1170   *
   1171   * @param {UrlbarResult} result The result to examine.
   1172   * @returns {boolean} Whether the result is a search suggestion.
   1173   */
   1174  #resultIsSearchSuggestion(result) {
   1175    return Boolean(
   1176      result &&
   1177        result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH &&
   1178        result.payload.suggestion
   1179    );
   1180  }
   1181 
   1182  /**
   1183   * Checks whether the given row index can be update to the result we want
   1184   * to apply. This is used in #updateResults to avoid flickering of results, by
   1185   * reusing existing rows.
   1186   *
   1187   * @param {number} rowIndex Index of the row to examine.
   1188   * @param {UrlbarResult} result The result we'd like to apply.
   1189   * @param {boolean} seenSearchSuggestion Whether the view update has
   1190   *        encountered an existing row with a search suggestion result.
   1191   * @returns {boolean} Whether the row can be updated to this result.
   1192   */
   1193  #rowCanUpdateToResult(rowIndex, result, seenSearchSuggestion) {
   1194    // The heuristic result must always be current, thus it's always compatible.
   1195    // Note that the `updateResults` code, when updating the selection, relies
   1196    // on the fact the heuristic is the first selectable row.
   1197    if (result.heuristic) {
   1198      return true;
   1199    }
   1200    let row = this.#rows.children[rowIndex];
   1201    // Don't replace a suggestedIndex result with a non-suggestedIndex result
   1202    // or vice versa.
   1203    if (result.hasSuggestedIndex != row.result.hasSuggestedIndex) {
   1204      return false;
   1205    }
   1206    // Don't replace a suggestedIndex result with another suggestedIndex
   1207    // result if the suggestedIndex values are different.
   1208    if (
   1209      result.hasSuggestedIndex &&
   1210      result.suggestedIndex != row.result.suggestedIndex
   1211    ) {
   1212      return false;
   1213    }
   1214    // To avoid flickering results while typing, don't try to reuse results from
   1215    // different providers.
   1216    // For example user types "moz", provider A returns results much earlier
   1217    // than provider B, but results from provider B stabilize in the view at the
   1218    // end of the search. Typing the next letter "i" results from the faster
   1219    // provider A would temporarily replace old results from provider B, just
   1220    // to be replaced as soon as provider B returns its results.
   1221    if (result.providerName != row.result.providerName) {
   1222      return false;
   1223    }
   1224    let resultIsSearchSuggestion = this.#resultIsSearchSuggestion(result);
   1225    // If the row is same type, just update it.
   1226    if (
   1227      resultIsSearchSuggestion == this.#resultIsSearchSuggestion(row.result)
   1228    ) {
   1229      return true;
   1230    }
   1231    // If the row has a different type, update it if we are in a compatible
   1232    // index range.
   1233    // In practice we don't want to overwrite a search suggestion with a non
   1234    // search suggestion, but we allow the opposite.
   1235    return resultIsSearchSuggestion && seenSearchSuggestion;
   1236  }
   1237 
   1238  #updateResults() {
   1239    // TODO: For now this just compares search suggestions to the rest, in the
   1240    // future we should make it support any type of result. Or, even better,
   1241    // results should be grouped, thus we can directly update groups.
   1242 
   1243    // Discard tentative exposures. This is analogous to marking the
   1244    // hypothetical hidden rows of hidden-exposure results as stale.
   1245    this.controller.engagementEvent.discardTentativeExposures();
   1246 
   1247    // Walk rows and find an insertion index for results. To avoid flicker, we
   1248    // skip rows until we find one compatible with the result we want to apply.
   1249    // If we couldn't find a compatible range, we'll just update.
   1250    let results = this.#queryContext.results;
   1251    if (results[0]?.heuristic && !this.#shouldShowHeuristic(results[0])) {
   1252      // Exclude the heuristic.
   1253      results = results.slice(1);
   1254    }
   1255    let rowIndex = 0;
   1256    // Make a copy of results, as we'll consume it along the way.
   1257    let resultsToInsert = results.slice();
   1258    let visibleSpanCount = 0;
   1259    let seenMisplacedResult = false;
   1260    let seenSearchSuggestion = false;
   1261 
   1262    // Update each row with the next new result until we either encounter a row
   1263    // that can't be updated or run out of new results. At that point, mark
   1264    // remaining rows as stale.
   1265    while (rowIndex < this.#rows.children.length && resultsToInsert.length) {
   1266      let row = this.#rows.children[rowIndex];
   1267      if (this.#isElementVisible(row)) {
   1268        visibleSpanCount += lazy.UrlbarUtils.getSpanForResult(row.result);
   1269      }
   1270 
   1271      if (!seenMisplacedResult) {
   1272        let result = resultsToInsert[0];
   1273        seenSearchSuggestion =
   1274          seenSearchSuggestion ||
   1275          (!row.result.heuristic && this.#resultIsSearchSuggestion(row.result));
   1276        if (
   1277          this.#rowCanUpdateToResult(rowIndex, result, seenSearchSuggestion)
   1278        ) {
   1279          // We can replace the row's current result with the new one.
   1280          resultsToInsert.shift();
   1281 
   1282          if (result.isHiddenExposure) {
   1283            // Don't increment `rowIndex` because we're not actually updating
   1284            // the row. We'll visit it again in the next iteration.
   1285            this.controller.engagementEvent.addExposure(
   1286              result,
   1287              this.#queryContext
   1288            );
   1289            continue;
   1290          }
   1291 
   1292          this.#updateRow(row, result);
   1293          rowIndex++;
   1294          continue;
   1295        }
   1296 
   1297        if (
   1298          (result.hasSuggestedIndex || row.result.hasSuggestedIndex) &&
   1299          !result.isHiddenExposure
   1300        ) {
   1301          seenMisplacedResult = true;
   1302        }
   1303      }
   1304 
   1305      row.setAttribute("stale", "true");
   1306      rowIndex++;
   1307    }
   1308 
   1309    // Mark all the remaining rows as stale and update the visible span count.
   1310    // We include stale rows in the count because we should never show more than
   1311    // maxResults spans at one time.  Later we'll remove stale rows and unhide
   1312    // excess non-stale rows.
   1313    for (; rowIndex < this.#rows.children.length; ++rowIndex) {
   1314      let row = this.#rows.children[rowIndex];
   1315      row.setAttribute("stale", "true");
   1316      if (this.#isElementVisible(row)) {
   1317        visibleSpanCount += lazy.UrlbarUtils.getSpanForResult(row.result);
   1318      }
   1319    }
   1320 
   1321    // Add remaining results, if we have fewer rows than results.
   1322    for (let result of resultsToInsert) {
   1323      if (
   1324        !seenMisplacedResult &&
   1325        result.hasSuggestedIndex &&
   1326        !result.isHiddenExposure
   1327      ) {
   1328        if (result.isSuggestedIndexRelativeToGroup) {
   1329          // We can't know at this point what the right index of a group-
   1330          // relative suggestedIndex result will be. To avoid all all possible
   1331          // flicker, don't make it (and all rows after it) visible until stale
   1332          // rows are removed.
   1333          seenMisplacedResult = true;
   1334        } else {
   1335          // We need to check whether the new suggestedIndex result will end up
   1336          // at its right index if we append it here. The "right" index is the
   1337          // final index the result will occupy once the update is done and all
   1338          // stale rows have been removed. We could use a more flexible
   1339          // definition, but we use this strict one in order to avoid all
   1340          // perceived flicker and movement of suggestedIndex results. Once
   1341          // stale rows are removed, the final number of rows in the view will
   1342          // be the new result count, so we base our arithmetic here on it.
   1343          let finalIndex =
   1344            result.suggestedIndex >= 0
   1345              ? Math.min(results.length - 1, result.suggestedIndex)
   1346              : Math.max(0, results.length + result.suggestedIndex);
   1347          if (this.#rows.children.length != finalIndex) {
   1348            seenMisplacedResult = true;
   1349          }
   1350        }
   1351      }
   1352      let newSpanCount =
   1353        visibleSpanCount +
   1354        lazy.UrlbarUtils.getSpanForResult(result, {
   1355          includeHiddenExposures: true,
   1356        });
   1357      let canBeVisible =
   1358        newSpanCount <= this.#queryContext.maxResults && !seenMisplacedResult;
   1359      if (result.isHiddenExposure) {
   1360        if (canBeVisible) {
   1361          this.controller.engagementEvent.addExposure(
   1362            result,
   1363            this.#queryContext
   1364          );
   1365        } else {
   1366          // Add a tentative exposure: The hypothetical row for this
   1367          // hidden-exposure result can't be visible now, but as long as it were
   1368          // not marked stale in a later update, it would be shown when stale
   1369          // rows are removed.
   1370          this.controller.engagementEvent.addTentativeExposure(
   1371            result,
   1372            this.#queryContext
   1373          );
   1374        }
   1375        continue;
   1376      }
   1377      let row = this.#createRow();
   1378      this.#updateRow(row, result);
   1379      if (canBeVisible) {
   1380        visibleSpanCount = newSpanCount;
   1381      } else {
   1382        // The new row must be hidden at first because the view is already
   1383        // showing maxResults spans, or we encountered a new suggestedIndex
   1384        // result that couldn't be placed in the right spot. We'll show it when
   1385        // stale rows are removed.
   1386        this.#setRowVisibility(row, false);
   1387      }
   1388      this.#rows.appendChild(row);
   1389    }
   1390 
   1391    this.#updateIndices();
   1392  }
   1393 
   1394  #createRow() {
   1395    let item = this.#createElement("div");
   1396    item.className = "urlbarView-row";
   1397    item._elements = new Map();
   1398    item._buttons = new Map();
   1399 
   1400    // A note about row selection. Any element in a row that can be selected
   1401    // will have the `selectable` attribute set on it. For typical rows, the
   1402    // selectable element is not the `.urlbarView-row` itself but rather the
   1403    // `.urlbarView-row-inner` inside it. That's because the `.urlbarView-row`
   1404    // also contains the row's buttons, which should not be selected when the
   1405    // main part of the row -- `.urlbarView-row-inner` -- is selected.
   1406    //
   1407    // Since it's the row itself and not the row-inner that is a child of the
   1408    // `role=listbox` element (the rows container, `this.#rows`), screen readers
   1409    // will not automatically recognize the row-inner as a listbox option. To
   1410    // compensate, we set `role=option` on the row-inner and `role=presentation`
   1411    // on the row itself so that screen readers ignore it.
   1412    item.setAttribute("role", "presentation");
   1413 
   1414    // These are used to cleanup result specific entities when row contents are
   1415    // cleared to reuse the row for a different result.
   1416    item._sharedAttributes = new Set(
   1417      [...item.attributes].map(v => v.name).concat(["stale", "id", "hidden"])
   1418    );
   1419    item._sharedClassList = new Set(item.classList);
   1420 
   1421    return item;
   1422  }
   1423 
   1424  #createRowContent(item) {
   1425    // The url is the only element that can wrap, thus all the other elements
   1426    // are child of noWrap.
   1427    let noWrap = this.#createElement("span");
   1428    noWrap.className = "urlbarView-no-wrap";
   1429    item._content.appendChild(noWrap);
   1430 
   1431    let favicon = this.#createElement("img");
   1432    favicon.className = "urlbarView-favicon";
   1433    noWrap.appendChild(favicon);
   1434    item._elements.set("favicon", favicon);
   1435 
   1436    let typeIcon = this.#createElement("span");
   1437    typeIcon.className = "urlbarView-type-icon";
   1438    noWrap.appendChild(typeIcon);
   1439 
   1440    let tailPrefix = this.#createElement("span");
   1441    tailPrefix.className = "urlbarView-tail-prefix";
   1442    noWrap.appendChild(tailPrefix);
   1443    item._elements.set("tailPrefix", tailPrefix);
   1444    // tailPrefix holds text only for alignment purposes so it should never be
   1445    // read to screen readers.
   1446    tailPrefix.toggleAttribute("aria-hidden", true);
   1447 
   1448    let tailPrefixStr = this.#createElement("span");
   1449    tailPrefixStr.className = "urlbarView-tail-prefix-string";
   1450    tailPrefix.appendChild(tailPrefixStr);
   1451    item._elements.set("tailPrefixStr", tailPrefixStr);
   1452 
   1453    let tailPrefixChar = this.#createElement("span");
   1454    tailPrefixChar.className = "urlbarView-tail-prefix-char";
   1455    tailPrefix.appendChild(tailPrefixChar);
   1456    item._elements.set("tailPrefixChar", tailPrefixChar);
   1457 
   1458    let title = this.#createElement("span");
   1459    title.classList.add("urlbarView-title", "urlbarView-overflowable");
   1460    noWrap.appendChild(title);
   1461    item._elements.set("title", title);
   1462 
   1463    let tagsContainer = this.#createElement("span");
   1464    tagsContainer.classList.add("urlbarView-tags", "urlbarView-overflowable");
   1465    noWrap.appendChild(tagsContainer);
   1466    item._elements.set("tagsContainer", tagsContainer);
   1467 
   1468    let titleSeparator = this.#createElement("span");
   1469    titleSeparator.className = "urlbarView-title-separator";
   1470    noWrap.appendChild(titleSeparator);
   1471    item._elements.set("titleSeparator", titleSeparator);
   1472 
   1473    let action = this.#createElement("span");
   1474    action.className = "urlbarView-action";
   1475    noWrap.appendChild(action);
   1476    item._elements.set("action", action);
   1477 
   1478    let url = this.#createElement("span");
   1479    url.className = "urlbarView-url";
   1480    item._content.appendChild(url);
   1481    item._elements.set("url", url);
   1482  }
   1483 
   1484  /**
   1485   * Updates different aspects of an element given an update object. This method
   1486   * is designed to be used for elements in dynamic result type rows, but it can
   1487   * can be used for any element.
   1488   *
   1489   * @param {Element} element
   1490   *   The element to update.
   1491   * @param {object} update
   1492   *   An object that describes how the element should be updated. It can have
   1493   *   the following optional properties:
   1494   *
   1495   *   {object} attributes
   1496   *     Attribute names to values mapping. For each name-value pair, an
   1497   *     attribute is set on the element, except for `null` as a value which
   1498   *     signals an attribute should be removed, and `undefined` in which case
   1499   *     the attribute won't be set nor removed. The `id` attribute is reserved
   1500   *     and cannot be set here.
   1501   *   {object} dataset
   1502   *     Maps element dataset keys to values. Values should be strings with the
   1503   *     following exceptions: `undefined` is ignored, and `null` causes the key
   1504   *     to be removed from the dataset.
   1505   *   {Array} classList
   1506   *     An array of CSS classes to set on the element. If this is defined, the
   1507   *     element's previous classes will be cleared first!
   1508   *
   1509   * @param {Element} item
   1510   *   The row element.
   1511   * @param {UrlbarResult} result
   1512   *   The UrlbarResult displayed to the node. This is optional.
   1513   */
   1514  #updateElementForDynamicType(element, update, item, result = null) {
   1515    if (update.attributes) {
   1516      for (let [name, value] of Object.entries(update.attributes)) {
   1517        if (name == "id") {
   1518          // IDs are managed externally to ensure they are unique.
   1519          console.error(
   1520            `Not setting id="${value}", as dynamic attributes may not include IDs.`
   1521          );
   1522          continue;
   1523        }
   1524        if (value === undefined) {
   1525          continue;
   1526        }
   1527        if (value === null) {
   1528          element.removeAttribute(name);
   1529        } else if (typeof value == "boolean") {
   1530          element.toggleAttribute(name, value);
   1531        } else if (Blob.isInstance(value) && result) {
   1532          element.setAttribute(name, this.#getBlobUrlForResult(result, value));
   1533        } else {
   1534          element.setAttribute(name, value);
   1535        }
   1536      }
   1537    }
   1538 
   1539    if (update.dataset) {
   1540      for (let [name, value] of Object.entries(update.dataset)) {
   1541        if (value === null) {
   1542          delete element.dataset[name];
   1543        } else if (value !== undefined) {
   1544          if (typeof value != "string") {
   1545            console.error(
   1546              `Trying to set a dataset value that is not a string`,
   1547              { element, value }
   1548            );
   1549          } else {
   1550            element.dataset[name] = value;
   1551          }
   1552        }
   1553      }
   1554    }
   1555 
   1556    if (update.classList) {
   1557      if (element == item._content) {
   1558        element.className = "urlbarView-row-inner";
   1559      } else {
   1560        element.className = "";
   1561      }
   1562      element.classList.add(...update.classList);
   1563    }
   1564  }
   1565 
   1566  #createRowContentForDynamicType(item, result) {
   1567    let { dynamicType } = result.payload;
   1568    let provider = this.#providersManager.getProvider(result.providerName);
   1569    let viewTemplate =
   1570      provider.getViewTemplate?.(result) ||
   1571      UrlbarView.dynamicViewTemplatesByName.get(dynamicType);
   1572    if (!viewTemplate) {
   1573      console.error(`No viewTemplate found for ${result.providerName}`);
   1574      return;
   1575    }
   1576    let classes = this.#buildViewForDynamicType(
   1577      dynamicType,
   1578      item._content,
   1579      item._elements,
   1580      viewTemplate,
   1581      item
   1582    );
   1583    item.toggleAttribute("has-url", classes.has("urlbarView-url"));
   1584    item.toggleAttribute("has-action", classes.has("urlbarView-action"));
   1585    this.#setRowSelectable(item, item._content.hasAttribute("selectable"));
   1586  }
   1587 
   1588  /**
   1589   * Recursively builds a row's DOM for a dynamic result type.
   1590   *
   1591   * @param {string} type
   1592   *   The name of the dynamic type.
   1593   * @param {Element} parentNode
   1594   *   The element being recursed into. Pass `row._content`
   1595   *   (i.e., the row's `.urlbarView-row-inner`) to start with.
   1596   * @param {Map} elementsByName
   1597   *   The `row._elements` map.
   1598   * @param {object} template
   1599   *   The template object being recursed into. Pass the top-level template
   1600   *   object to start with.
   1601   * @param {Element} item
   1602   *   The row element.
   1603   * @param {Set} classes
   1604   *   The CSS class names of all elements in the row's subtree are recursively
   1605   *   collected in this set. Don't pass anything to start with so that the
   1606   *   default argument, a new Set, is used.
   1607   * @returns {Set}
   1608   *   The `classes` set, which on return will contain the CSS class names of
   1609   *   all elements in the row's subtree.
   1610   */
   1611  #buildViewForDynamicType(
   1612    type,
   1613    parentNode,
   1614    elementsByName,
   1615    template,
   1616    item,
   1617    classes = new Set()
   1618  ) {
   1619    this.#updateElementForDynamicType(parentNode, template, item);
   1620 
   1621    if (template.classList) {
   1622      for (let c of template.classList) {
   1623        classes.add(c);
   1624      }
   1625    }
   1626    if (template.overflowable) {
   1627      parentNode.classList.add("urlbarView-overflowable");
   1628    }
   1629 
   1630    if (template.name) {
   1631      parentNode.setAttribute("name", template.name);
   1632      parentNode.classList.add(`urlbarView-dynamic-${type}-${template.name}`);
   1633      elementsByName.set(template.name, parentNode);
   1634    }
   1635 
   1636    // Recurse into children.
   1637    for (let childTemplate of template.children || []) {
   1638      let child = this.#createElement(childTemplate.tag);
   1639      parentNode.appendChild(child);
   1640      this.#buildViewForDynamicType(
   1641        type,
   1642        child,
   1643        elementsByName,
   1644        childTemplate,
   1645        item,
   1646        classes
   1647      );
   1648    }
   1649 
   1650    return classes;
   1651  }
   1652 
   1653  #createRowContentForRichSuggestion(item, result) {
   1654    item._content.toggleAttribute("selectable", true);
   1655 
   1656    let favicon = this.#createElement("img");
   1657    favicon.className = "urlbarView-favicon";
   1658    item._content.appendChild(favicon);
   1659    item._elements.set("favicon", favicon);
   1660 
   1661    let body = this.#createElement("span");
   1662    body.className = "urlbarView-row-body";
   1663    item._content.appendChild(body);
   1664 
   1665    let top = this.#createElement("div");
   1666    top.className = "urlbarView-row-body-top";
   1667    body.appendChild(top);
   1668 
   1669    let noWrap = this.#createElement("div");
   1670    noWrap.className = "urlbarView-row-body-top-no-wrap";
   1671    top.appendChild(noWrap);
   1672    item._elements.set("noWrap", noWrap);
   1673 
   1674    let title = this.#createElement("span");
   1675    title.classList.add("urlbarView-title", "urlbarView-overflowable");
   1676    noWrap.appendChild(title);
   1677    item._elements.set("title", title);
   1678 
   1679    let titleSeparator = this.#createElement("span");
   1680    titleSeparator.className = "urlbarView-title-separator";
   1681    noWrap.appendChild(titleSeparator);
   1682    item._elements.set("titleSeparator", titleSeparator);
   1683 
   1684    let action = this.#createElement("span");
   1685    action.className = "urlbarView-action";
   1686    noWrap.appendChild(action);
   1687    item._elements.set("action", action);
   1688 
   1689    let url = this.#createElement("span");
   1690    url.className = "urlbarView-url";
   1691    top.appendChild(url);
   1692    item._elements.set("url", url);
   1693 
   1694    let description = this.#createElement("div");
   1695    description.classList.add("urlbarView-row-body-description");
   1696    body.appendChild(description);
   1697    item._elements.set("description", description);
   1698 
   1699    if (result.payload.descriptionLearnMoreTopic) {
   1700      let learnMoreLink = this.#createElement("a");
   1701      learnMoreLink.setAttribute("data-l10n-name", "learn-more-link");
   1702      description.appendChild(learnMoreLink);
   1703    }
   1704 
   1705    let bottom = this.#createElement("div");
   1706    bottom.className = "urlbarView-row-body-bottom";
   1707    body.appendChild(bottom);
   1708    item._elements.set("bottom", bottom);
   1709  }
   1710 
   1711  #needsNewButtons(item, oldResult, newResult) {
   1712    if (!oldResult) {
   1713      return true;
   1714    }
   1715 
   1716    if (
   1717      !!this.#getResultMenuCommands(newResult) !=
   1718      item._buttons.has("result-menu")
   1719    ) {
   1720      return true;
   1721    }
   1722 
   1723    if (!!oldResult.showFeedbackMenu != !!newResult.showFeedbackMenu) {
   1724      return true;
   1725    }
   1726 
   1727    if (
   1728      oldResult.payload.buttons?.length != newResult.payload.buttons?.length ||
   1729      !lazy.ObjectUtils.deepEqual(
   1730        oldResult.payload.buttons,
   1731        newResult.payload.buttons
   1732      )
   1733    ) {
   1734      return true;
   1735    }
   1736 
   1737    return newResult.testForceNewContent;
   1738  }
   1739 
   1740  #updateRowButtons(item, oldResult, result) {
   1741    for (let i = 0; i < result.payload.buttons?.length; i++) {
   1742      // We hold the name to each button data in payload to enable to get the
   1743      // data from button element by the name. This name is mainly used for
   1744      // button that has menu (Split Button).
   1745      let button = result.payload.buttons[i];
   1746      button.name ??= i.toString();
   1747    }
   1748 
   1749    if (!this.#needsNewButtons(item, oldResult, result)) {
   1750      return;
   1751    }
   1752 
   1753    let container = item._elements.get("buttons");
   1754    if (container) {
   1755      container.innerHTML = "";
   1756    } else {
   1757      container = this.#createElement("div");
   1758      container.className = "urlbarView-row-buttons";
   1759      item.appendChild(container);
   1760      item._elements.set("buttons", container);
   1761    }
   1762 
   1763    item._buttons.clear();
   1764 
   1765    if (result.payload.buttons) {
   1766      for (let button of result.payload.buttons) {
   1767        this.#addRowButton(item, button);
   1768      }
   1769    }
   1770 
   1771    // TODO: `buttonText` is intended only for WebExtensions. We should remove
   1772    // it and the WebExtensions urlbar API since we're no longer using it.
   1773    if (result.payload.buttonText) {
   1774      this.#addRowButton(item, {
   1775        name: "tip",
   1776        url: result.payload.buttonUrl,
   1777      });
   1778      item._buttons.get("tip").textContent = result.payload.buttonText;
   1779    }
   1780 
   1781    if (this.#getResultMenuCommands(result)) {
   1782      this.#addRowButton(item, {
   1783        name: "result-menu",
   1784        classList: ["urlbarView-button-menu"],
   1785        l10n: result.showFeedbackMenu
   1786          ? { id: "urlbar-result-menu-button-feedback" }
   1787          : { id: "urlbar-result-menu-button" },
   1788        attributes: lazy.UrlbarPrefs.get("resultMenu.keyboardAccessible")
   1789          ? null
   1790          : {
   1791              "keyboard-inaccessible": true,
   1792            },
   1793      });
   1794    }
   1795  }
   1796 
   1797  #addRowButton(
   1798    item,
   1799    {
   1800      name,
   1801      command,
   1802      l10n,
   1803      url,
   1804      classList = [],
   1805      attributes = {},
   1806      menu = null,
   1807      input = null,
   1808    }
   1809  ) {
   1810    let button = this.#createElement("span");
   1811    this.#updateElementForDynamicType(
   1812      button,
   1813      {
   1814        attributes: {
   1815          ...attributes,
   1816          role: "button",
   1817        },
   1818        classList: [
   1819          ...classList,
   1820          "urlbarView-button",
   1821          "urlbarView-button-" + name,
   1822        ],
   1823        dataset: {
   1824          name,
   1825          command,
   1826          url,
   1827          input,
   1828        },
   1829      },
   1830      item
   1831    );
   1832 
   1833    button.id = `${item.id}-button-${name}`;
   1834    if (l10n) {
   1835      this.#l10nCache.setElementL10n(button, l10n);
   1836    }
   1837 
   1838    item._buttons.set(name, button);
   1839 
   1840    if (!menu) {
   1841      item._elements.get("buttons").appendChild(button);
   1842      return;
   1843    }
   1844 
   1845    // Split Button.
   1846    let container = this.#createElement("span");
   1847    container.classList.add("urlbarView-splitbutton");
   1848 
   1849    button.classList.add("urlbarView-splitbutton-main");
   1850    container.appendChild(button);
   1851 
   1852    let dropmarker = this.#createElement("span");
   1853    dropmarker.classList.add(
   1854      "urlbarView-button",
   1855      "urlbarView-button-menu",
   1856      "urlbarView-splitbutton-dropmarker"
   1857    );
   1858    this.#l10nCache.setElementL10n(dropmarker, {
   1859      id: "urlbar-splitbutton-dropmarker",
   1860    });
   1861    dropmarker.setAttribute("role", "button");
   1862    container.appendChild(dropmarker);
   1863 
   1864    item._elements.get("buttons").appendChild(container);
   1865  }
   1866 
   1867  #createSecondaryAction(action, global = false) {
   1868    let actionContainer = this.#createElement("div");
   1869    actionContainer.classList.add("urlbarView-actions-container");
   1870 
   1871    let button = this.#createElement("span");
   1872    button.classList.add("urlbarView-action-btn");
   1873    if (global) {
   1874      button.classList.add("urlbarView-global-action-btn");
   1875    }
   1876    if (action.classList) {
   1877      button.classList.add(...action.classList);
   1878    }
   1879    button.setAttribute("role", "button");
   1880    if (action.icon) {
   1881      let icon = this.#createElement("img");
   1882      icon.src = action.icon;
   1883      button.appendChild(icon);
   1884    }
   1885    for (let key in action.dataset ?? {}) {
   1886      button.dataset[key] = action.dataset[key];
   1887    }
   1888    button.dataset.action = action.key;
   1889    button.dataset.providerName = action.providerName;
   1890 
   1891    let label = this.#createElement("span");
   1892    if (action.l10nId) {
   1893      this.#l10nCache.setElementL10n(label, {
   1894        id: action.l10nId,
   1895        args: action.l10nArgs,
   1896      });
   1897    } else {
   1898      this.document.l10n.setAttributes(label, action.label, action.l10nArgs);
   1899    }
   1900    button.appendChild(label);
   1901    actionContainer.appendChild(button);
   1902    return actionContainer;
   1903  }
   1904 
   1905  #needsNewContent(item, oldResult, newResult) {
   1906    if (!oldResult) {
   1907      return true;
   1908    }
   1909 
   1910    if (
   1911      (oldResult.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC) !=
   1912      (newResult.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC)
   1913    ) {
   1914      return true;
   1915    }
   1916 
   1917    if (
   1918      oldResult.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC &&
   1919      newResult.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC &&
   1920      oldResult.payload.dynamicType != newResult.payload.dynamicType
   1921    ) {
   1922      return true;
   1923    }
   1924 
   1925    if (oldResult.isRichSuggestion != newResult.isRichSuggestion) {
   1926      return true;
   1927    }
   1928 
   1929    // Reusing a non-heuristic as a heuristic is risky as it may have DOM
   1930    // nodes/attributes/classes that are normally not present in a heuristic
   1931    // result. This may happen for example when switching from a zero-prefix
   1932    // search not having a heuristic to a search string one.
   1933    if (oldResult.heuristic != newResult.heuristic) {
   1934      return true;
   1935    }
   1936 
   1937    // Container switch-tab results have a more complex DOM content that is
   1938    // only updated correctly by another switch-tab result.
   1939    if (
   1940      oldResult.type == lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH &&
   1941      newResult.type != oldResult.type &&
   1942      lazy.UrlbarProviderOpenTabs.isContainerUserContextId(
   1943        oldResult.payload.userContextId
   1944      )
   1945    ) {
   1946      return true;
   1947    }
   1948 
   1949    if (
   1950      newResult.providerName == lazy.UrlbarProviderQuickSuggest.name &&
   1951      // Check if the `RESULT_TYPE` is `DYNAMIC` because otherwise the
   1952      // `suggestionType` and `items` checks aren't relevant.
   1953      newResult.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC &&
   1954      (oldResult.payload.suggestionType != newResult.payload.suggestionType ||
   1955        oldResult.payload.items?.length != newResult.payload.items?.length)
   1956    ) {
   1957      return true;
   1958    }
   1959 
   1960    if (newResult.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC) {
   1961      if (oldResult.providerName != newResult.providerName) {
   1962        return true;
   1963      }
   1964 
   1965      let provider = this.#providersManager.getProvider(newResult.providerName);
   1966      if (
   1967        !lazy.ObjectUtils.deepEqual(
   1968          provider.getViewTemplate?.(oldResult),
   1969          provider.getViewTemplate?.(newResult)
   1970        )
   1971      ) {
   1972        return true;
   1973      }
   1974    }
   1975 
   1976    return newResult.testForceNewContent;
   1977  }
   1978 
   1979  // eslint-disable-next-line complexity
   1980  #updateRow(item, result) {
   1981    let oldResult = item.result;
   1982    item.result = result;
   1983    item.removeAttribute("stale");
   1984    item.id = getUniqueId("urlbarView-row-");
   1985 
   1986    if (this.#needsNewContent(item, oldResult, result)) {
   1987      // Recreate the row content except the buttons, which we'll reuse below.
   1988      let buttons = item._elements.get("buttons");
   1989      while (item.lastChild) {
   1990        item.lastChild.remove();
   1991      }
   1992      item._elements.clear();
   1993      item._content = this.#createElement("span");
   1994      item._content.className = "urlbarView-row-inner";
   1995      item.appendChild(item._content);
   1996      // Clear previously set attributes and classes that may refer to a
   1997      // different result type.
   1998      for (const attribute of [...item.attributes]) {
   1999        if (!item._sharedAttributes.has(attribute.name)) {
   2000          item.removeAttribute(attribute.name);
   2001        }
   2002      }
   2003      for (const className of item.classList) {
   2004        if (!item._sharedClassList.has(className)) {
   2005          item.classList.remove(className);
   2006        }
   2007      }
   2008      if (item.result.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC) {
   2009        this.#createRowContentForDynamicType(item, result);
   2010      } else if (result.isRichSuggestion) {
   2011        this.#createRowContentForRichSuggestion(item, result);
   2012      } else {
   2013        this.#createRowContent(item, result);
   2014      }
   2015 
   2016      if (buttons) {
   2017        item.appendChild(buttons);
   2018        item._elements.set("buttons", buttons);
   2019      }
   2020    }
   2021 
   2022    this.#updateRowButtons(item, oldResult, result);
   2023 
   2024    item._content.id = item.id + "-inner";
   2025 
   2026    let isFirstChild = item === this.#rows.children[0];
   2027    let secAction = result.payload.action;
   2028    let container = item.querySelector(".urlbarView-actions-container");
   2029    item.toggleAttribute("secondary-action", !!secAction);
   2030    if (secAction && !container) {
   2031      item.appendChild(this.#createSecondaryAction(secAction, isFirstChild));
   2032    } else if (
   2033      secAction &&
   2034      secAction.key != container.firstChild.dataset.action
   2035    ) {
   2036      item.replaceChild(
   2037        this.#createSecondaryAction(secAction, isFirstChild),
   2038        container
   2039      );
   2040    } else if (!secAction && container) {
   2041      item.removeChild(container);
   2042    }
   2043 
   2044    item.removeAttribute("feedback-acknowledgment");
   2045 
   2046    if (
   2047      result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH &&
   2048      !result.payload.providesSearchMode &&
   2049      !result.payload.inPrivateWindow
   2050    ) {
   2051      item.setAttribute("type", "search");
   2052    } else if (result.type == lazy.UrlbarUtils.RESULT_TYPE.REMOTE_TAB) {
   2053      item.setAttribute("type", "remotetab");
   2054    } else if (result.type == lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH) {
   2055      item.setAttribute("type", "switchtab");
   2056    } else if (result.type == lazy.UrlbarUtils.RESULT_TYPE.TIP) {
   2057      item.setAttribute("type", "tip");
   2058      item.setAttribute("tip-type", result.payload.type);
   2059 
   2060      // Due to role=button, the button and help icon can sometimes become
   2061      // focused. We want to prevent that because the input should always be
   2062      // focused instead. (This happens when input.search("", { focus: false })
   2063      // is called, a tip is the first result but not heuristic, and the user
   2064      // tabs the into the button from the navbar buttons. The input is skipped
   2065      // and the focus goes straight to the tip button.)
   2066      item.addEventListener("focus", () => this.input.focus(), true);
   2067 
   2068      if (
   2069        result.providerName == "UrlbarProviderSearchTips" ||
   2070        result.payload.type == "dismissalAcknowledgment"
   2071      ) {
   2072        // For a11y, we treat search tips as alerts. We use A11yUtils.announce
   2073        // instead of role="alert" because role="alert" will only fire an alert
   2074        // event when the alert (or something inside it) is the root of an
   2075        // insertion. In this case, the entire tip result gets inserted into the
   2076        // a11y tree as a single insertion, so no alert event would be fired.
   2077        this.window.A11yUtils.announce(result.payload.titleL10n);
   2078      }
   2079    } else if (result.source == lazy.UrlbarUtils.RESULT_SOURCE.BOOKMARKS) {
   2080      item.setAttribute("type", "bookmark");
   2081    } else if (result.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC) {
   2082      item.setAttribute("type", "dynamic");
   2083      this.#updateRowForDynamicType(item, result);
   2084      return;
   2085    } else if (result.providerName == "UrlbarProviderTabToSearch") {
   2086      item.setAttribute("type", "tabtosearch");
   2087    } else if (result.providerName == "UrlbarProviderSemanticHistorySearch") {
   2088      item.setAttribute("type", "semantic-history");
   2089    } else if (result.providerName == "UrlbarProviderInputHistory") {
   2090      item.setAttribute("type", "adaptive-history");
   2091    } else {
   2092      item.setAttribute(
   2093        "type",
   2094        lazy.UrlbarUtils.searchEngagementTelemetryType(result)
   2095      );
   2096    }
   2097 
   2098    let favicon = item._elements.get("favicon");
   2099    favicon.src = this.#iconForResult(result);
   2100 
   2101    let title = item._elements.get("title");
   2102    this.#setResultTitle(result, title);
   2103 
   2104    if (result.payload.tail && result.payload.tailOffsetIndex > 0) {
   2105      this.#fillTailSuggestionPrefix(item, result);
   2106      title.setAttribute("aria-label", result.payload.suggestion);
   2107      item.toggleAttribute("tail-suggestion", true);
   2108    } else {
   2109      item.removeAttribute("tail-suggestion");
   2110      title.removeAttribute("aria-label");
   2111    }
   2112 
   2113    this.#updateOverflowTooltip(
   2114      title,
   2115      result.getDisplayableValueAndHighlights("title").value
   2116    );
   2117 
   2118    let tagsContainer = item._elements.get("tagsContainer");
   2119    if (tagsContainer) {
   2120      tagsContainer.textContent = "";
   2121 
   2122      let { value: tags, highlights } = result.getDisplayableValueAndHighlights(
   2123        "tags",
   2124        {
   2125          tokens: this.#queryContext.tokens,
   2126        }
   2127      );
   2128 
   2129      if (tags?.length) {
   2130        tagsContainer.append(
   2131          ...tags.map((tag, i) => {
   2132            const element = this.#createElement("span");
   2133            element.className = "urlbarView-tag";
   2134            lazy.UrlbarUtils.addTextContentWithHighlights(
   2135              element,
   2136              tag,
   2137              highlights[i]
   2138            );
   2139            return element;
   2140          })
   2141        );
   2142      }
   2143    }
   2144 
   2145    let action = item._elements.get("action");
   2146    let actionSetter = null;
   2147    let isVisitAction = false;
   2148    let setURL = false;
   2149    let isRowSelectable = true;
   2150    switch (result.type) {
   2151      case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
   2152        // Hide chiclet when showing secondaryActions.
   2153        if (!lazy.UrlbarPrefs.get("secondaryActions.switchToTab")) {
   2154          actionSetter = () => {
   2155            this.#setSwitchTabActionChiclet(result, action);
   2156          };
   2157        }
   2158        setURL = true;
   2159        break;
   2160      case lazy.UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
   2161        actionSetter = () => {
   2162          this.#l10nCache.removeElementL10n(action);
   2163          action.textContent = result.payload.device;
   2164        };
   2165        setURL = true;
   2166        break;
   2167      case lazy.UrlbarUtils.RESULT_TYPE.SEARCH:
   2168        if (
   2169          result.payload.suggestionObject?.suggestionType == "important_dates"
   2170        ) {
   2171          // Don't show action for important date results because clicking them
   2172          // searches for the name of the event which is in the description and
   2173          // not the title.
   2174          break;
   2175        }
   2176        if (result.payload.inPrivateWindow) {
   2177          if (result.payload.isPrivateEngine) {
   2178            actionSetter = () => {
   2179              this.#l10nCache.setElementL10n(action, {
   2180                id: "urlbar-result-action-search-in-private-w-engine",
   2181                args: { engine: result.payload.engine },
   2182              });
   2183            };
   2184          } else {
   2185            actionSetter = () => {
   2186              this.#l10nCache.setElementL10n(action, {
   2187                id: "urlbar-result-action-search-in-private",
   2188              });
   2189            };
   2190          }
   2191        } else if (result.providerName == "UrlbarProviderTabToSearch") {
   2192          actionSetter = () => {
   2193            this.#l10nCache.setElementL10n(action, {
   2194              id: result.payload.isGeneralPurposeEngine
   2195                ? "urlbar-result-action-tabtosearch-web"
   2196                : "urlbar-result-action-tabtosearch-other-engine",
   2197              args: { engine: result.payload.engine },
   2198            });
   2199          };
   2200        } else if (!result.payload.providesSearchMode) {
   2201          actionSetter = () => {
   2202            this.#l10nCache.setElementL10n(action, {
   2203              id: "urlbar-result-action-search-w-engine",
   2204              args: { engine: result.payload.engine },
   2205            });
   2206          };
   2207        }
   2208        break;
   2209      case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD:
   2210        isVisitAction = result.payload.input.trim() == result.payload.keyword;
   2211        break;
   2212      case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX:
   2213        actionSetter = () => {
   2214          this.#l10nCache.removeElementL10n(action);
   2215          action.textContent = result.payload.content;
   2216        };
   2217        break;
   2218      case lazy.UrlbarUtils.RESULT_TYPE.TIP:
   2219        isRowSelectable = false;
   2220        break;
   2221      case lazy.UrlbarUtils.RESULT_TYPE.URL:
   2222        if (result.providerName == "UrlbarProviderClipboard") {
   2223          actionSetter = () => {
   2224            this.#l10nCache.setElementL10n(action, {
   2225              id: "urlbar-result-action-visit-from-clipboard",
   2226            });
   2227          };
   2228          title.toggleAttribute("is-url", true);
   2229 
   2230          let label = { id: "urlbar-result-action-visit-from-clipboard" };
   2231          this.#l10nCache.ensure(label).then(() => {
   2232            let { value } = this.#l10nCache.get(label);
   2233 
   2234            // We don't have to unset these attributes because, excluding heuristic results,
   2235            // we never reuse results from different providers. Thus clipboard results can
   2236            // only be reused by other clipboard results.
   2237            title.setAttribute("aria-label", `${value}, ${title.innerText}`);
   2238            action.setAttribute("aria-hidden", "true");
   2239          });
   2240          break;
   2241        }
   2242      // fall-through
   2243      default:
   2244        if (
   2245          result.heuristic &&
   2246          result.payload.url &&
   2247          result.providerName != "UrlbarProviderHistoryUrlHeuristic" &&
   2248          !result.autofill?.noVisitAction
   2249        ) {
   2250          isVisitAction = true;
   2251        } else if (
   2252          (result.providerName != lazy.UrlbarProviderQuickSuggest.name ||
   2253            result.payload.shouldShowUrl) &&
   2254          !result.payload.providesSearchMode
   2255        ) {
   2256          setURL = true;
   2257        }
   2258        break;
   2259    }
   2260 
   2261    this.#setRowSelectable(item, isRowSelectable);
   2262 
   2263    action.toggleAttribute(
   2264      "slide-in",
   2265      result.providerName == "UrlbarProviderTabToSearch"
   2266    );
   2267 
   2268    item.toggleAttribute("pinned", !!result.payload.isPinned);
   2269 
   2270    let sponsored =
   2271      result.payload.isSponsored &&
   2272      result.type != lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH &&
   2273      result.providerName != lazy.UrlbarProviderQuickSuggest.name;
   2274    item.toggleAttribute("sponsored", !!sponsored);
   2275    if (sponsored) {
   2276      actionSetter = () => {
   2277        this.#l10nCache.setElementL10n(action, {
   2278          id: "urlbar-result-action-sponsored",
   2279        });
   2280      };
   2281    }
   2282 
   2283    item.toggleAttribute("rich-suggestion", !!result.isRichSuggestion);
   2284    if (result.isRichSuggestion) {
   2285      this.#updateRowForRichSuggestion(item, result);
   2286    }
   2287 
   2288    item.toggleAttribute("has-url", setURL);
   2289    let url = item._elements.get("url");
   2290    if (setURL) {
   2291      let { value: displayedUrl, highlights } =
   2292        result.getDisplayableValueAndHighlights("url", {
   2293          tokens: this.#queryContext.tokens,
   2294          isURL: true,
   2295        });
   2296      this.#updateOverflowTooltip(url, displayedUrl);
   2297 
   2298      if (lazy.UrlbarUtils.isTextDirectionRTL(displayedUrl, this.window)) {
   2299        // Stripping the url prefix may change the initial text directionality,
   2300        // causing parts of it to jump to the end. To prevent that we insert a
   2301        // LRM character in place of the prefix.
   2302        displayedUrl = "\u200e" + displayedUrl;
   2303        highlights = this.#offsetHighlights(highlights, 1);
   2304      }
   2305      lazy.UrlbarUtils.addTextContentWithHighlights(
   2306        url,
   2307        displayedUrl,
   2308        highlights
   2309      );
   2310    } else {
   2311      url.textContent = "";
   2312      this.#updateOverflowTooltip(url, "");
   2313    }
   2314 
   2315    title.toggleAttribute("is-url", isVisitAction);
   2316    if (isVisitAction) {
   2317      actionSetter = () => {
   2318        this.#l10nCache.setElementL10n(action, {
   2319          id: "urlbar-result-action-visit",
   2320        });
   2321      };
   2322    }
   2323 
   2324    item.toggleAttribute("has-action", actionSetter);
   2325    if (actionSetter) {
   2326      actionSetter();
   2327      item._originalActionSetter = actionSetter;
   2328    } else {
   2329      item._originalActionSetter = () => {
   2330        this.#l10nCache.removeElementL10n(action);
   2331        action.textContent = "";
   2332      };
   2333      item._originalActionSetter();
   2334    }
   2335 
   2336    if (!title.hasAttribute("is-url")) {
   2337      title.setAttribute("dir", "auto");
   2338    } else {
   2339      title.removeAttribute("dir");
   2340    }
   2341  }
   2342 
   2343  #setRowSelectable(item, isRowSelectable) {
   2344    item.toggleAttribute("row-selectable", isRowSelectable);
   2345    item._content.toggleAttribute("selectable", isRowSelectable);
   2346 
   2347    // Set or remove role="option" on the inner. "option" should be set iff the
   2348    // row is selectable. Some providers may set a different role if the inner
   2349    // is not selectable, so when removing it, only do so if it's "option".
   2350    if (isRowSelectable) {
   2351      item._content.setAttribute("role", "option");
   2352    } else if (item._content.getAttribute("role") == "option") {
   2353      item._content.removeAttribute("role");
   2354    }
   2355  }
   2356 
   2357  #iconForResult(result, iconUrlOverride = null) {
   2358    if (
   2359      result.source == lazy.UrlbarUtils.RESULT_SOURCE.HISTORY &&
   2360      (result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH ||
   2361        result.type == lazy.UrlbarUtils.RESULT_TYPE.KEYWORD)
   2362    ) {
   2363      return lazy.UrlbarUtils.ICON.HISTORY;
   2364    }
   2365 
   2366    if (iconUrlOverride) {
   2367      return iconUrlOverride;
   2368    }
   2369 
   2370    if (result.payload.icon) {
   2371      return result.payload.icon;
   2372    }
   2373    if (result.payload.iconBlob) {
   2374      let blobUrl = this.#getBlobUrlForResult(result, result.payload.iconBlob);
   2375      if (blobUrl) {
   2376        return blobUrl;
   2377      }
   2378    }
   2379 
   2380    if (
   2381      result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH &&
   2382      result.payload.trending
   2383    ) {
   2384      return lazy.UrlbarUtils.ICON.TRENDING;
   2385    }
   2386 
   2387    if (
   2388      result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH ||
   2389      result.type == lazy.UrlbarUtils.RESULT_TYPE.KEYWORD
   2390    ) {
   2391      return lazy.UrlbarUtils.ICON.SEARCH_GLASS;
   2392    }
   2393 
   2394    return lazy.UrlbarUtils.ICON.DEFAULT;
   2395  }
   2396 
   2397  #getBlobUrlForResult(result, blob) {
   2398    // For some Suggest results, `url` is a value that is modified at query time
   2399    // and that is potentially unique per query. For example, it might contain
   2400    // timestamps or query-related search params. Those results will also have
   2401    // an `originalUrl` that is the unmodified URL, and it should be used as the
   2402    // map key.
   2403    let resultUrl = result.payload.originalUrl || result.payload.url;
   2404    if (resultUrl) {
   2405      let blobUrl = this.#blobUrlsByResultUrl?.get(resultUrl);
   2406      if (!blobUrl) {
   2407        blobUrl = URL.createObjectURL(blob);
   2408        // Since most users will not trigger results with blob icons, we
   2409        // create this map lazily.
   2410        this.#blobUrlsByResultUrl ||= new Map();
   2411        this.#blobUrlsByResultUrl.set(resultUrl, blobUrl);
   2412      }
   2413      return blobUrl;
   2414    }
   2415    return null;
   2416  }
   2417 
   2418  async #updateRowForDynamicType(item, result) {
   2419    item.setAttribute("dynamicType", result.payload.dynamicType);
   2420 
   2421    let idsByName = new Map();
   2422    for (let [name, node] of item._elements) {
   2423      node.id = `${item.id}-${name}`;
   2424      idsByName.set(name, node.id);
   2425    }
   2426 
   2427    // Get the view update from the result's provider.
   2428    let provider = this.#providersManager.getProvider(result.providerName);
   2429    let viewUpdate = await provider.getViewUpdate(result, idsByName);
   2430    if (item.result != result) {
   2431      return;
   2432    }
   2433 
   2434    // Update each node in the view by name.
   2435    for (let [nodeName, update] of Object.entries(viewUpdate)) {
   2436      if (!update) {
   2437        continue;
   2438      }
   2439      let node = item.querySelector(`#${item.id}-${nodeName}`);
   2440      this.#updateElementForDynamicType(node, update, item, result);
   2441      if (update.style) {
   2442        for (let [styleName, value] of Object.entries(update.style)) {
   2443          if (styleName.includes("-")) {
   2444            // Expect hyphen-case. e.g. "background-image", "--a-variable".
   2445            node.style.setProperty(styleName, value);
   2446          } else {
   2447            // Expect camel-case. e.g. "backgroundImage"
   2448            // NOTE: If want to define the variable, please use hyphen-case.
   2449            node.style[styleName] = value;
   2450          }
   2451        }
   2452      }
   2453      if (update.l10n) {
   2454        this.#l10nCache.setElementL10n(node, update.l10n);
   2455      } else if (update.hasOwnProperty("textContent")) {
   2456        this.#l10nCache.removeElementL10n(node);
   2457        lazy.UrlbarUtils.addTextContentWithHighlights(
   2458          node,
   2459          update.textContent,
   2460          update.highlights
   2461        );
   2462      }
   2463    }
   2464  }
   2465 
   2466  #updateRowForRichSuggestion(item, result) {
   2467    this.#setRowSelectable(
   2468      item,
   2469      result.type != lazy.UrlbarUtils.RESULT_TYPE.TIP
   2470    );
   2471 
   2472    let favicon = item._elements.get("favicon");
   2473    if (result.richSuggestionIconSize) {
   2474      item.setAttribute("icon-size", result.richSuggestionIconSize);
   2475      favicon.setAttribute("icon-size", result.richSuggestionIconSize);
   2476    } else {
   2477      item.removeAttribute("icon-size");
   2478      favicon.removeAttribute("icon-size");
   2479    }
   2480 
   2481    if (result.richSuggestionIconVariation) {
   2482      favicon.setAttribute(
   2483        "icon-variation",
   2484        result.richSuggestionIconVariation
   2485      );
   2486    } else {
   2487      favicon.removeAttribute("icon-variation");
   2488    }
   2489 
   2490    let description = item._elements.get("description");
   2491    if (result.payload.descriptionL10n) {
   2492      this.#l10nCache.setElementL10n(
   2493        description,
   2494        result.payload.descriptionL10n
   2495      );
   2496 
   2497      if (result.payload.descriptionLearnMoreTopic) {
   2498        let learnMoreLink = description.querySelector(
   2499          "[data-l10n-name=learn-more-link]"
   2500        );
   2501        if (learnMoreLink) {
   2502          learnMoreLink.dataset.url = this.window.getHelpLinkURL(
   2503            result.payload.descriptionLearnMoreTopic
   2504          );
   2505        } else {
   2506          console.warn("learn-more-link was not found");
   2507        }
   2508      }
   2509    } else {
   2510      this.#l10nCache.removeElementL10n(description);
   2511      if (result.payload.description) {
   2512        description.textContent = result.payload.description;
   2513      }
   2514    }
   2515 
   2516    let bottom = item._elements.get("bottom");
   2517    if (result.payload.bottomTextL10n) {
   2518      this.#l10nCache.setElementL10n(bottom, result.payload.bottomTextL10n);
   2519    } else {
   2520      this.#l10nCache.removeElementL10n(bottom);
   2521    }
   2522  }
   2523 
   2524  /**
   2525   * Performs a final pass over all rows in the view after a view update, stale
   2526   * rows are removed, and other changes to the number of rows. Sets `rowIndex`
   2527   * on each result, updates row labels, and performs other tasks that must be
   2528   * deferred until all rows have been updated.
   2529   */
   2530  #updateIndices() {
   2531    this.visibleResults = [];
   2532 
   2533    // `lastVisibleLabel` is the l10n object of the last-seen visible row label
   2534    // as we iterate through the rows. When we encounter a row whose label is
   2535    // different from `lastVisibleLabel`, we make that row's label visible and
   2536    // it becomes the new `lastVisibleLabel`. We hide the labels for all other
   2537    // rows, so no label will appear adjacent to itself. (A label may appear
   2538    // more than once, but there will be at least one different label in
   2539    // between.) Each row's label is determined by `#rowLabel()`.
   2540    let lastVisibleLabel = null;
   2541 
   2542    // Keeps track of whether we've seen only the heuristic or search suggestions.
   2543    let seenOnlyHeuristicOrSearchSuggestions = true;
   2544 
   2545    for (let i = 0; i < this.#rows.children.length; i++) {
   2546      let item = this.#rows.children[i];
   2547      let { result } = item;
   2548      result.rowIndex = i;
   2549 
   2550      let visible = this.#isElementVisible(item);
   2551      if (visible) {
   2552        this.visibleResults.push(result);
   2553        seenOnlyHeuristicOrSearchSuggestions &&=
   2554          result.heuristic ||
   2555          (result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH &&
   2556            result.payload.suggestion);
   2557        if (result.exposureTelemetry) {
   2558          this.controller.engagementEvent.addExposure(
   2559            result,
   2560            this.#queryContext
   2561          );
   2562        }
   2563      }
   2564 
   2565      lastVisibleLabel = this.#updateRowLabel(
   2566        item,
   2567        visible,
   2568        lastVisibleLabel,
   2569        seenOnlyHeuristicOrSearchSuggestions
   2570      );
   2571    }
   2572 
   2573    let selectableElement = this.getFirstSelectableElement();
   2574    let uiIndex = 0;
   2575    while (selectableElement) {
   2576      selectableElement.elementIndex = uiIndex++;
   2577      selectableElement = this.#getNextSelectableElement(selectableElement);
   2578    }
   2579 
   2580    if (this.visibleResults.length) {
   2581      this.panel.removeAttribute("noresults");
   2582    } else {
   2583      this.panel.setAttribute("noresults", "true");
   2584    }
   2585  }
   2586 
   2587  /**
   2588   * Sets or removes the group label from a row. Designed to be called
   2589   * iteratively over each row.
   2590   *
   2591   * @param {Element} item
   2592   *   A row in the view.
   2593   * @param {boolean} isItemVisible
   2594   *   Whether the row is visible. This can be computed by the method itself,
   2595   *   but it's a parameter as an optimization since the caller is expected to
   2596   *   know it.
   2597   * @param {object} lastVisibleLabel
   2598   *   The last-seen visible group label during row iteration.
   2599   * @param {boolean} seenOnlyHeuristicOrSearchSuggestions
   2600   *   Whether the iteration has encountered only the heuristic or search
   2601   *   suggestions so far.
   2602   * @returns {object}
   2603   *   The l10n object for the new last-visible label. If the row's label should
   2604   *   be visible, this will be that label. Otherwise it will be the passed-in
   2605   *   `lastVisibleLabel`.
   2606   */
   2607  #updateRowLabel(
   2608    item,
   2609    isItemVisible,
   2610    lastVisibleLabel,
   2611    seenOnlyHeuristicOrSearchSuggestions
   2612  ) {
   2613    let label = null;
   2614    if (
   2615      isItemVisible &&
   2616      // Show the search suggestions label only if there are other visible
   2617      // results before this one that aren't the heuristic or suggestions.
   2618      !(
   2619        item.result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH &&
   2620        item.result.payload.suggestion &&
   2621        seenOnlyHeuristicOrSearchSuggestions
   2622      )
   2623    ) {
   2624      label = this.#rowLabel(item);
   2625    }
   2626 
   2627    // When the row-inner is selected, screen readers won't naturally read the
   2628    // label because it's a pseudo-element of the row, not the row-inner. To
   2629    // compensate, for rows that have labels we add an element to the row-inner
   2630    // with `aria-label` and no text content. Rows that don't have labels won't
   2631    // have this element.
   2632    let groupAriaLabel = item._elements.get("groupAriaLabel");
   2633 
   2634    if (
   2635      !label ||
   2636      item.result.hideRowLabel ||
   2637      lazy.ObjectUtils.deepEqual(label, lastVisibleLabel)
   2638    ) {
   2639      this.#l10nCache.removeElementL10n(item, { attribute: "label" });
   2640      if (groupAriaLabel) {
   2641        groupAriaLabel.remove();
   2642        item._elements.delete("groupAriaLabel");
   2643      }
   2644      return lastVisibleLabel;
   2645    }
   2646 
   2647    this.#l10nCache.setElementL10n(item, {
   2648      attribute: "label",
   2649      id: label.id,
   2650      args: label.args,
   2651    });
   2652 
   2653    if (!groupAriaLabel) {
   2654      groupAriaLabel = this.#createElement("span");
   2655      groupAriaLabel.className = "urlbarView-group-aria-label";
   2656      item._content.insertBefore(groupAriaLabel, item._content.firstChild);
   2657      item._elements.set("groupAriaLabel", groupAriaLabel);
   2658    }
   2659 
   2660    // `aria-label` must be a string, not an l10n ID, so first fetch the
   2661    // localized value and then set it as the attribute. There's no relevant
   2662    // aria attribute that uses l10n IDs.
   2663    this.#l10nCache.ensure(label).then(() => {
   2664      let message = this.#l10nCache.get(label);
   2665      groupAriaLabel.setAttribute("aria-label", message?.attributes.label);
   2666    });
   2667 
   2668    return label;
   2669  }
   2670 
   2671  /**
   2672   * Returns the group label to use for a row. Designed to be called iteratively
   2673   * over each row.
   2674   *
   2675   * @param {Element} row
   2676   *   A row in the view.
   2677   * @returns {object}
   2678   *   If the current row should not have a label, returns null. Otherwise
   2679   *   returns an l10n object for the label's l10n string: `{ id, args }`
   2680   */
   2681  #rowLabel(row) {
   2682    if (!lazy.UrlbarPrefs.get("groupLabels.enabled")) {
   2683      return null;
   2684    }
   2685 
   2686    if (row.result.rowLabel) {
   2687      return row.result.rowLabel;
   2688    }
   2689 
   2690    let engineName =
   2691      row.result.payload.engine || Services.search.defaultEngine.name;
   2692 
   2693    if (row.result.payload.trending) {
   2694      return {
   2695        id: "urlbar-group-trending",
   2696        args: { engine: engineName },
   2697      };
   2698    }
   2699 
   2700    if (row.result.providerName == "UrlbarProviderRecentSearches") {
   2701      return { id: "urlbar-group-recent-searches" };
   2702    }
   2703 
   2704    if (
   2705      row.result.isBestMatch &&
   2706      row.result.providerName == lazy.UrlbarProviderQuickSuggest.name
   2707    ) {
   2708      switch (row.result.payload.telemetryType) {
   2709        case "adm_sponsored":
   2710          if (!lazy.UrlbarPrefs.get("quickSuggestSponsoredPriority")) {
   2711            return { id: "urlbar-group-sponsored" };
   2712          }
   2713          break;
   2714        case "amo":
   2715          return { id: "urlbar-group-addon" };
   2716        case "mdn":
   2717          return { id: "urlbar-group-mdn" };
   2718        case "yelp":
   2719          return { id: "urlbar-group-local" };
   2720      }
   2721    }
   2722 
   2723    if (row.result.isBestMatch) {
   2724      return { id: "urlbar-group-best-match" };
   2725    }
   2726 
   2727    // Show "Shortcuts" if there's another result before that group.
   2728    if (
   2729      row.result.providerName == "UrlbarProviderTopSites" &&
   2730      this.#queryContext.results[0].providerName != "UrlbarProviderTopSites"
   2731    ) {
   2732      return { id: "urlbar-group-shortcuts" };
   2733    }
   2734 
   2735    if (!this.#queryContext?.searchString || row.result.heuristic) {
   2736      return null;
   2737    }
   2738 
   2739    if (row.result.providerName == lazy.UrlbarProviderQuickSuggest.name) {
   2740      if (
   2741        row.result.payload.provider == "Weather" &&
   2742        !row.result.payload.showRowLabel
   2743      ) {
   2744        return null;
   2745      }
   2746      return { id: "urlbar-group-firefox-suggest" };
   2747    }
   2748 
   2749    switch (row.result.type) {
   2750      case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD:
   2751      case lazy.UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
   2752      case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
   2753      case lazy.UrlbarUtils.RESULT_TYPE.URL:
   2754        return { id: "urlbar-group-firefox-suggest" };
   2755      case lazy.UrlbarUtils.RESULT_TYPE.SEARCH:
   2756        return {
   2757          id: "urlbar-group-search-suggestions",
   2758          args: { engine: engineName },
   2759        };
   2760      case lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC:
   2761        if (row.result.providerName == "quickactions") {
   2762          return { id: "urlbar-group-quickactions" };
   2763        }
   2764        break;
   2765    }
   2766 
   2767    return null;
   2768  }
   2769 
   2770  #setRowVisibility(row, visible) {
   2771    row.toggleAttribute("hidden", !visible);
   2772 
   2773    if (
   2774      !visible &&
   2775      row.result.type != lazy.UrlbarUtils.RESULT_TYPE.TIP &&
   2776      row.result.type != lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC
   2777    ) {
   2778      // Reset the overflow state of elements that can overflow in case their
   2779      // content changes while they're hidden. When making the row visible
   2780      // again, we'll get new overflow events if needed.
   2781      this.#setElementOverflowing(row._elements.get("title"), false);
   2782      this.#setElementOverflowing(row._elements.get("url"), false);
   2783      let tagsContainer = row._elements.get("tagsContainer");
   2784      if (tagsContainer) {
   2785        this.#setElementOverflowing(tagsContainer, false);
   2786      }
   2787    }
   2788  }
   2789 
   2790  /**
   2791   * Returns true if the given element and its row are both visible.
   2792   *
   2793   * @param {Element} element
   2794   *   An element in the view.
   2795   * @returns {boolean}
   2796   *   True if the given element and its row are both visible.
   2797   */
   2798  #isElementVisible(element) {
   2799    if (!element || element.style.display == "none") {
   2800      return false;
   2801    }
   2802    let row = this.#getRowFromElement(element);
   2803    return row && !row.hasAttribute("hidden");
   2804  }
   2805 
   2806  #removeStaleRows() {
   2807    let row = this.#rows.lastElementChild;
   2808    while (row) {
   2809      let next = row.previousElementSibling;
   2810      if (row.hasAttribute("stale")) {
   2811        row.remove();
   2812      } else {
   2813        this.#setRowVisibility(row, true);
   2814      }
   2815      row = next;
   2816    }
   2817    this.#updateIndices();
   2818 
   2819    // Reset actionmode if we left the actions search mode.
   2820    // We do this after updating the result rows to ensure the attribute stays
   2821    // active the entire time the actions list is visible.
   2822 
   2823    // this.input.searchMode updates early, so only checking it would cause a
   2824    // flicker, and the first visible result's source being an action doesn't
   2825    // necessarily imply we are in actions mode, therefore we should check both.
   2826    if (
   2827      this.input.searchMode?.source != lazy.UrlbarUtils.RESULT_SOURCE.ACTIONS &&
   2828      this.visibleResults[0]?.source != lazy.UrlbarUtils.RESULT_SOURCE.ACTIONS
   2829    ) {
   2830      this.#rows.toggleAttribute("actionmode", false);
   2831    }
   2832 
   2833    // Accept tentative exposures. This is analogous to unhiding the
   2834    // hypothetical non-stale hidden rows of hidden-exposure results.
   2835    this.controller.engagementEvent.acceptTentativeExposures();
   2836  }
   2837 
   2838  #startRemoveStaleRowsTimer() {
   2839    this.#removeStaleRowsTimer = this.window.setTimeout(() => {
   2840      this.#removeStaleRowsTimer = null;
   2841      this.#removeStaleRows();
   2842    }, UrlbarView.removeStaleRowsTimeout);
   2843  }
   2844 
   2845  #cancelRemoveStaleRowsTimer() {
   2846    if (this.#removeStaleRowsTimer) {
   2847      this.window.clearTimeout(this.#removeStaleRowsTimer);
   2848      this.#removeStaleRowsTimer = null;
   2849    }
   2850  }
   2851 
   2852  #selectElement(
   2853    element,
   2854    { updateInput = true, setAccessibleFocus = true } = {}
   2855  ) {
   2856    if (element && !element.matches(KEYBOARD_SELECTABLE_ELEMENT_SELECTOR)) {
   2857      throw new Error("Element is not keyboard-selectable");
   2858    }
   2859 
   2860    if (this.#selectedElement) {
   2861      this.#selectedElement.toggleAttribute("selected", false);
   2862      this.#selectedElement.removeAttribute("aria-selected");
   2863      let row = this.#getSelectedRow();
   2864      row?.toggleAttribute("selected", false);
   2865      row?.toggleAttribute("descendant-selected", false);
   2866    }
   2867    let row = this.#getRowFromElement(element);
   2868    if (element) {
   2869      element.toggleAttribute("selected", true);
   2870      element.setAttribute("aria-selected", "true");
   2871      if (row?.hasAttribute("row-selectable")) {
   2872        row?.toggleAttribute("selected", true);
   2873      }
   2874      if (element != row) {
   2875        row?.toggleAttribute("descendant-selected", true);
   2876      }
   2877    }
   2878 
   2879    let result = row?.result;
   2880    let provider = this.#providersManager.getProvider(result?.providerName);
   2881    if (provider) {
   2882      provider.tryMethod("onBeforeSelection", result, element);
   2883    }
   2884 
   2885    this.#setAccessibleFocus(setAccessibleFocus && element);
   2886    this.#rawSelectedElement = element;
   2887 
   2888    if (updateInput) {
   2889      let urlOverride = null;
   2890      if (element?.classList?.contains("urlbarView-button")) {
   2891        // Clear the input when a button is selected.
   2892        urlOverride = "";
   2893      }
   2894      this.input.setValueFromResult({ result, urlOverride, element });
   2895    } else {
   2896      this.input.setResultForCurrentValue(result);
   2897    }
   2898 
   2899    if (provider) {
   2900      provider.tryMethod("onSelection", result, element);
   2901    }
   2902  }
   2903 
   2904  /**
   2905   * Returns the element closest to the given element that can be
   2906   * selected/picked.  If the element itself can be selected, it's returned.  If
   2907   * there is no such element, null is returned.
   2908   *
   2909   * @param {Element} element
   2910   *   An element in the view.
   2911   * @param {object} [options]
   2912   *   Options object.
   2913   * @param {boolean} [options.byMouse]
   2914   *   If true, include elements that are only selectable by mouse.
   2915   * @returns {Element}
   2916   *   The closest element that can be picked including the element itself, or
   2917   *   null if there is no such element.
   2918   */
   2919  #getClosestSelectableElement(element, { byMouse = false } = {}) {
   2920    let closest = element.closest(
   2921      byMouse
   2922        ? SELECTABLE_ELEMENT_SELECTOR
   2923        : KEYBOARD_SELECTABLE_ELEMENT_SELECTOR
   2924    );
   2925    if (closest && this.#isElementVisible(closest)) {
   2926      return closest;
   2927    }
   2928    // When clicking on a gap within a row or on its border or padding, treat
   2929    // this as if the main part was clicked.
   2930    if (
   2931      element.classList.contains("urlbarView-row") &&
   2932      element.hasAttribute("row-selectable")
   2933    ) {
   2934      return element._content;
   2935    }
   2936    return null;
   2937  }
   2938 
   2939  /**
   2940   * Returns true if the given element is keyboard-selectable.
   2941   *
   2942   * @param {Element} element
   2943   *   The element to test.
   2944   * @returns {boolean}
   2945   *   True if the element is selectable and false if not.
   2946   */
   2947  #isSelectableElement(element) {
   2948    return this.#getClosestSelectableElement(element) == element;
   2949  }
   2950 
   2951  /**
   2952   * Returns the first keyboard-selectable element in the view.
   2953   *
   2954   * @returns {Element}
   2955   *   The first selectable element in the view.
   2956   */
   2957  getFirstSelectableElement() {
   2958    let element = this.#rows.firstElementChild;
   2959    if (element && !this.#isSelectableElement(element)) {
   2960      element = this.#getNextSelectableElement(element);
   2961    }
   2962    return element;
   2963  }
   2964 
   2965  /**
   2966   * Returns the last keyboard-selectable element in the view.
   2967   *
   2968   * @returns {Element}
   2969   *   The last selectable element in the view.
   2970   */
   2971  getLastSelectableElement() {
   2972    let element = this.#rows.lastElementChild;
   2973    if (element && !this.#isSelectableElement(element)) {
   2974      element = this.#getPreviousSelectableElement(element);
   2975    }
   2976    return element;
   2977  }
   2978 
   2979  /**
   2980   * Returns the next keyboard-selectable element after the given element.  If
   2981   * the element is the last selectable element, returns null.
   2982   *
   2983   * @param {Element} element
   2984   *   An element in the view.
   2985   * @returns {Element}
   2986   *   The next selectable element after `element` or null if `element` is the
   2987   *   last selectable element.
   2988   */
   2989  #getNextSelectableElement(element) {
   2990    let row = this.#getRowFromElement(element);
   2991    if (!row) {
   2992      return null;
   2993    }
   2994 
   2995    let next = row.nextElementSibling;
   2996    let selectables = this.#getKeyboardSelectablesInRow(row);
   2997    if (selectables.length) {
   2998      let index = selectables.indexOf(element);
   2999      if (index < selectables.length - 1) {
   3000        next = selectables[index + 1];
   3001      }
   3002    }
   3003 
   3004    if (next && !this.#isSelectableElement(next)) {
   3005      next = this.#getNextSelectableElement(next);
   3006    }
   3007 
   3008    return next;
   3009  }
   3010 
   3011  /**
   3012   * Returns the previous keyboard-selectable element before the given element.
   3013   * If the element is the first selectable element, returns null.
   3014   *
   3015   * @param {Element} element
   3016   *   An element in the view.
   3017   * @returns {Element}
   3018   *   The previous selectable element before `element` or null if `element` is
   3019   *   the first selectable element.
   3020   */
   3021  #getPreviousSelectableElement(element) {
   3022    let row = this.#getRowFromElement(element);
   3023    if (!row) {
   3024      return null;
   3025    }
   3026 
   3027    let previous = row.previousElementSibling;
   3028    let selectables = this.#getKeyboardSelectablesInRow(row);
   3029    if (selectables.length) {
   3030      let index = selectables.indexOf(element);
   3031      if (index < 0) {
   3032        previous = selectables[selectables.length - 1];
   3033      } else if (index > 0) {
   3034        previous = selectables[index - 1];
   3035      }
   3036    }
   3037 
   3038    if (previous && !this.#isSelectableElement(previous)) {
   3039      previous = this.#getPreviousSelectableElement(previous);
   3040    }
   3041 
   3042    return previous;
   3043  }
   3044 
   3045  #getKeyboardSelectablesInRow(row) {
   3046    let selectables = [
   3047      ...row.querySelectorAll(KEYBOARD_SELECTABLE_ELEMENT_SELECTOR),
   3048    ];
   3049 
   3050    // Sort links last. This assumes that any links in the row are informational
   3051    // and should be deprioritized with regard to selection compared to buttons
   3052    // and other elements.
   3053    selectables.sort(
   3054      (a, b) => Number(a.localName == "a") - Number(b.localName == "a")
   3055    );
   3056 
   3057    return selectables;
   3058  }
   3059 
   3060  /**
   3061   * Returns the currently selected row. Useful when this.#selectedElement may
   3062   * be a non-row element, such as a descendant element of RESULT_TYPE.TIP.
   3063   *
   3064   * @returns {Element}
   3065   *   The currently selected row, or ancestor row of the currently selected
   3066   *   item.
   3067   */
   3068  #getSelectedRow() {
   3069    return this.#getRowFromElement(this.#selectedElement);
   3070  }
   3071 
   3072  /**
   3073   * @param {Element} element
   3074   *   An element that is potentially a row or descendant of a row.
   3075   * @returns {Element}
   3076   *   The row containing `element`, or `element` itself if it is a row.
   3077   */
   3078  #getRowFromElement(element) {
   3079    return element?.closest(".urlbarView-row");
   3080  }
   3081 
   3082  #setAccessibleFocus(item) {
   3083    if (item) {
   3084      if (!item.id) {
   3085        // Assign an id to dynamic actions as required by aria-activedescendant.
   3086        item.id = getUniqueId("aria-activedescendant-target-");
   3087      }
   3088      this.input.inputField.setAttribute("aria-activedescendant", item.id);
   3089    } else {
   3090      this.input.inputField.removeAttribute("aria-activedescendant");
   3091    }
   3092  }
   3093 
   3094  /**
   3095   * Sets `result`'s title in `titleNode`'s DOM.
   3096   *
   3097   * @param {UrlbarResult} result
   3098   *   The result for which the title is being set.
   3099   * @param {Element} titleNode
   3100   *   The DOM node for the result's tile.
   3101   */
   3102  #setResultTitle(result, titleNode) {
   3103    if (result.payload.titleL10n) {
   3104      this.#l10nCache.setElementL10n(titleNode, result.payload.titleL10n);
   3105      return;
   3106    }
   3107 
   3108    // TODO: `text` is intended only for WebExtensions. We should remove it and
   3109    // the WebExtensions urlbar API since we're no longer using it.
   3110    if (result.payload.text) {
   3111      titleNode.textContent = result.payload.text;
   3112      return;
   3113    }
   3114 
   3115    if (result.payload.providesSearchMode) {
   3116      if (result.type == lazy.UrlbarUtils.RESULT_TYPE.RESTRICT) {
   3117        let localSearchMode =
   3118          result.payload.l10nRestrictKeywords[0].toLowerCase();
   3119        let keywords = result.payload.l10nRestrictKeywords
   3120          .map(keyword => `@${keyword.toLowerCase()}`)
   3121          .join(", ");
   3122 
   3123        this.#l10nCache.setElementL10n(titleNode, {
   3124          id: "urlbar-result-search-with-local-search-mode",
   3125          args: {
   3126            keywords,
   3127            localSearchMode,
   3128          },
   3129        });
   3130      } else if (
   3131        result.providerName == "UrlbarProviderTokenAliasEngines" &&
   3132        lazy.UrlbarPrefs.getScotchBonnetPref(
   3133          "searchRestrictKeywords.featureGate"
   3134        )
   3135      ) {
   3136        this.#l10nCache.setElementL10n(titleNode, {
   3137          id: "urlbar-result-search-with-engine-keywords",
   3138          args: {
   3139            keywords: result.payload.keywords,
   3140            engine: result.payload.engine,
   3141          },
   3142        });
   3143      } else {
   3144        // Keyword offers are the only result that require a localized title.
   3145        // We localize the title instead of using the action text as a title
   3146        // because some keyword offer results use both a title and action text
   3147        // (e.g., tab-to-search).
   3148        this.#l10nCache.setElementL10n(titleNode, {
   3149          id: "urlbar-result-action-search-w-engine",
   3150          args: { engine: result.payload.engine },
   3151        });
   3152      }
   3153 
   3154      return;
   3155    }
   3156 
   3157    this.#l10nCache.removeElementL10n(titleNode);
   3158 
   3159    let titleAndHighlights = result.getDisplayableValueAndHighlights("title", {
   3160      tokens: this.#queryContext.tokens,
   3161    });
   3162    lazy.UrlbarUtils.addTextContentWithHighlights(
   3163      titleNode,
   3164      titleAndHighlights.value,
   3165      titleAndHighlights.highlights
   3166    );
   3167  }
   3168 
   3169  /**
   3170   * Offsets all highlight ranges by a given amount.
   3171   *
   3172   * @param {Array} highlights The highlights which should be offset.
   3173   * @param {int} startOffset
   3174   *    The number by which we want to offset the highlights range starts.
   3175   * @returns {Array} The offset highlights.
   3176   */
   3177  #offsetHighlights(highlights, startOffset) {
   3178    return highlights.map(highlight => [
   3179      highlight[0] + startOffset,
   3180      highlight[1],
   3181    ]);
   3182  }
   3183 
   3184  /**
   3185   * Sets the content of the 'Switch To Tab' chiclet.
   3186   *
   3187   * @param {UrlbarResult} result
   3188   *   The result for which the content is being set.
   3189   * @param {Element} actionNode
   3190   *   The DOM node for the result's action.
   3191   */
   3192  #setSwitchTabActionChiclet(result, actionNode) {
   3193    actionNode.classList.add("urlbarView-switchToTab");
   3194 
   3195    let contextualIdentityAction = actionNode.parentNode.querySelector(
   3196      ".action-contextualidentity"
   3197    );
   3198 
   3199    if (
   3200      lazy.UrlbarPrefs.get("switchTabs.searchAllContainers") &&
   3201      result.type == lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH &&
   3202      lazy.UrlbarProviderOpenTabs.isContainerUserContextId(
   3203        result.payload.userContextId
   3204      )
   3205    ) {
   3206      if (!contextualIdentityAction) {
   3207        contextualIdentityAction = actionNode.cloneNode(true);
   3208        contextualIdentityAction.classList.add("action-contextualidentity");
   3209        this.#l10nCache.removeElementL10n(contextualIdentityAction);
   3210        actionNode.parentNode.insertBefore(
   3211          contextualIdentityAction,
   3212          actionNode
   3213        );
   3214      }
   3215 
   3216      this.#addContextualIdentityToSwitchTabChiclet(
   3217        result,
   3218        contextualIdentityAction
   3219      );
   3220    } else {
   3221      contextualIdentityAction?.remove();
   3222    }
   3223 
   3224    let tabGroupAction = actionNode.parentNode.querySelector(
   3225      ".urlbarView-tabGroup"
   3226    );
   3227 
   3228    if (
   3229      result.type == lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH &&
   3230      result.payload.tabGroup
   3231    ) {
   3232      if (!tabGroupAction) {
   3233        tabGroupAction = actionNode.cloneNode(true);
   3234        this.#l10nCache.removeElementL10n(tabGroupAction);
   3235        actionNode.parentNode.insertBefore(tabGroupAction, actionNode);
   3236      }
   3237 
   3238      this.#addGroupToSwitchTabChiclet(result, tabGroupAction);
   3239    } else {
   3240      tabGroupAction?.remove();
   3241    }
   3242 
   3243    this.#l10nCache.setElementL10n(actionNode, {
   3244      id: "urlbar-result-action-switch-tab",
   3245    });
   3246  }
   3247 
   3248  #addContextualIdentityToSwitchTabChiclet(result, actionNode) {
   3249    let label = lazy.ContextualIdentityService.getUserContextLabel(
   3250      result.payload.userContextId
   3251    );
   3252    // To avoid flicker don't update the label unless necessary.
   3253    if (
   3254      actionNode.classList.contains("urlbarView-userContext") &&
   3255      label &&
   3256      actionNode == label
   3257    ) {
   3258      return;
   3259    }
   3260    actionNode.innerHTML = "";
   3261    let identity = lazy.ContextualIdentityService.getPublicIdentityFromId(
   3262      result.payload.userContextId
   3263    );
   3264    if (identity) {
   3265      actionNode.classList.add("urlbarView-userContext");
   3266      actionNode.classList.remove("urlbarView-switchToTab");
   3267      if (identity.color) {
   3268        actionNode.className = actionNode.className.replace(
   3269          /identity-color-\w*/g,
   3270          ""
   3271        );
   3272        actionNode.classList.add("identity-color-" + identity.color);
   3273      }
   3274 
   3275      let textModeLabel = this.#createElement("div");
   3276      textModeLabel.classList.add("urlbarView-userContext-textMode");
   3277 
   3278      if (label) {
   3279        textModeLabel.innerText = label;
   3280        actionNode.appendChild(textModeLabel);
   3281 
   3282        let iconModeLabel = this.#createElement("div");
   3283        iconModeLabel.classList.add("urlbarView-userContext-iconMode");
   3284        actionNode.appendChild(iconModeLabel);
   3285        if (identity.icon) {
   3286          let userContextIcon = this.#createElement("img");
   3287          userContextIcon.classList.add("urlbarView-userContext-icon");
   3288          userContextIcon.setAttribute("alt", label);
   3289          userContextIcon.src =
   3290            "resource://usercontext-content/" + identity.icon + ".svg";
   3291          iconModeLabel.appendChild(userContextIcon);
   3292        }
   3293        actionNode.setAttribute("tooltiptext", label);
   3294      }
   3295    }
   3296  }
   3297 
   3298  #addGroupToSwitchTabChiclet(result, actionNode) {
   3299    const group = this.window.gBrowser.getTabGroupById(result.payload.tabGroup);
   3300    if (!group) {
   3301      actionNode.remove();
   3302      return;
   3303    }
   3304 
   3305    actionNode.classList.add("urlbarView-tabGroup");
   3306    actionNode.classList.remove("urlbarView-switchToTab");
   3307 
   3308    actionNode.innerHTML = "";
   3309    let fullWidthModeLabel = this.#createElement("div");
   3310    fullWidthModeLabel.classList.add("urlbarView-tabGroup-fullWidthMode");
   3311 
   3312    let narrowWidthModeLabel = this.#createElement("div");
   3313    narrowWidthModeLabel.classList.add("urlbarView-tabGroup-narrowWidthMode");
   3314 
   3315    if (group.label) {
   3316      fullWidthModeLabel.textContent = group.label;
   3317      narrowWidthModeLabel.textContent = group.label[0];
   3318    } else {
   3319      this.#l10nCache.setElementL10n(fullWidthModeLabel, {
   3320        id: `urlbar-result-action-tab-group-unnamed`,
   3321      });
   3322    }
   3323 
   3324    actionNode.appendChild(fullWidthModeLabel);
   3325    actionNode.appendChild(narrowWidthModeLabel);
   3326 
   3327    actionNode.style.setProperty(
   3328      "--tab-group-color",
   3329      group.style.getPropertyValue("--tab-group-color")
   3330    );
   3331    actionNode.style.setProperty(
   3332      "--tab-group-color-invert",
   3333      group.style.getPropertyValue("--tab-group-color-invert")
   3334    );
   3335    actionNode.style.setProperty(
   3336      "--tab-group-color-pale",
   3337      group.style.getPropertyValue("--tab-group-color-pale")
   3338    );
   3339  }
   3340 
   3341  /**
   3342   * Adds markup for a tail suggestion prefix to a row.
   3343   *
   3344   * @param {Element} item
   3345   *   The node for the result row.
   3346   * @param {UrlbarResult} result
   3347   *   A UrlbarResult representing a tail suggestion.
   3348   */
   3349  #fillTailSuggestionPrefix(item, result) {
   3350    let tailPrefixStrNode = item._elements.get("tailPrefixStr");
   3351    let tailPrefixStr = result.payload.suggestion.substring(
   3352      0,
   3353      result.payload.tailOffsetIndex
   3354    );
   3355    tailPrefixStrNode.textContent = tailPrefixStr;
   3356 
   3357    let tailPrefixCharNode = item._elements.get("tailPrefixChar");
   3358    tailPrefixCharNode.textContent = result.payload.tailPrefix;
   3359  }
   3360 
   3361  #enableOrDisableRowWrap() {
   3362    let wrap = getBoundsWithoutFlushing(this.input).width < 650;
   3363    this.#rows.toggleAttribute("wrap", wrap);
   3364    this.oneOffSearchButtons?.container.toggleAttribute("wrap", wrap);
   3365  }
   3366 
   3367  /**
   3368   * @param {Element} element
   3369   *   The element
   3370   * @returns {boolean}
   3371   *   Whether we track this element's overflow status in order to fade it out
   3372   *   and add a tooltip when needed.
   3373   */
   3374  #canElementOverflow(element) {
   3375    let { classList } = element;
   3376    return (
   3377      classList.contains("urlbarView-overflowable") ||
   3378      classList.contains("urlbarView-url")
   3379    );
   3380  }
   3381 
   3382  /**
   3383   * Marks an element as overflowing or not overflowing.
   3384   *
   3385   * @param {Element} element
   3386   *   The element
   3387   * @param {boolean} overflowing
   3388   *   Whether the element is overflowing
   3389   */
   3390  #setElementOverflowing(element, overflowing) {
   3391    element.toggleAttribute("overflow", overflowing);
   3392    this.#updateOverflowTooltip(element);
   3393  }
   3394 
   3395  /**
   3396   * Sets an overflowing element's tooltip, or removes the tooltip if the
   3397   * element isn't overflowing. Also optionally updates the string that should
   3398   * be used as the tooltip in case of overflow.
   3399   *
   3400   * @param {Element} element
   3401   *   The element
   3402   * @param {string} [tooltip]
   3403   *   The string that should be used in the tooltip. This will be stored and
   3404   *   re-used next time the element overflows.
   3405   */
   3406  #updateOverflowTooltip(element, tooltip) {
   3407    if (typeof tooltip == "string") {
   3408      element._tooltip = tooltip;
   3409    }
   3410    if (element.hasAttribute("overflow") && element._tooltip) {
   3411      element.setAttribute("title", element._tooltip);
   3412    } else {
   3413      element.removeAttribute("title");
   3414    }
   3415  }
   3416 
   3417  /**
   3418   * If the view is open and showing a single search tip, this method picks it
   3419   * and closes the view.  This counts as an engagement, so this method should
   3420   * only be called due to user interaction.
   3421   *
   3422   * @param {event} event
   3423   *   The user-initiated event for the interaction.  Should not be null.
   3424   * @returns {boolean}
   3425   *   True if this method picked a tip, false otherwise.
   3426   */
   3427  #pickSearchTipIfPresent(event) {
   3428    if (
   3429      !this.isOpen ||
   3430      !this.#queryContext ||
   3431      this.#queryContext.results.length != 1
   3432    ) {
   3433      return false;
   3434    }
   3435    let result = this.#queryContext.results[0];
   3436    if (result.type != lazy.UrlbarUtils.RESULT_TYPE.TIP) {
   3437      return false;
   3438    }
   3439    let buttons = this.#rows.firstElementChild._buttons;
   3440    let tipButton = buttons.get("tip") || buttons.get("0");
   3441    if (!tipButton) {
   3442      throw new Error("Expected a tip button");
   3443    }
   3444    this.input.pickElement(tipButton, event);
   3445    return true;
   3446  }
   3447 
   3448  /**
   3449   * Caches some l10n strings used by the view. Strings that are already cached
   3450   * are not cached again.
   3451   *
   3452   * Note:
   3453   *   Currently strings are never evicted from the cache, so do not cache
   3454   *   strings whose arguments include the search string or other values that
   3455   *   can cause the cache to grow unbounded. Suitable strings include those
   3456   *   without arguments or those whose arguments depend on a small set of
   3457   *   static values like search engine names.
   3458   */
   3459  async #cacheL10nStrings() {
   3460    let idArgs = [
   3461      ...this.#cacheL10nIDArgsForSearchService(),
   3462      { id: "urlbar-result-action-search-bookmarks" },
   3463      { id: "urlbar-result-action-search-history" },
   3464      { id: "urlbar-result-action-search-in-private" },
   3465      { id: "urlbar-result-action-search-tabs" },
   3466      { id: "urlbar-result-action-switch-tab" },
   3467      { id: "urlbar-result-action-visit" },
   3468      { id: "urlbar-result-action-visit-from-clipboard" },
   3469    ];
   3470 
   3471    let suggestSponsoredEnabled =
   3472      lazy.UrlbarPrefs.get("quickSuggestEnabled") &&
   3473      lazy.UrlbarPrefs.get("suggest.quicksuggest.sponsored");
   3474 
   3475    if (lazy.UrlbarPrefs.get("groupLabels.enabled")) {
   3476      idArgs.push({ id: "urlbar-group-firefox-suggest" });
   3477      idArgs.push({ id: "urlbar-group-best-match" });
   3478      if (lazy.UrlbarPrefs.get("quickSuggestEnabled")) {
   3479        if (lazy.UrlbarPrefs.get("addonsFeatureGate")) {
   3480          idArgs.push({ id: "urlbar-group-addon" });
   3481        }
   3482        if (lazy.UrlbarPrefs.get("mdn.featureGate")) {
   3483          idArgs.push({ id: "urlbar-group-mdn" });
   3484        }
   3485        if (lazy.UrlbarPrefs.get("yelpFeatureGate")) {
   3486          idArgs.push({ id: "urlbar-group-local" });
   3487        }
   3488        if (
   3489          suggestSponsoredEnabled &&
   3490          lazy.UrlbarPrefs.get("quickSuggestAmpTopPickCharThreshold")
   3491        ) {
   3492          idArgs.push({ id: "urlbar-group-sponsored" });
   3493        }
   3494      }
   3495    }
   3496 
   3497    if (suggestSponsoredEnabled) {
   3498      idArgs.push({ id: "urlbar-result-action-sponsored" });
   3499    }
   3500 
   3501    await this.#l10nCache.ensureAll(idArgs);
   3502  }
   3503 
   3504  /**
   3505   * A helper for l10n string caching that returns `{ id, args }` objects for
   3506   * strings that depend on the search service.
   3507   *
   3508   * @returns {Array}
   3509   *   Array of `{ id, args }` objects, possibly empty.
   3510   */
   3511  #cacheL10nIDArgsForSearchService() {
   3512    // The search service may not be initialized if the user opens the view very
   3513    // quickly after startup. Skip caching related strings in that case. Strings
   3514    // are cached opportunistically every time the view opens, so they'll be
   3515    // cached soon. We could use the search service's async methods, which
   3516    // internally await initialization, but that would allow previously cached
   3517    // out-of-date strings to appear in the view while the async calls are
   3518    // ongoing. Generally there's no reason for our string-caching paths to be
   3519    // async and it may even be a bad idea (except for the final necessary
   3520    // `this.#l10nCache.ensureAll()` call).
   3521    if (!Services.search.hasSuccessfullyInitialized) {
   3522      return [];
   3523    }
   3524 
   3525    let idArgs = [];
   3526 
   3527    let { defaultEngine, defaultPrivateEngine } = Services.search;
   3528    let engineNames = [defaultEngine?.name, defaultPrivateEngine?.name].filter(
   3529      name => name
   3530    );
   3531 
   3532    if (defaultPrivateEngine) {
   3533      idArgs.push({
   3534        id: "urlbar-result-action-search-in-private-w-engine",
   3535        args: { engine: defaultPrivateEngine.name },
   3536      });
   3537    }
   3538 
   3539    let engineStringIDs = [
   3540      "urlbar-result-action-tabtosearch-web",
   3541      "urlbar-result-action-tabtosearch-other-engine",
   3542      "urlbar-result-action-search-w-engine",
   3543    ];
   3544    for (let id of engineStringIDs) {
   3545      idArgs.push(...engineNames.map(name => ({ id, args: { engine: name } })));
   3546    }
   3547 
   3548    if (lazy.UrlbarPrefs.get("groupLabels.enabled")) {
   3549      idArgs.push(
   3550        ...engineNames.map(name => ({
   3551          id: "urlbar-group-search-suggestions",
   3552          args: { engine: name },
   3553        }))
   3554      );
   3555    }
   3556 
   3557    return idArgs;
   3558  }
   3559 
   3560  /**
   3561   * @param {UrlbarResult} result
   3562   *   The result to get menu commands for.
   3563   * @returns {Array}
   3564   *   Array of menu commands available for the result, null if there are none.
   3565   */
   3566  #getResultMenuCommands(result) {
   3567    if (this.#resultMenuCommands.has(result)) {
   3568      return this.#resultMenuCommands.get(result);
   3569    }
   3570 
   3571    /**
   3572     * @type {?UrlbarResultCommand[]}
   3573     */
   3574    let commands = this.#providersManager
   3575      .getProvider(result.providerName)
   3576      ?.tryMethod("getResultCommands", result);
   3577    if (commands) {
   3578      this.#resultMenuCommands.set(result, commands);
   3579      return commands;
   3580    }
   3581 
   3582    commands = [];
   3583    if (result.payload.isBlockable) {
   3584      commands.push({
   3585        name: RESULT_MENU_COMMANDS.DISMISS,
   3586        l10n: result.payload.blockL10n || {
   3587          id: "urlbar-result-menu-dismiss-suggestion",
   3588        },
   3589      });
   3590    }
   3591    if (result.payload.helpUrl) {
   3592      commands.push({
   3593        name: RESULT_MENU_COMMANDS.HELP,
   3594        l10n: result.payload.helpL10n || {
   3595          id: "urlbar-result-menu-learn-more",
   3596        },
   3597      });
   3598    }
   3599    if (result.payload.isManageable) {
   3600      commands.push({
   3601        name: RESULT_MENU_COMMANDS.MANAGE,
   3602        l10n: {
   3603          id: "urlbar-result-menu-manage-firefox-suggest",
   3604        },
   3605      });
   3606    }
   3607 
   3608    let rv = commands.length ? commands : null;
   3609    this.#resultMenuCommands.set(result, rv);
   3610    return rv;
   3611  }
   3612 
   3613  /**
   3614   * Popuplates the result menu with commands.
   3615   *
   3616   * @param {object} options
   3617   * @param {XULTextElement} options.menupopup
   3618   * @param {UrlbarResultCommand[]} options.commands
   3619   */
   3620  #populateResultMenu({ menupopup = this.resultMenu, commands }) {
   3621    menupopup.textContent = "";
   3622    for (let data of commands) {
   3623      if (data.children) {
   3624        let popup = this.document.createXULElement("menupopup");
   3625        this.#populateResultMenu({
   3626          menupopup: popup,
   3627          commands: data.children,
   3628        });
   3629        let menu = this.document.createXULElement("menu");
   3630        this.#l10nCache.setElementL10n(menu, data.l10n);
   3631        menu.appendChild(popup);
   3632        menupopup.appendChild(menu);
   3633        continue;
   3634      }
   3635      if (data.name == "separator") {
   3636        menupopup.appendChild(this.document.createXULElement("menuseparator"));
   3637        continue;
   3638      }
   3639      let menuitem = this.document.createXULElement("menuitem");
   3640      menuitem.dataset.command = data.name;
   3641      menuitem.classList.add("urlbarView-result-menuitem");
   3642      this.#l10nCache.setElementL10n(menuitem, data.l10n);
   3643      menupopup.appendChild(menuitem);
   3644    }
   3645  }
   3646 
   3647  // Event handlers below.
   3648 
   3649  on_SelectedOneOffButtonChanged() {
   3650    if (!this.isOpen || !this.#queryContext) {
   3651      return;
   3652    }
   3653 
   3654    let engine = this.oneOffSearchButtons.selectedButton?.engine;
   3655    let source = this.oneOffSearchButtons.selectedButton?.source;
   3656    let icon = this.oneOffSearchButtons.selectedButton?.image;
   3657 
   3658    let localSearchMode;
   3659    if (source) {
   3660      localSearchMode = lazy.UrlbarUtils.LOCAL_SEARCH_MODES.find(
   3661        m => m.source == source
   3662      );
   3663    }
   3664 
   3665    for (let item of this.#rows.children) {
   3666      let result = item.result;
   3667 
   3668      let isPrivateSearchWithoutPrivateEngine =
   3669        result.payload.inPrivateWindow && !result.payload.isPrivateEngine;
   3670      let isSearchHistory =
   3671        result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH &&
   3672        result.source == lazy.UrlbarUtils.RESULT_SOURCE.HISTORY;
   3673      let isSearchSuggestion = result.payload.suggestion && !isSearchHistory;
   3674 
   3675      // For one-off buttons having a source, we update the action for the
   3676      // heuristic result, or for any non-heuristic that is a remote search
   3677      // suggestion or a private search with no private engine.
   3678      if (
   3679        !result.heuristic &&
   3680        !isSearchSuggestion &&
   3681        !isPrivateSearchWithoutPrivateEngine
   3682      ) {
   3683        continue;
   3684      }
   3685 
   3686      // If there is no selected button and we are in full search mode, it is
   3687      // because the user just confirmed a one-off button, thus starting a new
   3688      // query. Don't change the heuristic result because it would be
   3689      // immediately replaced with the search mode heuristic, causing flicker.
   3690      if (
   3691        result.heuristic &&
   3692        !engine &&
   3693        !localSearchMode &&
   3694        this.input.searchMode &&
   3695        !this.input.searchMode.isPreview
   3696      ) {
   3697        continue;
   3698      }
   3699 
   3700      let action = item._elements.get("action");
   3701      let favicon = item._elements.get("favicon");
   3702      let title = item._elements.get("title");
   3703 
   3704      // If a one-off button is the only selection, force the heuristic result
   3705      // to show its action text, so the engine name is visible.
   3706      if (
   3707        result.heuristic &&
   3708        !this.selectedElement &&
   3709        (localSearchMode || engine)
   3710      ) {
   3711        item.setAttribute("show-action-text", "true");
   3712      } else {
   3713        item.removeAttribute("show-action-text");
   3714      }
   3715 
   3716      // If an engine is selected, update search results to use that engine.
   3717      // Otherwise, restore their original engines.
   3718      if (result.type == lazy.UrlbarUtils.RESULT_TYPE.SEARCH) {
   3719        if (engine) {
   3720          if (!result.payload.originalEngine) {
   3721            result.payload.originalEngine = result.payload.engine;
   3722          }
   3723          result.payload.engine = engine.name;
   3724        } else if (result.payload.originalEngine) {
   3725          result.payload.engine = result.payload.originalEngine;
   3726          delete result.payload.originalEngine;
   3727        }
   3728      }
   3729 
   3730      // If the result is the heuristic and a one-off is selected (i.e.,
   3731      // localSearchMode || engine), then restyle it to look like a search
   3732      // result; otherwise, remove such styling. For restyled results, we
   3733      // override the usual result-picking behaviour in UrlbarInput.pickResult.
   3734      if (result.heuristic) {
   3735        title.textContent =
   3736          localSearchMode || engine
   3737            ? this.#queryContext.searchString
   3738            : result.getDisplayableValueAndHighlights("title").value;
   3739 
   3740        // Set the restyled-search attribute so the action text and title
   3741        // separator are shown or hidden via CSS as appropriate.
   3742        if (localSearchMode || engine) {
   3743          item.setAttribute("restyled-search", "true");
   3744        } else {
   3745          item.removeAttribute("restyled-search");
   3746        }
   3747      }
   3748 
   3749      // Update result action text.
   3750      if (localSearchMode) {
   3751        // Update the result action text for a local one-off.
   3752        const messageIDs = {
   3753          actions: "urlbar-result-action-search-actions",
   3754          bookmarks: "urlbar-result-action-search-bookmarks",
   3755          history: "urlbar-result-action-search-history",
   3756          tabs: "urlbar-result-action-search-tabs",
   3757        };
   3758        let name = lazy.UrlbarUtils.getResultSourceName(localSearchMode.source);
   3759        this.#l10nCache.setElementL10n(action, {
   3760          id: messageIDs[name],
   3761        });
   3762        if (result.heuristic) {
   3763          item.setAttribute("source", name);
   3764        }
   3765      } else if (engine && !result.payload.inPrivateWindow) {
   3766        // Update the result action text for an engine one-off.
   3767        this.#l10nCache.setElementL10n(action, {
   3768          id: "urlbar-result-action-search-w-engine",
   3769          args: { engine: engine.name },
   3770        });
   3771      } else {
   3772        // No one-off is selected. If we replaced the action while a one-off
   3773        // button was selected, it should be restored.
   3774        if (item._originalActionSetter) {
   3775          item._originalActionSetter();
   3776          if (result.heuristic) {
   3777            favicon.src = result.payload.icon || lazy.UrlbarUtils.ICON.DEFAULT;
   3778          }
   3779        } else {
   3780          console.error("An item is missing the action setter");
   3781        }
   3782        item.removeAttribute("source");
   3783      }
   3784 
   3785      // Update result favicons.
   3786      let iconOverride = localSearchMode?.icon;
   3787      // If the icon is the default one-off search placeholder, assume we
   3788      // don't have an icon for the engine.
   3789      if (
   3790        !iconOverride &&
   3791        icon != "chrome://browser/skin/search-engine-placeholder.png"
   3792      ) {
   3793        iconOverride = icon;
   3794      }
   3795      if (!iconOverride && (localSearchMode || engine)) {
   3796        // For one-offs without an icon, do not allow restyled URL results to
   3797        // use their own icons.
   3798        iconOverride = lazy.UrlbarUtils.ICON.SEARCH_GLASS;
   3799      }
   3800      if (
   3801        result.heuristic ||
   3802        (result.payload.inPrivateWindow && !result.payload.isPrivateEngine)
   3803      ) {
   3804        // If we just changed the engine from the original engine and it had an
   3805        // icon, then make sure the result now uses the new engine's icon or
   3806        // failing that the default icon.  If we changed it back to the original
   3807        // engine, go back to the original or default icon.
   3808        favicon.src = this.#iconForResult(result, iconOverride);
   3809      }
   3810    }
   3811  }
   3812 
   3813  on_blur() {
   3814    // If the view is open without the input being focused, it will not close
   3815    // automatically when the window loses focus. We might be in this state
   3816    // after a Search Tip is shown on an engine homepage.
   3817    if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
   3818      this.close();
   3819    }
   3820  }
   3821 
   3822  on_mousedown(event) {
   3823    if (event.button == 2) {
   3824      // Ignore right clicks.
   3825      return;
   3826    }
   3827 
   3828    let element = this.#getClosestSelectableElement(event.target, {
   3829      byMouse: true,
   3830    });
   3831    if (!element) {
   3832      // Ignore clicks on elements that can't be selected/picked.
   3833      return;
   3834    }
   3835 
   3836    this.window.top.addEventListener("mouseup", this);
   3837 
   3838    // Select the element and open a speculative connection unless it's a
   3839    // button. Buttons are special in the two ways listed below. Some buttons
   3840    // may be exceptions to these two criteria, but to provide a consistent UX
   3841    // and avoid complexity, we apply this logic to all of them.
   3842    //
   3843    // (1) Some buttons do not close the view when clicked, like the block and
   3844    // menu buttons. Clicking these buttons should not have any side effects in
   3845    // the view or input beyond their primary purpose. For example, the block
   3846    // button should remove the row but it should not change the input value or
   3847    // page proxy state, and ideally it shouldn't change the input's selection
   3848    // or caret either. It probably also shouldn't change the view's selection
   3849    // (if the removed row isn't selected), but that may be more debatable.
   3850    //
   3851    // It may be possible to select buttons on mousedown and then clear the
   3852    // selection on mouseup as usual while meeting these requirements. However,
   3853    // it's not simple because clearing the selection has surprising side
   3854    // effects in the input like the ones mentioned above.
   3855    //
   3856    // (2) Most buttons don't have URLs, so there's nothing to speculatively
   3857    // connect to. If a button does have a URL, it's typically different from
   3858    // the primary URL of its related result, so it's not critical to open a
   3859    // speculative connection anyway.
   3860    if (!element.classList.contains("urlbarView-button")) {
   3861      this.#mousedownSelectedElement = element;
   3862      this.#selectElement(element, { updateInput: false });
   3863      this.controller.speculativeConnect(
   3864        this.selectedResult,
   3865        this.#queryContext,
   3866        "mousedown"
   3867      );
   3868    }
   3869  }
   3870 
   3871  on_mouseup(event) {
   3872    if (event.button == 2) {
   3873      // Ignore right clicks.
   3874      return;
   3875    }
   3876 
   3877    this.window.top.removeEventListener("mouseup", this);
   3878 
   3879    // When mouseup outside of browser, as the target will not be element,
   3880    // ignore it.
   3881    let element =
   3882      event.target.nodeType === event.target.ELEMENT_NODE
   3883        ? this.#getClosestSelectableElement(event.target, { byMouse: true })
   3884        : null;
   3885    if (element) {
   3886      this.input.pickElement(element, event);
   3887    }
   3888 
   3889    // If the element that was selected on mousedown is still in the view, clear
   3890    // the selection. Do it after calling `pickElement()` above since code that
   3891    // reacts to picks may assume the selected element is the picked element.
   3892    //
   3893    // If the element is no longer in the view, then it must be because its row
   3894    // was removed in response to the pick. If the element was not a button, we
   3895    // selected it on mousedown and then `onQueryResultRemoved()` selected the
   3896    // next row; we shouldn't unselect it here. If the element was a button,
   3897    // then we didn't select anything on mousedown; clearing the selection seems
   3898    // like it would be harmless, but it has side effects in the input we want
   3899    // to avoid (see `on_mousedown()`).
   3900    if (this.#mousedownSelectedElement?.isConnected) {
   3901      this.clearSelection();
   3902    }
   3903    this.#mousedownSelectedElement = null;
   3904  }
   3905 
   3906  #isRelevantOverflowEvent(event) {
   3907    // We're interested only in the horizontal axis.
   3908    // 0 - vertical, 1 - horizontal, 2 - both
   3909    return event.detail != 0;
   3910  }
   3911 
   3912  on_overflow(event) {
   3913    if (
   3914      this.#isRelevantOverflowEvent(event) &&
   3915      this.#canElementOverflow(event.target)
   3916    ) {
   3917      this.#setElementOverflowing(event.target, true);
   3918    }
   3919  }
   3920 
   3921  on_underflow(event) {
   3922    if (
   3923      this.#isRelevantOverflowEvent(event) &&
   3924      this.#canElementOverflow(event.target)
   3925    ) {
   3926      this.#setElementOverflowing(event.target, false);
   3927    }
   3928  }
   3929 
   3930  on_resize() {
   3931    this.#enableOrDisableRowWrap();
   3932  }
   3933 
   3934  on_command(event) {
   3935    if (event.currentTarget == this.resultMenu) {
   3936      let result = this.#resultMenuResult;
   3937      this.#resultMenuResult = null;
   3938      let menuitem = event.target;
   3939      switch (menuitem.dataset.command) {
   3940        case RESULT_MENU_COMMANDS.HELP:
   3941          menuitem.dataset.url =
   3942            result.payload.helpUrl ||
   3943            Services.urlFormatter.formatURLPref("app.support.baseURL") +
   3944              "awesome-bar-result-menu";
   3945          break;
   3946      }
   3947      this.input.pickResult(result, event, menuitem);
   3948    }
   3949  }
   3950 
   3951  on_popupshowing(event) {
   3952    if (event.target == this.resultMenu) {
   3953      let commands;
   3954 
   3955      let splitButton = event.triggerEvent?.detail.target?.closest(
   3956        ".urlbarView-splitbutton"
   3957      );
   3958      if (splitButton) {
   3959        // Show the commands the are defined in its Split Button.
   3960        let mainButton = splitButton.firstElementChild;
   3961        let name = mainButton.dataset.name;
   3962        commands = this.#resultMenuResult.payload.buttons.find(
   3963          b => b.name == name
   3964        ).menu;
   3965      } else {
   3966        commands = this.#getResultMenuCommands(this.#resultMenuResult);
   3967      }
   3968 
   3969      this.#populateResultMenu({ commands });
   3970    }
   3971  }
   3972 }
   3973 
   3974 /**
   3975 * Implements a QueryContext cache, working as a circular buffer, when a new
   3976 * entry is added at the top, the last item is remove from the bottom.
   3977 */
   3978 class QueryContextCache {
   3979  #cache;
   3980  #size;
   3981  #topSitesContext;
   3982  #topSitesListener;
   3983 
   3984  /**
   3985   * Constructor.
   3986   *
   3987   * @param {number} size The number of entries to keep in the cache.
   3988   */
   3989  constructor(size) {
   3990    this.#size = size;
   3991    this.#cache = [];
   3992 
   3993    // We store the top-sites context separately since it will often be needed
   3994    // and therefore shouldn't be evicted except when the top sites change.
   3995    this.#topSitesContext = null;
   3996    this.#topSitesListener = () => (this.#topSitesContext = null);
   3997    lazy.UrlbarProviderTopSites.addTopSitesListener(this.#topSitesListener);
   3998  }
   3999 
   4000  /**
   4001   * @returns {number} The number of entries to keep in the cache.
   4002   */
   4003  get size() {
   4004    return this.#size;
   4005  }
   4006 
   4007  /**
   4008   * @returns {UrlbarQueryContext} The cached top-sites context or null if none.
   4009   */
   4010  get topSitesContext() {
   4011    return this.#topSitesContext;
   4012  }
   4013 
   4014  /**
   4015   * Adds a new entry to the cache.
   4016   *
   4017   * @param {UrlbarQueryContext} queryContext The UrlbarQueryContext to add.
   4018   * Note: QueryContexts without results are ignored and not added. Contexts
   4019   *       with an empty searchString that are not the top-sites context are
   4020   *       also ignored.
   4021   */
   4022  put(queryContext) {
   4023    if (!queryContext.results.length) {
   4024      return;
   4025    }
   4026 
   4027    let searchString = queryContext.searchString;
   4028    if (!searchString) {
   4029      // Cache the context if it's the top-sites context. An empty search string
   4030      // doesn't necessarily imply top sites since there are other queries that
   4031      // use it too, like search mode. If any result is from the top-sites
   4032      // provider, assume the context is top sites.
   4033      // However, if contextual opt-in message is shown, disable the cache. The
   4034      // message might hide when beginning of query, this cache will be shown
   4035      // for a moment.
   4036      if (
   4037        queryContext.results?.some(
   4038          r => r.providerName == "UrlbarProviderTopSites"
   4039        ) &&
   4040        !queryContext.results.some(
   4041          r => r.providerName == "UrlbarProviderQuickSuggestContextualOptIn"
   4042        )
   4043      ) {
   4044        this.#topSitesContext = queryContext;
   4045      }
   4046      return;
   4047    }
   4048 
   4049    let index = this.#cache.findIndex(e => e.searchString == searchString);
   4050    if (index != -1) {
   4051      if (this.#cache[index] == queryContext) {
   4052        return;
   4053      }
   4054      this.#cache.splice(index, 1);
   4055    }
   4056    if (this.#cache.unshift(queryContext) > this.size) {
   4057      this.#cache.length = this.size;
   4058    }
   4059  }
   4060 
   4061  get(searchString) {
   4062    return this.#cache.find(e => e.searchString == searchString);
   4063  }
   4064 }
   4065 
   4066 /**
   4067 * Adds a dynamic result type stylesheet to a specified window.
   4068 *
   4069 * @param {Window} window
   4070 *   The window to which to add the stylesheet.
   4071 * @param {string} stylesheetURL
   4072 *   The stylesheet's URL.
   4073 */
   4074 async function addDynamicStylesheet(window, stylesheetURL) {
   4075  // Try-catch all of these so that failing to load a stylesheet doesn't break
   4076  // callers and possibly the urlbar.  If a stylesheet does fail to load, the
   4077  // dynamic results that depend on it will appear broken, but at least we
   4078  // won't break the whole urlbar.
   4079  try {
   4080    let uri = Services.io.newURI(stylesheetURL);
   4081    let sheet = await lazy.styleSheetService.preloadSheetAsync(
   4082      uri,
   4083      Ci.nsIStyleSheetService.AGENT_SHEET
   4084    );
   4085    window.windowUtils.addSheet(sheet, Ci.nsIDOMWindowUtils.AGENT_SHEET);
   4086  } catch (ex) {
   4087    console.error("Error adding dynamic stylesheet:", ex);
   4088  }
   4089 }
   4090 
   4091 /**
   4092 * Removes a dynamic result type stylesheet from the view's window.
   4093 *
   4094 * @param {Window} window
   4095 *   The window from which to remove the stylesheet.
   4096 * @param {string} stylesheetURL
   4097 *   The stylesheet's URL.
   4098 */
   4099 function removeDynamicStylesheet(window, stylesheetURL) {
   4100  // Try-catch for the same reason as desribed in addDynamicStylesheet.
   4101  try {
   4102    window.windowUtils.removeSheetUsingURIString(
   4103      stylesheetURL,
   4104      Ci.nsIDOMWindowUtils.AGENT_SHEET
   4105    );
   4106  } catch (ex) {
   4107    console.error("Error removing dynamic stylesheet:", ex);
   4108  }
   4109 }