ScrollAnchoring: Prioritize focus/find-in-page during anchor selection.

This patch updates the scroll anchoring selection to consider
- focused element
- find-in-page active match results

If an anchor is selected out of those candidates, then it is used.
Otherwise, we proceed with the DOM traversal.

This is updated due to changes in the spec:
https://github.com/w3c/csswg-drafts/commit/396d0cf9dc9585bd77a611f18124770ac8ce704b

Fixed: 1066924, 1066982
R=chrishtr@chromium.org, flackr@chromium.org

Change-Id: I1af88dfdeea374c17161b86981b10fbdcecc3a1a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2199603
Commit-Queue: Chris Harrelson <chrishtr@chromium.org>
Reviewed-by: Chris Harrelson <chrishtr@chromium.org>
Cr-Commit-Position: refs/heads/master@{#768809}
diff --git a/third_party/blink/renderer/core/dom/document.cc b/third_party/blink/renderer/core/dom/document.cc
index f35dd4f..12a99b4 100644
--- a/third_party/blink/renderer/core/dom/document.cc
+++ b/third_party/blink/renderer/core/dom/document.cc
@@ -351,6 +351,16 @@
   return false;
 }
 
+// Helper function to notify both `first` and `second` that the priority scroll
+// anchor status changed. This is used when, for example, a focused element
+// changes from `first` to `second`.
+void NotifyPriorityScrollAnchorStatusChanged(Node* first, Node* second) {
+  if (first)
+    first->NotifyPriorityScrollAnchorStatusChanged();
+  if (second)
+    second->NotifyPriorityScrollAnchorStatusChanged();
+}
+
 }  // namespace
 
 class DocumentOutliveTimeReporter : public BlinkGCObserver {
@@ -5363,6 +5373,9 @@
     if (GetSettings()->GetSpatialNavigationEnabled())
       GetPage()->GetSpatialNavigationController().FocusedNodeChanged(this);
   }
+
+  blink::NotifyPriorityScrollAnchorStatusChanged(old_focused_element,
+                                                 new_focused_element);
 }
 
 void Document::SetSequentialFocusNavigationStartingPoint(Node* node) {
@@ -8263,6 +8276,7 @@
   visitor->Trace(has_trust_tokens_answerer_);
   visitor->Trace(pending_has_trust_tokens_resolvers_);
   visitor->Trace(font_preload_manager_);
+  visitor->Trace(find_in_page_active_match_node_);
   Supplementable<Document>::Trace(visitor);
   TreeScope::Trace(visitor);
   ContainerNode::Trace(visitor);
@@ -8622,6 +8636,16 @@
   }
 }
 
+void Document::SetFindInPageActiveMatchNode(Node* node) {
+  blink::NotifyPriorityScrollAnchorStatusChanged(
+      find_in_page_active_match_node_, node);
+  find_in_page_active_match_node_ = node;
+}
+
+const Node* Document::GetFindInPageActiveMatchNode() const {
+  return find_in_page_active_match_node_;
+}
+
 template class CORE_TEMPLATE_EXPORT Supplement<Document>;
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/dom/document.h b/third_party/blink/renderer/core/dom/document.h
index 0a680ab..f520ea3 100644
--- a/third_party/blink/renderer/core/dom/document.h
+++ b/third_party/blink/renderer/core/dom/document.h
@@ -1688,6 +1688,9 @@
   FontPreloadManager& GetFontPreloadManager() { return font_preload_manager_; }
   void FontPreloadingFinishedOrTimedOut();
 
+  void SetFindInPageActiveMatchNode(Node*);
+  const Node* GetFindInPageActiveMatchNode() const;
+
  protected:
   void ClearXMLVersion() { xml_version_ = String(); }
 
@@ -2262,6 +2265,8 @@
       pending_has_trust_tokens_resolvers_;
 
   FontPreloadManager font_preload_manager_;
+
+  WeakMember<Node> find_in_page_active_match_node_;
 };
 
 extern template class CORE_EXTERN_TEMPLATE_EXPORT Supplement<Document>;
diff --git a/third_party/blink/renderer/core/dom/node.cc b/third_party/blink/renderer/core/dom/node.cc
index cd4ca95..87bf4f19 100644
--- a/third_party/blink/renderer/core/dom/node.cc
+++ b/third_party/blink/renderer/core/dom/node.cc
@@ -1758,6 +1758,16 @@
   return parent ? parent->CanStartSelection() : true;
 }
 
