[PEPC] Add new flag PermissionElementDialogPositioning

This new flags enables positioning the permission dialog over the
permission element, instead of in the center of the web view.

Bug: 1462930
Change-Id: I9f29f02c2668e5682e231d48ff04dd7f0c5794e0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5125716
Reviewed-by: Thomas Nguyen <[email protected]>
Reviewed-by: Arthur Sonzogni <[email protected]>
Commit-Queue: Andy Paicu <[email protected]>
Reviewed-by: Dominik Röttsches <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1270104}
diff --git a/chrome/browser/ui/views/permissions/embedded_permission_prompt.cc b/chrome/browser/ui/views/permissions/embedded_permission_prompt.cc
index be2f366..2d9ac55 100644
--- a/chrome/browser/ui/views/permissions/embedded_permission_prompt.cc
+++ b/chrome/browser/ui/views/permissions/embedded_permission_prompt.cc
@@ -159,7 +159,9 @@
     PrioritizeAndMergeNewVariant(current_request_variant, type);
   }
 
-  raw_ptr<EmbeddedPermissionPromptBaseView> prompt_view = nullptr;
+  RebuildRequests();
+
+  EmbeddedPermissionPromptBaseView* prompt_view = nullptr;
 
   switch (embedded_prompt_variant_) {
     case Variant::kAsk:
@@ -207,7 +209,6 @@
   }
 
   if (prompt_view) {
-    RebuildRequests();
     prompt_view_tracker_.SetView(prompt_view);
     content_scrim_widget_ =
         EmbeddedPermissionPromptContentScrimView::CreateScrimWidget(
diff --git a/chrome/browser/ui/views/permissions/embedded_permission_prompt_base_view.cc b/chrome/browser/ui/views/permissions/embedded_permission_prompt_base_view.cc
index b131b8c..0f004f4a 100644
--- a/chrome/browser/ui/views/permissions/embedded_permission_prompt_base_view.cc
+++ b/chrome/browser/ui/views/permissions/embedded_permission_prompt_base_view.cc
@@ -13,11 +13,13 @@
 #include "chrome/browser/ui/views/chrome_widget_sublevel.h"
 #include "chrome/browser/ui/views/frame/browser_view.h"
 #include "components/vector_icons/vector_icons.h"
+#include "content/public/common/content_features.h"
 #include "ui/base/interaction/element_identifier.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/base/metadata/metadata_impl_macros.h"
 #include "ui/base/models/image_model.h"
 #include "ui/base/ui_base_features.h"
+#include "ui/gfx/geometry/rect.h"
 #include "ui/gfx/paint_vector_icon.h"
 #include "ui/views/bubble/bubble_frame_view.h"
 #include "ui/views/controls/button/md_text_button.h"
@@ -81,6 +83,10 @@
       browser_(browser),
       delegate_(delegate) {
   SetProperty(views::kElementIdentifierKey, kMainViewId);
+
+  CHECK_GT(delegate_->Requests().size(), 0u);
+  element_rect_ = delegate_->Requests()[0]->GetAnchorElementPosition().value_or(
+      gfx::Rect());
 }
 
 EmbeddedPermissionPromptBaseView::~EmbeddedPermissionPromptBaseView() = default;
@@ -176,7 +182,6 @@
 
 void EmbeddedPermissionPromptBaseView::ShowWidget() {
   GetWidget()->Show();
-
   SizeToContents();
 }
 
@@ -184,7 +189,12 @@
   SetAnchorView(widget->GetContentsView());
   set_parent_window(
       platform_util::GetViewForWindow(browser_->window()->GetNativeWindow()));
-  SetArrow(views::BubbleBorder::Arrow::FLOAT);
+
+  if (ShouldOverrideBubbleBounds()) {
+    SetArrow(views::BubbleBorder::Arrow::BOTTOM_LEFT);
+  } else {
+    SetArrow(views::BubbleBorder::Arrow::FLOAT);
+  }
 }
 
 bool EmbeddedPermissionPromptBaseView::ShouldShowCloseButton() const {
@@ -286,5 +296,54 @@
   buttons_container.AddChildView(std::move(button_view));
 }
 
+gfx::Rect EmbeddedPermissionPromptBaseView::GetBubbleBounds() {
+  if (!ShouldOverrideBubbleBounds()) {
+    return views::BubbleDialogDelegateView::GetBubbleBounds();
+  }
+
+  gfx::Rect default_bounds = views::BubbleDialogDelegateView::GetBubbleBounds();
+
+  content::WebContents* web_contents =
+      delegate_->GetPermissionPromptDelegate()->GetAssociatedWebContents();
+
+  gfx::Rect container_bounds = web_contents->GetContainerBounds();
+
+  // First, attempt to position the prompt below the PEPC, if it would not
+  // overflow the container bounds.
+  gfx::Rect prompt_bounds(
+      default_bounds.x() + element_rect_.bottom_center().x() -
+          default_bounds.width() / 2,
+      default_bounds.y() + element_rect_.bottom_center().y() +
+          default_bounds.height(),
+      default_bounds.width(), default_bounds.height());
+
+  if (container_bounds.Contains(prompt_bounds)) {
+    return prompt_bounds;
+  }
+
+  // Second, attempt to position the prompt above the PEPC, if it would not
+  // overflow the container bounds.
+  prompt_bounds =
+      gfx::Rect(default_bounds.x() + element_rect_.top_center().x() -
+                    default_bounds.width() / 2,
+                default_bounds.y() + element_rect_.top_center().y(),
+                default_bounds.width(), default_bounds.height());
+
+  if (container_bounds.Contains(prompt_bounds)) {
+    return prompt_bounds;
+  }
+
+  // Otherwise, place it in the middle of the container bounds.
+  return gfx::Rect(
+      container_bounds.CenterPoint().x() - default_bounds.width() / 2,
+      container_bounds.CenterPoint().y() - default_bounds.height() / 2,
+      default_bounds.width(), default_bounds.height());
+}
+
+bool EmbeddedPermissionPromptBaseView::ShouldOverrideBubbleBounds() const {
+  return features::kPermissionElementDialogPositioning.Get() &&
+         !element_rect_.IsEmpty();
+}
+
 BEGIN_METADATA(EmbeddedPermissionPromptBaseView)
 END_METADATA
diff --git a/chrome/browser/ui/views/permissions/embedded_permission_prompt_base_view.h b/chrome/browser/ui/views/permissions/embedded_permission_prompt_base_view.h
index 13a5af9..8e06667 100644
--- a/chrome/browser/ui/views/permissions/embedded_permission_prompt_base_view.h
+++ b/chrome/browser/ui/views/permissions/embedded_permission_prompt_base_view.h
@@ -122,8 +122,11 @@
   void AddButton(views::View& buttons_container,
                  const ButtonConfiguration& button);
   std::unique_ptr<views::FlexLayoutView> CreateLoadingIcon();
+  gfx::Rect GetBubbleBounds() override;
+  bool ShouldOverrideBubbleBounds() const;
 
   const raw_ptr<Browser> browser_;
+  gfx::Rect element_rect_;
   base::WeakPtr<EmbeddedPermissionPromptViewDelegate> delegate_;
 };
 
