commit 8b6a7f8464a618aab8a17b2e1aada5e9ca1cf7eb parent 705736c7f354698ca4aa80f484b41ce4624b9a83 Author: Mike Conley <mconley@mozilla.com> Date: Wed, 7 Jan 2026 17:39:20 +0000 Bug 2002027 - Part 2: Add ContentSearchHandoffUI component and styling, and embed in about:newtab. r=search-reviewers,home-newtab-reviewers,urlbar-reviewers,Standard8,places-reviewers,frontend-codestyle-reviewers,nbarrett This uses a pref in firefox.js to choose the new component, or the old script, to maintain train-hop compatibility. Differential Revision: https://phabricator.services.mozilla.com/D273883 Diffstat:
25 files changed, 955 insertions(+), 117 deletions(-)
diff --git a/.stylelintrc.js b/.stylelintrc.js @@ -432,6 +432,11 @@ module.exports = { // into the HTML backup archive files that exist on a user's file system // and can be opened in any browser. "browser/components/backup/content/archive.css", + // Bug 2003877 - this is a centralization of a bunch of rules that had + // been spread across about:newtab and about:privatebrowsing. We'll + // fix these design tokens issues in a follow-up (presuming the + // replacement of the handoff bar doesn't land and remove this first). + "browser/components/search/content/contentSearchHandoffUI.css", ], rules: { "stylelint-plugin-mozilla/use-design-tokens": null, diff --git a/browser/actors/ContentSearchParent.sys.mjs b/browser/actors/ContentSearchParent.sys.mjs @@ -5,6 +5,7 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { + AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", BrowserSearchTelemetry: "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs", BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", @@ -493,6 +494,88 @@ export let ContentSearch = { } }, + _onMessageSearchHandoff({ browser, data, actor }) { + let win = browser.ownerGlobal; + let text = data.text; + let urlBar = win.gURLBar; + let inPrivateBrowsing = lazy.PrivateBrowsingUtils.isBrowserPrivate(browser); + let searchEngine = inPrivateBrowsing + ? Services.search.defaultPrivateEngine + : Services.search.defaultEngine; + let isFirstChange = true; + + // It's possible that this is a handoff from about:home / about:newtab, + // in which case we want to include the newtab_session_id in our call to + // urlBar.handoff. We have to jump through some unfortunate hoops to get + // that. + let newtabSessionId = null; + let newtabActor = + browser.browsingContext?.currentWindowGlobal?.getExistingActor( + "AboutNewTab" + ); + if (newtabActor) { + const portID = newtabActor.getTabDetails()?.portID; + if (portID) { + newtabSessionId = lazy.AboutNewTab.activityStream.store.feeds + .get("feeds.telemetry") + ?.sessions.get(portID)?.session_id; + } + } + + if (!text) { + urlBar.setHiddenFocus(); + } else { + // Pass the provided text to the awesomebar + urlBar.handoff(text, searchEngine, newtabSessionId); + isFirstChange = false; + } + + let checkFirstChange = () => { + // Check if this is the first change since we hidden focused. If it is, + // remove hidden focus styles, prepend the search alias and hide the + // in-content search. + if (isFirstChange) { + isFirstChange = false; + urlBar.removeHiddenFocus(true); + urlBar.handoff("", searchEngine, newtabSessionId); + actor.sendAsyncMessage("DisableSearch"); + urlBar.removeEventListener("compositionstart", checkFirstChange); + urlBar.removeEventListener("paste", checkFirstChange); + } + }; + + let onKeydown = ev => { + // Check if the keydown will cause a value change. + if (ev.key.length === 1 && !ev.altKey && !ev.ctrlKey && !ev.metaKey) { + checkFirstChange(); + } + // If the Esc button is pressed, we are done. Show in-content search and cleanup. + if (ev.key === "Escape") { + onDone(); + } + }; + + let onDone = ev => { + // We are done. Show in-content search again and cleanup. + const forceSuppressFocusBorder = ev?.type === "mousedown"; + urlBar.removeHiddenFocus(forceSuppressFocusBorder); + + urlBar.removeEventListener("keydown", onKeydown); + urlBar.removeEventListener("mousedown", onDone); + urlBar.removeEventListener("blur", onDone); + urlBar.removeEventListener("compositionstart", checkFirstChange); + urlBar.removeEventListener("paste", checkFirstChange); + + actor.sendAsyncMessage("ShowSearch"); + }; + + urlBar.addEventListener("keydown", onKeydown); + urlBar.addEventListener("mousedown", onDone); + urlBar.addEventListener("blur", onDone); + urlBar.addEventListener("compositionstart", checkFirstChange); + urlBar.addEventListener("paste", checkFirstChange); + }, + async _onObserve(eventItem) { let engine; switch (eventItem.data) { diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js @@ -1863,6 +1863,17 @@ pref("browser.newtabpage.activity-stream.mobileDownloadModal.variant-c", false); // Show refined card layout on newtab pref("browser.newtabpage.activity-stream.discoverystream.refinedCardsLayout.enabled", true); +/** + * @backward-compat { version 148 } + * + * Temporary dual implementation to support train hopping. The old handoff UI + * is kept alongside the new contentSearchHandoffUI.mjs custom element until + * the module lands on all channels. Controlled by the pref + * browser.newtabpage.activity-stream.search.useHandoffComponent. + * Remove the old implementation and the pref once this ships to Release. + */ +pref("browser.newtabpage.activity-stream.search.useHandoffComponent", true); + // Mozilla Ad Routing Service (MARS) unified ads service pref("browser.newtabpage.activity-stream.unifiedAds.tiles.enabled", true); pref("browser.newtabpage.activity-stream.unifiedAds.spocs.enabled", true); diff --git a/browser/base/content/test/static/browser_parsable_css.js b/browser/base/content/test/static/browser_parsable_css.js @@ -145,6 +145,10 @@ let propNameAllowlist = [ { propName: "--panel-shadow", isFromDevTools: true }, { propName: "--panel-shadow-margin", isFromDevTools: true }, + // These variables are set in host CSS but consumed in shadow DOM CSS + // (content-search-handoff-ui component), which confuses the test. + { propName: /^--content-search-handoff-ui-/, isFromDevTools: false }, + // These variables are used in JS in viewer.mjs (PDF.js). { propName: "--scale-round-x", diff --git a/browser/components/search/content/content-search-handoff-ui.stories.mjs b/browser/components/search/content/content-search-handoff-ui.stories.mjs @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// eslint-disable-next-line import/no-unresolved +import { html } from "lit.all.mjs"; +import "./contentSearchHandoffUI.mjs"; + +window.MozXULElement.insertFTLIfNeeded("branding/brand.ftl"); +window.MozXULElement.insertFTLIfNeeded("browser/newtab/newtab.ftl"); +window.MozXULElement.insertFTLIfNeeded("browser/aboutPrivateBrowsing.ftl"); + +export default { + title: "Domain-specific UI Widgets/Search/Handoff Search Bar", + component: "content-search-handoff-ui", + argTypes: {}, +}; + +/** + * This little dance lets us mock out the response that the ContentSearch + * parent/child actor pair returns when the ContentSearchHandoffUIController + * requests engine information. + */ +addEventListener("ContentSearchClient", e => { + switch (e.detail.type) { + case "GetEngine": { + // We use the setTimeout(0) to queue up the response to occur on the next + // tick of the event loop. + setTimeout(() => { + e.target.dispatchEvent( + new CustomEvent("ContentSearchService", { + detail: { + type: "Engine", + data: { + engine: { + name: "Google", + iconData: "chrome://global/skin/icons/search-glass.svg", + isConfigEngine: true, + }, + isPrivateEngine: false, + }, + }, + }) + ); + }, 0); + break; + } + } +}); + +const Template = ({ fakeFocus, disabled }) => html` + <style> + .search-inner-wrapper { + display: flex; + min-height: 52px; + margin: 0 auto; + width: 720px; + } + content-search-handoff-ui { + --content-search-handoff-ui-fill: light-dark(#000000, #ffffff); + height: 50px; + width: 100%; + } + </style> + + <div class="search-inner-wrapper"> + <content-search-handoff-ui + ?fakeFocus=${fakeFocus} + ?disabled=${disabled} + ></content-search-handoff-ui> + </div> +`; + +export const Focused = Template.bind({}); +Focused.args = { + fakeFocus: true, + disabled: false, +}; + +export const Unfocused = Template.bind({}); +Unfocused.args = { + fakeFocus: false, + disabled: false, +}; +export const Disabled = Template.bind({}); +Disabled.args = { + fakeFocus: true, + disabled: true, +}; diff --git a/browser/components/search/content/contentSearchHandoffUI.css b/browser/components/search/content/contentSearchHandoffUI.css @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +:host { + display: flex; + width: 100%; + --content-search-handoff-ui-background-color: Field; + --content-search-handoff-ui-color: FieldText; + --content-search-handoff-ui-fill: FieldText; + --content-search-handoff-ui-caret-color: FieldText; + --content-search-handoff-ui-border-width: 1px; + --content-search-handoff-ui-unfocused-border-color: transparent; + --content-search-handoff-ui-fakefocus-border-color: SelectedItem; + --content-search-handoff-ui-fakefocus-box-shadow-inner: color-mix(in srgb, SelectedItem 25%, transparent); + --content-search-handoff-ui-fakefocus-box-shadow-outer: color-mix(in srgb, SelectedItem 25%, transparent); +} + +.fake-editable { + height: 100%; + opacity: 0; + position: absolute; + inset: 0; +} + +.fake-editable:focus { + outline: none; + caret-color: transparent; +} + +.fake-textbox { + opacity: 0.54; + text-align: start; + -webkit-line-clamp: 1; + overflow: hidden; + margin-inline-start: var(--space-xsmall); + + /** + * It's not clear to me why I need to do this, but for some reason I don't + * inherit the font-size through the shadow DOM... + */ + font-size: var(--font-size-root); +} + +.search-handoff-button { + position: relative; + background: var(--content-search-handoff-ui-background-color) var(--newtab-search-icon) 16px center no-repeat; + background-size: var(--size-item-medium); + padding-inline-start: calc(2 * var(--space-xlarge)); + padding-inline-end: var(--space-small); + padding-block: 0; + width: 100%; + box-shadow: var(--box-shadow-level-3); + border: var(--content-search-handoff-ui-border-width) solid var(--content-search-handoff-ui-unfocused-border-color); + border-radius: var(--border-radius-medium); + color: var(--content-search-handoff-ui-color); + -moz-context-properties: fill; + fill: var(--content-search-handoff-ui-fill); + + &:dir(rtl) { + background-position-x: right 16px; + } +} + +@keyframes caret-animation { + to { + visibility: hidden; + } +} + +.fake-caret { + /* To replicate the default caret blink rate of 567ms (https://searchfox.org/mozilla-central/source/widget/cocoa/nsLookAndFeel.mm#397): + - Multiply the blink time by 2 to cover both the visible and hidden states. + - Use steps(2, start) to divide the animation into 2 phases: + 1. First 567ms (Step 1) → Caret is visible + 2. Next 567ms (Step 2) → Caret is hidden + This gives a sharp ON/OFF effect instead of a smooth transition. */ + animation: caret-animation var(--caret-blink-time, 1134ms) steps(2, start) var(--caret-blink-count, infinite); + background: var(--content-search-handoff-ui-caret-color); + display: none; + inset-inline-start: calc(2 * var(--space-xlarge)); + width: 1px; + /** + * We use the negative margin trick here to overlap the same area as the + * fake-textbox. + */ + height: 17px; + margin-top: -17px; +} + +:host([fakefocus]:not([disabled])) .search-handoff-button { + border: var(--content-search-handoff-ui-border-width) solid var(--content-search-handoff-ui-fakefocus-border-color); + box-shadow: + 0 0 0 2px var(--content-search-handoff-ui-fakefocus-box-shadow-inner), + 0 0 0 4px var(--content-search-handoff-ui-fakefocus-box-shadow-outer); + + .fake-caret { + display: block; + } +} + +:host([disabled]) .search-handoff-button { + opacity: 0.5; +} diff --git a/browser/components/search/content/contentSearchHandoffUI.mjs b/browser/components/search/content/contentSearchHandoffUI.mjs @@ -2,15 +2,23 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { html } from "chrome://global/content/vendor/lit.all.mjs"; +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + /** * Handles handing off searches from an in-page search input field to the * browser's main URL bar. Communicates with the parent via the ContentSearch * actor, using custom events to talk to the child actor. */ class ContentSearchHandoffUIController { - constructor() { + #ui = null; + #shadowRoot = null; + + constructor(ui) { this._isPrivateEngine = false; this._engineIcon = null; + this.#ui = ui; + this.#shadowRoot = ui.shadowRoot; window.addEventListener("ContentSearchService", this); this._sendMsg("GetEngine"); @@ -28,6 +36,10 @@ class ContentSearchHandoffUIController { return this._defaultEngine; } + doSearchHandoff(text) { + this._sendMsg("SearchHandoff", { text }); + } + static privateBrowsingRegex = /^about:privatebrowsing([#?]|$)/i; get _isAboutPrivateBrowsing() { return ContentSearchHandoffUIController.privateBrowsingRegex.test( @@ -57,6 +69,15 @@ class ContentSearchHandoffUIController { this._updatel10nIds(); } + _onMsgDisableSearch() { + this.#ui.disabled = true; + } + + _onMsgShowSearch() { + this.#ui.disabled = false; + this.#ui.fakeFocus = false; + } + _updateEngine(engine) { this._defaultEngine = engine; if (this._engineIcon) { @@ -82,8 +103,8 @@ class ContentSearchHandoffUIController { _updatel10nIds() { let engine = this._defaultEngine; - let fakeButton = document.querySelector(".search-handoff-button"); - let fakeInput = document.querySelector(".fake-textbox"); + let fakeButton = this.#shadowRoot.querySelector(".search-handoff-button"); + let fakeInput = this.#shadowRoot.querySelector(".fake-textbox"); if (!fakeButton || !fakeInput) { return; } @@ -167,3 +188,82 @@ class ContentSearchHandoffUIController { } window.ContentSearchHandoffUIController = ContentSearchHandoffUIController; + +/** + * This custom element encapsulates the UI for the search handoff experience + * for about:newtab and about:privatebrowsing. It is a temporary component + * while we wait for the multi-context address bar (MCAB) to be available. + */ +class ContentSearchHandoffUI extends MozLitElement { + static queries = { + fakeCaret: ".fake-caret", + }; + + static properties = { + fakeFocus: { type: Boolean, reflect: true }, + disabled: { type: Boolean, reflect: true }, + }; + + #controller = null; + + #doSearchHandoff(text = "") { + this.fakeFocus = true; + this.#controller.doSearchHandoff(text); + } + + #onSearchHandoffClick(event) { + // When search hand-off is enabled, we render a big button that is styled to + // look like a search textbox. If the button is clicked, we style + // the button as if it was a focused search box and show a fake cursor but + // really focus the awesomebar without the focus styles ("hidden focus"). + event.preventDefault(); + this.#doSearchHandoff(); + } + + #onSearchHandoffPaste(event) { + event.preventDefault(); + this.#doSearchHandoff(event.clipboardData.getData("Text")); + } + + #onSearchHandoffDrop(event) { + event.preventDefault(); + let text = event.dataTransfer.getData("text"); + if (text) { + this.#doSearchHandoff(text); + } + } + + connectedCallback() { + super.connectedCallback(); + if (!this.#controller) { + this.#controller = new window.ContentSearchHandoffUIController(this); + } + } + + render() { + return html` + <link + rel="stylesheet" + href="chrome://browser/content/contentSearchHandoffUI.css" + /> + <button + class="search-handoff-button" + @click=${this.#onSearchHandoffClick} + tabindex="-1" + > + <div class="fake-textbox"></div> + <input + type="search" + class="fake-editable" + tabindex="-1" + aria-hidden="true" + @drop=${this.#onSearchHandoffDrop} + @paste=${this.#onSearchHandoffPaste} + /> + <div class="fake-caret"></div> + </button> + `; + } +} + +customElements.define("content-search-handoff-ui", ContentSearchHandoffUI); diff --git a/browser/components/search/jar.mn b/browser/components/search/jar.mn @@ -9,6 +9,7 @@ browser.jar: content/browser/search/autocomplete-popup.js (content/autocomplete-popup.js) content/browser/search/searchbar.js (content/searchbar.js) content/browser/contentSearchHandoffUI.mjs (content/contentSearchHandoffUI.mjs) + content/browser/contentSearchHandoffUI.css (content/contentSearchHandoffUI.css) search-extensions/ (extensions/**) % resource search-extensions %search-extensions/ contentaccessible=yes diff --git a/browser/components/search/test/browser/browser_contentSearchUI_default.js b/browser/components/search/test/browser/browser_contentSearchUI_default.js @@ -98,10 +98,16 @@ async function ensurePlaceholder(tab, expectedId, expectedEngine) { await ContentTaskUtils.waitForCondition(() => !content.document.hidden); await ContentTaskUtils.waitForCondition( - () => content.document.querySelector(".search-handoff-button"), - "l10n ID not set." + () => content.document.querySelector("content-search-handoff-ui"), + "content-search-handoff-ui not found." + ); + let handoffUI = content.document.querySelector( + "content-search-handoff-ui" + ); + await handoffUI.updateComplete; + let buttonNode = handoffUI.shadowRoot.querySelector( + ".search-handoff-button" ); - let buttonNode = content.document.querySelector(".search-handoff-button"); let expectedAttributes = { id, args: engine ? { engine } : null }; Assert.deepEqual( content.document.l10n.getAttributes(buttonNode), diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_sources.js @@ -283,11 +283,14 @@ add_task(async function test_source_urlbar_handoff() { await BrowserTestUtils.browserStopped(tab.linkedBrowser, "about:newtab"); info("Focus on search input in newtab content"); - await BrowserTestUtils.synthesizeMouseAtCenter( - ".fake-editable", - {}, - tab.linkedBrowser - ); + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + let handoffUI = content.document.querySelector( + "content-search-handoff-ui" + ); + await handoffUI.updateComplete; + let fakeEditable = handoffUI.shadowRoot.querySelector(".fake-editable"); + fakeEditable.click(); + }); info("Get suggestions"); for (const c of "searchSuggestion".split("")) { diff --git a/browser/components/storybook/.storybook/main.js b/browser/components/storybook/.storybook/main.js @@ -31,6 +31,8 @@ module.exports = { `${projectRoot}/browser/components/backup/content/**/*.stories.mjs`, // Settings components stories `${projectRoot}/browser/components/preferences/widgets/**/*.stories.mjs`, + // Search components stories + `${projectRoot}/browser/components/search/**/*.stories.mjs`, // Reader View components stories `${projectRoot}/toolkit/components/reader/**/*.stories.mjs`, // megalist components stories diff --git a/browser/components/storybook/component-status/components.json b/browser/components/storybook/component-status/components.json @@ -0,0 +1,267 @@ +{ + "generatedAt": "2025-12-18T15:49:39.588Z", + "count": 29, + "items": [ + { + "component": "moz-badge", + "title": "UI Widgets/Badge", + "status": "in-development", + "storyId": "ui-widgets-badge--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-badge--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-badge", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1987750" + }, + { + "component": "moz-box-button", + "title": "UI Widgets/Box Button", + "status": "in-development", + "storyId": "ui-widgets-box-button--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-box-button--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-box-button", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1855803" + }, + { + "component": "moz-box-group", + "title": "UI Widgets/Box Group", + "status": "in-development", + "storyId": "ui-widgets-box-group--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-box-group--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-box-group", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1941000" + }, + { + "component": "moz-box-item", + "title": "UI Widgets/Box Item", + "status": "in-development", + "storyId": "ui-widgets-box-item--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-box-item--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-box-item", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1956560" + }, + { + "component": "moz-box-link", + "title": "UI Widgets/Box Link", + "status": "in-development", + "storyId": "ui-widgets-box-link--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-box-link--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-box-link", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1941939" + }, + { + "component": "moz-breadcrumb-group", + "title": "UI Widgets/Breadcrumb Group", + "status": "in-development", + "storyId": "ui-widgets-breadcrumb-group--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-breadcrumb-group--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-breadcrumb-group", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1948410" + }, + { + "component": "moz-button", + "title": "UI Widgets/Button", + "status": "stable", + "storyId": "ui-widgets-button--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-button--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-button", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1855803" + }, + { + "component": "moz-button-group", + "title": "UI Widgets/Button Group", + "status": "stable", + "storyId": "ui-widgets-button-group--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-button-group--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-button-group", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1801325" + }, + { + "component": "moz-card", + "title": "UI Widgets/Card", + "status": "stable", + "storyId": "ui-widgets-card--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-card--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-card", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1846844" + }, + { + "component": "moz-checkbox", + "title": "UI Widgets/Checkbox", + "status": "in-development", + "storyId": "ui-widgets-checkbox--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-checkbox--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-checkbox", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1894485" + }, + { + "component": "moz-fieldset", + "title": "UI Widgets/Fieldset", + "status": "in-development", + "storyId": "ui-widgets-fieldset--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-fieldset--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-fieldset", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1980498" + }, + { + "component": "moz-five-star", + "title": "UI Widgets/Five Star", + "status": "in-development", + "storyId": "ui-widgets-five-star--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-five-star--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-five-star", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1856385" + }, + { + "component": "moz-input-color", + "title": "UI Widgets/Input Color", + "status": "in-development", + "storyId": "ui-widgets-input-color--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-input-color--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-input-color", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1889950" + }, + { + "component": "moz-input-folder", + "title": "UI Widgets/Input Folder", + "status": "in-development", + "storyId": "ui-widgets-input-folder--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-input-folder--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-input-folder", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1941432" + }, + { + "component": "moz-input-password", + "title": "UI Widgets/Input Password", + "status": "in-development", + "storyId": "ui-widgets-input-password--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-input-password--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-input-password", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1941060" + }, + { + "component": "moz-input-search", + "title": "UI Widgets/Input Search", + "status": "in-development", + "storyId": "ui-widgets-input-search--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-input-search--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-input-search", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1801329" + }, + { + "component": "moz-input-text", + "title": "UI Widgets/Input Text", + "status": "in-development", + "storyId": "ui-widgets-input-text--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-input-text--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-input-text", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1912089" + }, + { + "component": "moz-label", + "title": "UI Widgets/Label", + "status": "stable", + "storyId": "ui-widgets-label--accesskey", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-label--accesskey", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-label", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1878744" + }, + { + "component": "moz-message-bar", + "title": "UI Widgets/Message Bar", + "status": "stable", + "storyId": "ui-widgets-message-bar--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-message-bar--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-message-bar", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1837469" + }, + { + "component": "moz-page-header", + "title": "UI Widgets/Page Header", + "status": "in-development", + "storyId": "ui-widgets-page-header--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-page-header--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-page-header", + "bugUrl": "" + }, + { + "component": "moz-page-nav", + "title": "UI Widgets/Page Nav", + "status": "in-development", + "storyId": "ui-widgets-page-nav--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-page-nav--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-page-nav", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1878042" + }, + { + "component": "moz-promo", + "title": "UI Widgets/Promo", + "status": "in-development", + "storyId": "ui-widgets-promo--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-promo--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-promo", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1966422" + }, + { + "component": "moz-radio-group", + "title": "UI Widgets/Radio Group", + "status": "in-development", + "storyId": "ui-widgets-radio-group--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-radio-group--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-radio-group", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1894497" + }, + { + "component": "moz-reorderable-list", + "title": "UI Widgets/Reorderable List", + "status": "in-development", + "storyId": "ui-widgets-reorderable-list--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-reorderable-list--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-reorderable-list", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1828933" + }, + { + "component": "moz-select", + "title": "UI Widgets/Select", + "status": "in-development", + "storyId": "ui-widgets-select--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-select--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-select", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1931441" + }, + { + "component": "moz-support-link", + "title": "UI Widgets/Support Link", + "status": "stable", + "storyId": "ui-widgets-support-link--withamourl", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-support-link--withamourl", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-support-link", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1801924" + }, + { + "component": "moz-toggle", + "title": "UI Widgets/Toggle", + "status": "stable", + "storyId": "ui-widgets-toggle--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-toggle--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-toggle", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1799466" + }, + { + "component": "moz-visual-picker", + "title": "UI Widgets/Visual Picker", + "status": "in-development", + "storyId": "ui-widgets-visual-picker--default", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-visual-picker--default", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/moz-visual-picker", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1941063" + }, + { + "component": "panel-list", + "title": "UI Widgets/Panel List", + "status": "stable", + "storyId": "ui-widgets-panel-list--simple", + "storyUrl": "https://firefoxux.github.io/firefox-desktop-components/?path=/story/ui-widgets-panel-list--simple", + "sourceUrl": "https://searchfox.org/firefox-main/source/toolkit/content/widgets/panel-list", + "bugUrl": "https://bugzilla.mozilla.org/show_bug.cgi?id=1811282" + } + ] +} diff --git a/browser/components/urlbar/tests/browser/browser_suppressFocusBorder.js b/browser/components/urlbar/tests/browser/browser_suppressFocusBorder.js @@ -318,10 +318,12 @@ async function testInteractionFeature(interaction, win) { info("Click on search-handoff-button in newtab page"); await ContentTask.spawn(win.gBrowser.selectedBrowser, null, async () => { - await ContentTaskUtils.waitForCondition(() => - content.document.querySelector(".search-handoff-button") - ); - content.document.querySelector(".search-handoff-button").click(); + await ContentTaskUtils.waitForCondition(() => { + return content.document.querySelector("content-search-handoff-ui"); + }, "Handoff UI has loaded"); + let handoffUI = content.document.querySelector("content-search-handoff-ui"); + await handoffUI.updateComplete; + handoffUI.shadowRoot.querySelector(".search-handoff-button").click(); }); await BrowserTestUtils.waitForCondition( @@ -387,5 +389,7 @@ async function openAboutNewTab(win = window) { () => win.gBrowser.tabs.length === tabCount + 1, "Waiting for background about:newtab to open." ); - return win.gBrowser.tabs[win.gBrowser.tabs.length - 1]; + let tab = win.gBrowser.tabs[win.gBrowser.tabs.length - 1]; + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + return tab; } diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_handoff.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_handoff.js @@ -77,10 +77,19 @@ add_task(async function test_search() { info("Focus on search input in newtab content"); await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { - const searchInput = content.document.querySelector(".fake-editable"); + let handoffUI = content.document.querySelector("content-search-handoff-ui"); + await handoffUI.updateComplete; + let searchInput = handoffUI.shadowRoot.querySelector( + ".search-handoff-button" + ); searchInput.click(); }); + await BrowserTestUtils.waitForCondition( + () => window.gURLBar._hideFocus, + "Wait until _hideFocus will be true" + ); + info("Search and wait the result"); const onLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); EventUtils.synthesizeKey("q"); @@ -106,10 +115,19 @@ add_task(async function test_search_private_mode() { info("Focus on search input in newtab content"); await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { - const searchInput = content.document.querySelector(".fake-editable"); + let handoffUI = content.document.querySelector("content-search-handoff-ui"); + await handoffUI.updateComplete; + let searchInput = handoffUI.shadowRoot.querySelector( + ".search-handoff-button" + ); searchInput.click(); }); + await BrowserTestUtils.waitForCondition( + () => privateWindow.gURLBar._hideFocus, + "Wait until _hideFocus will be true" + ); + info("Search and wait the result"); const onLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); EventUtils.synthesizeKey("q", {}, privateWindow); diff --git a/browser/components/urlbar/tests/engagementTelemetry/browser/head-sap.js b/browser/components/urlbar/tests/engagementTelemetry/browser/head-sap.js @@ -29,8 +29,15 @@ async function doHandoffTest({ trigger, assert }) { await doTest(async browser => { BrowserTestUtils.startLoadingURIString(browser, "about:newtab"); await BrowserTestUtils.browserStopped(browser, "about:newtab"); - await SpecialPowers.spawn(browser, [], function () { - const searchInput = content.document.querySelector(".fake-editable"); + await SpecialPowers.spawn(browser, [], async function () { + await ContentTaskUtils.waitForCondition(() => + content.document.querySelector("content-search-handoff-ui") + ); + let handoffUI = content.document.querySelector( + "content-search-handoff-ui" + ); + await handoffUI.updateComplete; + const searchInput = handoffUI.shadowRoot.querySelector(".fake-editable"); searchInput.click(); }); EventUtils.synthesizeKey("x"); diff --git a/browser/extensions/newtab/content-src/components/Base/Base.jsx b/browser/extensions/newtab/content-src/components/Base/Base.jsx @@ -206,6 +206,20 @@ export class BaseContent extends React.PureComponent { global.addEventListener("keydown", this.handleOnKeyDown); const prefs = this.props.Prefs.values; const wallpapersEnabled = prefs["newtabWallpapers.enabled"]; + + if (prefs["search.useHandoffComponent"]) { + // Dynamically import the contentSearchHandoffUI module, but don't worry + // about webpacking this one. + import( + /* webpackIgnore: true */ "chrome://browser/content/contentSearchHandoffUI.mjs" + ); + } else { + const scriptURL = "chrome://browser/content/contentSearchHandoffUI.js"; + const scriptEl = document.createElement("script"); + scriptEl.src = scriptURL; + document.head.appendChild(scriptEl); + } + if (this.props.document.visibilityState === VISIBLE) { this.onVisible(); } else { diff --git a/browser/extensions/newtab/content-src/components/Search/Search.jsx b/browser/extensions/newtab/content-src/components/Search/Search.jsx @@ -4,6 +4,16 @@ /* globals ContentSearchHandoffUIController */ +/** + * @backward-compat { version 148 } + * + * Temporary dual implementation to support train hopping. The old handoff UI + * is kept alongside the new contentSearchHandoffUI.mjs custom element until + * the module lands on all channels. Controlled by the pref + * browser.newtabpage.activity-stream.search.useHandoffComponent. + * Remove the old implementation and the pref once this ships to Release. + */ + import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { connect } from "react-redux"; import { Logo } from "content-src/components/Logo/Logo"; @@ -61,21 +71,42 @@ export class _Search extends React.PureComponent { } componentDidMount() { - const caret = this.fakeCaret; - const { caretBlinkCount, caretBlinkTime } = this.props.Prefs.values; - - if (caret) { - // If caret blink count isn't defined, use the default infinite behavior for animation - caret.style.setProperty( - "--caret-blink-count", - caretBlinkCount > -1 ? caretBlinkCount : "infinite" - ); - - // Apply custom blink rate if set, else fallback to default (567ms on/off --> 1134ms total) - caret.style.setProperty( - "--caret-blink-time", - caretBlinkTime > 0 ? `${caretBlinkTime * 2}ms` : `${1134}ms` - ); + const { + caretBlinkCount, + caretBlinkTime, + "search.useHandoffComponent": useHandoffComponent, + } = this.props.Prefs.values; + + if (useHandoffComponent) { + const { handoffUI } = this; + if (handoffUI) { + // If caret blink count isn't defined, use the default infinite behavior for animation + handoffUI.style.setProperty( + "--caret-blink-count", + caretBlinkCount > -1 ? caretBlinkCount : "infinite" + ); + + // Apply custom blink rate if set, else fallback to default (567ms on/off --> 1134ms total) + handoffUI.style.setProperty( + "--caret-blink-time", + caretBlinkTime > 0 ? `${caretBlinkTime * 2}ms` : `${1134}ms` + ); + } + } else { + const caret = this.fakeCaret; + if (caret) { + // If caret blink count isn't defined, use the default infinite behavior for animation + caret.style.setProperty( + "--caret-blink-count", + caretBlinkCount > -1 ? caretBlinkCount : "infinite" + ); + + // Apply custom blink rate if set, else fallback to default (567ms on/off --> 1134ms total) + caret.style.setProperty( + "--caret-blink-time", + caretBlinkTime > 0 ? `${caretBlinkTime * 2}ms` : `${1134}ms` + ); + } } } @@ -98,6 +129,24 @@ export class _Search extends React.PureComponent { * in order to execute searches in various tests */ render() { + const useHandoffComponent = + this.props.Prefs.values["search.useHandoffComponent"]; + + if (useHandoffComponent) { + return ( + <div className="search-wrapper"> + {this.props.showLogo && <Logo />} + <div className="search-inner-wrapper"> + <content-search-handoff-ui + ref={el => { + this.handoffUI = el; + }} + ></content-search-handoff-ui> + </div> + </div> + ); + } + const wrapperClassName = [ "search-wrapper", this.props.disable && "search-disabled", diff --git a/browser/extensions/newtab/content-src/components/Search/_Search.scss b/browser/extensions/newtab/content-src/components/Search/_Search.scss @@ -67,6 +67,15 @@ $glyph-forward: url('chrome://browser/skin/forward.svg'); } } + /** + * @backward-compat { version 148 } + * + * Temporary dual implementation to support train hopping. The old handoff UI + * is kept alongside the new contentSearchHandoffUI.mjs custom element until + * the module lands on all channels. Controlled by the pref + * browser.newtabpage.activity-stream.search.useHandoffComponent. + * Remove the old implementation and the pref once this ships to Release. + */ .search-handoff-button { background: var(--newtab-background-color-secondary) var(--newtab-search-icon) $search-icon-padding center no-repeat; background-size: $search-icon-size; @@ -87,6 +96,15 @@ $glyph-forward: url('chrome://browser/skin/forward.svg'); } } + /** + * @backward-compat { version 148 } + * + * Temporary dual implementation to support train hopping. The old handoff UI + * is kept alongside the new contentSearchHandoffUI.mjs custom element until + * the module lands on all channels. Controlled by the pref + * browser.newtabpage.activity-stream.search.useHandoffComponent. + * Remove the old implementation and the pref once this ships to Release. + */ &.fake-focus:not(.search.disabled) { .search-handoff-button { border: 1px solid var(--newtab-primary-action-background); @@ -94,6 +112,15 @@ $glyph-forward: url('chrome://browser/skin/forward.svg'); } } + /** + * @backward-compat { version 148 } + * + * Temporary dual implementation to support train hopping. The old handoff UI + * is kept alongside the new contentSearchHandoffUI.mjs custom element until + * the module lands on all channels. Controlled by the pref + * browser.newtabpage.activity-stream.search.useHandoffComponent. + * Remove the old implementation and the pref once this ships to Release. + */ .search-handoff-button { padding-inline-end: var(--space-large); color: var(--newtab-text-primary-color); @@ -111,6 +138,16 @@ $glyph-forward: url('chrome://browser/skin/forward.svg'); } } + content-search-handoff-ui { + --content-search-handoff-ui-background-color: var(--newtab-background-color-secondary); + --content-search-handoff-ui-color: var(--newtab-text-primary-color); + --content-search-handoff-ui-fill: var(--newtab-text-secondary-color); + --content-search-handoff-ui-caret-color: var(--newtab-text-primary-color); + --content-search-handoff-ui-fakefocus-border-color: var(--newtab-primary-action-background); + --content-search-handoff-ui-fakefocus-box-shadow-inner: var(--newtab-primary-action-background-dimmed); + --content-search-handoff-ui-fakefocus-box-shadow-outer: transparent; + } + &.visible-logo { .logo-and-wordmark { .wordmark { diff --git a/browser/extensions/newtab/css/activity-stream.css b/browser/extensions/newtab/css/activity-stream.css @@ -1470,6 +1470,33 @@ main section { padding: 0; margin-block: var(--space-xxlarge); margin-block-start: 0; + /** + * @backward-compat { version 148 } + * + * Temporary dual implementation to support train hopping. The old handoff UI + * is kept alongside the new contentSearchHandoffUI.mjs custom element until + * the module lands on all channels. Controlled by the pref + * browser.newtabpage.activity-stream.search.useHandoffComponent. + * Remove the old implementation and the pref once this ships to Release. + */ + /** + * @backward-compat { version 148 } + * + * Temporary dual implementation to support train hopping. The old handoff UI + * is kept alongside the new contentSearchHandoffUI.mjs custom element until + * the module lands on all channels. Controlled by the pref + * browser.newtabpage.activity-stream.search.useHandoffComponent. + * Remove the old implementation and the pref once this ships to Release. + */ + /** + * @backward-compat { version 148 } + * + * Temporary dual implementation to support train hopping. The old handoff UI + * is kept alongside the new contentSearchHandoffUI.mjs custom element until + * the module lands on all channels. Controlled by the pref + * browser.newtabpage.activity-stream.search.useHandoffComponent. + * Remove the old implementation and the pref once this ships to Release. + */ } .search-wrapper .logo-and-wordmark { margin-block-end: var(--space-xxlarge); @@ -1555,6 +1582,15 @@ main section { .search-wrapper .search-handoff-button .fake-caret:dir(rtl) { background-position-x: right 16px; } +.search-wrapper content-search-handoff-ui { + --content-search-handoff-ui-background-color: var(--newtab-background-color-secondary); + --content-search-handoff-ui-color: var(--newtab-text-primary-color); + --content-search-handoff-ui-fill: var(--newtab-text-secondary-color); + --content-search-handoff-ui-caret-color: var(--newtab-text-primary-color); + --content-search-handoff-ui-fakefocus-border-color: var(--newtab-primary-action-background); + --content-search-handoff-ui-fakefocus-box-shadow-inner: var(--newtab-primary-action-background-dimmed); + --content-search-handoff-ui-fakefocus-box-shadow-outer: transparent; +} .search-wrapper.visible-logo .logo-and-wordmark .wordmark { fill: var(--newtab-wordmark-color); } diff --git a/browser/extensions/newtab/data/content/activity-stream.bundle.js b/browser/extensions/newtab/data/content/activity-stream.bundle.js @@ -14868,6 +14868,16 @@ function Logo() { /* globals ContentSearchHandoffUIController */ +/** + * @backward-compat { version 148 } + * + * Temporary dual implementation to support train hopping. The old handoff UI + * is kept alongside the new contentSearchHandoffUI.mjs custom element until + * the module lands on all channels. Controlled by the pref + * browser.newtabpage.activity-stream.search.useHandoffComponent. + * Remove the old implementation and the pref once this ships to Release. + */ + @@ -14928,17 +14938,31 @@ class _Search extends (external_React_default()).PureComponent { } } componentDidMount() { - const caret = this.fakeCaret; const { caretBlinkCount, - caretBlinkTime + caretBlinkTime, + "search.useHandoffComponent": useHandoffComponent } = this.props.Prefs.values; - if (caret) { - // If caret blink count isn't defined, use the default infinite behavior for animation - caret.style.setProperty("--caret-blink-count", caretBlinkCount > -1 ? caretBlinkCount : "infinite"); + if (useHandoffComponent) { + const { + handoffUI + } = this; + if (handoffUI) { + // If caret blink count isn't defined, use the default infinite behavior for animation + handoffUI.style.setProperty("--caret-blink-count", caretBlinkCount > -1 ? caretBlinkCount : "infinite"); + + // Apply custom blink rate if set, else fallback to default (567ms on/off --> 1134ms total) + handoffUI.style.setProperty("--caret-blink-time", caretBlinkTime > 0 ? `${caretBlinkTime * 2}ms` : `${1134}ms`); + } + } else { + const caret = this.fakeCaret; + if (caret) { + // If caret blink count isn't defined, use the default infinite behavior for animation + caret.style.setProperty("--caret-blink-count", caretBlinkCount > -1 ? caretBlinkCount : "infinite"); - // Apply custom blink rate if set, else fallback to default (567ms on/off --> 1134ms total) - caret.style.setProperty("--caret-blink-time", caretBlinkTime > 0 ? `${caretBlinkTime * 2}ms` : `${1134}ms`); + // Apply custom blink rate if set, else fallback to default (567ms on/off --> 1134ms total) + caret.style.setProperty("--caret-blink-time", caretBlinkTime > 0 ? `${caretBlinkTime * 2}ms` : `${1134}ms`); + } } } onInputMountHandoff(input) { @@ -14959,6 +14983,18 @@ class _Search extends (external_React_default()).PureComponent { * in order to execute searches in various tests */ render() { + const useHandoffComponent = this.props.Prefs.values["search.useHandoffComponent"]; + if (useHandoffComponent) { + return /*#__PURE__*/external_React_default().createElement("div", { + className: "search-wrapper" + }, this.props.showLogo && /*#__PURE__*/external_React_default().createElement(Logo, null), /*#__PURE__*/external_React_default().createElement("div", { + className: "search-inner-wrapper" + }, /*#__PURE__*/external_React_default().createElement("content-search-handoff-ui", { + ref: el => { + this.handoffUI = el; + } + }))); + } const wrapperClassName = ["search-wrapper", this.props.disable && "search-disabled", this.props.fakeFocus && "fake-focus"].filter(v => v).join(" "); return /*#__PURE__*/external_React_default().createElement("div", { className: wrapperClassName @@ -15705,6 +15741,16 @@ class BaseContent extends (external_React_default()).PureComponent { __webpack_require__.g.addEventListener("keydown", this.handleOnKeyDown); const prefs = this.props.Prefs.values; const wallpapersEnabled = prefs["newtabWallpapers.enabled"]; + if (prefs["search.useHandoffComponent"]) { + // Dynamically import the contentSearchHandoffUI module, but don't worry + // about webpacking this one. + import(/* webpackIgnore: true */"chrome://browser/content/contentSearchHandoffUI.mjs"); + } else { + const scriptURL = "chrome://browser/content/contentSearchHandoffUI.js"; + const scriptEl = document.createElement("script"); + scriptEl.src = scriptURL; + document.head.appendChild(scriptEl); + } if (this.props.document.visibilityState === Base_VISIBLE) { this.onVisible(); } else { diff --git a/browser/extensions/newtab/karma.mc.config.js b/browser/extensions/newtab/karma.mc.config.js @@ -354,6 +354,12 @@ module.exports = function (config) { functions: 31.2, branches: 31.2, }, + "content-src/components/Search/Search.jsx": { + statements: 38, + lines: 39, + functions: 28, + branches: 25, + }, "content-src/components/**/*.jsx": { statements: 51.1, lines: 52.38, diff --git a/browser/extensions/newtab/lib/PlacesFeed.sys.mjs b/browser/extensions/newtab/lib/PlacesFeed.sys.mjs @@ -323,6 +323,13 @@ export class PlacesFeed { ]; } + /** + * @backward-compat { version 148 } + * + * This, and all newtab-specific handoff searchbar handling can be removed + * once 147 is released, as all handoff UI and logic will be handled by + * contentSearchHandoffUI and the ContentSearch JSWindowActors. + */ handoffSearchToAwesomebar(action) { const { _target, data, meta } = action; const searchEngine = this._getDefaultSearchEngine( diff --git a/browser/extensions/newtab/lib/PrefsFeed.sys.mjs b/browser/extensions/newtab/lib/PrefsFeed.sys.mjs @@ -348,6 +348,7 @@ export class PrefsFeed { this._setStringPref(values, "discoverystream.spocs-endpoint", ""); this._setStringPref(values, "discoverystream.spocs-endpoint-query", ""); this._setStringPref(values, "newNewtabExperience.colors", ""); + this._setBoolPref(values, "search.useHandoffComponent", false); // Set the initial state of all prefs in redux this.store.dispatch( diff --git a/browser/extensions/newtab/test/browser/browser_as_render.js b/browser/extensions/newtab/test/browser/browser_as_render.js @@ -2,8 +2,17 @@ test_newtab({ test: function test_render_search_handoff() { - let search = content.document.querySelector(".search-handoff-button"); - ok(search, "Got the search handoff button"); + const usingHandoffComponent = Services.prefs.getBoolPref( + "browser.newtabpage.activity-stream.search.useHandoffComponent", + false + ); + + const selector = usingHandoffComponent + ? "content-search-handoff-ui" + : ".search-handoff-button"; + + let search = content.document.querySelector(selector); + ok(search, "Got the content search handoff UI"); }, }); diff --git a/browser/extensions/newtab/test/unit/content-src/components/Search.test.jsx b/browser/extensions/newtab/test/unit/content-src/components/Search.test.jsx @@ -6,7 +6,7 @@ import { Logo } from "content-src/components/Logo/Logo"; const DEFAULT_PROPS = { dispatch() {}, - Prefs: { values: { featureConfig: {} } }, + Prefs: { values: { featureConfig: {}, "search.useHandoffComponent": true } }, }; describe("<Search>", () => { @@ -48,78 +48,7 @@ describe("<Search>", () => { it("should render a Search hand-off element", () => { const wrapper = shallow(<Search {...DEFAULT_PROPS} />); assert.ok(wrapper.exists()); - assert.equal(wrapper.find(".search-handoff-button").length, 1); - }); - it("should hand-off search when button is clicked", () => { - const dispatch = sinon.spy(); - const wrapper = shallow( - <Search {...DEFAULT_PROPS} dispatch={dispatch} /> - ); - wrapper - .find(".search-handoff-button") - .simulate("click", { preventDefault: () => {} }); - assert.calledThrice(dispatch); - assert.calledWith(dispatch, { - data: { text: undefined }, - meta: { - from: "ActivityStream:Content", - skipLocal: true, - to: "ActivityStream:Main", - }, - type: "HANDOFF_SEARCH_TO_AWESOMEBAR", - }); - assert.calledWith(dispatch, { type: "FAKE_FOCUS_SEARCH" }); - const [action] = dispatch.thirdCall.args; - assert.isUserEventAction(action); - assert.propertyVal(action.data, "event", "SEARCH_HANDOFF"); - }); - it("should hand-off search on paste", () => { - const dispatch = sinon.spy(); - const wrapper = mount(<Search {...DEFAULT_PROPS} dispatch={dispatch} />); - wrapper.instance()._searchHandoffButton = { contains: () => true }; - wrapper.instance().onSearchHandoffPaste({ - clipboardData: { - getData: () => "some copied text", - }, - preventDefault: () => {}, - }); - assert.equal(dispatch.callCount, 4); - assert.calledWith(dispatch, { - data: { text: "some copied text" }, - meta: { - from: "ActivityStream:Content", - skipLocal: true, - to: "ActivityStream:Main", - }, - type: "HANDOFF_SEARCH_TO_AWESOMEBAR", - }); - assert.calledWith(dispatch, { type: "DISABLE_SEARCH" }); - const [action] = dispatch.thirdCall.args; - assert.isUserEventAction(action); - assert.propertyVal(action.data, "event", "SEARCH_HANDOFF"); - }); - it("should properly handle drop events", () => { - const dispatch = sinon.spy(); - const wrapper = mount(<Search {...DEFAULT_PROPS} dispatch={dispatch} />); - const preventDefault = sinon.spy(); - wrapper.find(".fake-editable").simulate("drop", { - dataTransfer: { getData: () => "dropped text" }, - preventDefault, - }); - assert.equal(dispatch.callCount, 4); - assert.calledWith(dispatch, { - data: { text: "dropped text" }, - meta: { - from: "ActivityStream:Content", - skipLocal: true, - to: "ActivityStream:Main", - }, - type: "HANDOFF_SEARCH_TO_AWESOMEBAR", - }); - assert.calledWith(dispatch, { type: "DISABLE_SEARCH" }); - const [action] = dispatch.thirdCall.args; - assert.isUserEventAction(action); - assert.propertyVal(action.data, "event", "SEARCH_HANDOFF"); + assert.equal(wrapper.find("content-search-handoff-ui").length, 1); }); }); });