Bug 1206133 - Add popuppositioning state and popuppositioned event to improve arrow panel position handling. r=enndeakin
authorKirk Steuber <ksteuber@mozilla.com>
Tue, 16 Aug 2016 15:33:05 -0700
changeset 315936 bca4534616adca94e6e9b5dd85f18919768e174a
parent 315935 c0be513c03c8d04eb3a8fbe24bb11e3bbd01621f
child 315937 9401602eabf337e087f9723eba1d5d1869d7ba6c
push id20634
push usercbook@mozilla.com
push dateFri, 30 Sep 2016 10:10:13 +0000
treeherderfx-team@afe79b010d13 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersenndeakin
bugs1206133
milestone52.0a1
Bug 1206133 - Add popuppositioning state and popuppositioned event to improve arrow panel position handling. r=enndeakin MozReview-Commit-ID: Dh1npORCQ6J
browser/base/content/browser-places.js
browser/components/places/content/editBookmarkOverlay.js
dom/base/nsGkAtomList.h
dom/events/EventNameList.h
layout/xul/PopupBoxObject.cpp
layout/xul/nsMenuPopupFrame.cpp
layout/xul/nsMenuPopupFrame.h
layout/xul/nsXULPopupManager.cpp
layout/xul/nsXULPopupManager.h
toolkit/content/tests/widgets/test_popupanchor.xul
toolkit/content/widgets/popup.xml
toolkit/modules/PopupNotifications.jsm
widget/EventMessageList.h
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -255,22 +255,36 @@ var StarUI = {
         }
         parent = parent.parentNode;
       }
       if (parent) {
         this._anchorToolbarButton = parent;
         parent.setAttribute("open", "true");
       }
     }
