Bug 610545, arrow panels should animate when opening and when cancelling, r=neil,dao
authorNeil Deakin <neil@mozilla.com>
Tue, 08 Apr 2014 08:45:52 -0400
changeset 189587 e4f3408af3611efb382736e25fd366cbdd1e193a
parent 189586 8513f4c3ad9440003187bcd163b821e84f70a50a
child 189588 3d79336c9d1960b682514e2ef65af15574afb725
push id5832
push userraliiev@mozilla.com
push dateMon, 28 Apr 2014 18:43:22 +0000
treeherdermozilla-aurora@4974d9da2f7d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersneil, dao
bugs610545
milestone31.0a1
Bug 610545, arrow panels should animate when opening and when cancelling, r=neil,dao
browser/components/customizableui/content/panelUI.inc.xul
content/xul/content/src/nsXULPopupListener.cpp
layout/xul/nsMenuBarFrame.cpp
layout/xul/nsMenuBarListener.cpp
layout/xul/nsMenuBoxObject.cpp
layout/xul/nsMenuFrame.cpp
layout/xul/nsPopupBoxObject.cpp
layout/xul/nsXULPopupManager.cpp
layout/xul/nsXULPopupManager.h
layout/xul/nsXULTooltipListener.cpp
toolkit/content/tests/chrome/test_arrowpanel.xul
toolkit/content/tests/widgets/test_popupanchor.xul
toolkit/content/widgets/popup.xml
toolkit/content/xul.css
toolkit/themes/linux/global/popup.css
toolkit/themes/osx/global/popup.css
toolkit/themes/windows/global/popup.css
view/src/nsView.cpp
widget/xpwidgets/nsBaseDragService.cpp
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -2,16 +2,17 @@
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <panel id="PanelUI-popup"
        role="group"
        type="arrow"
        hidden="true"
        flip="slide"
+       animate="false"
        position="bottomcenter topright"
        noautofocus="true">
   <panelmultiview id="PanelUI-multiView" mainViewId="PanelUI-mainView">
     <panelview id="PanelUI-mainView" context="customizationPanelContextMenu">
       <vbox id="PanelUI-contents-scroller">
         <vbox id="PanelUI-contents" class="panelUI-grid"/>
       </vbox>
 
--- a/content/xul/content/src/nsXULPopupListener.cpp
+++ b/content/xul/content/src/nsXULPopupListener.cpp
@@ -292,17 +292,17 @@ void
 nsXULPopupListener::ClosePopup()
 {
   if (mPopupContent) {
     // this is called when the listener is going away, so make sure that the
     // popup is hidden. Use asynchronous hiding just to be safe so we don't
     // fire events during destruction.  
     nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
     if (pm)
-      pm->HidePopup(mPopupContent, false, true, true);
+      pm->HidePopup(mPopupContent, false, true, true, false);
     mPopupContent = nullptr;  // release the popup
   }
 } // ClosePopup
 
 static already_AddRefed<nsIContent>
 GetImmediateChild(nsIContent* aContent, nsIAtom *aTag) 
 {
   for (nsIContent* child = aContent->GetFirstChild();
--- a/layout/xul/nsMenuBarFrame.cpp
+++ b/layout/xul/nsMenuBarFrame.cpp
@@ -233,17 +233,17 @@ nsMenuBarFrame::FindMenuWithShortcut(nsI
     if (soundInterface)
       soundInterface->Beep();
   }
 
   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
   if (pm) {
     nsIFrame* popup = pm->GetTopPopup(ePopupTypeAny);
     if (popup)
-      pm->HidePopup(popup->GetContent(), true, true, true);
+      pm->HidePopup(popup->GetContent(), true, true, true, false);
   }
 
   SetCurrentMenuItem(nullptr);
   SetActive(false);
 
 #endif  // #ifdef XP_WIN
 
   return nullptr;
@@ -303,17 +303,17 @@ public:
     if (mOldMenu && mNewMenu) {
       menubar = do_QueryFrame(mMenuBar->GetPrimaryFrame());
       if (menubar)
         menubar->SetStayActive(true);
     }
 
     if (mOldMenu) {
       nsWeakFrame weakMenuBar(menubar);
-      pm->HidePopup(mOldMenu, false, false, false);
+      pm->HidePopup(mOldMenu, false, false, false, false);
       // clear the flag again
       if (mNewMenu && weakMenuBar.IsAlive())
         menubar->SetStayActive(false);
     }
 
     if (mNewMenu)
       pm->ShowMenu(mNewMenu, mSelectFirstItem, false);
 
--- a/layout/xul/nsMenuBarListener.cpp
+++ b/layout/xul/nsMenuBarListener.cpp
@@ -101,17 +101,17 @@ void nsMenuBarListener::InitAccessKey()
 void
 nsMenuBarListener::ToggleMenuActiveState()
 {
   nsMenuFrame* closemenu = mMenuBarFrame->ToggleMenuActiveState();
   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
   if (pm && closemenu) {
     nsMenuPopupFrame* popupFrame = closemenu->GetPopup();
     if (popupFrame)
-      pm->HidePopup(popupFrame->GetContent(), false, false, true);
+      pm->HidePopup(popupFrame->GetContent(), false, false, true, false);
   }
 }
 
 ////////////////////////////////////////////////////////////////////////
 nsresult
 nsMenuBarListener::KeyUp(nsIDOMEvent* aKeyEvent)
 {  
   nsCOMPtr<nsIDOMKeyEvent> keyEvent = do_QueryInterface(aKeyEvent);
--- a/layout/xul/nsMenuBoxObject.cpp
+++ b/layout/xul/nsMenuBoxObject.cpp
@@ -44,17 +44,17 @@ NS_IMETHODIMP nsMenuBoxObject::OpenMenu(
         nsCOMPtr<nsIContent> content = mContent;
         pm->ShowMenu(content, false, false);
       }
       else {
         nsMenuFrame* menu = do_QueryFrame(frame);
         if (menu) {
           nsMenuPopupFrame* popupFrame = menu->GetPopup();
           if (popupFrame)
-            pm->HidePopup(popupFrame->GetContent(), false, true, false);
+            pm->HidePopup(popupFrame->GetContent(), false, true, false, false);
         }
       }
     }
   }
 
   return NS_OK;
 }
 
