Bug 1109868 - Panels should watch their anchors for position and visibility changes and update accordingly. r=tn, a=lizzard
authorNeil Deakin <neil@mozilla.com>
Thu, 16 Feb 2017 08:53:59 -0500
changeset 379039 2f9fc3b858d76c82088eef7604e6cb577483de64
parent 379038 a5ccc32310c9a424ccb4f5106f5155ca20dd360a
child 379040 0b68f73fd7e9e91ddddf756c91d9d6c8f806ef52
push id1419
push userjlund@mozilla.com
push dateMon, 10 Apr 2017 20:44:07 +0000
treeherdermozilla-release@5e6801b73ef6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstn, lizzard
bugs1109868
milestone53.0
Bug 1109868 - Panels should watch their anchors for position and visibility changes and update accordingly. r=tn, a=lizzard
browser/base/content/popup-notifications.inc
dom/base/nsGkAtomList.h
layout/base/nsRefreshDriver.cpp
layout/xul/nsMenuPopupFrame.cpp
layout/xul/nsMenuPopupFrame.h
layout/xul/nsXULPopupManager.cpp
layout/xul/nsXULPopupManager.h
toolkit/content/tests/chrome/chrome.ini
toolkit/content/tests/chrome/test_panel_anchoradjust.xul
toolkit/content/tests/chrome/window_panel_anchoradjust.xul
--- a/browser/base/content/popup-notifications.inc
+++ b/browser/base/content/popup-notifications.inc
@@ -1,16 +1,17 @@
 # to be included inside a popupset element
 
     <panel id="notification-popup"
            type="arrow"
            position="after_start"
            hidden="true"
            orient="vertical"
            noautofocus="true"
+           followanchor="false"
            role="alert"/>
 
     <popupnotification id="webRTC-shareDevices-notification" hidden="true">
       <popupnotificationcontent id="webRTC-selectCamera" orient="vertical">
         <label value="&getUserMedia.selectCamera.label;"
                accesskey="&getUserMedia.selectCamera.accesskey;"
                control="webRTC-selectCamera-menulist"/>
         <menulist id="webRTC-selectCamera-menulist">
--- a/dom/base/nsGkAtomList.h
+++ b/dom/base/nsGkAtomList.h
@@ -390,16 +390,17 @@ GK_ATOM(flags, "flags")
 GK_ATOM(flex, "flex")
 GK_ATOM(flexgroup, "flexgroup")
 GK_ATOM(flip, "flip")
 GK_ATOM(floating, "floating")
 GK_ATOM(floor, "floor")
 GK_ATOM(flowlength, "flowlength")
 GK_ATOM(focus, "focus")
 GK_ATOM(focused, "focused")
+GK_ATOM(followanchor, "followanchor")
 GK_ATOM(following, "following")
 GK_ATOM(followingSibling, "following-sibling")
 GK_ATOM(font, "font")
 GK_ATOM(fontWeight, "font-weight")
 GK_ATOM(fontpicker, "fontpicker")
 GK_ATOM(footer, "footer")
 GK_ATOM(_for, "for")
 GK_ATOM(forEach, "for-each")
--- a/layout/base/nsRefreshDriver.cpp
+++ b/layout/base/nsRefreshDriver.cpp
@@ -65,16 +65,20 @@
 #include "mozilla/VsyncDispatcher.h"
 #include "nsThreadUtils.h"
 #include "mozilla/Unused.h"
 #include "mozilla/TimelineConsumers.h"
 #include "nsAnimationManager.h"
 #include "nsIDOMEvent.h"
 #include "nsDisplayList.h"
 
+#ifdef MOZ_XUL
+#include "nsXULPopupManager.h"
+#endif
+
 using namespace mozilla;
 using namespace mozilla::widget;
 using namespace mozilla::ipc;
 using namespace mozilla::layout;
 
 static mozilla::LazyLogModule sRefreshDriverLog("nsRefreshDriver");
 #define LOG(...) MOZ_LOG(sRefreshDriverLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
 
@@ -1890,16 +1894,25 @@ nsRefreshDriver::Tick(int64_t aNowEpoch,
       aNowTime >= mNextRecomputeVisibilityTick &&
       !presShell->IsPaintingSuppressed()) {
     mNextRecomputeVisibilityTick = aNowTime + mMinRecomputeVisibilityInterval;
     mNeedToRecomputeVisibility = false;
 
     presShell->ScheduleApproximateFrameVisibilityUpdateNow();
   }
 
