diff --git a/widget/cocoa/nsMenuX.h b/widget/cocoa/nsMenuX.h
index 9b9068d8a81a65e0dd147be30f559d99d72f2318..91f1dbd6194cd82bb194be2548a8df268638c851 100644
--- a/widget/cocoa/nsMenuX.h
+++ b/widget/cocoa/nsMenuX.h
@@ -89,7 +89,6 @@ class nsMenuX final : public nsMenuParentX,
 
   // Called from the menu delegate during menuWillOpen, or to simulate opening.
   // Ignored if the menu is already considered open.
-  // Fires the popupshown event, if it hasn't been sent yet for this opening.
   // When calling this method, the caller must hold a strong reference to this object, because other
   // references to this object can be dropped during the handling of the DOM event.
   void MenuOpened();
@@ -171,6 +170,9 @@ class nsMenuX final : public nsMenuParentX,
   // number of visible previous siblings of aChild in mMenuChildren.
   NSInteger CalculateNativeInsertionPoint(nsMenuX* aChild);
 
+  // Fires the popupshown event.
+  void MenuOpenedAsync();
+
   // Called from mPendingAsyncMenuCloseRunnable asynchronously after MenuClosed(), so that it runs
   // after any potential menuItemHit calls for clicked menu items.
   // Fires popuphiding and popuphidden events.
@@ -178,6 +180,10 @@ class nsMenuX final : public nsMenuParentX,
   // references to this object can be dropped during the handling of the DOM event.
   void MenuClosedAsync();
 
+  // If mPendingAsyncMenuOpenRunnable is non-null, call MenuOpenedAsync() to send out the pending
+  // popupshown event.
+  void FlushMenuOpenedRunnable();
+
   // If mPendingAsyncMenuCloseRunnable is non-null, call MenuClosedAsync() to send out pending
   // popuphiding/popuphidden events.
   void FlushMenuClosedRunnable();
@@ -196,6 +202,9 @@ class nsMenuX final : public nsMenuParentX,
 
   Observer* mObserver = nullptr;  // non-owning pointer to our observer
 
+  // Non-null between a call to MenuOpened() and MenuOpenedAsync().
+  RefPtr<mozilla::CancelableRunnable> mPendingAsyncMenuOpenRunnable;
+
   // Non-null between a call to MenuClosed() and MenuClosedAsync().
   // This is asynchronous so that, if a menu item is clicked, we can fire popuphiding *after* we
   // execute the menu item command. The macOS menu system calls menuWillClose *before* it calls