--- a/layout/xul/nsMenuFrame.cpp
+++ b/layout/xul/nsMenuFrame.cpp
@@ -708,17 +708,17 @@ nsMenuFrame::OpenMenu(bool aSelectFirstI
 void
 nsMenuFrame::CloseMenu(bool aDeselectMenu)
 {
   gEatMouseMove = true;
 
   // Close the menu asynchronously
   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
   if (pm && HasPopup())
-    pm->HidePopup(GetPopup()->GetContent(), false, aDeselectMenu, true);
+    pm->HidePopup(GetPopup()->GetContent(), false, aDeselectMenu, true, false);
 }
 
 bool
 nsMenuFrame::IsSizedToPopup(nsIContent* aContent, bool aRequireAlways)
 {
   nsAutoString sizedToPopup;
   aContent->GetAttr(kNameSpaceID_None, nsGkAtoms::sizetopopup, sizedToPopup);
   return sizedToPopup.EqualsLiteral("always") ||
@@ -802,17 +802,17 @@ nsMenuFrame::Enter(WidgetGUIEvent* aEven
   if (IsDisabled()) {
 #ifdef XP_WIN
     // behavior on Windows - close the popup chain
     if (mMenuParent) {
       nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
       if (pm) {
         nsIFrame* popup = pm->GetTopPopup(ePopupTypeAny);
         if (popup)
-          pm->HidePopup(popup->GetContent(), true, true, true);
+          pm->HidePopup(popup->GetContent(), true, true, true, false);
       }
     }
 #endif   // #ifdef XP_WIN
     // this menu item was disabled - exit
     return nullptr;
   }
 
   if (!IsOpen()) {
--- a/layout/xul/nsPopupBoxObject.cpp
+++ b/layout/xul/nsPopupBoxObject.cpp
@@ -45,17 +45,17 @@ nsPopupBoxObject::GetPopupSetFrame()
   return rootBox->GetPopupSetFrame();
 }
 
 NS_IMETHODIMP
 nsPopupBoxObject::HidePopup()
 {
   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
   if (pm && mContent)
-    pm->HidePopup(mContent, false, true, false);
+    pm->HidePopup(mContent, false, true, false, false);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsPopupBoxObject::ShowPopup(nsIDOMElement* aAnchorElement,
                             nsIDOMElement* aPopupElement,
                             int32_t aXPos, int32_t aYPos,
--- a/layout/xul/nsXULPopupManager.cpp
+++ b/layout/xul/nsXULPopupManager.cpp
@@ -225,17 +225,17 @@ nsXULPopupManager::Rollup(uint32_t aCoun
       while (--aCount && last->GetParent()) {
         last = last->GetParent();
       }
       if (last) {
         lastPopup = last->Content();
       }
     }
 
-    HidePopup(item->Content(), true, true, false, lastPopup);
+    HidePopup(item->Content(), true, true, false, true, lastPopup);
   }
 
   return consume;
 }
 
 ////////////////////////////////////////////////////////////////////////
 bool nsXULPopupManager::ShouldRollupOnMouseWheelEvent()
 {
@@ -821,16 +821,17 @@ nsXULPopupManager::ShowPopupCallback(nsI
   CheckCaretDrawingState();
 }
 
 void
 nsXULPopupManager::HidePopup(nsIContent* aPopup,
                              bool aHideChain,
                              bool aDeselectMenu,
                              bool aAsynchronous,
+                             bool aIsRollup,
                              nsIContent* aLastPopup)
 {
   // if the popup is on the nohide panels list, remove it but don't close any
   // other panels
   nsMenuPopupFrame* popupFrame = nullptr;
   bool foundPanel = false;
   nsMenuChainItem* item = mNoHidePanels;
   while (item) {
@@ -913,33 +914,69 @@ nsXULPopupManager::HidePopup(nsIContent*
     // run again. In the invisible state, we just want the events to fire.
     if (state != ePopupInvisible)
       popupFrame->SetPopupState(ePopupHiding);
 
     // for menus, popupToHide is always the frontmost item in the list to hide.
     if (aAsynchronous) {
       nsCOMPtr<nsIRunnable> event =
         new nsXULPopupHidingEvent(popupToHide, nextPopup, lastPopup,
-                                  type, deselectMenu);
+                                  type, deselectMenu, aIsRollup);
         NS_DispatchToCurrentThread(event);
     }
     else {
       FirePopupHidingEvent(popupToHide, nextPopup, lastPopup,
-                           popupFrame->PresContext(), type, deselectMenu);
+                           popupFrame->PresContext(), type, deselectMenu, aIsRollup);
     }
   }
 }
 
+// This is used to hide the popup after a transition finishes.
+class TransitionEnder : public nsIDOMEventListener
+{
+public:
+
+  nsCOMPtr<nsIContent> mContent;
+  bool mDeselectMenu;
+
+  NS_DECL_ISUPPORTS
+
+  TransitionEnder(nsIContent* aContent, bool aDeselectMenu)
+    : mContent(aContent), mDeselectMenu(aDeselectMenu)
+  {
+  }
+
+  virtual ~TransitionEnder() { }
+
+  NS_IMETHOD HandleEvent(nsIDOMEvent* aEvent) MOZ_OVERRIDE
+  {
+    mContent->RemoveSystemEventListener(NS_LITERAL_STRING("transitionend"), this, false);
+
+    // Now hide the popup. There could be other properties transitioning, but
+    // we'll assume they all end at the same time and just hide the popup upon
+    // the first one ending.
+    nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+    if (pm) {
+      pm->HidePopupFrame(mContent, mDeselectMenu);
+    }
+
+    return NS_OK;
+  }
+};
+
+NS_IMPL_ISUPPORTS1(TransitionEnder, nsIDOMEventListener)
+
 void
 nsXULPopupManager::HidePopupCallback(nsIContent* aPopup,
                                      nsMenuPopupFrame* aPopupFrame,
                                      nsIContent* aNextPopup,
                                      nsIContent* aLastPopup,
                                      nsPopupType aPopupType,
-                                     bool aDeselectMenu)
+                                     bool aDeselectMenu,
+                                     bool aIsRollup)
 {
   if (mCloseTimer && mTimerMenu == aPopupFrame) {
     mCloseTimer->Cancel();
     mCloseTimer = nullptr;
     mTimerMenu = nullptr;
   }
 
   // The popup to hide is aPopup. Search the list again to find the item that
@@ -965,28 +1002,41 @@ nsXULPopupManager::HidePopupCallback(nsI
         break;
       }
       item = item->GetParent();
     }
   }
 
   delete item;
 
-  nsWeakFrame weakFrame(aPopupFrame);
-  aPopupFrame->HidePopup(aDeselectMenu, ePopupClosed);
-  ENSURE_TRUE(weakFrame.IsAlive());
+  // If the popup has an animate attribute and it is not set to false, assume
+  // that it has a closing transition and wait for it to finish. The transition
+  // may still occur either way, but the view will be hidden and you won't be
+  // able to see it. If there is a next popup, indicating that mutliple popups
+  // are rolling up, don't wait and hide the popup right away since the effect
+  // would likely be undesirable. This also does a quick check to see if the
+  // popup has a transition defined, and skips the wait if not.
+  if (!aNextPopup && aPopup->HasAttr(kNameSpaceID_None, nsGkAtoms::animate) &&
+       aPopupFrame->StyleDisplay()->mTransitionPropertyCount > 0) {
+    nsAutoString animate;
+    aPopup->GetAttr(kNameSpaceID_None, nsGkAtoms::animate, animate);
 
-  // send the popuphidden event synchronously. This event has no default
-  // behaviour.
-  nsEventStatus status = nsEventStatus_eIgnore;
-  WidgetMouseEvent event(true, NS_XUL_POPUP_HIDDEN, nullptr,
-                         WidgetMouseEvent::eReal);
-  EventDispatcher::Dispatch(aPopup, aPopupFrame->PresContext(),
-                            &event, nullptr, &status);
-  ENSURE_TRUE(weakFrame.IsAlive());
+    // If animate="false" then don't transition at all. If animate="cancel",
+    // only show the transition if cancelling the popup or rolling up.
+    // Otherwise, always show the transition.
+    if (!animate.EqualsLiteral("false") &&
+        (!animate.EqualsLiteral("cancel") || aIsRollup)) {
+      nsCOMPtr<TransitionEnder> ender = new TransitionEnder(aPopup, aDeselectMenu);
+      aPopup->AddSystemEventListener(NS_LITERAL_STRING("transitionend"),
+                                     ender, false, false);
+      return;
+    }
+  }
+
+  HidePopupFrame(aPopup, aDeselectMenu);
 
   // if there are more popups to close, look for the next one
   if (aNextPopup && aPopup != aLastPopup) {
     nsMenuChainItem* foundMenu = nullptr;
     nsMenuChainItem* item = mPopups;
     while (item) {
       if (item->Content() == aNextPopup) {
         foundMenu = item;
@@ -1013,27 +1063,47 @@ nsXULPopupManager::HidePopupCallback(nsI
       nsPopupState state = popupFrame->PopupState();
       if (state == ePopupHiding)
         return;
       if (state != ePopupInvisible)
         popupFrame->SetPopupState(ePopupHiding);
 
       FirePopupHidingEvent(popupToHide, nextPopup, aLastPopup,
                            popupFrame->PresContext(),
-                           foundMenu->PopupType(), aDeselectMenu);
+                           foundMenu->PopupType(), aDeselectMenu, false);
     }
   }
 }
 
 void
+nsXULPopupManager::HidePopupFrame(nsIContent* aPopup, bool aDeselectMenu)
+{
+  nsMenuPopupFrame* popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
+  if (!popupFrame)
+    return;
+
+  nsWeakFrame weakFrame(popupFrame);
+  popupFrame->HidePopup(aDeselectMenu, ePopupClosed);
+  ENSURE_TRUE(weakFrame.IsAlive());
+
+  // send the popuphidden event synchronously. This event has no default
+  // behaviour.
+  nsEventStatus status = nsEventStatus_eIgnore;
+  WidgetMouseEvent event(true, NS_XUL_POPUP_HIDDEN, nullptr,
+                         WidgetMouseEvent::eReal);
+  EventDispatcher::Dispatch(aPopup, popupFrame->PresContext(),
+                            &event, nullptr, &status);
+}
+
+void
 nsXULPopupManager::HidePopup(nsIFrame* aFrame)
 {
   nsMenuPopupFrame* popup = do_QueryFrame(aFrame);
   if (popup)
-    HidePopup(aFrame->GetContent(), false, true, false);
+    HidePopup(aFrame->GetContent(), false, true, false, false);
 }
 
 void
 nsXULPopupManager::HidePopupAfterDelay(nsMenuPopupFrame* aPopup)
 {
   // Don't close up immediately.
   // Kick off a close timer.
   KillMenuTimer();
@@ -1282,17 +1352,18 @@ nsXULPopupManager::FirePopupShowingEvent
 }
 
 void
 nsXULPopupManager::FirePopupHidingEvent(nsIContent* aPopup,
                                         nsIContent* aNextPopup,
                                         nsIContent* aLastPopup,
                                         nsPresContext *aPresContext,
                                         nsPopupType aPopupType,
-                                        bool aDeselectMenu)
+                                        bool aDeselectMenu,
+                                        bool aIsRollup)
 {
   nsCOMPtr<nsIPresShell> presShell = aPresContext->PresShell();
 
   nsEventStatus status = nsEventStatus_eIgnore;
   WidgetMouseEvent event(true, NS_XUL_POPUP_HIDING, nullptr,
                          WidgetMouseEvent::eReal);
   EventDispatcher::Dispatch(aPopup, aPresContext, &event, nullptr, &status);
 
@@ -1322,17 +1393,17 @@ nsXULPopupManager::FirePopupHidingEvent(
     // state back to open. Only popups in chrome shells can prevent a popup
     // from hiding.
     if (status == nsEventStatus_eConsumeNoDefault &&
         !popupFrame->IsInContentShell()) {
       popupFrame->SetPopupState(ePopupOpenAndVisible);
     }
     else {
       HidePopupCallback(aPopup, popupFrame, aNextPopup, aLastPopup,
-                        aPopupType, aDeselectMenu);
+                        aPopupType, aDeselectMenu, aIsRollup);
     }
   }
 }
 
 bool
 nsXULPopupManager::IsPopupOpen(nsIContent* aPopup)
 {
   // a popup is open if it is in the open list. The assertions ensure that the
@@ -1575,17 +1646,17 @@ nsXULPopupManager::PopupDestroyed(nsMenu
           // it asynchronously since we are in the middle of frame destruction.
           nsMenuPopupFrame* childframe = child->Frame();
           if (nsLayoutUtils::IsProperAncestorFrame(frame, childframe)) {
             popupsToHide.AppendElement(childframe);
           }
           else {
             // HidePopup will take care of hiding any of its children, so
             // break out afterwards
-            HidePopup(child->Content(), false, false, true);
+            HidePopup(child->Content(), false, false, true, false);
             break;
           }
 
           child = child->GetChild();
         }
       }
 
       item->Detach(&mPopups);