+#ifdef MOZ_XUL
+  // Update any popups that may need to be moved or hidden due to their
+  // anchor changing.
+  nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+  if (pm) {
+    pm->UpdatePopupPositions(this);
+  }
+#endif
+
   bool notifyIntersectionObservers = false;
   if (aNowTime >= mNextNotifyIntersectionObserversTick) {
     mNextNotifyIntersectionObserversTick =
       aNowTime + mMinNotifyIntersectionObserversInterval;
     notifyIntersectionObservers = true;
   }
   nsCOMArray<nsIDocument> documents;
   CollectDocuments(mPresContext->Document(), &documents);
--- a/layout/xul/nsMenuPopupFrame.cpp
+++ b/layout/xul/nsMenuPopupFrame.cpp
@@ -1303,16 +1303,37 @@ nsMenuPopupFrame::FlipOrResize(nscoord& 
   // If popupSize ended up being negative, or the original size was actually
   // smaller than the calculated popup size, just use the original size instead.
   if (popupSize <= 0 || aSize < popupSize) {
     popupSize = aSize;
   }
   return std::min(popupSize, aScreenEnd - aScreenPoint);
 }
 
+nsRect
+nsMenuPopupFrame::ComputeAnchorRect(nsPresContext* aRootPresContext, nsIFrame* aAnchorFrame)
+{
+  // Get the root frame for a reference
+  nsIFrame* rootFrame = aRootPresContext->FrameManager()->GetRootFrame();
+
+  // The dimensions of the anchor
+  nsRect anchorRect = aAnchorFrame->GetRectRelativeToSelf();
+
+  // Relative to the root
+  anchorRect = nsLayoutUtils::TransformFrameRectToAncestor(aAnchorFrame,
+                                                           anchorRect,
+                                                           rootFrame);
+  // Relative to the screen
+  anchorRect.MoveBy(rootFrame->GetScreenRectInAppUnits().TopLeft());
+
+  // In its own app units
+  return anchorRect.ScaleToOtherAppUnitsRoundOut(aRootPresContext->AppUnitsPerDevPixel(),
+                                                 PresContext()->AppUnitsPerDevPixel());
+}
+
 nsresult
 nsMenuPopupFrame::SetPopupPosition(nsIFrame* aAnchorFrame, bool aIsMove, bool aSizedToPopup, bool aNotify)
 {
   if (!mShouldAutoPosition)
     return NS_OK;
 
   // If this is due to a move, return early if the popup hasn't been laid out
   // yet. On Windows, this can happen when using a drag popup before it opens.
@@ -1359,32 +1380,17 @@ nsMenuPopupFrame::SetPopupPosition(nsIFr
 
         if (!aAnchorFrame) {
           aAnchorFrame = rootFrame;
           if (!aAnchorFrame)
             return NS_OK;
         }
       }
 
-      // And then we need its root frame for a reference
-      nsIFrame* referenceFrame = rootPresContext->FrameManager()->GetRootFrame();
-
-      // the dimensions of the anchor
-      nsRect parentRect = aAnchorFrame->GetRectRelativeToSelf();
-      // Relative to the root
-      anchorRect = nsLayoutUtils::TransformFrameRectToAncestor(aAnchorFrame,
-                                                               parentRect,
-                                                               referenceFrame);
-      // Relative to the screen
-      anchorRect.MoveBy(referenceFrame->GetScreenRectInAppUnits().TopLeft());
-
-      // In its own app units
-      anchorRect =
-        anchorRect.ScaleToOtherAppUnitsRoundOut(rootPresContext->AppUnitsPerDevPixel(),
-                                                presContext->AppUnitsPerDevPixel());
+      anchorRect = ComputeAnchorRect(rootPresContext, aAnchorFrame);
     }
 
     // The width is needed when aSizedToPopup is true
     parentWidth = anchorRect.width;
   }
 
   // Set the popup's size to the preferred size. Below, this size will be
   // adjusted to fit on the screen or within the content area. If the anchor