-    this.panel.openPopup(aAnchorElement, aPosition);
+    let panel = this.panel;
+    let target = panel;
+    if (target.parentNode) {
+      // By targeting the panel's parent and using a capturing listener, we
+      // can have our listener called before others waiting for the panel to
+      // be shown (which probably expect the panel to be fully initialized)
+      target = target.parentNode;
+    }
+    target.addEventListener("popupshown", function shownListener(event) {
+      if (event.target == panel) {
+        target.removeEventListener("popupshown", shownListener, true);
 
-    gEditItemOverlay.initPanel({ node: aNode
-                               , hiddenRows: ["description", "location",
-                                              "loadInSidebar", "keyword"]
-                               , focusedElement: "preferred" });
+        gEditItemOverlay.initPanel({ node: aNode
+                                   , hiddenRows: ["description", "location",
+                                                  "loadInSidebar", "keyword"]
+                                   , focusedElement: "preferred"});
+      }
+    }, true);
+
+    this.panel.openPopup(aAnchorElement, aPosition);
   }),
 
   panelShown:
   function SU_panelShown(aEvent) {
     if (aEvent.target == this.panel) {
       if (this._element("editBookmarkPanelContent").hidden) {
         // Note this isn't actually used anymore, we should remove this
         // once we decide not to bring back the page bookmarked notification
--- a/browser/components/places/content/editBookmarkOverlay.js
+++ b/browser/components/places/content/editBookmarkOverlay.js
@@ -172,16 +172,28 @@ var gEditItemOverlay = {
    *          - hiddenRows (Strings array): list of rows to be hidden regardless
    *            of the item edited. Possible values: "title", "location",
    *            "description", "keyword", "loadInSidebar", "feedLocation",
    *            "siteLocation", folderPicker"
    */
   initPanel(aInfo) {
     if (typeof(aInfo) != "object" || aInfo === null)
       throw new Error("aInfo must be an object.");
+    if ("node" in aInfo) {
+      try {
+        aInfo.node.type;
+      } catch (e) {
+        // If the lazy loader for |type| generates an exception, it means that
+        // this bookmark could not be loaded. This sometimes happens when tests
+        // create a bookmark by clicking the bookmark star, then try to cleanup
+        // before the bookmark panel has finished opening. Either way, if we
+        // cannot retrieve the bookmark information, we cannot open the panel.
+        return;
+      }
+    }
 
     // For sanity ensure that the implementer has uninited the panel before
     // trying to init it again, or we could end up leaking due to observers.
     if (this.initialized)
       this.uninitPanel(false);
 
     let { itemId, itemGuid, isItem,
           isURI, uri, title,
--- a/dom/base/nsGkAtomList.h
+++ b/dom/base/nsGkAtomList.h
@@ -879,16 +879,17 @@ GK_ATOM(onpairingconsentreq, "onpairingc
 GK_ATOM(onpaste, "onpaste")
 GK_ATOM(onpendingchange, "onpendingchange")
 GK_ATOM(onpichange, "onpichange")
 GK_ATOM(onpicture, "onpicture")
 GK_ATOM(onpointerlockchange, "onpointerlockchange")
 GK_ATOM(onpointerlockerror, "onpointerlockerror")
 GK_ATOM(onpopuphidden, "onpopuphidden")
 GK_ATOM(onpopuphiding, "onpopuphiding")
+GK_ATOM(onpopuppositioned, "onpopuppositioned")
 GK_ATOM(onpopupshowing, "onpopupshowing")
 GK_ATOM(onpopupshown, "onpopupshown")
 GK_ATOM(onposter, "onposter")
 GK_ATOM(onpreviewstatechange, "onpreviewstatechange")
 GK_ATOM(onpullphonebookreq, "onpullphonebookreq")
 GK_ATOM(onpullvcardentryreq, "onpullvcardentryreq")
 GK_ATOM(onpullvcardlistingreq, "onpullvcardlistingreq")
 GK_ATOM(onpush, "onpush")
--- a/dom/events/EventNameList.h
+++ b/dom/events/EventNameList.h
@@ -776,16 +776,20 @@ NON_IDL_EVENT(close,
 NON_IDL_EVENT(popupshowing,
               eXULPopupShowing,
               EventNameType_XUL,
               eBasicEventClass)
 NON_IDL_EVENT(popupshown,
               eXULPopupShown,
               EventNameType_XUL,
               eBasicEventClass)
+NON_IDL_EVENT(popuppositioned,
+              eXULPopupPositioned,
+              EventNameType_XUL,
+              eBasicEventClass)
 NON_IDL_EVENT(popuphiding,
               eXULPopupHiding,
               EventNameType_XUL,
               eBasicEventClass)
 NON_IDL_EVENT(popuphidden,
               eXULPopupHidden,
               EventNameType_XUL,
               eBasicEventClass)
--- a/layout/xul/PopupBoxObject.cpp
+++ b/layout/xul/PopupBoxObject.cpp
@@ -227,16 +227,17 @@ PopupBoxObject::GetPopupState(nsString& 
 
   nsMenuPopupFrame *menuPopupFrame = mContent ? do_QueryFrame(mContent->GetPrimaryFrame()) : nullptr;
   if (menuPopupFrame) {
     switch (menuPopupFrame->PopupState()) {
       case ePopupShown:
         aState.AssignLiteral("open");
         break;
       case ePopupShowing:
+      case ePopupPositioning:
       case ePopupOpening:
       case ePopupVisible:
         aState.AssignLiteral("showing");
         break;
       case ePopupHiding:
       case ePopupInvisible:
         aState.AssignLiteral("hiding");
         break;
--- a/layout/xul/nsMenuPopupFrame.cpp
+++ b/layout/xul/nsMenuPopupFrame.cpp
@@ -431,17 +431,17 @@ nsMenuPopupFrame::LayoutPopup(nsBoxLayou
 
   SchedulePaint();
 
   bool shouldPosition = true;
   bool isOpen = IsOpen();
   if (!isOpen) {
     // if the popup is not open, only do layout while showing or if the menu
     // is sized to the popup
-    shouldPosition = (mPopupState == ePopupShowing);
+    shouldPosition = (mPopupState == ePopupShowing || mPopupState == ePopupPositioning);
     if (!shouldPosition && !aSizedToPopup) {
       RemoveStateBits(NS_FRAME_FIRST_REFLOW);
       return;
     }
   }
 
   // if the popup has just been opened, make sure the scrolled window is at 0,0
   // Don't scroll menulists as they will scroll to their selected item on their own.
@@ -471,17 +471,17 @@ nsMenuPopupFrame::LayoutPopup(nsBoxLayou
   bool sizeChanged = (mPrefSize != prefSize);
   if (sizeChanged) {
     SetXULBounds(aState, nsRect(0, 0, prefSize.width, prefSize.height), false);
     mPrefSize = prefSize;
   }
 
   bool needCallback = false;
   if (shouldPosition) {
-    SetPopupPosition(aAnchor, false, aSizedToPopup);
+    SetPopupPosition(aAnchor, false, aSizedToPopup, mPopupState == ePopupPositioning);
     needCallback = true;
   }
 
   nsRect bounds(GetRect());
   XULLayout(aState);
 
   // if the width or height changed, readjust the popup position. This is a
   // special case for tooltips where the preferred height doesn't include the
@@ -497,17 +497,17 @@ nsMenuPopupFrame::LayoutPopup(nsBoxLayou
       if (isOpen) {
         rePosition = true;
         needCallback = true;
       }
     }
   }
 
   if (rePosition) {
-    SetPopupPosition(aAnchor, false, aSizedToPopup);
+    SetPopupPosition(aAnchor, false, aSizedToPopup, false);
   }
 
   nsPresContext* pc = PresContext();
   nsView* view = GetView();
 
   if (sizeChanged) {
     // If the size of the popup changed, apply any size constraints.
     nsIWidget* widget = view->GetWidget();
@@ -556,17 +556,17 @@ nsMenuPopupFrame::LayoutPopup(nsBoxLayou
     pc->PresShell()->PostReflowCallback(this);
     mReflowCallbackData.MarkPosted(aAnchor, aSizedToPopup);
   }
 }
 
 bool
 nsMenuPopupFrame::ReflowFinished()
 {
-  SetPopupPosition(mReflowCallbackData.mAnchor, false, mReflowCallbackData.mSizedToPopup);
+  SetPopupPosition(mReflowCallbackData.mAnchor, false, mReflowCallbackData.mSizedToPopup, false);
 
   mReflowCallbackData.Clear();
 
   return false;
 }
 
 void
 nsMenuPopupFrame::ReflowCallbackCanceled()
@@ -860,17 +860,17 @@ nsMenuPopupFrame::InitializePopupWithAnc
 
 void
 nsMenuPopupFrame::ShowPopup(bool aIsContextMenu)
 {
   mIsContextMenu = aIsContextMenu;
 
   InvalidateFrameSubtree();
 
-  if (mPopupState == ePopupShowing) {
+  if (mPopupState == ePopupShowing || mPopupState == ePopupPositioning) {
     mPopupState = ePopupOpening;
     mIsOpenChanged = true;
 
     // Clear mouse capture when a popup is opened.
     if (mPopupType == ePopupTypeMenu) {
       nsIPresShell::SetCapturingContent(nullptr, 0);
     }
 
@@ -901,17 +901,18 @@ void
 nsMenuPopupFrame::HidePopup(bool aDeselectMenu, nsPopupState aNewState)
 {
   NS_ASSERTION(aNewState == ePopupClosed || aNewState == ePopupInvisible,
                "popup being set to unexpected state");
 
   ClearPopupShownDispatcher();
 
   // don't hide the popup when it isn't open
-  if (mPopupState == ePopupClosed || mPopupState == ePopupShowing)
+  if (mPopupState == ePopupClosed || mPopupState == ePopupShowing ||
+      mPopupState == ePopupPositioning)
     return;
 
   // clear the trigger content if the popup is being closed. But don't clear
   // it if the popup is just being made invisible as a popuphiding or command
   // event may want to retrieve it.
   if (aNewState == ePopupClosed) {
     // if the popup had a trigger node set, clear the global window popup node
     // as well
@@ -1179,16 +1180,18 @@ nsMenuPopupFrame::SlideOrResize(nscoord&
 nscoord
 nsMenuPopupFrame::FlipOrResize(nscoord& aScreenPoint, nscoord aSize, 
                                nscoord aScreenBegin, nscoord aScreenEnd,
                                nscoord aAnchorBegin, nscoord aAnchorEnd,
                                nscoord aMarginBegin, nscoord aMarginEnd,
                                nscoord aOffsetForContextMenu, FlipStyle aFlip,
                                bool* aFlipSide)
 {
+  *aFlipSide = false;
+
   // all of the coordinates used here are in app units relative to the screen
   nscoord popupSize = aSize;
   if (aScreenPoint < aScreenBegin) {
     // at its current position, the popup would extend past the left or top
     // edge of the screen, so it will have to be moved or resized.
     if (aFlip) {
       // for inside flips, we flip on the opposite side of the anchor
       nscoord startpos = aFlip == FlipStyle_Outside ? aAnchorBegin : aAnchorEnd;
@@ -1279,17 +1282,17 @@ nsMenuPopupFrame::FlipOrResize(nscoord& 
   // 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);
 }
 
 nsresult
-nsMenuPopupFrame::SetPopupPosition(nsIFrame* aAnchorFrame, bool aIsMove, bool aSizedToPopup)
+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.
   if (aIsMove && (mPrefSize.width == -1 || mPrefSize.height == -1)) {
     return NS_OK;
@@ -1584,16 +1587,27 @@ nsMenuPopupFrame::SetPopupPosition(nsIFr
   nsBoxFrame::SetPosition(viewPoint - GetParent()->GetOffsetTo(rootFrame));
 
   if (aSizedToPopup) {
     nsBoxLayoutState state(PresContext());
     // XXXndeakin can parentSize.width still extend outside?
     SetXULBounds(state, mRect);
   }
 
+  // If the popup is in the positioned state or if it is shown and the position
+  // or size changed, dispatch a popuppositioned event if the popup wants it.
+  nsIntRect newRect(screenPoint.x, screenPoint.y, mRect.width, mRect.height);
+  if (mPopupState == ePopupPositioning ||
+      (mPopupState == ePopupShown && !newRect.IsEqualEdges(mUsedScreenRect))) {
+    mUsedScreenRect = newRect;
+    if (aNotify) {
+      nsXULPopupPositionedEvent::DispatchIfNeeded(mContent, false, false);
+    }
+  }
+
   return NS_OK;
 }
 
 /* virtual */ nsMenuFrame*
 nsMenuPopupFrame::GetCurrentMenuItem()
 {
   return mCurrentMenu;
 }
@@ -2265,17 +2279,17 @@ nsMenuPopupFrame::MoveTo(const CSSIntPoi
                      LookAndFeel::eIntID_ContextMenuOffsetVertical));
   }
 
   nsPresContext* presContext = PresContext();
   mAnchorType = MenuPopupAnchorType_Point;
   mScreenRect.x = aPos.x - presContext->AppUnitsToIntCSSPixels(margin.left);
   mScreenRect.y = aPos.y - presContext->AppUnitsToIntCSSPixels(margin.top);
 
-  SetPopupPosition(nullptr, true, false);
+  SetPopupPosition(nullptr, true, false, true);
 
   nsCOMPtr<nsIContent> popup = mContent;
   if (aUpdateAttrs && (popup->HasAttr(kNameSpaceID_None, nsGkAtoms::left) ||
                        popup->HasAttr(kNameSpaceID_None, nsGkAtoms::top)))
   {
     nsAutoString left, top;
     left.AppendInt(aPos.x);
     top.AppendInt(aPos.y);
@@ -2294,17 +2308,17 @@ nsMenuPopupFrame::MoveToAnchor(nsIConten
 
   nsPopupState oldstate = mPopupState;
   InitializePopup(aAnchorContent, mTriggerContent, aPosition,
                   aXPos, aYPos, MenuPopupAnchorType_Node, aAttributesOverride);
   // InitializePopup changed the state so reset it.
   mPopupState = oldstate;
 
   // Pass false here so that flipping and adjusting to fit on the screen happen.
-  SetPopupPosition(nullptr, false, false);
+  SetPopupPosition(nullptr, false, false, true);
 }
 
 bool
 nsMenuPopupFrame::GetAutoPosition()
 {
   return mShouldAutoPosition;
 }
 
--- a/layout/xul/nsMenuPopupFrame.h
+++ b/layout/xul/nsMenuPopupFrame.h
@@ -43,16 +43,18 @@ class nsIWidget;
 //                  the popup is removed.
 //   ePopupClosed - the popup's widget is made invisible.
 enum nsPopupState {
   // state when a popup is not open
   ePopupClosed,
   // state from when a popup is requested to be shown to after the
   // popupshowing event has been fired.
   ePopupShowing,
+  // state while a popup is waiting to be laid out and positioned
+  ePopupPositioning,
   // state while a popup is open but the widget is not yet visible
   ePopupOpening,
   // state while a popup is visible and waiting for the popupshown event
   ePopupVisible,
   // state while a popup is open and visible on screen
   ePopupShown,
   // state from when a popup is requested to be hidden to when it is closed.
   ePopupHiding,
@@ -246,18 +248,20 @@ public:
                    nsIFrame* aAnchor, bool aSizedToPopup);
 
   nsView* GetRootViewForPopup(nsIFrame* aStartFrame);
 
   // Set the position of the popup either relative to the anchor aAnchorFrame
   // (or the frame for mAnchorContent if aAnchorFrame is null), anchored at a
   // rectangle, or at a specific point if a screen position is set. The popup
   // will be adjusted so that it is on screen. If aIsMove is true, then the
-  // popup is being moved, and should not be flipped.
-  nsresult SetPopupPosition(nsIFrame* aAnchorFrame, bool aIsMove, bool aSizedToPopup);
+  // popup is being moved, and should not be flipped. If aNotify is true, then
+  // a popuppositioned event is sent.
+  nsresult SetPopupPosition(nsIFrame* aAnchorFrame, bool aIsMove,
+                            bool aSizedToPopup, bool aNotify);
 
   bool HasGeneratedChildren() { return mGeneratedChildren; }
   void SetGeneratedChildren() { mGeneratedChildren = true; }
 
   // called when the Enter key is pressed while the popup is open. This will
   // just pass the call down to the current menu, if any. If a current menu
   // should be opened as a result, this method should return the frame for
   // that menu, or null if no menu should be opened. Also, calling Enter will
@@ -420,16 +424,21 @@ public:
       mPopupShownDispatcher->CancelListener();
       mPopupShownDispatcher = nullptr;
       return true;
     }
 
     return false;
   }
 
+  void ShowWithPositionedEvent() {
+    mPopupState = ePopupPositioning;
+    mShouldAutoPosition = true;
+  }
+
   // nsIReflowCallback
   virtual bool ReflowFinished() override;
   virtual void ReflowCallbackCanceled() override;
 
 protected:
 
   // returns the popup's level.
   nsPopupLevel PopupLevel(bool aIsNoAutoHide) const;
@@ -519,16 +528,19 @@ protected:
   // the content that triggered the popup, typically the node where the mouse
   // was clicked. It will be cleared when the popup is hidden.
   nsCOMPtr<nsIContent> mTriggerContent;
 
   nsMenuFrame* mCurrentMenu; // The current menu that is active.
 
   RefPtr<nsXULPopupShownEvent> mPopupShownDispatcher;
 
+  // The popup's screen rectangle in app units.
+  nsIntRect mUsedScreenRect;
+
   // A popup's preferred size may be different than its actual size stored in
   // mRect in the case where the popup was resized because it was too large
   // for the screen. The preferred size mPrefSize holds the full size the popup
   // would be before resizing. Computations are performed using this size.
   nsSize mPrefSize;
 
   // The position of the popup, in CSS pixels.
   // The screen coordinates, if set to values other than -1,
--- a/layout/xul/nsXULPopupManager.cpp
+++ b/layout/xul/nsXULPopupManager.cpp
@@ -439,17 +439,17 @@ nsXULPopupManager::AdjustPopupsOnWindowC
         }
       }
     }
 
     item = item->GetParent();
   }
 
   for (int32_t l = list.Length() - 1; l >= 0; l--) {
-    list[l]->SetPopupPosition(nullptr, true, false);
+    list[l]->SetPopupPosition(nullptr, true, false, true);
   }
 }
 
 void nsXULPopupManager::AdjustPopupsOnWindowChange(nsIPresShell* aPresShell)
 {
   if (aPresShell->GetDocument()) {
     AdjustPopupsOnWindowChange(aPresShell->GetDocument()->GetWindow());
   }
@@ -495,17 +495,17 @@ nsXULPopupManager::PopupMoved(nsIFrame* 
   }
 
   // Update the popup's position using SetPopupPosition if the popup is
   // anchored and at the parent level as these maintain their position
   // relative to the parent window. Otherwise, just update the popup to
   // the specified screen coordinates.
   if (menuPopupFrame->IsAnchored() &&
       menuPopupFrame->PopupLevel() == ePopupLevelParent) {
-    menuPopupFrame->SetPopupPosition(nullptr, true, false);
+    menuPopupFrame->SetPopupPosition(nullptr, true, false, true);
   }
   else {
     CSSPoint cssPos = LayoutDeviceIntPoint::FromUnknownPoint(aPnt)
                     / menuPopupFrame->PresContext()->CSSToDevPixelScale();
     menuPopupFrame->MoveTo(RoundedToInt(cssPos), false);
   }
 }
 
@@ -1033,16 +1033,34 @@ nsXULPopupManager::HidePopup(nsIContent*
     // entire chain or the item to hide isn't the topmost popup.
     if (parent && (aHideChain || topMenu != foundMenu))
       nextPopup = parent->Content();
 
     lastPopup = aLastPopup ? aLastPopup : (aHideChain ? nullptr : aPopup);
   }
   else if (foundPanel) {
     popupToHide = aPopup;
+  } else {
+    // When the popup is in the popuppositioning state, it will not be in the
+    // mPopups list. We need another way to find it and make sure it does not
+    // continue the popup showing process.
+    popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
+    if (popupFrame) {
+      if (popupFrame->PopupState() == ePopupPositioning) {
+        // Do basically the same thing we would have done if we had found the
+        // popup in the mPopups list.
+        deselectMenu = aDeselectMenu;
+        popupToHide = aPopup;
+        type = popupFrame->PopupType();
+      } else {
+        // The popup is not positioning. If we were supposed to have handled
+        // closing it, it should have been in mPopups or mNoHidePanels
+        popupFrame = nullptr;
+      }
+    }
   }
 
   if (popupFrame) {
     nsPopupState state = popupFrame->PopupState();
     // if the popup is already being hidden, don't attempt to hide it again
     if (state == ePopupHiding)
       return;
     // change the popup state to hiding. Don't set the hiding state if the
@@ -1489,17 +1507,29 @@ nsXULPopupManager::FirePopupShowingEvent
   if (popupFrame) {
     // if the event was cancelled, don't open the popup, reset its state back
     // to closed and clear its trigger content.
     if (status == nsEventStatus_eConsumeNoDefault) {
       popupFrame->SetPopupState(ePopupClosed);
       popupFrame->ClearTriggerContent();
     }
     else {
-      ShowPopupCallback(aPopup, popupFrame, aIsContextMenu, aSelectFirstItem);
+      // Now check if we need to fire the popuppositioned event. If not, call
+      // ShowPopupCallback directly.
+
+      // The popuppositioned event only fires on arrow panels for now.
+      if (popup->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+                          nsGkAtoms::arrow, eCaseMatters)) {
+        popupFrame->ShowWithPositionedEvent();
+        presShell->FrameNeedsReflow(popupFrame, nsIPresShell::eTreeChange,
+                                    NS_FRAME_HAS_DIRTY_CHILDREN);
+      }
+      else {
+        ShowPopupCallback(popup, popupFrame, aIsContextMenu, aSelectFirstItem);
+      }
     }
   }
 }
 
 void
 nsXULPopupManager::FirePopupHidingEvent(nsIContent* aPopup,
                                         nsIContent* aNextPopup,
                                         nsIContent* aLastPopup,
@@ -1719,17 +1749,18 @@ nsXULPopupManager::MayShowPopup(nsMenuPo
   NS_ASSERTION(!aPopup->IsOpen() || IsPopupOpen(aPopup->GetContent()),
                "popup frame state doesn't match XULPopupManager open state");
 
   nsPopupState state = aPopup->PopupState();
 
   // if the popup is not in the open popup chain, then it must have a state that
   // is either closed, in the process of being shown, or invisible.
   NS_ASSERTION(IsPopupOpen(aPopup->GetContent()) || state == ePopupClosed ||
-               state == ePopupShowing || state == ePopupInvisible,
+               state == ePopupShowing || state == ePopupPositioning ||
+               state == ePopupInvisible,
                "popup not in XULPopupManager open list is open");
 
   // don't show popups unless they are closed or invisible
   if (state != ePopupClosed && state != ePopupInvisible)
     return false;
 
   // Don't show popups that we already have in our popup chain
   if (IsPopupOpen(aPopup->GetContent())) {
@@ -2700,16 +2731,71 @@ nsXULPopupHidingEvent::Run()
                                  context, mPopupType, mDeselectMenu, mIsRollup);
       }
     }
   }
 
   return NS_OK;
 }
 
+bool
+nsXULPopupPositionedEvent::DispatchIfNeeded(nsIContent *aPopup,
+                                            bool aIsContextMenu,
+                                            bool aSelectFirstItem)
+{
+  // The popuppositioned event only fires on arrow panels for now.
+  if (aPopup->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+                          nsGkAtoms::arrow, eCaseMatters)) {
+    nsCOMPtr<nsIRunnable> event =
+      new nsXULPopupPositionedEvent(aPopup, aIsContextMenu, aSelectFirstItem);
+    NS_DispatchToCurrentThread(event);
+
+    return true;
+  }
+
+  return false;
+}
+
+NS_IMETHODIMP
+nsXULPopupPositionedEvent::Run()
+{
+  nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+  if (pm) {
+    nsMenuPopupFrame* popupFrame = do_QueryFrame(mPopup->GetPrimaryFrame());
+    if (popupFrame) {
+      // At this point, hidePopup may have been called but it currently has no
+      // way to stop this event. However, if hidePopup was called, the popup
+      // will now be in the hiding or closed state. If we are in the shown or
+      // positioning state instead, we can assume that we are still clear to
+      // open/move the popup
+      nsPopupState state = popupFrame->PopupState();
+      if (state != ePopupPositioning && state != ePopupShown) {
+        return NS_OK;
+      }
+      nsEventStatus status = nsEventStatus_eIgnore;
+      WidgetMouseEvent event(true, eXULPopupPositioned, nullptr,
+                             WidgetMouseEvent::eReal);
+      EventDispatcher::Dispatch(mPopup, popupFrame->PresContext(),
+                                &event, nullptr, &status);
+
+      // Get the popup frame and make sure it is still in the positioning
+      // state. If it isn't, someone may have tried to reshow or hide it
+      // during the popuppositioned event.
+      // Alternately, this event may have been fired in reponse to moving the
+      // popup rather than opening it. In that case, we are done.
+      nsMenuPopupFrame* popupFrame = do_QueryFrame(mPopup->GetPrimaryFrame());
+      if (popupFrame && popupFrame->PopupState() == ePopupPositioning) {
+        pm->ShowPopupCallback(mPopup, popupFrame, mIsContextMenu, mSelectFirstItem);
+      }
+    }
+  }
+
+  return NS_OK;
+}
+
 NS_IMETHODIMP
 nsXULMenuCommandEvent::Run()
 {
   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
   if (!pm)
     return NS_OK;
 
   // The order of the nsViewManager and nsIPresShell COM pointers is
--- a/layout/xul/nsXULPopupManager.h
+++ b/layout/xul/nsXULPopupManager.h
@@ -232,16 +232,44 @@ private:
   nsCOMPtr<nsIContent> mPopup;
   nsCOMPtr<nsIContent> mNextPopup;
   nsCOMPtr<nsIContent> mLastPopup;
   nsPopupType mPopupType;
   bool mDeselectMenu;
   bool mIsRollup;
 };
 
+// this class is used for dispatching popuppositioned events asynchronously.
+class nsXULPopupPositionedEvent : public mozilla::Runnable
+{
+public:
+  explicit nsXULPopupPositionedEvent(nsIContent *aPopup,
+                                     bool aIsContextMenu,
+                                     bool aSelectFirstItem)
+    : mPopup(aPopup)
+    , mIsContextMenu(aIsContextMenu)
+    , mSelectFirstItem(aSelectFirstItem)
+  {
+    NS_ASSERTION(aPopup, "null popup supplied to nsXULPopupShowingEvent constructor");
+  }
+
+  NS_IMETHOD Run() override;
+
+  // Asynchronously dispatch a popuppositioned event at aPopup if this is a
+  // panel that should receieve such events. Return true if the event was sent.
+  static bool DispatchIfNeeded(nsIContent *aPopup,
+                               bool aIsContextMenu,
+                               bool aSelectFirstItem);
+
+private:
+  nsCOMPtr<nsIContent> mPopup;
+  bool mIsContextMenu;
+  bool mSelectFirstItem;
+};
+
 // this class is used for dispatching menu command events asynchronously.
 class nsXULMenuCommandEvent : public mozilla::Runnable
 {
 public:
   nsXULMenuCommandEvent(nsIContent *aMenu,
                         bool aIsTrusted,
                         bool aShift,
                         bool aControl,
@@ -282,16 +310,17 @@ class nsXULPopupManager final : public n
                                 public nsIRollupListener,
                                 public nsITimerCallback,
                                 public nsIObserver
 {
 
 public:
   friend class nsXULPopupShowingEvent;
   friend class nsXULPopupHidingEvent;
+  friend class nsXULPopupPositionedEvent;
   friend class nsXULMenuCommandEvent;
   friend class TransitionEnder;
 
   NS_DECL_ISUPPORTS
   NS_DECL_NSIOBSERVER
   NS_DECL_NSITIMERCALLBACK
   NS_DECL_NSIDOMEVENTLISTENER
 
--- a/toolkit/content/tests/widgets/test_popupanchor.xul
+++ b/toolkit/content/tests/widgets/test_popupanchor.xul
@@ -83,16 +83,25 @@ function openSlidingPopup(position, call
   _openPopup(position, callback);
 }
 
 function openPopup(position, callback) {
   panel.setAttribute("flip", "both");
   _openPopup(position, callback);
 }
 
+function waitForPopupPositioned(actionFn, callback)
+{
+  panel.addEventListener("popuppositioned", function listener() {
+    panel.removeEventListener("popuppositioned", listener, false);
+    callback();
+  }, false);
+  actionFn();
+}
+
 function _openPopup(position, callback) {
   // this is very ugly: the panel CSS sets the arrow's list-style-image based
   // on the 'side' attribute.  If the setting of the 'side' attribute causes
   // the image to change, we may get the popupshown event before the new
   // image has loaded - which causes the size of the arrow to be incorrect
   // for a brief moment - right when we are measuring it!
   // So we work around this in 2 steps:
   // * Force the 'side' attribute to a value which causes the CSS to not
@@ -195,41 +204,57 @@ var tests = [
     // anchored to the right-hand side of the anchor.
     panel.sizeTo(anchorRight - 10, 100);
     openPopup("after_end", function() {
       isArrowPositionedOn("right");
       // Ask for it to be anchored 1/2 way between the left edge of the window
       // and the anchor right - it can't fit with the panel on the left/arrow
       // on the right, so it must flip (arrow on the left, panel on the right)
       var offset = Math.floor(-anchorRight / 2);
-      panel.moveToAnchor(anchor, "after_end", offset, 0);
-      isArrowPositionedOn("left", offset); // should have flipped and have the offset.
-      // resize back to original and move to a zero offset - it should flip back.
-      panel.sizeTo(anchorRight - 10, 100);
-      panel.moveToAnchor(anchor, "after_end", 0, 0);
-      isArrowPositionedOn("right"); // should have flipped back and no offset
-      next();
+
+      waitForPopupPositioned(
+        () => panel.moveToAnchor(anchor, "after_end", offset, 0),
+        () => {
+          isArrowPositionedOn("left", offset); // should have flipped and have the offset.
+          // resize back to original and move to a zero offset - it should flip back.
+
+          panel.sizeTo(anchorRight - 10, 100);
+          waitForPopupPositioned(
+            () => panel.moveToAnchor(anchor, "after_end", 0, 0),
+            () => {
+              isArrowPositionedOn("right"); // should have flipped back and no offset
+              next();
+            });
+        });
     });
   }],
 
   // Do a moveToAnchor that causes the panel to flip vertically
   ['flippingMoveToAnchorVertical', 'middle', function(next) {
     var anchorBottom = anchor.getBoundingClientRect().bottom;
     // See comments above in flippingMoveToAnchorHorizontal, but read
     // "top/bottom" instead of "left/right"
     panel.sizeTo(100, anchorBottom - 10);
     openPopup("start_after", function() {
       isArrowPositionedOn("bottom");
       var offset = Math.floor(-anchorBottom / 2);
-      panel.moveToAnchor(anchor, "start_after", 0, offset);
-      isArrowPositionedOn("top", offset);
-      panel.sizeTo(100, anchorBottom - 10);
-      panel.moveToAnchor(anchor, "start_after", 0, 0);
-      isArrowPositionedOn("bottom");
-      next();
+
+      waitForPopupPositioned(
+        () => panel.moveToAnchor(anchor, "start_after", 0, offset),
+        () => {
+          isArrowPositionedOn("top", offset);
+          panel.sizeTo(100, anchorBottom - 10);
+
+          waitForPopupPositioned(
+            () => panel.moveToAnchor(anchor, "start_after", 0, 0),
+            () => {
+              isArrowPositionedOn("bottom");
+              next();
+            });
+        });
     });
   }],
 
   ['veryWidePanel-after_end', 'middle', function(next) {
     openSlidingPopup("after_end", function() {
       var origArrowRect = arrow.getBoundingClientRect();
       // Now move it such that the arrow can't be at either end of the panel but
       // instead somewhere in the middle as that is the only way things fit,
@@ -374,20 +399,18 @@ function runTests() {
   runNextTest();
 }
 
 SimpleTest.waitForExplicitFinish();
 
 addEventListener("load", function() {
   // anchor is set by the test runner above
   panel = document.getElementById("testPanel");
+
   arrow = SpecialPowers.wrap(document).getAnonymousElementByAttribute(panel, "anonid", "arrow");
-  // Cancel the arrow panel slide-in transition (bug 767133) so the size and
-  // position are "stable" enough to test without jumping through hoops...
-  arrow.style.transition = "none";
   runTests();
 });
 
 ]]>
 </script>
 
 <body xmlns="http://www.w3.org/1999/xhtml">
 <!-- Our tests assume at least 100px around the anchor on all sides, else the
--- a/toolkit/content/widgets/popup.xml
+++ b/toolkit/content/widgets/popup.xml
@@ -369,67 +369,52 @@
     <implementation>
       <field name="_fadeTimer">null</field>
       <method name="sizeTo">
         <parameter name="aWidth"/>
         <parameter name="aHeight"/>
         <body>
         <![CDATA[
           this.popupBoxObject.sizeTo(aWidth, aHeight);
-          if (this.state == "open")
+          if (this.state == "open") {
             this.adjustArrowPosition();
-        ]]>
-        </body>
-      </method>
-      <method name="moveTo">
-        <parameter name="aLeft"/>
-        <parameter name="aTop"/>
-        <body>
-        <![CDATA[
-          this.popupBoxObject.moveTo(aLeft, aTop);
-          if (this.state == "open")
-            this.adjustArrowPosition();
+          }
         ]]>
         </body>
       </method>
       <method name="moveToAnchor">
         <parameter name="aAnchorElement"/>
         <parameter name="aPosition"/>
         <parameter name="aX"/>
         <parameter name="aY"/>
         <parameter name="aAttributesOverride"/>
         <body>
         <![CDATA[
           this.popupBoxObject.moveToAnchor(aAnchorElement, aPosition, aX, aY, aAttributesOverride);
-          if (this.state == "open")
-            this.adjustArrowPosition();
         ]]>
         </body>
       </method>
       <method name="adjustArrowPosition">
         <body>
         <![CDATA[
         var arrow = document.getAnonymousElementByAttribute(this, "anonid", "arrow");
 
         var anchor = this.anchorNode;
         if (!anchor) {
-          arrow.hidden = true;
           return;
         }
 
         var container = document.getAnonymousElementByAttribute(this, "anonid", "container");
         var arrowbox = document.getAnonymousElementByAttribute(this, "anonid", "arrowbox");
 
         var position = this.alignmentPosition;
         var offset = this.alignmentOffset;
 
         this.setAttribute("arrowposition", position);
 
-        // if this panel has a "sliding" arrow, we may have previously set margins...
-        arrowbox.style.removeProperty("transform");
         if (position.indexOf("start_") == 0 || position.indexOf("end_") == 0) {
           container.orient = "horizontal";
           arrowbox.orient = "vertical";
           if (position.indexOf("_after") > 0) {
             arrowbox.pack = "end";
           } else {
             arrowbox.pack = "start";
           }
@@ -461,26 +446,30 @@
             container.dir = "reverse";
             this.setAttribute("side", "bottom");
           }
           else {
             container.dir = "";
             this.setAttribute("side", "top");
           }
         }
-
-        arrow.hidden = false;
         ]]>
         </body>
       </method>
     </implementation>
     <handlers>
       <handler event="popupshowing" phase="target">
       <![CDATA[
+        var arrow = document.getAnonymousElementByAttribute(this, "anonid", "arrow");
+        arrow.hidden = this.anchorNode == null;
+        document.getAnonymousElementByAttribute(this, "anonid", "arrowbox")
+                .style.removeProperty("transform");
+
         this.adjustArrowPosition();
+
         if (this.getAttribute("animate") != "false") {
           this.setAttribute("animate", "open");
         }
 
         // set fading
         var fade = this.getAttribute("fade");
         var fadeDelay = 0;
         if (fade == "fast") {
@@ -513,16 +502,19 @@
         this.setAttribute("panelopen", "true");
       </handler>
       <handler event="popuphidden" phase="target">
         this.removeAttribute("panelopen");
         if (this.getAttribute("animate") != "false") {
           this.removeAttribute("animate");
         }
       </handler>
+      <handler event="popuppositioned" phase="target">
+        this.adjustArrowPosition();
+      </handler>
     </handlers>
   </binding>
 
   <binding id="tooltip" role="xul:tooltip"
            extends="chrome://global/content/bindings/popup.xml#popup-base">
     <content>
       <children>
         <xul:label class="tooltip-label" xbl:inherits="xbl:text=label" flex="1"/>
--- a/toolkit/modules/PopupNotifications.jsm
+++ b/toolkit/modules/PopupNotifications.jsm
@@ -843,23 +843,45 @@ PopupNotifications.prototype = {
         // Remember the time the notification was shown for the security delay.
         n.timeShown = this.window.performance.now();
       }, this);
 
       // Unless the panel closing is triggered by a specific known code path,
       // the next reason will be that the user clicked elsewhere.
       this.nextDismissReason = TELEMETRY_STAT_DISMISSAL_CLICK_ELSEWHERE;
 
+      let target = this.panel;
+      if (target.parentNode) {
+        // NOTIFICATION_EVENT_SHOWN should be fired for the panel before
+        // anyone listening for popupshown on the panel gets run. Otherwise,
+        // the panel will not be initialized when the popupshown event
+        // listeners run.
+        // By targeting the panel's parent and using a capturing listener, we
+        // can have our listener called before others waiting for the panel to
+        // be shown (which probably expect the panel to be fully initialized)
+        target = target.parentNode;
+      }
+      if (this._popupshownListener) {
+        target.removeEventListener("popupshown", this._popupshownListener, true);
+      }
+      this._popupshownListener = function (e) {
+        target.removeEventListener("popupshown", this._popupshownListener, true);
+        this._popupshownListener = null;
+
+        notificationsToShow.forEach(function (n) {
+          this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
+        }, this);
+        // This notification is used by tests to know when all the processing
+        // required to display the panel has happened.
+        this.panel.dispatchEvent(new this.window.CustomEvent("Shown"));
+      };
+      this._popupshownListener = this._popupshownListener.bind(this);
+      target.addEventListener("popupshown", this._popupshownListener, true);
+
       this.panel.openPopup(anchorElement, "bottomcenter topleft");
-      notificationsToShow.forEach(function (n) {
-        this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
-      }, this);
-      // This notification is used by tests to know when all the processing
-      // required to display the panel has happened.
-      this.panel.dispatchEvent(new this.window.CustomEvent("Shown"));
     });
   },
 
   /**
    * Updates the notification state in response to window activation or tab
    * selection changes.
    *
    * @param notifications an array of Notification instances. if null,
--- a/widget/EventMessageList.h
+++ b/widget/EventMessageList.h
@@ -137,16 +137,17 @@ NS_EVENT_MESSAGE(eDragEnd)
 NS_EVENT_MESSAGE(eDragStart)
 NS_EVENT_MESSAGE(eDrop)
 NS_EVENT_MESSAGE(eDragLeave)
 NS_EVENT_MESSAGE_FIRST_LAST(eDragDropEvent, eDragEnter, eDragLeave)
 
 // XUL specific events
 NS_EVENT_MESSAGE(eXULPopupShowing)
 NS_EVENT_MESSAGE(eXULPopupShown)
+NS_EVENT_MESSAGE(eXULPopupPositioned)
 NS_EVENT_MESSAGE(eXULPopupHiding)
 NS_EVENT_MESSAGE(eXULPopupHidden)
 NS_EVENT_MESSAGE(eXULBroadcast)
 NS_EVENT_MESSAGE(eXULCommandUpdate)
 
 // Legacy mouse scroll (wheel) events
 NS_EVENT_MESSAGE(eLegacyMouseLineOrPageScroll)
 NS_EVENT_MESSAGE(eLegacyMousePixelScroll)