+void Node::NotifyPriorityScrollAnchorStatusChanged() {
+  auto* node = this;
+  while (node && !node->GetLayoutObject())
+    node = FlatTreeTraversal::Parent(*node);
+  if (node) {
+    DCHECK(node->GetLayoutObject());
+    node->GetLayoutObject()->NotifyPriorityScrollAnchorStatusChanged();
+  }
+}
+
 // StyledElements allow inline style (style="border: 1px"), presentational
 // attributes (ex. color), class names (ex. class="foo bar") and other non-basic
 // styling features. They also control if this element can participate in style
diff --git a/third_party/blink/renderer/core/dom/node.h b/third_party/blink/renderer/core/dom/node.h
index 92e870b..087c213 100644
--- a/third_party/blink/renderer/core/dom/node.h
+++ b/third_party/blink/renderer/core/dom/node.h
@@ -682,6 +682,8 @@
   // Whether or not a selection can be started in this object
   virtual bool CanStartSelection() const;
 
+  void NotifyPriorityScrollAnchorStatusChanged();
+
   // ---------------------------------------------------------------------------
   // Integration with layout tree
 
diff --git a/third_party/blink/renderer/core/editing/finder/text_finder.cc b/third_party/blink/renderer/core/editing/finder/text_finder.cc
index 31dd0d9..8198fba 100644
--- a/third_party/blink/renderer/core/editing/finder/text_finder.cc
+++ b/third_party/blink/renderer/core/editing/finder/text_finder.cc
@@ -716,11 +716,11 @@
 bool TextFinder::SetMarkerActive(Range* range, bool active) {
   if (!range || range->collapsed())
     return false;
-  return OwnerFrame()
-      .GetFrame()
-      ->GetDocument()
-      ->Markers()
-      .SetTextMatchMarkersActive(EphemeralRange(range), active);
+  Document* document = OwnerFrame().GetFrame()->GetDocument();
+  document->SetFindInPageActiveMatchNode(active ? range->startContainer()
+                                                : nullptr);
+  return document->Markers().SetTextMatchMarkersActive(EphemeralRange(range),
+                                                       active);
 }
 
 void TextFinder::UnmarkAllTextMatches() {
diff --git a/third_party/blink/renderer/core/layout/layout_object.cc b/third_party/blink/renderer/core/layout/layout_object.cc
index 9f4470b..14e443c 100644
--- a/third_party/blink/renderer/core/layout/layout_object.cc
+++ b/third_party/blink/renderer/core/layout/layout_object.cc
@@ -455,6 +455,19 @@
   children->RemoveChildNode(this, old_child);
 }
 
+void LayoutObject::NotifyPriorityScrollAnchorStatusChanged() {
+  if (!Parent())
+    return;
+  for (auto* layer = Parent()->EnclosingLayer(); layer;
+       layer = layer->Parent()) {
+    if (PaintLayerScrollableArea* scrollable_area =
+            layer->GetScrollableArea()) {
+      DCHECK(scrollable_area->GetScrollAnchor());
+      scrollable_area->GetScrollAnchor()->ClearSelf();
+    }
+  }
+}
+
 void LayoutObject::RegisterSubtreeChangeListenerOnDescendants(bool value) {
   // If we're set to the same value then we're done as that means it's
   // set down the tree that way already.
diff --git a/third_party/blink/renderer/core/layout/layout_object.h b/third_party/blink/renderer/core/layout/layout_object.h
index 668591db..c0ca4d1 100644
--- a/third_party/blink/renderer/core/layout/layout_object.h
+++ b/third_party/blink/renderer/core/layout/layout_object.h
@@ -551,6 +551,8 @@
            ShouldApplySizeContainment();
   }
 
+  void NotifyPriorityScrollAnchorStatusChanged();
+
  private:
   //////////////////////////////////////////
   // Helper functions. Dangerous to use!
diff --git a/third_party/blink/renderer/core/layout/scroll_anchor.cc b/third_party/blink/renderer/core/layout/scroll_anchor.cc
index d796a7f..15a749a 100644
--- a/third_party/blink/renderer/core/layout/scroll_anchor.cc
+++ b/third_party/blink/renderer/core/layout/scroll_anchor.cc
@@ -315,7 +315,11 @@
 void ScrollAnchor::FindAnchor() {
   TRACE_EVENT0("blink", "ScrollAnchor::findAnchor");
   SCOPED_BLINK_UMA_HISTOGRAM_TIMER("Layout.ScrollAnchor.TimeToFindAnchor");
-  FindAnchorRecursive(ScrollerLayoutBox(scroller_));
+
+  bool found_priority_anchor = FindAnchorInPriorityCandidates();
+  if (!found_priority_anchor)
+    FindAnchorRecursive(ScrollerLayoutBox(scroller_));
+
   if (anchor_object_) {
     anchor_object_->SetIsScrollAnchorObject();
     saved_relative_offset_ =
@@ -323,6 +327,61 @@
   }
 }
 