@@ -2209,16 +2215,23 @@ nsMenuPopupFrame::AttributeChanged(int32
 #ifndef MOZ_GTK2
   if (aAttribute == nsGkAtoms::noautohide) {
     nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
     if (pm)
       pm->EnableRollup(mContent, !IsNoAutoHide());
   }
 #endif
 
+  if (aAttribute == nsGkAtoms::followanchor) {
+    nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+    if (pm) {
+      pm->UpdateFollowAnchor(this);
+    }
+  }
+
   if (aAttribute == nsGkAtoms::label) {
     // set the label for the titlebar
     nsView* view = GetView();
     if (view) {
       nsIWidget* widget = view->GetWidget();
       if (widget) {
         nsAutoString title;
         mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::label, title);
@@ -2353,16 +2366,20 @@ nsMenuPopupFrame::GetAutoPosition()
 {
   return mShouldAutoPosition;
 }
 
 void
 nsMenuPopupFrame::SetAutoPosition(bool aShouldAutoPosition)
 {
   mShouldAutoPosition = aShouldAutoPosition;
+  nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+  if (pm) {
+    pm->UpdateFollowAnchor(this);
+  }
 }
 
 void
 nsMenuPopupFrame::SetConsumeRollupEvent(uint32_t aConsumeMode)
 {
   mConsumeRollupEvent = aConsumeMode;
 }
 
@@ -2441,8 +2458,102 @@ nsMenuPopupFrame::CreatePopupView()
   viewManager->InsertChild(parentView, view, nullptr, true);
 
   // Remember our view
   SetView(view);
 
   NS_FRAME_LOG(NS_FRAME_TRACE_CALLS,
     ("nsMenuPopupFrame::CreatePopupView: frame=%p view=%p", this, view));
 }
+
+bool
+nsMenuPopupFrame::ShouldFollowAnchor()
+{
+  if (!mShouldAutoPosition ||
+      mAnchorType != MenuPopupAnchorType_Node || !mAnchorContent) {
+    return false;
+  }
+
+  // Follow anchor mode is used when followanchor="true" is set or for arrow panels.
+  if (mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::followanchor,
+                            nsGkAtoms::_true, eCaseMatters)) {
+    return true;
+  }
+
+  if (mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::followanchor,
+                            nsGkAtoms::_false, eCaseMatters)) {
+    return false;
+  }
+
+  return (mPopupType == ePopupTypePanel &&
+          mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+                                nsGkAtoms::arrow, eCaseMatters));
+}
+
+bool
+nsMenuPopupFrame::ShouldFollowAnchor(nsRect& aRect)
+{
+  if (!ShouldFollowAnchor()) {
+    return false;
+  }
+
+  nsIFrame* anchorFrame = mAnchorContent->GetPrimaryFrame();
+  if (anchorFrame) {
+    nsPresContext* rootPresContext = PresContext()->GetRootPresContext();
+    if (rootPresContext) {
+      aRect = ComputeAnchorRect(rootPresContext, anchorFrame);
+    }
+  }
+
+  return true;
+}
+
+void
+nsMenuPopupFrame::CheckForAnchorChange(nsRect& aRect)
+{
+  // Don't update if the popup isn't visible or we shouldn't be following the anchor.
+  if (!IsVisible() || !ShouldFollowAnchor()) {
+    return;
+  }
+
+  bool shouldHide = false;
+
+  nsPresContext* rootPresContext = PresContext()->GetRootPresContext();
+
+  // If the frame for the anchor has gone away, hide the popup.
+  nsIFrame* anchor = mAnchorContent->GetPrimaryFrame();
+  if (!anchor || !rootPresContext) {
+    shouldHide = true;
+  } else if (!anchor->IsVisibleConsideringAncestors(VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) {
+    // If the anchor is now inside something that is invisible, hide the popup.
+    shouldHide = true;
+  } else {
+    // If the anchor is now inside a hidden parent popup, hide the popup.
+    nsIFrame* frame = anchor;
+    while (frame) {
+      nsMenuPopupFrame* popup = do_QueryFrame(frame);
+      if (popup && popup->PopupState() != ePopupShown) {
+        shouldHide = true;
+        break;
+      }
+
+      frame = frame->GetParent();
+    }
+  }
+
+  if (shouldHide) {
+    nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+    if (pm) {
+      // As the caller will be iterating over the open popups, hide asyncronously.
+      pm->HidePopup(mContent, false, true, true, false);
+    }
+
+    return;
+  }
+
+  nsRect anchorRect = ComputeAnchorRect(rootPresContext, anchor);
+
+  // If the rectangles are different, move the popup.
+  if (!anchorRect.IsEqualEdges(aRect)) {
+    aRect = anchorRect;
+    SetPopupPosition(nullptr, true, false, true);
+  }
+}
--- a/layout/xul/nsMenuPopupFrame.h
+++ b/layout/xul/nsMenuPopupFrame.h
@@ -432,16 +432,23 @@ public:
     return false;
   }
 
   void ShowWithPositionedEvent() {
     mPopupState = ePopupPositioning;
     mShouldAutoPosition = true;
   }
 
