From e5fe1be376e58bbc95c254a937932d2deba0f43b Mon Sep 17 00:00:00 2001 From: Gregory Pappas Date: Wed, 3 Jul 2024 00:40:42 +0000 Subject: [PATCH] Bug 1889503 - Implement new document.caretPositionFromPoint Shadow DOM behavior r=webidl,smaug,emilio Spec - https://drafts.csswg.org/cssom-view/Overview#dom-document-caretpositionfrompoint Spec PR - https://github.com/w3c/csswg-drafts/pull/10200 TAG Review - https://github.com/w3ctag/design-reviews/issues/949 Differential Revision: https://phabricator.services.mozilla.com/D215184 --- dom/base/Document.cpp | 70 +++++++++++++------ dom/base/Document.h | 4 +- dom/webidl/Document.webidl | 7 +- layout/base/tests/mochitest.toml | 2 + ...itionFromPoint_insertAnonymousContent.html | 33 +++++++++ modules/libpref/init/StaticPrefList.yaml | 11 +++ ...-caretPositionFromPoint.tentative.html.ini | 16 +---- 7 files changed, 103 insertions(+), 40 deletions(-) create mode 100644 layout/base/tests/test_caretPositionFromPoint_insertAnonymousContent.html diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp index 972ef05e5a15..235e2fcfccda 100644 --- a/dom/base/Document.cpp +++ b/dom/base/Document.cpp @@ -13881,8 +13881,9 @@ already_AddRefed Document::CreateTouchList( return retval.forget(); } +// https://drafts.csswg.org/cssom-view/Overview#dom-document-caretpositionfrompoint already_AddRefed Document::CaretPositionFromPoint( - float aX, float aY) { + float aX, float aY, const CaretPositionFromPointOptions& aOptions) { using FrameForPointOption = nsLayoutUtils::FrameForPointOption; nscoord x = nsPresContext::CSSPixelsToAppUnits(aX); @@ -13922,34 +13923,59 @@ already_AddRefed Document::CaretPositionFromPoint( nsIFrame::ContentOffsets offsets = ptFrame->GetContentOffsetsFromPoint(adjustedPoint); - nsCOMPtr node = offsets.content; + nsCOMPtr node = offsets.content; uint32_t offset = offsets.offset; - nsCOMPtr anonNode = node; + nsCOMPtr anonNode = node; bool nodeIsAnonymous = node && node->IsInNativeAnonymousSubtree(); if (nodeIsAnonymous) { node = ptFrame->GetContent(); - nsIContent* nonanon = node->FindFirstNonChromeOnlyAccessContent(); - HTMLTextAreaElement* textArea = HTMLTextAreaElement::FromNode(nonanon); - nsITextControlFrame* textFrame = do_QueryFrame(nonanon->GetPrimaryFrame()); - if (textFrame) { - // If the anonymous content node has a child, then we need to make sure - // that we get the appropriate child, as otherwise the offset may not be - // correct when we construct a range for it. - nsCOMPtr firstChild = anonNode->GetFirstChild(); - if (firstChild) { - anonNode = firstChild; - } + nsINode* nonChrome = + node->AsContent()->FindFirstNonChromeOnlyAccessContent(); + HTMLTextAreaElement* textArea = HTMLTextAreaElement::FromNode(nonChrome); + nsITextControlFrame* textFrame = + do_QueryFrame(nonChrome->AsContent()->GetPrimaryFrame()); - if (textArea) { - offset = - nsContentUtils::GetAdjustedOffsetInTextControl(ptFrame, offset); - } + if (!textFrame) { + return nullptr; + } - node = nonanon; - } else { - node = nullptr; - offset = 0; + // If the anonymous content node has a child, then we need to make sure + // that we get the appropriate child, as otherwise the offset may not be + // correct when we construct a range for it. + nsCOMPtr firstChild = anonNode->GetFirstChild(); + if (firstChild) { + anonNode = firstChild; + } + + if (textArea) { + offset = nsContentUtils::GetAdjustedOffsetInTextControl(ptFrame, offset); + } + + node = nonChrome; + } + + bool offsetAndNodeNeedsAdjustment = false; + + if (StaticPrefs:: + dom_shadowdom_new_caretPositionFromPoint_behavior_enabled()) { + while (node->IsInShadowTree() && + !aOptions.mShadowRoots.Contains(node->GetContainingShadow())) { + node = node->GetContainingShadowHost(); + offsetAndNodeNeedsAdjustment = true; + } + } + + if (offsetAndNodeNeedsAdjustment) { + const Maybe maybeIndex = node->ComputeIndexInParentContent(); + if (MOZ_UNLIKELY(maybeIndex.isNothing())) { + // Unlikely to happen, but still return nullptr to avoid leaking + // information about the shadow tree. + return nullptr; } + // 5.3.1: Set startOffset to index of startNode’s root's host. + offset = maybeIndex.value(); + // 5.3.2: Set startNode to startNode’s root's host's parent. + node = node->GetParentNode(); } RefPtr aCaretPos = new nsDOMCaretPosition(node, offset); diff --git a/dom/base/Document.h b/dom/base/Document.h index f0bf94c3bb09..0021e452414f 100644 --- a/dom/base/Document.h +++ b/dom/base/Document.h @@ -3451,8 +3451,8 @@ class Document : public nsINode, * @param aY Vertical point at which to determine the caret position, in * page coordinates. */ - already_AddRefed CaretPositionFromPoint(float aX, - float aY); + already_AddRefed CaretPositionFromPoint( + float aX, float aY, const CaretPositionFromPointOptions& aOptions); Element* GetScrollingElement(); // A way to check whether a given element is what would get returned from diff --git a/dom/webidl/Document.webidl b/dom/webidl/Document.webidl index ba9fe86557ff..666bb33b5d72 100644 --- a/dom/webidl/Document.webidl +++ b/dom/webidl/Document.webidl @@ -379,9 +379,14 @@ partial interface Document { undefined enableStyleSheetsForSet (DOMString? name); }; +dictionary CaretPositionFromPointOptions { + [Pref="dom.shadowdom.new_caretPositionFromPoint_behavior.enabled"] + sequence shadowRoots = []; +}; + // https://drafts.csswg.org/cssom-view/#extensions-to-the-document-interface partial interface Document { - CaretPosition? caretPositionFromPoint (float x, float y); + CaretPosition? caretPositionFromPoint(float x, float y, optional CaretPositionFromPointOptions options = {}); readonly attribute Element? scrollingElement; }; diff --git a/layout/base/tests/mochitest.toml b/layout/base/tests/mochitest.toml index 6c414c0eb155..1b3b73a3bb75 100644 --- a/layout/base/tests/mochitest.toml +++ b/layout/base/tests/mochitest.toml @@ -237,6 +237,8 @@ support-files = ["bug1448730.html"] ["test_caret_browsing_around_form_controls.html"] skip-if = ["os == 'android'"] +["test_caretPositionFromPoint_insertAnonymousContent.html"] + ["test_dynamic_toolbar_max_height.html"] support-files = ["file_dynamic_toolbar_max_height.html"] diff --git a/layout/base/tests/test_caretPositionFromPoint_insertAnonymousContent.html b/layout/base/tests/test_caretPositionFromPoint_insertAnonymousContent.html new file mode 100644 index 000000000000..a03bcaa0ede4 --- /dev/null +++ b/layout/base/tests/test_caretPositionFromPoint_insertAnonymousContent.html @@ -0,0 +1,33 @@ + + + + + Test for caretPositionFromPoint with anonymous content + + + + + + + + diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index 52e558e7fcfd..797d5ccc35eb 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -4657,6 +4657,17 @@ value: @IS_NIGHTLY_BUILD@ mirror: always +# When this pref is enabled: +# - Shadow DOM is not pierced by default anymore +# - The method accepts optional CaretPositionFromPointOptions to allow piercing +# certain ShadowRoots +# +# https://drafts.csswg.org/cssom-view/#dom-document-caretpositionfrompoint +- name: dom.shadowdom.new_caretPositionFromPoint_behavior.enabled + type: bool + value: @IS_NIGHTLY_BUILD@ + mirror: always + # NOTE: This preference is used in unit tests. If it is removed or its default # value changes, please update test_sharedMap_static_prefs.js accordingly. - name: dom.webcomponents.shadowdom.report_usage diff --git a/testing/web-platform/meta/shadow-dom/Document-caretPositionFromPoint.tentative.html.ini b/testing/web-platform/meta/shadow-dom/Document-caretPositionFromPoint.tentative.html.ini index a54badc783ce..e4fc3287cf0d 100644 --- a/testing/web-platform/meta/shadow-dom/Document-caretPositionFromPoint.tentative.html.ini +++ b/testing/web-platform/meta/shadow-dom/Document-caretPositionFromPoint.tentative.html.ini @@ -1,18 +1,4 @@ +prefs: [dom.shadowdom.new_caretPositionFromPoint_behavior.enabled:true] [Document-caretPositionFromPoint.tentative.html] - [document.caretPositionFromPoint() throws when called without the correct parameters] - expected: FAIL - [document.caretPositionFromPoint() should return a CaretPosition at the specified location pointing to a textarea element which is the offsetNode.] expected: FAIL - - [document.caretPositionFromPoint() should return a CaretPosition at the specified location pointing to the input element's shadow host's parent when the shadow tree is not specified as an argument.] - expected: FAIL - - [document.caretPositionFromPoint() should return a CaretPosition at the specified location pointing to the shadow host's parent when the shadow tree is not specified as an argument] - expected: FAIL - - [document.caretPositionFromPoint() should return a CaretPosition at the specified location pointing to the outer shadow host's parent when the point is in an inner shadow tree and no shadow tree is specified as an argument] - expected: FAIL - - [document.caretPositionFromPoint() should return a CaretPosition at the specified location pointing to the outer shadow tree when the point is in an inner shadow tree and the outer shadow tree is specified as an argument] - expected: FAIL