@@ -1775,17 +1846,17 @@ nsXULPopupManager::Notify(nsITimer* aTim
 void
 nsXULPopupManager::KillMenuTimer()
 {
   if (mCloseTimer && mTimerMenu) {
     mCloseTimer->Cancel();
     mCloseTimer = nullptr;
 
     if (mTimerMenu->IsOpen())
-      HidePopup(mTimerMenu->GetContent(), false, false, true);
+      HidePopup(mTimerMenu->GetContent(), false, false, true, false);
   }
 
   mTimerMenu = nullptr;
 }
 
 void
 nsXULPopupManager::CancelMenuTimer(nsMenuParent* aMenuParent)
 {
@@ -1971,17 +2042,17 @@ nsXULPopupManager::HandleKeyboardNavigat
       return true;
     }
   }
   else if (currentMenu && isContainer && isOpen) {
     if (aDir == eNavigationDirection_Start) {
       // close a submenu when Left is pressed
       nsMenuPopupFrame* popupFrame = currentMenu->GetPopup();
       if (popupFrame)
-        HidePopup(popupFrame->GetContent(), false, false, false);
+        HidePopup(popupFrame->GetContent(), false, false, false, false);
       return true;
     }
   }
 
   return false;
 }
 
 bool
@@ -1991,17 +2062,17 @@ nsXULPopupManager::HandleKeyboardEventWi
 {
   uint32_t keyCode;
   aKeyEvent->GetKeyCode(&keyCode);
 
   // Escape should close panels, but the other keys should have no effect.
   if (aTopVisibleMenuItem &&
       aTopVisibleMenuItem->PopupType() != ePopupTypeMenu) {
     if (keyCode == nsIDOMKeyEvent::DOM_VK_ESCAPE) {
-      HidePopup(aTopVisibleMenuItem->Content(), false, false, false);
+      HidePopup(aTopVisibleMenuItem->Content(), false, false, false, true);
       aKeyEvent->StopPropagation();
       aKeyEvent->PreventDefault();
     }
     return true;
   }
 
   bool consume = (mPopups || mActiveMenuBar);
   switch (keyCode) {
@@ -2015,17 +2086,17 @@ nsXULPopupManager::HandleKeyboardEventWi
       break;
 
     case nsIDOMKeyEvent::DOM_VK_ESCAPE:
       // Pressing Escape hides one level of menus only. If no menu is open,
       // check if a menubar is active and inform it that a menu closed. Even
       // though in this latter case, a menu didn't actually close, the effect
       // ends up being the same. Similar for the tab key below.
       if (aTopVisibleMenuItem) {
-        HidePopup(aTopVisibleMenuItem->Content(), false, false, false);
+        HidePopup(aTopVisibleMenuItem->Content(), false, false, false, true);
       } else if (mActiveMenuBar) {
         mActiveMenuBar->MenuClosed();
       }
       break;
 
     case nsIDOMKeyEvent::DOM_VK_TAB:
 #ifndef XP_MACOSX
     case nsIDOMKeyEvent::DOM_VK_F10:
@@ -2322,17 +2393,17 @@ nsXULPopupHidingEvent::Run()
 
   nsIDocument *document = mPopup->GetCurrentDoc();
   if (pm && document) {
     nsIPresShell* presShell = document->GetShell();
     if (presShell) {
       nsPresContext* context = presShell->GetPresContext();
       if (context) {
         pm->FirePopupHidingEvent(mPopup, mNextPopup, mLastPopup,
-                                 context, mPopupType, mDeselectMenu);
+                                 context, mPopupType, mDeselectMenu, mIsRollup);
       }
     }
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
@@ -2383,12 +2454,12 @@ nsXULMenuCommandEvent::Run()
 
     AutoHandlingUserInputStatePusher userInpStatePusher(mUserInput, nullptr,
                                                         shell->GetDocument());
     nsContentUtils::DispatchXULCommand(mMenu, mIsTrusted, nullptr, shell,
                                        mControl, mAlt, mShift, mMeta);
   }
 
   if (popup && mCloseMenuMode != CloseMenuMode_None)
-    pm->HidePopup(popup, mCloseMenuMode == CloseMenuMode_Auto, true, false);
+    pm->HidePopup(popup, mCloseMenuMode == CloseMenuMode_Auto, true, false, false);
 
   return NS_OK;
 }