diff --git a/chrome/browser/ui/views/permissions/embedded_permission_prompt_interactive_uitest.cc b/chrome/browser/ui/views/permissions/embedded_permission_prompt_interactive_uitest.cc
index 1b3a02b..c0dcfed 100644
--- a/chrome/browser/ui/views/permissions/embedded_permission_prompt_interactive_uitest.cc
+++ b/chrome/browser/ui/views/permissions/embedded_permission_prompt_interactive_uitest.cc
@@ -251,9 +251,11 @@
         }));
   }
 
+ protected:
+  base::test::ScopedFeatureList feature_list_;
+
  private:
   std::unique_ptr<net::EmbeddedTestServer> https_server_;
-  base::test::ScopedFeatureList feature_list_;
 };
 
 // Failing on Windows, though manual testing of the same flow does not reproduce
@@ -263,11 +265,15 @@
 #define MAYBE_BasicFlowCamera DISABLED_BasicFlowCamera
 #define MAYBE_BasicFlowCameraMicrophone DISABLED_BasicFlowCameraMicrophone
 #define MAYBE_TestPartialPermissionsLabels DISABLED_TestPartialPermissionsLabels
+#define MAYBE_TestPermissionElementDialogPositioning \
+  DISABLED_TestPermissionElementDialogPositioning
 #else
 #define MAYBE_BasicFlowMicrophone BasicFlowMicrophone
 #define MAYBE_BasicFlowCamera BasicFlowCamera
 #define MAYBE_BasicFlowCameraMicrophone BasicFlowCameraMicrophone
 #define MAYBE_TestPartialPermissionsLabels TestPartialPermissionsLabels