+  // Checks for the anchor to change and either moves or hides the popup
+  // accordingly. The original position of the anchor should be supplied as
+  // the argument. If the popup needs to be hidden, HidePopup will be called by
+  // CheckForAnchorChange. If the popup needs to be moved, aRect will be updated
+  // with the new rectangle.
+  void CheckForAnchorChange(nsRect& aRect);
+
   // nsIReflowCallback
   virtual bool ReflowFinished() override;
   virtual void ReflowCallbackCanceled() override;
 
 protected:
 
   // returns the popup's level.
   nsPopupLevel PopupLevel(bool aIsNoAutoHide) const;
@@ -497,16 +504,20 @@ protected:
   //   aOffset - the amount by which the arrow must be slid such that it is
   //             still aligned with the anchor.
   // Result is the new size of the popup, which will typically be the same
   // as aSize, unless aSize is greater than the screen width/height.
   nscoord SlideOrResize(nscoord& aScreenPoint, nscoord aSize,
                         nscoord aScreenBegin, nscoord aScreenEnd,
                         nscoord *aOffset);
 
+  // Given an anchor frame, compute the anchor rectangle relative to the screen,
+  // using the popup frame's app units, and taking into account transforms.
+  nsRect ComputeAnchorRect(nsPresContext* aRootPresContext, nsIFrame* aAnchorFrame);
+
   // Move the popup to the position specified in its |left| and |top| attributes.
   void MoveToAttributePosition();
 
   /**
    * Return whether the popup direction should be RTL.
    * If the popup has an anchor, its direction is the anchor direction.
    * Otherwise, its the general direction of the UI.
    *
@@ -517,16 +528,27 @@ protected:
       ? mAnchorContent->GetPrimaryFrame()->StyleVisibility()->mDirection == NS_STYLE_DIRECTION_RTL
       : StyleVisibility()->mDirection == NS_STYLE_DIRECTION_RTL;
   }
 
   // Create a popup view for this frame. The view is added a child of the root
   // view, and is initially hidden.
   void CreatePopupView();
 
+  // Returns true if the popup should try to remain at the same relative
+  // location as the anchor while it is open. If the anchor becomes hidden
+  // either directly or indirectly because a parent popup or other element
+  // is no longer visible, or a parent deck page is changed, the popup hides
+  // as well. The second variation also sets the anchor rectangle, relative to
+  // the popup frame.
+  bool ShouldFollowAnchor();
+public:
+  bool ShouldFollowAnchor(nsRect& aRect);
+
+protected:
   nsString     mIncrementalString;  // for incremental typing navigation
 
   // the content that the popup is anchored to, if any, which may be in a
   // different document than the popup.
   nsCOMPtr<nsIContent> mAnchorContent;
 
   // the content that triggered the popup, typically the node where the mouse
   // was clicked. It will be cleared when the popup is hidden.
--- a/layout/xul/nsXULPopupManager.cpp
+++ b/layout/xul/nsXULPopupManager.cpp
@@ -105,16 +105,30 @@ void nsMenuChainItem::Detach(nsMenuChain
     // An item without a child should be the first item in the chain, so set
     // the first item pointer, pointed to by aRoot, to the parent.
     NS_ASSERTION(this == *aRoot, "Unexpected - popup with no child not at end of chain");
     *aRoot = mParent;
     SetParent(nullptr);
   }
 }
 
+void
+nsMenuChainItem::UpdateFollowAnchor()
+{
+  mFollowAnchor = mFrame->ShouldFollowAnchor(mCurrentRect);
+}
+
+void
+nsMenuChainItem::CheckForAnchorChange()
+{
+  if (mFollowAnchor) {
+    mFrame->CheckForAnchorChange(mCurrentRect);
+  }
+}
+
 bool nsXULPopupManager::sDevtoolsDisableAutoHide = false;
 
 const char* kPrefDevtoolsDisableAutoHide =
   "ui.popup.disable_autohide";
 
 NS_IMPL_ISUPPORTS(nsXULPopupManager,
                   nsIDOMEventListener,
                   nsITimerCallback,
@@ -939,16 +953,18 @@ nsXULPopupManager::ShowPopupCallback(nsI
     nsIContent* oldmenu = nullptr;
     if (mPopups)
       oldmenu = mPopups->Content();
     item->SetParent(mPopups);
     mPopups = item;
     SetCaptureState(oldmenu);
   }
 
+  item->UpdateFollowAnchor();
+
   if (aSelectFirstItem) {
     nsMenuFrame* next = GetNextMenuItem(aPopupFrame, nullptr, true, false);
     aPopupFrame->SetCurrentMenuItem(next);
   }
 
   if (ismenu)
     UpdateMenuItems(aPopup);
 
@@ -1221,24 +1237,16 @@ nsXULPopupManager::HidePopupCallback(nsI
       FirePopupHidingEvent(popupToHide, nextPopup, aLastPopup,
                            popupFrame->PresContext(),
                            foundMenu->PopupType(), aDeselectMenu, false);
     }
   }
 }
 
 void
-nsXULPopupManager::HidePopup(nsIFrame* aFrame)
-{
-  nsMenuPopupFrame* popup = do_QueryFrame(aFrame);
-  if (popup)
-    HidePopup(aFrame->GetContent(), false, true, false, false);
-}
-
-void
 nsXULPopupManager::HidePopupAfterDelay(nsMenuPopupFrame* aPopup)
 {
   // Don't close up immediately.
   // Kick off a close timer.
   KillMenuTimer();
 
   int32_t menuDelay =
     LookAndFeel::GetInt(LookAndFeel::eIntID_SubmenuDelay, 300); // ms
@@ -1361,16 +1369,51 @@ nsXULPopupManager::HidePopupsInDocShell(
     }
     item = parent;
   }
 
   HidePopupsInList(popupsToHide);
 }
 
 void
+nsXULPopupManager::UpdatePopupPositions(nsRefreshDriver* aRefreshDriver)
+{
+  if (!mPopups && !mNoHidePanels) {
+    return;
+  }
+
+  for (int32_t i = 0; i < 2; i++) {
+    nsMenuChainItem* item = i == 0 ? mPopups : mNoHidePanels;
+    while (item) {
+      if (item->Frame()->PresContext()->RefreshDriver() == aRefreshDriver) {
+        item->CheckForAnchorChange();
+      }
+
+      item = item->GetParent();
+    }
+  }
+}
+
+void
+nsXULPopupManager::UpdateFollowAnchor(nsMenuPopupFrame* aPopup)
+{
+  for (int32_t i = 0; i < 2; i++) {
+    nsMenuChainItem* item = i == 0 ? mPopups : mNoHidePanels;
+    while (item) {
+      if (item->Frame() == aPopup) {
+        item->UpdateFollowAnchor();
+        break;
+      }
+
+      item = item->GetParent();
+    }
+  }
+}
+
+void
 nsXULPopupManager::ExecuteMenu(nsIContent* aMenu, nsXULMenuCommandEvent* aEvent)
 {
   CloseMenuMode cmm = CloseMenuMode_Auto;
 
   static nsIContent::AttrValuesArray strings[] =
     {&nsGkAtoms::none, &nsGkAtoms::single, nullptr};
 
   switch (aMenu->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::closemenu,
--- a/layout/xul/nsXULPopupManager.h
+++ b/layout/xul/nsXULPopupManager.h
@@ -50,16 +50,17 @@
 class nsContainerFrame;
 class nsMenuFrame;
 class nsMenuPopupFrame;
 class nsMenuBarFrame;
 class nsMenuParent;
 class nsIDOMKeyEvent;
 class nsIDocShellTreeItem;
 class nsPIDOMWindowOuter;
+class nsRefreshDriver;
 
 // when a menu command is executed, the closemenu attribute may be used
 // to define how the menu should be closed up
 enum CloseMenuMode {
   CloseMenuMode_Auto, // close up the chain of menus, default value
   CloseMenuMode_None, // don't close up any menus
   CloseMenuMode_Single // close up only the menu the command is inside
 };
@@ -133,26 +134,34 @@ class nsMenuChainItem
 {
 private:
   nsMenuPopupFrame* mFrame; // the popup frame
   nsPopupType mPopupType; // the popup type of the frame
   bool mIsContext; // true for context menus
   bool mOnMenuBar; // true if the menu is on a menu bar
   nsIgnoreKeys mIgnoreKeys; // indicates how keyboard listeners should be used
 
+  // True if the popup should maintain its position relative to the anchor when
+  // the anchor moves.
+  bool mFollowAnchor;
+
+  // The last seen position of the anchor, relative to the screen.
+  nsRect mCurrentRect;
+
   nsMenuChainItem* mParent;
   nsMenuChainItem* mChild;
 
 public:
   nsMenuChainItem(nsMenuPopupFrame* aFrame, bool aIsContext, nsPopupType aPopupType)
     : mFrame(aFrame),
       mPopupType(aPopupType),
       mIsContext(aIsContext),
       mOnMenuBar(false),
       mIgnoreKeys(eIgnoreKeys_False),
+      mFollowAnchor(false),
       mParent(nullptr),
       mChild(nullptr)
   {
     NS_ASSERTION(aFrame, "null frame passed to nsMenuChainItem constructor");
     MOZ_COUNT_CTOR(nsMenuChainItem);
   }
 
   ~nsMenuChainItem()
@@ -166,22 +175,25 @@ public:
   bool IsMenu() { return mPopupType == ePopupTypeMenu; }
   bool IsContextMenu() { return mIsContext; }
   nsIgnoreKeys IgnoreKeys() { return mIgnoreKeys; }
   void SetIgnoreKeys(nsIgnoreKeys aIgnoreKeys) { mIgnoreKeys = aIgnoreKeys; }
   bool IsOnMenuBar() { return mOnMenuBar; }
   void SetOnMenuBar(bool aOnMenuBar) { mOnMenuBar = aOnMenuBar; }
   nsMenuChainItem* GetParent() { return mParent; }
   nsMenuChainItem* GetChild() { return mChild; }
+  bool FollowsAnchor() { return mFollowAnchor; }
+  void UpdateFollowAnchor();
+  void CheckForAnchorChange();
 
   // set the parent of this item to aParent, also changing the parent
   // to have this as a child.
   void SetParent(nsMenuChainItem* aParent);
 
-  // removes an item from the chain. The root pointer must be supplied in case
+  // Removes an item from the chain. The root pointer must be supplied in case
   // the item is the first item in the chain in which case the pointer will be
   // set to the next item, or null if there isn't another item. After detaching,
   // this item will not have a parent or a child.
   void Detach(nsMenuChainItem** aRoot);
 };
 
 // this class is used for dispatching popupshowing events asynchronously.
 class nsXULPopupShowingEvent : public mozilla::Runnable
@@ -495,22 +507,16 @@ public:
   void HidePopup(nsIContent* aPopup,
                  bool aHideChain,
                  bool aDeselectMenu,
                  bool aAsynchronous,
                  bool aIsCancel,
                  nsIContent* aLastPopup = nullptr);
 
   /**
-   * Hide the popup aFrame. This method is called by the view manager when the
-   * close button is pressed.
-   */
-  void HidePopup(nsIFrame* aFrame);
-
-  /**
    * Hide a popup after a short delay. This is used when rolling over menu items.
    * This timer is stored in mCloseTimer. The timer may be cancelled and the popup
    * closed by calling KillMenuTimer.
    */
   void HidePopupAfterDelay(nsMenuPopupFrame* aPopup);
 
   /**
    * Hide all of the popups from a given docshell. This should be called when the
@@ -522,16 +528,29 @@ public:
    * Enable or disable the dynamic noautohide state of a panel.
    *
    * aPanel - the panel whose state is to change
    * aShouldRollup - whether the panel is no longer noautohide
    */
   void EnableRollup(nsIContent* aPopup, bool aShouldRollup);
 
   /**
+   * Check if any popups need to be repositioned or hidden after a style or
+   * layout change. This will update, for example, any arrow type panels when
+   * the anchor that is is pointing to has moved, resized or gone away.
+   * Only those popups that pertain to the supplied aRefreshDriver are updated.
+   */
+  void UpdatePopupPositions(nsRefreshDriver* aRefreshDriver);
+
+  /**
+   * Enable or disable anchor following on the popup if needed.
+   */
+  void UpdateFollowAnchor(nsMenuPopupFrame* aPopup);
+
+  /**
    * Execute a menu command from the triggering event aEvent.
    *
    * aMenu - a menuitem to execute
    * aEvent - an nsXULMenuCommandEvent that contains all the info from the mouse
    *          event which triggered the menu to be executed, may not be null
    */
   void ExecuteMenu(nsIContent* aMenu, nsXULMenuCommandEvent* aEvent);
 