--- a/layout/xul/nsXULPopupManager.h
+++ b/layout/xul/nsXULPopupManager.h
@@ -199,35 +199,38 @@ private:
 // this class is used for dispatching popuphiding events asynchronously.
 class nsXULPopupHidingEvent : public nsRunnable
 {
 public:
   nsXULPopupHidingEvent(nsIContent *aPopup,
                         nsIContent* aNextPopup,
                         nsIContent* aLastPopup,
                         nsPopupType aPopupType,
-                        bool aDeselectMenu)
+                        bool aDeselectMenu,
+                        bool aIsRollup)
     : mPopup(aPopup),
       mNextPopup(aNextPopup),
       mLastPopup(aLastPopup),
       mPopupType(aPopupType),
-      mDeselectMenu(aDeselectMenu)
+      mDeselectMenu(aDeselectMenu),
+      mIsRollup(aIsRollup)
   {
     NS_ASSERTION(aPopup, "null popup supplied to nsXULPopupHidingEvent constructor");
     // aNextPopup and aLastPopup may be null
   }
 
   NS_IMETHOD Run() MOZ_OVERRIDE;
 
 private:
   nsCOMPtr<nsIContent> mPopup;
   nsCOMPtr<nsIContent> mNextPopup;
   nsCOMPtr<nsIContent> mLastPopup;
   nsPopupType mPopupType;
   bool mDeselectMenu;