diff --git a/widget/cocoa/nsMenuX.mm b/widget/cocoa/nsMenuX.mm
index c80024db5a1f3a3b0fb7993ad5caa1d02b597d79..43381f7838c18253d83ed9f880281484b4b74030 100644
--- a/widget/cocoa/nsMenuX.mm
+++ b/widget/cocoa/nsMenuX.mm
@@ -128,6 +128,9 @@ nsMenuX::nsMenuX(nsMenuParentX* aParent, nsMenuGroupOwnerX* aMenuGroupOwner, nsI
 nsMenuX::~nsMenuX() {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
 
+  // Make sure a pending popupshown event isn't dropped.
+  FlushMenuOpenedRunnable();
+
   if (mIsOpen) {
     [mNativeMenu cancelTracking];
   }
@@ -332,6 +335,10 @@ void nsMenuX::MenuOpened() {
     return;
   }
 
+  // Make sure we fire any pending popupshown / popuphiding / popuphidden events first.
+  FlushMenuOpenedRunnable();
+  FlushMenuClosedRunnable();
+
   if (!mDidFirePopupshowingAndIsApprovedToOpen) {
     // Fire popupshowing now.
     bool approvedToOpen = OnOpen();
@@ -346,8 +353,56 @@ void nsMenuX::MenuOpened() {
 
   mIsOpen = true;
 
-  // Make sure we fire any pending popuphiding / popuphidden events first.
-  FlushMenuClosedRunnable();
+  // Reset mDidFirePopupshowingAndIsApprovedToOpen for the next menu opening.
+  mDidFirePopupshowingAndIsApprovedToOpen = false;
+
+  if (mNeedsRebuild) {
+    OnHighlightedItemChanged(Nothing());
+    RemoveAll();
+    RebuildMenu();
+  }
+
+  // Fire the popupshown event in MenuOpenedAsync.
+  // MenuOpened() is called during menuWillOpen, and if cancelTracking is called now, menuDidClose
+  // will not be called.
+  // The runnable object must not hold a strong reference to the nsMenuX, so that there is no
+  // reference cycle.
+  class MenuOpenedAsyncRunnable final : public mozilla::CancelableRunnable {
+   public:
+    explicit MenuOpenedAsyncRunnable(nsMenuX* aMenu)
+        : CancelableRunnable("MenuOpenedAsyncRunnable"), mMenu(aMenu) {}
+
+    nsresult Run() override {
+      if (mMenu) {
+        RefPtr<nsMenuX> menu = mMenu;
+        menu->MenuOpenedAsync();
+        mMenu = nullptr;
+      }
+      return NS_OK;
+    }
+    nsresult Cancel() override {
+      mMenu = nullptr;
+      return NS_OK;
+    }
+
+   private:
+    nsMenuX* mMenu;  // weak, cleared by Cancel() and Run()
+  };
+  mPendingAsyncMenuOpenRunnable = new MenuOpenedAsyncRunnable(this);
+  NS_DispatchToCurrentThread(mPendingAsyncMenuOpenRunnable);
+}
+
+void nsMenuX::FlushMenuOpenedRunnable() {
+  if (mPendingAsyncMenuOpenRunnable) {
+    MenuOpenedAsync();
+  }
+}
+
+void nsMenuX::MenuOpenedAsync() {
+  if (mPendingAsyncMenuOpenRunnable) {
+    mPendingAsyncMenuOpenRunnable->Cancel();
+    mPendingAsyncMenuOpenRunnable = nullptr;
+  }
 
   mIsOpenForGecko = true;
 
@@ -356,26 +411,17 @@ void nsMenuX::MenuOpened() {
     mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"true"_ns, true);
   }
 
+  // Fire popupshown.
   nsEventStatus status = nsEventStatus_eIgnore;
   WidgetMouseEvent event(true, eXULPopupShown, nullptr, WidgetMouseEvent::eReal);
-
   nsCOMPtr<nsIContent> popupContent = GetMenuPopupContent();
   nsIContent* dispatchTo = popupContent ? popupContent : mContent;
   EventDispatcher::Dispatch(dispatchTo, nullptr, &event, nullptr, &status);
 
-  // Reset mDidFirePopupshowingAndIsApprovedToOpen for then next menu opening.
-  mDidFirePopupshowingAndIsApprovedToOpen = false;
-
   // Notify our observer.
   if (mObserver) {
     mObserver->OnMenuOpened();
   }
-
-  if (mNeedsRebuild) {
-    OnHighlightedItemChanged(Nothing());
-    RemoveAll();
-    RebuildMenu();
-  }
 }
 
 void nsMenuX::MenuClosed() {
@@ -383,6 +429,9 @@ void nsMenuX::MenuClosed() {
     return;
   }
 
+  // Make sure we fire any pending popupshown events first.
+  FlushMenuOpenedRunnable();
+
   // If any of our submenus were opened programmatically, make sure they get closed first.
   for (auto& child : mMenuChildren) {
     if (child.is<RefPtr<nsMenuX>>()) {
@@ -518,6 +567,8 @@ void nsMenuX::ActivateItemAndClose(RefPtr<nsMenuItemX>&& aItem, NSEventModifierF
 bool nsMenuX::Close() {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
 
+  FlushMenuOpenedRunnable();
+
   bool wasOpen = mIsOpenForGecko;
 
   if (mIsOpen) {