+#define MAYBE_TestPermissionElementDialogPositioning \
+  TestPermissionElementDialogPositioning
 #endif
 IN_PROC_BROWSER_TEST_F(EmbeddedPermissionPromptInteractiveTest,
                        MAYBE_BasicFlowMicrophone) {
@@ -334,3 +340,55 @@
       u"You previously didn't allow microphone on a.test:" +
           base::UTF8ToUTF16(GetOrigin().port()));
 }
+
+class EmbeddedPermissionPromptPositioningInteractiveTest
+    : public EmbeddedPermissionPromptInteractiveTest {
+ public:
+  EmbeddedPermissionPromptPositioningInteractiveTest() {
+    feature_list_.Reset();
+    feature_list_.InitWithFeaturesAndParameters(
+        {
+            {features::kPermissionElement,
+             {{"PermissionElementDialogPositioning", "true"}}},
+            {permissions::features::kOneTimePermission, {}},
+            {blink::features::kDisablePepcSecurityForTesting, {}},
+        },
+        {});
+  }
+};
+
+IN_PROC_BROWSER_TEST_F(EmbeddedPermissionPromptPositioningInteractiveTest,
+                       MAYBE_TestPermissionElementDialogPositioning) {
+  RunTestSequence(InstrumentTab(kWebContentsElementId),
+                  NavigateWebContents(kWebContentsElementId, GetURL()));
+
+  // Click on multiple elements in order from left to right, and ensure that
+  // dialog moves with each click
+  int previous_x = 0;
+  struct ElementAction {
+    std::string element_name;
+    ui::ElementIdentifier button_identifier;
+  };
+  std::vector<ElementAction> element_actions = {
+      {"microphone", EmbeddedPermissionPromptAskView::kAllowId},
+      {"camera", EmbeddedPermissionPromptAskView::kAllowId},
+      {"camera-microphone",
+       EmbeddedPermissionPromptPreviouslyGrantedView::kStopAllowingId},
+  };
+
+  for (const auto& element_action : element_actions) {
+    RunTestSequence(ClickOnPEPCElement(element_action.element_name),
+                    InAnyContext(WaitForShow(
+                        EmbeddedPermissionPromptBaseView::kMainViewId)),
+                    FlushEvents(),
+                    InAnyContext(CheckView(
+                        EmbeddedPermissionPromptBaseView::kMainViewId,
+                        [&previous_x](views::View* view) {
+                          gfx::Rect bounds = view->GetBoundsInScreen();
+                          previous_x = bounds.x();
+                          return bounds.x();
+                        },
+                        testing::Gt(previous_x))),
+                    PushPEPCPromptButton(element_action.button_identifier));
+  }
+}
diff --git a/components/permissions/permission_request.cc b/components/permissions/permission_request.cc
index 58fa158..3900210b 100644
--- a/components/permissions/permission_request.cc
+++ b/components/permissions/permission_request.cc
@@ -180,6 +180,10 @@
   return data_.embedded_permission_element_initiated;
 }
 