+  bool mIsRollup;
 };
 
 // this class is used for dispatching menu command events asynchronously.
 class nsXULMenuCommandEvent : public nsRunnable
 {
 public:
   nsXULMenuCommandEvent(nsIContent *aMenu,
                         bool aIsTrusted,
@@ -271,16 +274,17 @@ class nsXULPopupManager MOZ_FINAL : publ
                                     public nsITimerCallback,
                                     public nsIObserver
 {
 
 public:
   friend class nsXULPopupShowingEvent;
   friend class nsXULPopupHidingEvent;
   friend class nsXULMenuCommandEvent;
+  friend class TransitionEnder;
 
   NS_DECL_ISUPPORTS
   NS_DECL_NSIOBSERVER
   NS_DECL_NSITIMERCALLBACK
   NS_DECL_NSIDOMEVENTLISTENER
 
   // nsIRollupListener
   virtual bool Rollup(uint32_t aCount, const nsIntPoint* pos, nsIContent** aLastRolledUp) MOZ_OVERRIDE;
@@ -428,23 +432,25 @@ public:
    * aHideChain - true if the entire chain of menus should be closed. If false,
    *              only this popup is closed.
    * aDeselectMenu - true if the parent <menu> of the popup should be deselected.
    *                 This will be false when the menu is closed by pressing the
    *                 Escape key.
    * aAsynchronous - true if the first popuphiding event should be sent
    *                 asynchrously. This should be true if HidePopup is called
    *                 from a frame.
+   * aIsRollup - true if this popup is hiding due to a rollup or escape keypress.
    * aLastPopup - optional popup to close last when hiding a chain of menus.
    *              If null, then all popups will be closed.
    */
   void HidePopup(nsIContent* aPopup,
                  bool aHideChain,
                  bool aDeselectMenu,
                  bool aAsynchronous,
+                 bool aIsRollup,
                  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);
 
@@ -629,17 +635,19 @@ protected:
                          nsMenuPopupFrame* aPopupFrame,
                          bool aIsContextMenu,
                          bool aSelectFirstItem);
   void HidePopupCallback(nsIContent* aPopup,
                          nsMenuPopupFrame* aPopupFrame,
                          nsIContent* aNextPopup,
                          nsIContent* aLastPopup,
                          nsPopupType aPopupType,
-                         bool aDeselectMenu);
+                         bool aDeselectMenu,
+                         bool aIsRollup);
+  void HidePopupFrame(nsIContent* aPopup, bool aDeselectMenu);
 
   /**
    * Fire a popupshowing event on the popup and then open the popup.
    *
    * aPopup - the popup to open
    * aIsContextMenu - true for context menus
    * aSelectFirstItem - true to select the first item in the menu
    */
@@ -659,23 +667,25 @@ protected:
    * The caller must keep a strong reference to aPopup, aNextPopup and aLastPopup.
    *
    * aPopup - the popup to hide
    * aNextPopup - the next popup to hide
    * aLastPopup - the last popup in the chain to hide
    * aPresContext - nsPresContext for the popup's frame
    * aPopupType - the PopupType of the frame. 
    * aDeselectMenu - true to unhighlight the menu when hiding it
+   * aIsRollup - true if this popup is hiding due to a rollup or escape keypress
    */
   void FirePopupHidingEvent(nsIContent* aPopup,
                             nsIContent* aNextPopup,
                             nsIContent* aLastPopup,
                             nsPresContext *aPresContext,
                             nsPopupType aPopupType,
-                            bool aDeselectMenu);
+                            bool aDeselectMenu,
+                            bool aIsRollup);
 
   /**
    * Handle keyboard navigation within a menu popup specified by aItem.
    */
   bool HandleKeyboardNavigationInPopup(nsMenuChainItem* aItem,
                                          nsNavigationDirection aDir)
   {
     return HandleKeyboardNavigationInPopup(aItem, aItem->Frame(), aDir);
--- a/layout/xul/nsXULTooltipListener.cpp
+++ b/layout/xul/nsXULTooltipListener.cpp
@@ -517,17 +517,17 @@ nsXULTooltipListener::LaunchTooltip()
 nsresult
 nsXULTooltipListener::HideTooltip()
 {
 #ifdef MOZ_XUL
   nsCOMPtr<nsIContent> currentTooltip = do_QueryReferent(mCurrentTooltip);
   if (currentTooltip) {
     nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
     if (pm)
-      pm->HidePopup(currentTooltip, false, false, false);
+      pm->HidePopup(currentTooltip, false, false, false, false);
   }
 #endif
 
   DestroyTooltip();
   return NS_OK;
 }
 
 static void
--- a/toolkit/content/tests/chrome/test_arrowpanel.xul
+++ b/toolkit/content/tests/chrome/test_arrowpanel.xul
@@ -3,45 +3,55 @@
 <?xml-stylesheet href="/tests/SimpleTest/test.css" type="text/css"?>
 
 <window title="Arrow Panels"
         style="padding: 10px;"
         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"/>
 
 <stack flex="1">
   <label id="topleft" value="Top Left Corner" left="15" top="15"/>
   <label id="topright" value="Top Right" right="15" top="15"/>
   <label id="bottomleft" value="Bottom Left Corner" left="15" bottom="15"/>
   <label id="bottomright" value="Bottom Right" right="15" bottom="15"/>
   <!-- Our SimpleTest/TestRunner.js runs tests inside an iframe which sizes are W=500 H=300.
        'left' and 'top' values need to be set so that the panel (popup) has enough room to display on its 4 sides. -->
   <label id="middle" value="+/- Centered" left="225" top="135"/>
   <iframe id="frame" type="content"
           src="data:text/html,&lt;input id='input'&gt;" width="100" height="100" left="225" top="120"/>
 </stack>
 
-<panel id="panel" type="arrow" onpopupshown="checkPanelPosition(this)" onpopuphidden="runNextTest.next()">
+<panel id="panel" type="arrow" animate="false"
+       onpopupshown="checkPanelPosition(this)" onpopuphidden="runNextTest.next()">
   <label id="panellabel" value="This is some text..." height="65"/>
 </panel>
 
-<panel id="bigpanel" type="arrow" onpopupshown="checkBigPanel(this)" onpopuphidden="runNextTest.next()">
+<panel id="bigpanel" type="arrow" animate="false"
+       onpopupshown="checkBigPanel(this)" onpopuphidden="runNextTest.next()">
   <button label="This is some text..." height="3000"/>
 </panel>
 
+<panel id="animatepanel" type="arrow"
+       onpopuphidden="animatedPopupHidden = true; runNextTest.next();">
+  <label value="Animate Closed" height="40"/>
+</panel>
+
 <script type="application/javascript">
 <![CDATA[
 
 SimpleTest.waitForExplicitFinish();
 
 var expectedAnchor = null;
 var expectedSide = "", expectedAnchorEdge = "", expectedPack = "", expectedAlignment = "";
 var zoomFactor = 1;
+var animatedPopupHidden = false;
 var runNextTest;
 
 function startTest()
 {
   runNextTest = nextTest();
   runNextTest.next();
 }
 
@@ -161,17 +171,35 @@ function nextTest()
     $("bottomleft").setAttribute("right", "15");
     $("bottomright").setAttribute("left", "15");
     $("topleft").removeAttribute("left");
     $("topright").removeAttribute("right");
     $("bottomleft").removeAttribute("left");
     $("bottomright").removeAttribute("right");
   }
 
-  SimpleTest.finish();
+  var transitions = 0;
+  function transitionEnded(event) {
+    transitions++;
+    // Two properties transition so continue on the second one finishing.
+    if (!(transitions % 2)) {
+      SimpleTest.executeSoon(() => runNextTest.next());
+    }
+  }
+
+  // Check that the transition occurs for an arrow panel with animate="true"
+  window.addEventListener("transitionend", transitionEnded, false);
+  $("animatepanel").openPopup($("topleft"), "after_start", 0, 0, false, false, null, "start");
+  yield;
+  window.removeEventListener("transitionend", transitionEnded, false);
+  synthesizeKey("VK_ESCAPE", { });
+  ok(!animatedPopupHidden, "animated popup not hidden yet");
+  yield;
+
+  SimpleTest.finish()
   yield;
 }
 
 function setScale(win, scale)
 {
   var wn = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
               .getInterface(Components.interfaces.nsIWebNavigation);
   var shell = wn.QueryInterface(Components.interfaces.nsIDocShell);
--- a/toolkit/content/tests/widgets/test_popupanchor.xul
+++ b/toolkit/content/tests/widgets/test_popupanchor.xul
@@ -3,16 +3,17 @@
 <?xml-stylesheet href="/tests/SimpleTest/test.css" type="text/css"?>
 
 <window title="Popup Anchor Tests"
   xmlns:html="http://www.w3.org/1999/xhtml"
   xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <panel id="testPanel"
          type="arrow"
+         animate="false"
          noautohide="true">
   </panel>
 
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
 
 <script>
 <![CDATA[
 var anchor, panel, arrow;
--- a/toolkit/content/widgets/popup.xml
+++ b/toolkit/content/widgets/popup.xml
@@ -374,16 +374,19 @@
           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 {
@@ -427,44 +430,49 @@
         ]]>
         </body>
       </method>
     </implementation>
     <handlers>
       <handler event="popupshowing" phase="target">
       <![CDATA[
         this.adjustArrowPosition();
+        if (this.getAttribute("animate") != "false") {
+          this.setAttribute("animate", "open");
+        }
+
         // set fading
         var fade = this.getAttribute("fade");
         var fadeDelay = (fade == "fast") ? 1 : fade == "slow" ? 4000 : 0;
         if (fadeDelay) {
-          this._fadeTimer = setTimeout(function (self) {
-            self.style.opacity = 0.2;
-          }, fadeDelay, this);
+          this._fadeTimer = setTimeout(() => this.hidePopup(), fadeDelay, this);
         }
       ]]>
       </handler>
       <handler event="popuphiding" phase="target">
-        clearTimeout(this._fadeTimer);
-        this.style.removeProperty("opacity");
-      </handler>
-      <handler event="transitionend" phase="target">
-      <![CDATA[
-        if (event.propertyName == "opacity" &&
-            event.originalTarget == this) {
-          this.hidePopup();
-          this.style.removeProperty("opacity");
+        let animate = (this.getAttribute("animate") != "false");
+
+        if (this._fadeTimer) {
+          clearTimeout(this._fadeTimer);
+          if (animate) {
+            this.setAttribute("animate", "fade");
+          }
         }
-      ]]>
+        else if (animate) {
+          this.setAttribute("animate", "cancel");
+        }
       </handler>
       <handler event="popupshown" phase="target">
         this.setAttribute("panelopen", "true");
       </handler>
       <handler event="popuphidden" phase="target">
         this.removeAttribute("panelopen");
+        if (this.getAttribute("animate") != "false") {
+          this.removeAttribute("animate");
+        }
       </handler>
     </handlers>
   </binding>
 
   <binding id="tooltip" role="xul:tooltip"
            extends="chrome://global/content/bindings/popup.xml#popup-base">
     <content>
       <children>
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -421,16 +421,75 @@ tooltip {
   white-space: pre-wrap;
   margin-top: 21px;
 }
 
 panel[type="arrow"] {
   -moz-binding: url("chrome://global/content/bindings/popup.xml#arrowpanel");
 }
 
+panel[type="arrow"]:not([animate="false"]) {
+  transform: scale(.1);
+  opacity: 0;
+  transition-property: transform, opacity;
+  transition-duration: 0.25s;
+  transition-timing-function: cubic-bezier(.6, 1.0, .65, 1.0), ease;
+}
+
+panel[type="arrow"][animate="open"] {
+  transform: none;
+  opacity: 1.0;
+}
+
+panel[arrowposition="after_start"] {
+  transform-origin: 20px top;
+}
+
+panel[arrowposition="after_end"] {
+  transform-origin: calc(100% - 20px) top;
+}
+
+panel[arrowposition="before_start"] {
+  transform-origin: 20px bottom;
+}
+
+panel[arrowposition="before_end"] {
+  transform-origin: calc(100% - 20px) bottom;
+}
+
+panel[arrowposition="start_before"] {
+  transform-origin: right 20px;
+}
+
+panel[arrowposition="start_after"] {
+  transform-origin: right calc(100% - 20px);
+}
+
+panel[arrowposition="end_before"] {
+  transform-origin: left 20px;
+}
+
+panel[arrowposition="end_after"] {
+  transform-origin: left calc(100% - 20px);
+}
+
+panel[arrowposition="after_start"][animate="cancel"],
+panel[arrowposition="before_end"][animate="cancel"],
+panel[arrowposition="end_before"][animate="cancel"],
+panel[arrowposition="start_after"][animate="cancel"] {
+  transform: scale(.1) skew(30deg, 20deg);
+}
+
+panel[arrowposition="after_end"][animate="cancel"],
+panel[arrowposition="before_start"][animate="cancel"],
+panel[arrowposition="start_before"][animate="cancel"],
+panel[arrowposition="end_after"][animate="cancel"] {
+  transform: scale(.1) skew(-30deg, -20deg);
+}
+
 %ifdef XP_MACOSX
 .statusbar-resizerpanel {
   display: none;
 }
 %else
 window[sizemode="maximized"] statusbarpanel.statusbar-resizerpanel {
   visibility: collapse;
 }
--- a/toolkit/themes/linux/global/popup.css
+++ b/toolkit/themes/linux/global/popup.css
@@ -11,17 +11,16 @@ panel {
   -moz-appearance: menupopup;
   min-width: 1px;
   color: MenuText;
 }
 
 /* ::::: arrow panel ::::: */
 
 panel[type="arrow"] {
-  transition: opacity 300ms;
   -moz-appearance: none;
 }
 
 panel[type="arrow"][side="top"],
 panel[type="arrow"][side="bottom"] {
   margin-left: -16px;
   margin-right: -16px;
 }
--- a/toolkit/themes/osx/global/popup.css
+++ b/toolkit/themes/osx/global/popup.css
@@ -20,43 +20,16 @@ menupopup > menu > menupopup {
 
 panel[titlebar] {
   -moz-appearance: none; /* to disable rounded corners */
 }
 
 panel[type="arrow"] {
   -moz-appearance: none;
   background: transparent;
-  transition: opacity 300ms;
-}
-
-.panel-arrowcontainer[panelopen] {
-  transition-duration: 200ms, 150ms;
-  transition-property: opacity, transform;
-  transition-timing-function: ease-out;
-}
-
-.panel-arrowcontainer:not([panelopen]) {
-  opacity: 0;
-}
-
-.panel-arrowcontainer:not([panelopen])[side="top"] {
-  transform: translateY(-20px);
-}
-
-.panel-arrowcontainer:not([panelopen])[side="bottom"] {
-  transform: translateY(20px);
-}
-
-.panel-arrowcontainer:not([panelopen])[side="left"] {
-  transform: translateX(-20px);
-}
-
-.panel-arrowcontainer:not([panelopen])[side="right"] {
-  transform: translateX(20px);
 }
 
 panel[type="arrow"][side="top"],
 panel[type="arrow"][side="bottom"] {
   margin-left: -25px;
   margin-right: -25px;
 }
 
--- a/toolkit/themes/windows/global/popup.css
+++ b/toolkit/themes/windows/global/popup.css
@@ -28,43 +28,16 @@ menupopup > menu > menupopup {
   -moz-margin-start: -3px;
   margin-top: -3px;
 }
 
 panel[type="arrow"] {
   -moz-appearance: none;
   background: transparent;
   border: none;
-  transition: opacity 300ms;
-}
-
-.panel-arrowcontainer[panelopen] {
-  transition-duration: 200ms, 150ms;
-  transition-property: opacity, transform;
-  transition-timing-function: ease-out;
-}
-
-.panel-arrowcontainer:not([panelopen]) {
-  opacity: 0;
-}
-
-.panel-arrowcontainer:not([panelopen])[side="top"] {
-  transform: translateY(-20px);
-}
-
-.panel-arrowcontainer:not([panelopen])[side="bottom"] {
-  transform: translateY(20px);
-}
-
-.panel-arrowcontainer:not([panelopen])[side="left"] {
-  transform: translateX(-20px);
-}
-
-.panel-arrowcontainer:not([panelopen])[side="right"] {
-  transform: translateX(20px);
 }
 
 panel[type="arrow"][side="top"],
 panel[type="arrow"][side="bottom"] {
   margin-left: -20px;
   margin-right: -20px;
 }
 
--- a/view/src/nsView.cpp
+++ b/view/src/nsView.cpp
@@ -1019,17 +1019,17 @@ nsView::WindowResized(nsIWidget* aWidget
 
 bool
 nsView::RequestWindowClose(nsIWidget* aWidget)
 {
   if (mFrame && IsPopupWidget(aWidget) &&
       mFrame->GetType() == nsGkAtoms::menuPopupFrame) {
     nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
     if (pm) {
-      pm->HidePopup(mFrame->GetContent(), false, true, false);
+      pm->HidePopup(mFrame->GetContent(), false, true, false, false);
       return true;
     }
   }
 
   return false;
 }
 
 void
--- a/widget/xpwidgets/nsBaseDragService.cpp
+++ b/widget/xpwidgets/nsBaseDragService.cpp
@@ -334,17 +334,17 @@ nsBaseDragService::EndDragSession(bool a
   }
 
   if (aDoneDrag && !mSuppressLevel)
     FireDragEventAtSource(NS_DRAGDROP_END);
 
   if (mDragPopup) {
     nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
     if (pm) {
-      pm->HidePopup(mDragPopup, false, true, false);
+      pm->HidePopup(mDragPopup, false, true, false, false);
     }
   }
 
   mDoingDrag = false;
 
   // release the source we've been holding on to.
   mSourceDocument = nullptr;
   mSourceNode = nullptr;