+bool ScrollAnchor::FindAnchorInPriorityCandidates() {
+  auto* scroller_box = ScrollerLayoutBox(scroller_);
+  if (!scroller_box)
+    return false;
+
+  auto& document = scroller_box->GetDocument();
+
+  // Focused area.
+  LayoutObject* candidate =
+      PriorityCandidateFromNode(document.FocusedElement());
+  auto result = ExaminePriorityCandidate(candidate);
+  if (result.viable) {
+    anchor_object_ = candidate;
+    corner_ = result.corner;
+    return true;
+  }
+
+  // Active find-in-page match.
+  candidate =
+      PriorityCandidateFromNode(document.GetFindInPageActiveMatchNode());
+  result = ExaminePriorityCandidate(candidate);
+  if (result.viable) {
+    anchor_object_ = candidate;
+    corner_ = result.corner;
+    return true;
+  }
+  return false;
+}
+
+LayoutObject* ScrollAnchor::PriorityCandidateFromNode(const Node* node) const {
+  while (node) {
+    if (auto* layout_object = node->GetLayoutObject()) {
+      if (!layout_object->IsAnonymous() &&
+          (!layout_object->IsInline() ||
+           layout_object->IsAtomicInlineLevel())) {
+        return layout_object;
+      }
+    }
+    node = FlatTreeTraversal::Parent(*node);
+  }
+  return nullptr;
+}
+
+ScrollAnchor::ExamineResult ScrollAnchor::ExaminePriorityCandidate(
+    const LayoutObject* candidate) const {
+  auto* ancestor = candidate;
+  auto* scroller_box = ScrollerLayoutBox(scroller_);
+  while (ancestor && ancestor != scroller_box) {
+    if (ancestor->StyleRef().OverflowAnchor() == EOverflowAnchor::kNone)
+      return ExamineResult(kSkip);
+    ancestor = ancestor->Parent();
+  }
+  return ancestor ? Examine(candidate) : ExamineResult(kSkip);
+}
+
 bool ScrollAnchor::FindAnchorRecursive(LayoutObject* candidate) {
   ExamineResult result = Examine(candidate);
   if (result.viable) {
diff --git a/third_party/blink/renderer/core/layout/scroll_anchor.h b/third_party/blink/renderer/core/layout/scroll_anchor.h
index c637c9c..463b1749 100644
--- a/third_party/blink/renderer/core/layout/scroll_anchor.h
+++ b/third_party/blink/renderer/core/layout/scroll_anchor.h
@@ -116,6 +116,14 @@
   bool FindAnchorRecursive(LayoutObject*);
   bool ComputeScrollAnchorDisablingStyleChanged();
 
+  // Find viable anchor among the priority candidates. Returns true if anchor
+  // has been found; returns false if anchor was not found, and we should look
+  // for an anchor in the DOM order traversal.
+  bool FindAnchorInPriorityCandidates();
+  // Returns a closest ancestor layout object from the given node which isn't a
+  // non-atomic inline and is not anonymous.
+  LayoutObject* PriorityCandidateFromNode(const Node*) const;
+
   enum WalkStatus { kSkip = 0, kConstrain, kContinue, kReturn };
   struct ExamineResult {
     ExamineResult(WalkStatus s)
@@ -130,6 +138,11 @@
   };
 
   ExamineResult Examine(const LayoutObject*) const;
+  // Examines a given priority candidate. Note that this is similar to Examine()
+  // but it also checks that the given object is a descendant of the scroller
+  // and that there is no object that has overflow-anchor: none between the
+  // given object and the scroller.
+  ExamineResult ExaminePriorityCandidate(const LayoutObject*) const;
 
   IntSize ComputeAdjustment() const;
 
diff --git a/third_party/blink/renderer/core/layout/scroll_anchor_test.cc b/third_party/blink/renderer/core/layout/scroll_anchor_test.cc
index c967045c..6d26f04 100644
--- a/third_party/blink/renderer/core/layout/scroll_anchor_test.cc
+++ b/third_party/blink/renderer/core/layout/scroll_anchor_test.cc
@@ -7,8 +7,12 @@
 #include "build/build_config.h"
 #include "third_party/blink/public/common/input/web_mouse_event.h"
 #include "third_party/blink/renderer/core/dom/static_node_list.h"
+#include "third_party/blink/renderer/core/editing/finder/text_finder.h"
+#include "third_party/blink/renderer/core/frame/find_in_page.h"
+#include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
 #include "third_party/blink/renderer/core/frame/root_frame_viewport.h"
 #include "third_party/blink/renderer/core/frame/visual_viewport.h"
+#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
 #include "third_party/blink/renderer/core/geometry/dom_rect.h"
 #include "third_party/blink/renderer/core/layout/layout_box.h"
 #include "third_party/blink/renderer/core/page/print_context.h"
@@ -17,6 +21,7 @@
 #include "third_party/blink/renderer/core/testing/core_unit_test_helper.h"
 #include "third_party/blink/renderer/platform/testing/histogram_tester.h"
 #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
+#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
 
 namespace blink {
 
@@ -1066,4 +1071,186 @@
                                ->GetScrollAnimator()
                                .ImplOnlyAnimationAdjustmentForTesting());
 }
+
+class ScrollAnchorTestFindInPageClient : public mojom::blink::FindInPageClient {
+ public:
+  ~ScrollAnchorTestFindInPageClient() override = default;
+
+  void SetFrame(WebLocalFrameImpl* frame) {
+    frame->GetFindInPage()->SetClient(receiver_.BindNewPipeAndPassRemote());
+  }
+
+  void SetNumberOfMatches(
+      int request_id,
+      unsigned int current_number_of_matches,
+      mojom::blink::FindMatchUpdateType final_update) final {
+    count_ = current_number_of_matches;
+  }
+
+  void SetActiveMatch(int request_id,
+                      const gfx::Rect& active_match_rect,
+                      int active_match_ordinal,
+                      mojom::blink::FindMatchUpdateType final_update) final {}
+
+  int Count() const { return count_; }
+  void Reset() { count_ = -1; }
+
+ private:
+  int count_ = -1;
+  mojo::Receiver<mojom::blink::FindInPageClient> receiver_{this};
+};
+
+class ScrollAnchorFindInPageTest : public testing::Test {
+ public:
+  void SetUp() override { web_view_helper_.Initialize(); }
+  void TearDown() override { web_view_helper_.Reset(); }
+
+  Document& GetDocument() {
+    return *static_cast<Document*>(
+        web_view_helper_.LocalMainFrame()->GetDocument());
+  }
+  FindInPage* GetFindInPage() {
+    return web_view_helper_.LocalMainFrame()->GetFindInPage();
+  }
+  WebLocalFrameImpl* LocalMainFrame() {
+    return web_view_helper_.LocalMainFrame();
+  }
+
+  void UpdateAllLifecyclePhasesForTest() {
+    GetDocument().View()->UpdateAllLifecyclePhases(DocumentUpdateReason::kTest);
+  }
+
+  void SetHtmlInnerHTML(const char* content) {
+    GetDocument().documentElement()->setInnerHTML(String::FromUTF8(content));
+    UpdateAllLifecyclePhasesForTest();
+  }
+
+  void ResizeAndFocus() {
+    web_view_helper_.Resize(WebSize(640, 480));
+    web_view_helper_.GetWebView()->MainFrameWidget()->SetFocus(true);
+    test::RunPendingTasks();
+  }
+
+  mojom::blink::FindOptionsPtr FindOptions(bool find_next = false) {
+    auto find_options = mojom::blink::FindOptions::New();
+    find_options->run_synchronously_for_testing = true;
+    find_options->find_next = find_next;
+    find_options->forward = true;
+    return find_options;
+  }
+
+  void Find(String search_text,
+            ScrollAnchorTestFindInPageClient& client,
+            bool find_next = false) {
+    client.Reset();
+    GetFindInPage()->Find(FAKE_FIND_ID, search_text, FindOptions(find_next));
+    test::RunPendingTasks();
+  }
+
+  ScrollableArea* LayoutViewport() {
+    return GetDocument().View()->LayoutViewport();
+  }
+
+  const int FAKE_FIND_ID = 1;
+
+ private:
+  frame_test_helpers::WebViewHelper web_view_helper_;
+};
+
+TEST_F(ScrollAnchorFindInPageTest, FindInPageResultPrioritized) {
+  ResizeAndFocus();
+  SetHtmlInnerHTML(R"HTML(
+    <style>
+    body { height: 4000px }
+    .spacer { height: 100px }
+    #growing { height: 100px }
+    </style>
+
+    <div class=spacer></div>
+    <div class=spacer></div>
+    <div class=spacer></div>
+    <div class=spacer></div>
+    <div id=growing></div>
+    <div class=spacer></div>
+    <div id=target>findme</div>
+    <div class=spacer></div>
+    <div class=spacer></div>
+  )HTML");
+
+  LayoutViewport()->SetScrollOffset(ScrollOffset(0, 150),
+                                    mojom::blink::ScrollType::kUser);
+
+  const String search_text = "findme";
+  ScrollAnchorTestFindInPageClient client;
+  client.SetFrame(LocalMainFrame());
+  Find(search_text, client);
+  ASSERT_EQ(1, client.Count());
+
+  // Save the old bounds for comparison.
+  auto* old_bounds =
+      GetDocument().getElementById("target")->getBoundingClientRect();
+
+  GetDocument().getElementById("growing")->setAttribute(html_names::kStyleAttr,
+                                                        "height: 3000px");
+  UpdateAllLifecyclePhasesForTest();
+
+  auto* new_bounds =
+      GetDocument().getElementById("target")->getBoundingClientRect();
+
+  // The y coordinate of the target should not change.
+  EXPECT_EQ(old_bounds->y(), new_bounds->y());
+}
+
+TEST_F(ScrollAnchorFindInPageTest, FocusPrioritizedOverFindInPage) {
+  ResizeAndFocus();
+  SetHtmlInnerHTML(R"HTML(
+    <style>
+    body { height: 4000px }
+    .spacer { height: 100px }
+    #growing { height: 100px }
+    #focus_target { height: 10px }
+    </style>
+
+    <div class=spacer></div>
+    <div class=spacer></div>
+    <div class=spacer></div>
+    <div class=spacer></div>
+    <div id=focus_target tabindex=0></div>
+    <div id=growing></div>
+    <div id=find_target>findme</div>
+    <div class=spacer></div>
+    <div class=spacer></div>
+  )HTML");
+
+  LayoutViewport()->SetScrollOffset(ScrollOffset(0, 150),
+                                    mojom::blink::ScrollType::kUser);
+
+  const String search_text = "findme";
+  ScrollAnchorTestFindInPageClient client;
+  client.SetFrame(LocalMainFrame());
+  Find(search_text, client);
+  ASSERT_EQ(1, client.Count());
+
+  GetDocument().getElementById("focus_target")->focus();
+
+  // Save the old bounds for comparison.
+  auto* old_focus_bounds =
+      GetDocument().getElementById("focus_target")->getBoundingClientRect();
+  auto* old_find_bounds =
+      GetDocument().getElementById("find_target")->getBoundingClientRect();
+
+  GetDocument().getElementById("growing")->setAttribute(html_names::kStyleAttr,
+                                                        "height: 3000px");
+  UpdateAllLifecyclePhasesForTest();
+
+  auto* new_focus_bounds =
+      GetDocument().getElementById("focus_target")->getBoundingClientRect();
+  auto* new_find_bounds =
+      GetDocument().getElementById("find_target")->getBoundingClientRect();
+
+  // `focus_target` should remain where it is, since it is prioritized.
+  // `find_target`, however, is shifted.
+  EXPECT_EQ(old_focus_bounds->y(), new_focus_bounds->y());
+  EXPECT_NE(old_find_bounds->y(), new_find_bounds->y());
+}
 }
