Bug 610545, arrow panels should animate when opening and when cancelling, r=neil,dao
☠☠ backed out by 2c78aa5b01ff ☠ ☠
authorNeil Deakin <neil@mozilla.com>
Mon, 31 Mar 2014 08:42:32 -0400
changeset 176214 25877e8f89c2d1784958772337a383a32e1f2783
parent 176213 fe53a01dab3d004bf0c96aeea3d2e88571f7f82e
child 176215 efbf80a15a2b364ab02d19fa726cf72e71729316
push id26516
push userryanvm@gmail.com
push dateMon, 31 Mar 2014 21:14:11 +0000
treeherdermozilla-central@723177fe1452 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersneil, dao
bugs610545
milestone31.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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
@@ -291,17 +291,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()
 {
@@ -817,16 +817,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) {
@@ -909,33 +910,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
@@ -961,28 +998,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;
@@ -1009,27 +1059,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();
@@ -1278,17 +1348,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);
 
@@ -1318,17 +1389,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
@@ -1571,17 +1642,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);
@@ -1771,17 +1842,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)
 {
@@ -1967,17 +2038,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
@@ -1987,17 +2058,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) {
@@ -2011,17 +2082,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:
@@ -2318,17 +2389,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
@@ -2379,12 +2450,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
@@ -5,16 +5,17 @@
 <window title="Popup Anchor Tests"
   xmlns:html="http://www.w3.org/1999/xhtml"
   xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <title>Popup Popup Tests</title>
 
   <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;