+std::optional<gfx::Rect> PermissionRequest::GetAnchorElementPosition() const {
+  return data_.anchor_element_position;
+}
+
 #if !BUILDFLAG(IS_ANDROID)
 
 bool PermissionRequest::IsConfirmationChipSupported() {
diff --git a/components/permissions/permission_request.h b/components/permissions/permission_request.h
index 1b65018..641feea 100644
--- a/components/permissions/permission_request.h
+++ b/components/permissions/permission_request.h
@@ -151,6 +151,9 @@
   // element.
   bool IsEmbeddedPermissionElementInitiated() const;
 
+  // Returns the position of the element that caused the prompt to open.
+  std::optional<gfx::Rect> GetAnchorElementPosition() const;
+
   // Returns true if the request has two origins and should use the two origin
   // prompt. Returns false otherwise.
   bool ShouldUseTwoOriginPrompt() const;
diff --git a/components/permissions/permission_request_data.cc b/components/permissions/permission_request_data.cc
index 434f586..f724c59 100644
--- a/components/permissions/permission_request_data.cc
+++ b/components/permissions/permission_request_data.cc
@@ -20,6 +20,7 @@
       embedded_permission_element_initiated(
           request_description.embedded_permission_element_initiated),
       requesting_origin(canonical_requesting_origin),
+      anchor_element_position(request_description.anchor_element_position),
       requested_audio_capture_device_ids(
           request_description.requested_audio_capture_device_ids),
       requested_video_capture_device_ids(
diff --git a/content/public/common/content_features.cc b/content/public/common/content_features.cc
index 9855650a..5f4c73d 100644
--- a/content/public/common/content_features.cc
+++ b/content/public/common/content_features.cc
@@ -675,6 +675,11 @@
              "PermissionElement",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+// Enables different positioning of the permission dialog, so that it's placed
+// near the permission element, if possible.
+constexpr base::FeatureParam<bool> kPermissionElementDialogPositioning{
+    &kPermissionElement, "PermissionElementDialogPositioning", false};
+
 // Enables Persistent Origin Trials. It causes tokens for an origin to be stored
 // and persisted for the next navigation. This way, an origin trial can affect
 // things before receiving the response, for instance it can affect the next
diff --git a/content/public/common/content_features.h b/content/public/common/content_features.h
index ec544c1..aacbc65 100644
--- a/content/public/common/content_features.h
+++ b/content/public/common/content_features.h
@@ -166,6 +166,8 @@
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kFeaturePolicyHeader);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kPepperCrossOriginRedirectRestriction);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kPermissionElement);
+CONTENT_EXPORT extern const base::FeatureParam<bool>
+    kPermissionElementDialogPositioning;
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kPersistentOriginTrials);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kPrefetchNewLimits);
 CONTENT_EXPORT BASE_DECLARE_FEATURE(kPrefetchRedirects);
diff --git a/third_party/blink/renderer/core/html/html_permission_element.cc b/third_party/blink/renderer/core/html/html_permission_element.cc
index 550ab19..6b3bf1c 100644
--- a/third_party/blink/renderer/core/html/html_permission_element.cc
+++ b/third_party/blink/renderer/core/html/html_permission_element.cc
@@ -17,6 +17,7 @@
 #include "third_party/blink/renderer/core/events/mouse_event.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
+#include "third_party/blink/renderer/core/geometry/dom_rect.h"
 #include "third_party/blink/renderer/core/html/html_div_element.h"
 #include "third_party/blink/renderer/core/html/html_span_element.h"
 #include "third_party/blink/renderer/core/html/shadow/shadow_element_names.h"
@@ -367,7 +368,7 @@
   auto descriptor = EmbeddedPermissionRequestDescriptor::New();
   // TODO(crbug.com/1462930): Send element position to browser and use the
   // rect to calculate expected prompt position in screen coordinates.
-  descriptor->element_position = gfx::Rect(0, 0, 0, 0);
+  descriptor->element_position = GetBoundingClientRect()->ToEnclosingRect();
   descriptor->permissions = mojo::Clone(permission_descriptors_);
   GetPermissionService()->RequestPageEmbeddedPermission(
       std::move(descriptor),