diff --git a/third_party/blink/web_tests/external/wpt/css/css-scroll-anchoring/focus-prioritized.html b/third_party/blink/web_tests/external/wpt/css/css-scroll-anchoring/focus-prioritized.html
new file mode 100644
index 0000000..36e3c764
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-scroll-anchoring/focus-prioritized.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset="utf8">
+<title>CSS Scroll Anchoring: prioritize focused element</title>
+<link rel="author" title="Vladimir Levin" href="mailto:vmpstr@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/#anchor-node-selection">
+<meta name="assert" content="anchor selection prioritized focused element">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<style>
+body { height: 4000px }
+.spacer { height: 100px }
+#growing { height: 100px }
+#focused { height: 10px }
+</style>
+
+<div class=spacer></div>
+<div class=spacer></div>
+<div class=spacer></div>
+<div class=spacer></div>
+<div id=growing></div>
+<div class=spacer></div>
+<div id=focused tabindex=0></div>
+<div class=spacer></div>
+<div class=spacer></div>
+
+<script>
+async_test((t) => {
+  document.scrollingElement.scrollTop = 150;
+  focused.focus();
+
+  const target_rect = focused.getBoundingClientRect();
+  growing.style.height = "3000px";
+
+  requestAnimationFrame(() => {
+    t.step(() => {
+      const new_rect = focused.getBoundingClientRect();
+      assert_equals(new_rect.x, target_rect.x, "x coordinate");
+      assert_equals(new_rect.y, target_rect.y, "y coordinate");
+      assert_not_equals(document.scrollingElement.scrollTop, 150, "scroll adjusted");
+    });
+    t.done();
+  });
+}, "Anchor selection prioritized focused element.");
+</script>