1
0
Fork 1

Update textarea patch

This commit is contained in:
Laura Hausmann 2023-11-03 19:51:56 +01:00
parent 0365593990
commit f01e178c00
Signed by: zotan
GPG key ID: D044E84C5BE01605
2 changed files with 679 additions and 103 deletions

View file

@ -1,119 +1,576 @@
diff --git a/layout/generic/nsIFrame.cpp b/layout/generic/nsIFrame.cpp
index 3a336b25278ae..448a96fb0ae9b 100644
--- a/layout/generic/nsIFrame.cpp
+++ b/layout/generic/nsIFrame.cpp
@@ -8879,6 +8879,60 @@ nsresult nsIFrame::PeekOffsetForParagraph(PeekOffsetStruct* aPos) {
return NS_OK;
}
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index b3a09ea31391c..e16bc4df9606b 100644
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -72,7 +72,7 @@ ChromeUtils.defineESModuleGetters(this, {
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
ShoppingSidebarParent: "resource:///actors/ShoppingSidebarParent.sys.mjs",
- ShoppingUtils: "resource:///modules/ShoppingUtils.sys.mjs",
+ ShoppingSidebarManager: "resource:///actors/ShoppingSidebarParent.sys.mjs",
ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
SiteDataManager: "resource:///modules/SiteDataManager.sys.mjs",
SitePermissions: "resource:///modules/SitePermissions.sys.mjs",
@@ -2079,7 +2079,7 @@ var gBrowserInit = {
+nsresult nsIFrame::PeekOffsetForLineEdgeLogical(PeekOffsetStruct* aPos) {
+ nsIFrame* frame = this;
+ nsContentAndOffset blockFrameOrBR;
+ blockFrameOrBR.mContent = nullptr;
+ bool reachedLimit = frame->IsBlockOutside() || IsEditingHost(frame);
CaptivePortalWatcher.delayedStartup();
- ShoppingSidebarManager.init();
+ ShoppingSidebarManager.ensureInitialized();
SessionStore.promiseAllWindowsRestored.then(() => {
this._schedulePerWindowIdleTasks();
@@ -2506,8 +2506,6 @@ var gBrowserInit = {
FirefoxViewHandler.uninit();
- ShoppingSidebarManager.uninit();
-
// Now either cancel delayedStartup, or clean up the services initialized from
// it.
if (this._boundDelayedStartup) {
@@ -10082,208 +10080,3 @@ var FirefoxViewHandler = {
this.button?.toggleAttribute("attention", shouldShow);
},
};
-
-var ShoppingSidebarManager = {
- init() {
- this._updateVisibility = this._updateVisibility.bind(this);
- NimbusFeatures.shopping2023.onUpdate(this._updateVisibility);
- XPCOMUtils.defineLazyPreferenceGetter(
- this,
- "optedInPref",
- "browser.shopping.experience2023.optedIn",
- null,
- this._updateVisibility
- );
- XPCOMUtils.defineLazyPreferenceGetter(
- this,
- "isActive",
- ShoppingSidebarParent.SHOPPING_ACTIVE_PREF,
- true,
- this._updateVisibility
- );
- this._updateVisibility();
-
- gBrowser.tabContainer.addEventListener("TabSelect", this);
- window.addEventListener("visibilitychange", this);
- },
-
- uninit() {
- NimbusFeatures.shopping2023.offUpdate(this._updateVisibility);
- },
-
- _updateVisibility() {
- if (window.closed) {
- return;
- }
- let isPBM = PrivateBrowsingUtils.isWindowPrivate(window);
-
- // We are forced to cache this value because otherwise we access the pref
- // too many times.
- this.inEnabledBranch = NimbusFeatures.shopping2023.getVariable("enabled");
- this._enabled = this.inEnabledBranch && !isPBM;
-
- if (!this.isActive) {
- document.querySelectorAll("shopping-sidebar").forEach(sidebar => {
- sidebar.hidden = true;
- });
- }
-
- this._maybeToggleButton();
-
- if (!this._enabled) {
- document.querySelectorAll("shopping-sidebar").forEach(sidebar => {
- sidebar.remove();
- });
- return;
- }
-
- let { selectedBrowser, currentURI } = gBrowser;
- this._maybeToggleSidebar(selectedBrowser, currentURI, 0);
- },
-
- /**
- * Called by TabsProgressListener whenever any browser navigates from one
- * URL to another.
- * Note that this includes hash changes / pushState navigations, because
- * those can be significant for us.
- */
- onLocationChange(aBrowser, aLocationURI, aFlags) {
- ShoppingUtils.maybeRecordExposure(aLocationURI, aFlags);
-
- this._maybeToggleButton();
- this._maybeToggleSidebar(aBrowser, aLocationURI, aFlags);
- },
-
- // The strange signature is because this function was formerly the
- // onLocationChange function, but we needed to differentiate between
- // calls triggered by actual location changes and calls triggered by
- // TabSelect. We will refactor this code in bug 1845842.
- _maybeToggleSidebar(aBrowser, aLocationURI, aFlags) {
- if (!this._enabled) {
- return;
- }
-
- let browserPanel = gBrowser.getPanel(aBrowser);
- let sidebar = browserPanel.querySelector("shopping-sidebar");
- let actor;
- if (sidebar) {
- let { browsingContext } = sidebar.querySelector("browser");
- let global = browsingContext.currentWindowGlobal;
- actor = global.getExistingActor("ShoppingSidebar");
- }
- let isProduct = isProductURL(aLocationURI);
- if (isProduct && this.isActive) {
- if (!sidebar) {
- sidebar = document.createXULElement("shopping-sidebar");
- sidebar.setAttribute("style", "width: 320px");
- sidebar.hidden = false;
- browserPanel.appendChild(sidebar);
- } else {
- actor?.updateProductURL(aLocationURI, aFlags);
- sidebar.hidden = false;
- }
- } else if (sidebar && !sidebar.hidden) {
- actor?.updateProductURL(null);
- sidebar.hidden = true;
- }
-
- this._updateBCActiveness(aBrowser);
- this._setShoppingButtonState(aBrowser);
-
- if (
- sidebar &&
- !sidebar.hidden &&
- ShoppingUtils.isProductPageNavigation(aLocationURI, aFlags)
- ) {
- Glean.shopping.surfaceDisplayed.record();
- }
-
- if (isProduct) {
- // This is the auto-enable behavior that toggles the `active` pref. It
- // must be at the end of this function, or 2 sidebars could be created.
- ShoppingUtils.handleAutoActivateOnProduct();
-
- if (!this.isActive) {
- ShoppingUtils.sendTrigger({
- browser: aBrowser,
- id: "shoppingProductPageWithSidebarClosed",
- context: { isSidebarClosing: !!sidebar },
- });
- }
- }
- },
-
- _maybeToggleButton() {
- let optedOut = this.optedInPref === 2;
- let isPBM = PrivateBrowsingUtils.isWindowPrivate(window);
- if (this.inEnabledBranch && !isPBM && optedOut) {
- this._setShoppingButtonState(gBrowser.selectedBrowser);
- }
- },
-
- _updateBCActiveness(aBrowser) {
- let browserPanel = gBrowser.getPanel(aBrowser);
- let sidebar = browserPanel.querySelector("shopping-sidebar");
- if (!sidebar) {
- return;
- }
- let { browsingContext } = sidebar.querySelector("browser");
- try {
- // Tell Gecko when the sidebar visibility changes to avoid background
- // sidebars taking more CPU / energy than needed.
- browsingContext.isActive =
- !document.hidden &&
- aBrowser == gBrowser.selectedBrowser &&
- !sidebar.hidden;
- } catch (ex) {
- // The setter can throw and we do need to run the rest of this
- // code in that case.
- console.error(ex);
- }
- },
-
- _setShoppingButtonState(aBrowser) {
- if (aBrowser !== gBrowser.selectedBrowser) {
- return;
- }
-
- let button = document.getElementById("shopping-sidebar-button");
-
- let isCurrentBrowserProduct = isProductURL(
- gBrowser.selectedBrowser.currentURI
- );
-
- // Only record if the state of the icon will change from hidden to visible.
- if (button.hidden && isCurrentBrowserProduct) {
- Glean.shopping.addressBarIconDisplayed.record();
- }
-
- button.hidden = !isCurrentBrowserProduct;
- button.setAttribute("shoppingsidebaropen", !!this.isActive);
- let l10nId = this.isActive
- ? "shopping-sidebar-close-button2"
- : "shopping-sidebar-open-button2";
- document.l10n.setAttributes(button, l10nId);
- },
-
- handleEvent(event) {
- switch (event.type) {
- case "TabSelect": {
- if (!this._enabled) {
- return;
- }
- this._updateVisibility();
- if (event.detail?.previousTab.linkedBrowser) {
- this._updateBCActiveness(event.detail.previousTab.linkedBrowser);
- }
- break;
- }
- case "visibilitychange": {
- if (!this._enabled) {
- return;
- }
- this._updateBCActiveness(gBrowser.selectedBrowser);
- }
- }
- },
-};
diff --git a/browser/components/shopping/ShoppingSidebarParent.sys.mjs b/browser/components/shopping/ShoppingSidebarParent.sys.mjs
index a3d9737afd7cf..05145b249bb33 100644
--- a/browser/components/shopping/ShoppingSidebarParent.sys.mjs
+++ b/browser/components/shopping/ShoppingSidebarParent.sys.mjs
@@ -2,6 +2,18 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
+ EveryWindow: "resource:///modules/EveryWindow.sys.mjs",
+ isProductURL: "chrome://global/content/shopping/ShoppingProduct.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+ ShoppingUtils: "resource:///modules/ShoppingUtils.sys.mjs",
+});
+
+ auto traverse = [&aPos](nsIFrame* current) {
+ return aPos->mDirection == eDirPrevious ? current->GetPrevSibling()
+ : current->GetNextSibling();
+ };
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+ // Go through containing frames until reaching a block frame.
+ // In each step, search the previous (or next) siblings for the closest
+ // "stop frame" (a block frame or a BRFrame).
+ // If found, set it to be the selection boundary and abort.
+ while (!reachedLimit) {
+ nsIFrame* parent = frame->GetParent();
+ // Treat a frame associated with the root content as if it were a block
+ // frame.
+ if (!frame->mContent || !frame->mContent->GetParent()) {
+ reachedLimit = true;
+ break;
export class ShoppingSidebarParent extends JSWindowActorParent {
static SHOPPING_ACTIVE_PREF = "browser.shopping.experience2023.active";
static SHOPPING_OPTED_IN_PREF = "browser.shopping.experience2023.optedIn";
@@ -74,3 +86,251 @@ export class ShoppingSidebarParent extends JSWindowActorParent {
}
}
}
+
+class ShoppingSidebarManagerClass {
+ #initialized = false;
+ #everyWindowCallbackId = `shopping-${Services.uuid.generateUUID()}`;
+
+ ensureInitialized() {
+ if (this.#initialized) {
+ return;
+ }
+
+ if (aPos->mDirection == eDirNext) {
+ // Try to find our own line-break before looking at our siblings.
+ blockFrameOrBR = FindLineBreakInText(frame, eDirNext);
+ }
+ this.updateSidebarVisibility = this.updateSidebarVisibility.bind(this);
+ lazy.NimbusFeatures.shopping2023.onUpdate(this.updateSidebarVisibility);
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "optedInPref",
+ "browser.shopping.experience2023.optedIn",
+ null,
+ this.updateSidebarVisibility
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "isActive",
+ ShoppingSidebarParent.SHOPPING_ACTIVE_PREF,
+ true,
+ this.updateSidebarVisibility
+ );
+ this.updateSidebarVisibility();
+
+ nsIFrame* sibling = traverse(frame);
+ while (sibling && !blockFrameOrBR.mContent) {
+ blockFrameOrBR = FindLineBreakingFrame(sibling, aPos->mDirection);
+ sibling = traverse(sibling);
+ }
+ if (blockFrameOrBR.mContent) {
+ aPos->mResultContent = blockFrameOrBR.mContent;
+ aPos->mContentOffset = blockFrameOrBR.mOffset;
+ break;
+ }
+ frame = parent;
+ reachedLimit = frame && (frame->IsBlockOutside() || IsEditingHost(frame));
+ lazy.EveryWindow.registerCallback(
+ this.#everyWindowCallbackId,
+ window => {
+ let isPBM = lazy.PrivateBrowsingUtils.isWindowPrivate(window);
+ if (isPBM) {
+ return;
+ }
+
+ window.gBrowser.tabContainer.addEventListener("TabSelect", this);
+ window.addEventListener("visibilitychange", this);
+ },
+ window => {
+ let isPBM = lazy.PrivateBrowsingUtils.isWindowPrivate(window);
+ if (isPBM) {
+ return;
+ }
+
+ window.gBrowser.tabContainer.removeEventListener("TabSelect", this);
+ window.removeEventListener("visibilitychange", this);
+ }
+ );
+
+ this.#initialized = true;
+ }
+
+ if (reachedLimit) { // no "stop frame" found
+ aPos->mResultContent = frame->GetContent();
+ if (aPos->mDirection == eDirPrevious) {
+ aPos->mContentOffset = 0;
+ } else if (aPos->mResultContent) {
+ aPos->mContentOffset = aPos->mResultContent->GetChildCount();
+ updateSidebarVisibility() {
+ this.enabled = lazy.NimbusFeatures.shopping2023.getVariable("enabled");
+
+ for (let window of lazy.BrowserWindowTracker.orderedWindows) {
+ this.updateSidebarVisibilityForWindow(window);
+ }
+ }
+
+ updateSidebarVisibilityForWindow(window) {
+ if (window.closed) {
+ return;
+ }
+
+ let document = window.document;
+ if (document.visibilityState === "hidden") {
+ return;
+ }
+
+ if (!this.isActive) {
+ document.querySelectorAll("shopping-sidebar").forEach(sidebar => {
+ sidebar.hidden = true;
+ });
+ }
+
+ this._maybeToggleButton(window.gBrowser);
+
+ if (!this.enabled) {
+ document.querySelectorAll("shopping-sidebar").forEach(sidebar => {
+ sidebar.remove();
+ });
+ return;
+ }
+
+ let { selectedBrowser, currentURI } = window.gBrowser;
+ this._maybeToggleSidebar(selectedBrowser, currentURI, 0);
+ }
+
+ _maybeToggleSidebar(aBrowser, aLocationURI, aFlags) {
+ let gBrowser = aBrowser.getTabBrowser();
+ let document = aBrowser.ownerDocument;
+ if (!this.enabled) {
+ return;
+ }
+
+ let browserPanel = gBrowser.getPanel(aBrowser);
+ let sidebar = browserPanel.querySelector("shopping-sidebar");
+ let actor;
+ if (sidebar) {
+ let { browsingContext } = sidebar.querySelector("browser");
+ let global = browsingContext.currentWindowGlobal;
+ actor = global.getExistingActor("ShoppingSidebar");
+ }
+ let isProduct = lazy.isProductURL(aLocationURI);
+ if (isProduct && this.isActive) {
+ if (!sidebar) {
+ sidebar = document.createXULElement("shopping-sidebar");
+ sidebar.setAttribute("style", "width: 320px");
+ sidebar.hidden = false;
+ browserPanel.appendChild(sidebar);
+ } else {
+ actor?.updateProductURL(aLocationURI, aFlags);
+ sidebar.hidden = false;
+ }
+ } else if (sidebar && !sidebar.hidden) {
+ actor?.updateProductURL(null);
+ sidebar.hidden = true;
+ }
+
+ this._updateBCActiveness(aBrowser);
+ this._setShoppingButtonState(aBrowser);
+
+ if (
+ sidebar &&
+ !sidebar.hidden &&
+ lazy.ShoppingUtils.isProductPageNavigation(aLocationURI, aFlags)
+ ) {
+ Glean.shopping.surfaceDisplayed.record();
+ }
+
+ if (isProduct) {
+ // This is the auto-enable behavior that toggles the `active` pref. It
+ // must be at the end of this function, or 2 sidebars could be created.
+ lazy.ShoppingUtils.handleAutoActivateOnProduct();
+
+ if (!this.isActive) {
+ lazy.ShoppingUtils.sendTrigger({
+ browser: aBrowser,
+ id: "shoppingProductPageWithSidebarClosed",
+ context: { isSidebarClosing: !!sidebar },
+ });
+ }
+ }
+ }
+
+ _maybeToggleButton(gBrowser) {
+ let optedOut = this.optedInPref === 2;
+ if (this.enabled && optedOut) {
+ this._setShoppingButtonState(gBrowser.selectedBrowser);
+ }
+ }
+
+ _updateBCActiveness(aBrowser) {
+ let gBrowser = aBrowser.getTabBrowser();
+ let document = aBrowser.ownerDocument;
+ let browserPanel = gBrowser.getPanel(aBrowser);
+ let sidebar = browserPanel.querySelector("shopping-sidebar");
+ if (!sidebar) {
+ return;
+ }
+ let { browsingContext } = sidebar.querySelector("browser");
+ try {
+ // Tell Gecko when the sidebar visibility changes to avoid background
+ // sidebars taking more CPU / energy than needed.
+ browsingContext.isActive =
+ !document.hidden &&
+ aBrowser == gBrowser.selectedBrowser &&
+ !sidebar.hidden;
+ } catch (ex) {
+ // The setter can throw and we do need to run the rest of this
+ // code in that case.
+ console.error(ex);
+ }
+ }
+
+ _setShoppingButtonState(aBrowser) {
+ let gBrowser = aBrowser.getTabBrowser();
+ let document = aBrowser.ownerDocument;
+ if (aBrowser !== gBrowser.selectedBrowser) {
+ return;
+ }
+
+ let button = document.getElementById("shopping-sidebar-button");
+
+ let isCurrentBrowserProduct = lazy.isProductURL(
+ gBrowser.selectedBrowser.currentURI
+ );
+
+ // Only record if the state of the icon will change from hidden to visible.
+ if (button.hidden && isCurrentBrowserProduct) {
+ Glean.shopping.addressBarIconDisplayed.record();
+ }
+
+ button.hidden = !isCurrentBrowserProduct;
+ button.setAttribute("shoppingsidebaropen", !!this.isActive);
+ let l10nId = this.isActive
+ ? "shopping-sidebar-close-button2"
+ : "shopping-sidebar-open-button2";
+ document.l10n.setAttributes(button, l10nId);
+ }
+
+ /**
+ * Called by TabsProgressListener whenever any browser navigates from one
+ * URL to another.
+ * Note that this includes hash changes / pushState navigations, because
+ * those can be significant for us.
+ */
+ onLocationChange(aBrowser, aLocationURI, aFlags) {
+ let isPBM = lazy.PrivateBrowsingUtils.isWindowPrivate(aBrowser.ownerGlobal);
+ if (isPBM) {
+ return;
+ }
+
+ lazy.ShoppingUtils.maybeRecordExposure(aLocationURI, aFlags);
+
+ this._maybeToggleButton(aBrowser.getTabBrowser());
+ this._maybeToggleSidebar(aBrowser, aLocationURI, aFlags);
+ }
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "TabSelect": {
+ if (!this.enabled) {
+ return;
+ }
+ this.updateSidebarVisibility();
+ if (event.detail?.previousTab.linkedBrowser) {
+ this._updateBCActiveness(event.detail.previousTab.linkedBrowser);
+ }
+ break;
+ }
+ case "visibilitychange": {
+ if (!this.enabled) {
+ return;
+ }
+ this.updateSidebarVisibilityForWindow(event.target.ownerGlobal.top);
+ this._updateBCActiveness(
+ event.target.ownerGlobal.top.gBrowser.selectedBrowser
+ );
+ }
+ }
+ }
+ return NS_OK;
+}
+
// Determine movement direction relative to frame
static bool IsMovingInFrameDirection(const nsIFrame* frame,
nsDirection aDirection, bool aVisual) {
@@ -9202,7 +9256,7 @@ nsresult nsIFrame::PeekOffsetForLine(PeekOffsetStruct* aPos) {
return result;
}
+const ShoppingSidebarManager = new ShoppingSidebarManagerClass();
+export { ShoppingSidebarManager };
diff --git a/browser/components/shopping/tests/browser/browser_private_mode.js b/browser/components/shopping/tests/browser/browser_private_mode.js
index cf161e43c66ae..3002e602a6fe6 100644
--- a/browser/components/shopping/tests/browser/browser_private_mode.js
+++ b/browser/components/shopping/tests/browser/browser_private_mode.js
@@ -6,30 +6,10 @@
// This test verifies that the shopping sidebar is not initialized if the
// user visits a shopping product page while in private browsing mode.
-nsresult nsIFrame::PeekOffsetForLineEdge(PeekOffsetStruct* aPos) {
+nsresult nsIFrame::PeekOffsetForLineEdgeVisual(PeekOffsetStruct* aPos) {
// Adjusted so that the caret can't get confused when content changes
nsIFrame* frame = AdjustFrameForSelectionStyles(this);
Element* editingHost = frame->GetContent()->GetEditingHost();
@@ -9316,7 +9370,12 @@ nsresult nsIFrame::PeekOffset(PeekOffsetStruct* aPos) {
return PeekOffsetForLine(aPos);
case eSelectBeginLine:
case eSelectEndLine:
- return PeekOffsetForLineEdge(aPos);
+ if (StaticPrefs::dom_input_logical_textarea_caret_movement_style()) {
+ return PeekOffsetForLineEdgeLogical(aPos);
+ }
+ else {
+ return PeekOffsetForLineEdgeVisual(aPos);
+ }
case eSelectParagraph:
return PeekOffsetForParagraph(aPos);
default: {
diff --git a/layout/generic/nsIFrame.h b/layout/generic/nsIFrame.h
index 80c3504e1af36..af55881b95f95 100644
--- a/layout/generic/nsIFrame.h
+++ b/layout/generic/nsIFrame.h
@@ -3867,7 +3867,8 @@ class nsIFrame : public nsQueryFrame {
int32_t aOffset);
nsresult PeekOffsetForWord(mozilla::PeekOffsetStruct* aPos, int32_t aOffset);
nsresult PeekOffsetForLine(mozilla::PeekOffsetStruct* aPos);
- nsresult PeekOffsetForLineEdge(mozilla::PeekOffsetStruct* aPos);
+ nsresult PeekOffsetForLineEdgeLogical(mozilla::PeekOffsetStruct* aPos);
+ nsresult PeekOffsetForLineEdgeVisual(mozilla::PeekOffsetStruct* aPos);
-add_task(async function test_regular_window_enabled() {
- let nonPrivateWindow = await BrowserTestUtils.openNewBrowserWindow();
- ok(
- nonPrivateWindow.ShoppingSidebarManager._enabled,
- "Shopping sidebar should be enabled in a regular window"
- );
- await BrowserTestUtils.closeWindow(nonPrivateWindow);
-});
-
add_task(async function test_private_window_disabled() {
let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
private: true,
});
- ok(
- !privateWindow.ShoppingSidebarManager._enabled,
- "Shopping sidebar should not be enabled in a private window"
- );
- await BrowserTestUtils.closeWindow(privateWindow);
-});
-
-add_task(async function test_private_window_urlbar_button_hidden() {
- let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
- private: true,
- });
/**
* Search for the first paragraph boundary before or after the given position
diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml
index 7ec9237d9f26d..715059de80314 100644
--- a/modules/libpref/init/StaticPrefList.yaml
+++ b/modules/libpref/init/StaticPrefList.yaml
@@ -2773,6 +2773,12 @@
value: true
mirror: always
let browser = privateWindow.gBrowser.selectedBrowser;
BrowserTestUtils.startLoadingURIString(
@@ -46,5 +26,10 @@ add_task(async function test_private_window_urlbar_button_hidden() {
"Shopping Button should not be visible on a product page"
);
+# Textarea caret movement style. Disable this to restore previous behavior (visual movement).
+- name: dom.input.logical_textarea_caret_movement_style
+ type: bool
+ value: true
+ mirror: always
+ ok(
+ !privateWindow.document.querySelector("shopping-sidebar"),
+ "Shopping sidebar does not exist"
+ );
+
# How often to check for CPOW timeouts (ms). CPOWs are only timed
# out by the hang monitor.
- name: dom.ipc.cpow.timeout
await BrowserTestUtils.closeWindow(privateWindow);
});
diff --git a/browser/components/shopping/tests/browser/browser_shopping_message_triggers.js b/browser/components/shopping/tests/browser/browser_shopping_message_triggers.js
index 61a5b2a661edd..aec555dd5246f 100644
--- a/browser/components/shopping/tests/browser/browser_shopping_message_triggers.js
+++ b/browser/components/shopping/tests/browser/browser_shopping_message_triggers.js
@@ -3,6 +3,10 @@
"use strict";
+ChromeUtils.defineESModuleGetters(this, {
+ ShoppingUtils: "resource:///modules/ShoppingUtils.sys.mjs",
+});
+
const { ASRouter } = ChromeUtils.import(
"resource://activity-stream/lib/ASRouter.jsm"
);

View file

@ -0,0 +1,119 @@
diff --git a/layout/generic/nsIFrame.cpp b/layout/generic/nsIFrame.cpp
index 3a336b25278ae..448a96fb0ae9b 100644
--- a/layout/generic/nsIFrame.cpp
+++ b/layout/generic/nsIFrame.cpp
@@ -8879,6 +8879,60 @@ nsresult nsIFrame::PeekOffsetForParagraph(PeekOffsetStruct* aPos) {
return NS_OK;
}
+nsresult nsIFrame::PeekOffsetForLineEdgeLogical(PeekOffsetStruct* aPos) {
+ nsIFrame* frame = this;
+ nsContentAndOffset blockFrameOrBR;
+ blockFrameOrBR.mContent = nullptr;
+ bool reachedLimit = frame->IsBlockOutside() || IsEditingHost(frame);
+
+ auto traverse = [&aPos](nsIFrame* current) {
+ return aPos->mDirection == eDirPrevious ? current->GetPrevSibling()
+ : current->GetNextSibling();
+ };
+
+ // Go through containing frames until reaching a block frame.
+ // In each step, search the previous (or next) siblings for the closest
+ // "stop frame" (a block frame or a BRFrame).
+ // If found, set it to be the selection boundary and abort.
+ while (!reachedLimit) {
+ nsIFrame* parent = frame->GetParent();
+ // Treat a frame associated with the root content as if it were a block
+ // frame.
+ if (!frame->mContent || !frame->mContent->GetParent()) {
+ reachedLimit = true;
+ break;
+ }
+
+ if (aPos->mDirection == eDirNext) {
+ // Try to find our own line-break before looking at our siblings.
+ blockFrameOrBR = FindLineBreakInText(frame, eDirNext);
+ }
+
+ nsIFrame* sibling = traverse(frame);
+ while (sibling && !blockFrameOrBR.mContent) {
+ blockFrameOrBR = FindLineBreakingFrame(sibling, aPos->mDirection);
+ sibling = traverse(sibling);
+ }
+ if (blockFrameOrBR.mContent) {
+ aPos->mResultContent = blockFrameOrBR.mContent;
+ aPos->mContentOffset = blockFrameOrBR.mOffset;
+ break;
+ }
+ frame = parent;
+ reachedLimit = frame && (frame->IsBlockOutside() || IsEditingHost(frame));
+ }
+
+ if (reachedLimit) { // no "stop frame" found
+ aPos->mResultContent = frame->GetContent();
+ if (aPos->mDirection == eDirPrevious) {
+ aPos->mContentOffset = 0;
+ } else if (aPos->mResultContent) {
+ aPos->mContentOffset = aPos->mResultContent->GetChildCount();
+ }
+ }
+ return NS_OK;
+}
+
// Determine movement direction relative to frame
static bool IsMovingInFrameDirection(const nsIFrame* frame,
nsDirection aDirection, bool aVisual) {
@@ -9202,7 +9256,7 @@ nsresult nsIFrame::PeekOffsetForLine(PeekOffsetStruct* aPos) {
return result;
}
-nsresult nsIFrame::PeekOffsetForLineEdge(PeekOffsetStruct* aPos) {
+nsresult nsIFrame::PeekOffsetForLineEdgeVisual(PeekOffsetStruct* aPos) {
// Adjusted so that the caret can't get confused when content changes
nsIFrame* frame = AdjustFrameForSelectionStyles(this);
Element* editingHost = frame->GetContent()->GetEditingHost();
@@ -9316,7 +9370,12 @@ nsresult nsIFrame::PeekOffset(PeekOffsetStruct* aPos) {
return PeekOffsetForLine(aPos);
case eSelectBeginLine:
case eSelectEndLine:
- return PeekOffsetForLineEdge(aPos);
+ if (StaticPrefs::dom_input_logical_textarea_caret_movement_style()) {
+ return PeekOffsetForLineEdgeLogical(aPos);
+ }
+ else {
+ return PeekOffsetForLineEdgeVisual(aPos);
+ }
case eSelectParagraph:
return PeekOffsetForParagraph(aPos);
default: {
diff --git a/layout/generic/nsIFrame.h b/layout/generic/nsIFrame.h
index 80c3504e1af36..af55881b95f95 100644
--- a/layout/generic/nsIFrame.h
+++ b/layout/generic/nsIFrame.h
@@ -3867,7 +3867,8 @@ class nsIFrame : public nsQueryFrame {
int32_t aOffset);
nsresult PeekOffsetForWord(mozilla::PeekOffsetStruct* aPos, int32_t aOffset);
nsresult PeekOffsetForLine(mozilla::PeekOffsetStruct* aPos);
- nsresult PeekOffsetForLineEdge(mozilla::PeekOffsetStruct* aPos);
+ nsresult PeekOffsetForLineEdgeLogical(mozilla::PeekOffsetStruct* aPos);
+ nsresult PeekOffsetForLineEdgeVisual(mozilla::PeekOffsetStruct* aPos);
/**
* Search for the first paragraph boundary before or after the given position
diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml
index 7ec9237d9f26d..715059de80314 100644
--- a/modules/libpref/init/StaticPrefList.yaml
+++ b/modules/libpref/init/StaticPrefList.yaml
@@ -2773,6 +2773,12 @@
value: true
mirror: always
+# Textarea caret movement style. Disable this to restore previous behavior (visual movement).
+- name: dom.input.logical_textarea_caret_movement_style
+ type: bool
+ value: true
+ mirror: always
+
# How often to check for CPOW timeouts (ms). CPOWs are only timed
# out by the hang monitor.
- name: dom.ipc.cpow.timeout