--- a/toolkit/content/tests/chrome/chrome.ini
+++ b/toolkit/content/tests/chrome/chrome.ini
@@ -24,16 +24,17 @@ support-files =
   frame_subframe_origin_subframe2.xul
   popup_childframe_node.xul
   popup_trigger.js
   sample_entireword_latin1.html
   window_browser_drop.xul
   window_keys.xul
   window_largemenu.xul
   window_panel.xul
+  window_panel_anchoradjust.xul
   window_popup_anchor.xul
   window_popup_anchoratrect.xul
   window_popup_attribute.xul
   window_popup_button.xul
   window_popup_preventdefault_chrome.xul
   window_preferences.xul
   window_preferences2.xul
   window_preferences3.xul
@@ -120,16 +121,17 @@ skip-if = os == 'linux' && !debug #Bug 1
 [test_menulist.xul]
 [test_menulist_keynav.xul]
 [test_menulist_null_value.xul]
 [test_menulist_paging.xul]
 [test_menulist_position.xul]
 [test_mousescroll.xul]
 [test_notificationbox.xul]
 [test_panel.xul]
+[test_panel_anchoradjust.xul]
 [test_panelfrommenu.xul]
 [test_popup_anchor.xul]
 [test_popup_anchoratrect.xul]
 skip-if = os == 'linux' # 1167694
 [test_popup_attribute.xul]
 skip-if = os == 'linux' && asan #Bug 1131634
 [test_popup_button.xul]
 skip-if = os == 'linux' && asan # Bug 1281360
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/chrome/test_panel_anchoradjust.xul
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Test Panel Position When Anchor Changes"
+        onload="runTest()"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+<script>
+SimpleTest.waitForExplicitFinish();
+function runTest()
+{
+  window.open("window_panel_anchoradjust.xul", "_blank", "chrome,left=200,top=200,width=200,height=200");
+}
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<p id="display">
+</p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+
+</window>
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/chrome/window_panel_anchoradjust.xul
@@ -0,0 +1,183 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window width="200" height="200"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+<deck id="deck">
+  <hbox id="container">
+    <button id="anchor" label="Anchor"/>
+  </hbox>
+  <button id="anchor3" label="Anchor3"/>
+</deck>
+
+<hbox id="container2">
+  <button id="anchor2" label="Anchor2"/>
+</hbox>
+
+<button id="anchor4" label="Anchor4"/>
+
+<panel id="panel" type="arrow">
+  <button label="OK"/>
+</panel>
+
+<menupopup id="menupopup">
+  <menuitem label="One"/>
+  <menuitem id="menuanchor" label="Two"/>
+  <menuitem label="Three"/>
+</menupopup>
+
+<script type="application/javascript"><![CDATA[
+
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+SimpleTest.waitForExplicitFinish();
+
+function next()
+{
+  return new Promise(r => SimpleTest.executeSoon(r));
+}
+
+function waitForPanel(panel, event)  
+{
+  return new Promise(resolve => {
+    panel.addEventListener(event, () => { resolve(); }, { once: true });
+  });
+}
+
+function isWithinHalfPixel(a, b)
+{
+  return Math.abs(a - b) <= 0.5;
+}
+
+function runTests() {
+  Task.spawn(function*() {
+    let panel = document.getElementById("panel");
+    let anchor = document.getElementById("anchor");
+
+    let popupshown = waitForPanel(panel, "popupshown");
+    panel.openPopup(anchor, "after_start");
+    yield popupshown;
+
+    let anchorrect = anchor.getBoundingClientRect();
+    let panelrect = panel.getBoundingClientRect();
+    let xarrowdiff = panelrect.left - anchorrect.left;
+
+    // When the anchor is moved in some manner, the panel should be adjusted
+    let popuppositioned = waitForPanel(panel, "popuppositioned");
+    document.getElementById("anchor").style.marginLeft = "50px"
+    yield popuppositioned;
+
+    anchorrect = anchor.getBoundingClientRect();
+    panelrect = panel.getBoundingClientRect();
+    ok(isWithinHalfPixel(anchorrect.left, panelrect.left - xarrowdiff), "anchor moved x");
+    ok(isWithinHalfPixel(anchorrect.bottom, panelrect.top), "anchor moved y");
+
+    // moveToAnchor is used to change the anchor
+    let anchor2 = document.getElementById("anchor2");
+    popuppositioned = waitForPanel(panel, "popuppositioned");
+    panel.moveToAnchor(anchor2, "after_end");
+    yield popuppositioned;
+
+    let anchor2rect = anchor2.getBoundingClientRect();
+    panelrect = panel.getBoundingClientRect();
+    ok(isWithinHalfPixel(anchor2rect.right, panelrect.right + xarrowdiff), "new anchor x");
+    ok(isWithinHalfPixel(anchor2rect.bottom, panelrect.top), "new anchor y");
+
+    // moveToAnchor is used to change the anchor with an x and y offset
+    popuppositioned = waitForPanel(panel, "popuppositioned");
+    panel.moveToAnchor(anchor2, "after_end", 7, 9);
+    yield popuppositioned;
+
+    anchor2rect = anchor2.getBoundingClientRect();
+    panelrect = panel.getBoundingClientRect();
+    ok(isWithinHalfPixel(anchor2rect.right + 7, panelrect.right + xarrowdiff), "new anchor with offset x");
+    ok(isWithinHalfPixel(anchor2rect.bottom + 9, panelrect.top), "new anchor with offset y");
+
+    // When the container of the anchor is collapsed, the panel should be hidden.
+    let popuphidden = waitForPanel(panel, "popuphidden");
+    anchor2.parentNode.collapsed = true;
+    yield popuphidden;
+
+    popupshown = waitForPanel(panel, "popupshown");
+    panel.openPopup(anchor, "after_start");
+    yield popupshown;
+
+    // When the deck containing the anchor changes to a different page, the panel should be hidden.
+    popuphidden = waitForPanel(panel, "popuphidden");
+    document.getElementById("deck").selectedIndex = 1;
+    yield popuphidden;
+
+    let anchor3 = document.getElementById("anchor3");
+    popupshown = waitForPanel(panel, "popupshown");
+    panel.openPopup(anchor3, "after_start");
+    yield popupshown;
+
+    // When the anchor is hidden; the panel should be hidden.
+    popuphidden = waitForPanel(panel, "popuphidden");
+    anchor3.parentNode.hidden = true;
+    yield popuphidden;
+
+    // When the panel is anchored to an element in a popup, the panel should
+    // also be hidden when that popup is hidden.
+    let menupopup = document.getElementById("menupopup");
+    popupshown = waitForPanel(menupopup, "popupshown");
+    menupopup.openPopupAtScreen(200, 200);
+    yield popupshown;
+
+    popupshown = waitForPanel(panel, "popupshown");
+    panel.openPopup(document.getElementById("menuanchor"), "after_start");
+    yield popupshown;
+
+    popuphidden = waitForPanel(panel, "popuphidden");
+    menupopuphidden = waitForPanel(menupopup, "popuphidden");
+    menupopup.hidePopup();
+    yield popuphidden;
+    yield menupopuphidden;
+
+    // The panel should no longer follow anchors.
+    panel.setAttribute("followanchor", "false");
+
+    let anchor4 = document.getElementById("anchor4");
+    popupshown = waitForPanel(panel, "popupshown");
+    panel.openPopup(anchor4, "after_start");
+    yield popupshown;
+
+    let anchor4rect = anchor4.getBoundingClientRect();
+    panelrect = panel.getBoundingClientRect();
+
+    document.getElementById("anchor4").style.marginLeft = "50px"
+    yield next();
+
+    panelrect = panel.getBoundingClientRect();
+    ok(isWithinHalfPixel(anchor4rect.left, panelrect.left - xarrowdiff), "no follow anchor x");
+    ok(isWithinHalfPixel(anchor4rect.bottom, panelrect.top), "no follow anchor y");
+
+    popuphidden = waitForPanel(panel, "popuphidden");
+    panel.hidePopup();
+    yield popuphidden;
+
+    window.close();
+    window.opener.wrappedJSObject.SimpleTest.finish();
+  });
+}
+
+function ok(condition, message) {
+  window.opener.wrappedJSObject.SimpleTest.ok(condition, message);
+}
+
+function is(left, right, message) {
+  window.opener.wrappedJSObject.SimpleTest.is(left, right, message);
+}
+
+window.opener.wrappedJSObject.SimpleTest.waitForFocus(runTests, window);
+
+]]>
+</script>
+
+</window>