From e9ba620f38b2247c552e5d3fea3cc5c0c830dafc Mon Sep 17 00:00:00 2001
From: Andreas Fernandez <a.fernandez@scripting-base.de>
Date: Sat, 14 Aug 2021 15:16:15 +0200
Subject: [PATCH] [BUGFIX] Fix various context menu visibility issues

The context menu skeleton is partially visible for a very short time,
before its items are rendered and event listeners are attached. This is
revealed by some flaky acceptance tests and was previously workarounded
by an additional ->wait() call in the tests. The context menu stays
hidden now until everything is set up and the workaround in the AC tests
is removed again.

The context menu gets hidden after 500ms once the user moved the mouse
out of its boundaries. However, that timeout was never reset which closed
another context menu that might have opened within that 500ms time span.
To circument this issue, such timeouts are now monitored and cleared
again once a new context menu is requested.

Resolves: #94891
Releases: master, 10.4
Change-Id: Ia0551d8c0b41edeca27cd0b15e4285a89ad2c171
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/70592
Tested-by: core-ci <typo3@b13.com>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Alexander Nitsche <typo3@alexandernitsche.com>
Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Alexander Nitsche <typo3@alexandernitsche.com>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
---
 .../Public/TypeScript/ContextMenu.ts          | 48 ++++++++++++-------
 .../Public/JavaScript/ContextMenu.js          |  2 +-
 .../Acceptance/Backend/Impexp/ExportCest.php  |  8 ----
 .../Acceptance/Backend/Impexp/ImportCest.php  | 14 ------
 .../Acceptance/Backend/Impexp/UsersCest.php   | 18 -------
 5 files changed, 33 insertions(+), 57 deletions(-)

diff --git a/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/ContextMenu.ts b/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/ContextMenu.ts
index cd8a82381c6c..7018abc6b87a 100644
--- a/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/ContextMenu.ts
+++ b/Build/Sources/TypeScript/backend/Resources/Public/TypeScript/ContextMenu.ts
@@ -46,9 +46,15 @@ interface MenuItems {
  */
 class ContextMenu {
   private mousePos: MousePosition = {X: null, Y: null};
+
+  /**
+   * If this.delayContextMenuHide is set to true, any parent context menu will stay visibile even if the cursor is out
+   * of its boundaries.
+   */
   private delayContextMenuHide: boolean = false;
   private record: ActiveRecord = {uid: null, table: null};
   private eventSources: Element[] = [];
+  private closeMenuTimeout: { [key: string]: number } = {};
 
   /**
    * @param {MenuItem} item
@@ -88,8 +94,8 @@ class ContextMenu {
    */
   private static initializeContextMenuContainer(): void {
     if ($('#contentMenu0').length === 0) {
-      const code = '<div id="contentMenu0" class="context-menu"></div>'
-        + '<div id="contentMenu1" class="context-menu" style="display: block;"></div>';
+      const code = '<div id="contentMenu0" class="context-menu" style="display: none;"></div>'
+        + '<div id="contentMenu1" class="context-menu" style="display: none;"></div>';
       $('body').append(code);
     }
   }
@@ -128,6 +134,9 @@ class ContextMenu {
    * @param {Element} eventSource Source Element
    */
   public show(table: string, uid: number|string, context: string, enDisItems: string, addParams: string, eventSource: Element = null): void {
+    this.hideAll();
+    this.closeMenuTimeout = {};
+
     this.record = {table: table, uid: uid};
     // fix: [tabindex=-1] is not focusable!!!
     const focusableSource = eventSource.matches('a, button, [tabindex]') ? eventSource : eventSource.closest('a, button, [tabindex]');
@@ -435,34 +444,41 @@ class ContextMenu {
       this.hide(obj);
     } else if ($element.length > 0 && $element.is(':visible')) {
       this.delayContextMenuHide = true;
+      window.clearTimeout(this.closeMenuTimeout[obj]);
     }
   }
 
   /**
    * @param {string} obj
+   * @param {boolean} withDelay
    */
-  private hide(obj: string): void {
+  private hide(obj: string, withDelay: boolean = true): void {
     this.delayContextMenuHide = false;
-    window.setTimeout(
-      (): void => {
-        if (!this.delayContextMenuHide) {
-          $(obj).hide();
-          const source = this.eventSources.pop();
-          if (source) {
-            $(source).focus();
-          }
+    window.clearTimeout(this.closeMenuTimeout[obj]);
+
+    const delayHandler = () => {
+      if (!this.delayContextMenuHide) {
+        $(obj).hide();
+        const source = this.eventSources.pop();
+        if (source) {
+          $(source).focus();
         }
-      },
-      500
-    );
+      }
+    };
+
+    if (withDelay) {
+      this.closeMenuTimeout[obj] = window.setTimeout(delayHandler, 500);
+    } else {
+      delayHandler();
+    }
   }
 
   /**
    * Hides all context menus
    */
   private hideAll(): void {
-    this.hide('#contentMenu0');
-    this.hide('#contentMenu1');
+    this.hide('#contentMenu0', false);
+    this.hide('#contentMenu1', false);
   }
 }
 
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/ContextMenu.js b/typo3/sysext/backend/Resources/Public/JavaScript/ContextMenu.js
index dd727110f8b6..23ad3c4bf102 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/ContextMenu.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/ContextMenu.js
@@ -10,4 +10,4 @@
  *
  * The TYPO3 project - inspiring people to share!
  */
-var __importDefault=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};define(["require","exports","jquery","TYPO3/CMS/Core/Ajax/AjaxRequest","./ContextMenuActions","TYPO3/CMS/Core/Event/ThrottleEvent"],(function(t,e,s,i,n,o){"use strict";s=__importDefault(s);class a{constructor(){this.mousePos={X:null,Y:null},this.delayContextMenuHide=!1,this.record={uid:null,table:null},this.eventSources=[],this.storeMousePositionEvent=t=>{this.mousePos={X:t.pageX,Y:t.pageY},this.mouseOutFromMenu("#contentMenu0"),this.mouseOutFromMenu("#contentMenu1")},s.default(document).on("click contextmenu",".t3js-contextmenutrigger",t=>{const e=s.default(t.currentTarget);e.prop("onclick")&&"click"===t.type||(t.preventDefault(),this.show(e.data("table"),e.data("uid"),e.data("context"),e.data("iteminfo"),e.data("parameters"),t.target))}),new o("mousemove",this.storeMousePositionEvent.bind(this),50).bindTo(document)}static drawActionItem(t){const e=t.additionalAttributes||{};let s="";for(const t of Object.entries(e)){const[e,i]=t;s+=" "+e+'="'+i+'"'}return'<li role="menuitem" class="list-group-item" tabindex="-1" data-callback-action="'+t.callbackAction+'"'+s+'><span class="list-group-item-icon">'+t.icon+"</span> "+t.label+"</li>"}static within(t,e,s){const i=t.offset();return s>=i.top&&s<i.top+t.height()&&e>=i.left&&e<i.left+t.width()}static initializeContextMenuContainer(){if(0===s.default("#contentMenu0").length){const t='<div id="contentMenu0" class="context-menu"></div><div id="contentMenu1" class="context-menu" style="display: block;"></div>';s.default("body").append(t)}}show(t,e,s,i,n,o=null){this.record={table:t,uid:e};const a=o.matches("a, button, [tabindex]")?o:o.closest("a, button, [tabindex]");this.eventSources.push(a);let l="";void 0!==t&&(l+="table="+encodeURIComponent(t)),void 0!==e&&(l+=(l.length>0?"&":"")+"uid="+e),void 0!==s&&(l+=(l.length>0?"&":"")+"context="+s),void 0!==i&&(l+=(l.length>0?"&":"")+"enDisItems="+i),void 0!==n&&(l+=(l.length>0?"&":"")+"addParams="+n),this.fetch(l)}fetch(t){const e=TYPO3.settings.ajaxUrls.contextmenu;new i(e).withQueryArguments(t).get().then(async t=>{const e=await t.resolve();void 0!==t&&Object.keys(t).length>0&&this.populateData(e,0)})}populateData(e,i){a.initializeContextMenuContainer();const o=s.default("#contentMenu"+i);if(o.length&&(0===i||s.default("#contentMenu"+(i-1)).is(":visible"))){const a=this.drawMenu(e,i);o.html('<ul class="list-group">'+a+"</ul>"),s.default("li.list-group-item",o).on("click",e=>{e.preventDefault();const o=s.default(e.currentTarget);if(o.hasClass("list-group-item-submenu"))return void this.openSubmenu(i,o,!1);const a=o.data("callback-action"),l=o.data("callback-module");o.data("callback-module")?t([l],t=>{t[a].bind(o)(this.record.table,this.record.uid)}):n&&"function"==typeof n[a]?n[a].bind(o)(this.record.table,this.record.uid):console.log("action: "+a+" not found"),this.hideAll()}),s.default("li.list-group-item",o).on("keydown",t=>{const e=s.default(t.currentTarget);switch(t.key){case"Down":case"ArrowDown":this.setFocusToNextItem(e.get(0));break;case"Up":case"ArrowUp":this.setFocusToPreviousItem(e.get(0));break;case"Right":case"ArrowRight":if(!e.hasClass("list-group-item-submenu"))return;this.openSubmenu(i,e,!0);break;case"Home":this.setFocusToFirstItem(e.get(0));break;case"End":this.setFocusToLastItem(e.get(0));break;case"Enter":case"Space":e.click();break;case"Esc":case"Escape":case"Left":case"ArrowLeft":this.hide("#"+e.parents(".context-menu").first().attr("id"));break;case"Tab":this.hideAll();break;default:return}t.preventDefault()}),o.css(this.getPosition(o,!1)).show(),s.default("li.list-group-item[tabindex=-1]",o).first().focus()}}setFocusToPreviousItem(t){let e=this.getItemBackward(t.previousElementSibling);e||(e=this.getLastItem(t)),e.focus()}setFocusToNextItem(t){let e=this.getItemForward(t.nextElementSibling);e||(e=this.getFirstItem(t)),e.focus()}setFocusToFirstItem(t){let e=this.getFirstItem(t);e&&e.focus()}setFocusToLastItem(t){let e=this.getLastItem(t);e&&e.focus()}getItemBackward(t){for(;t&&(!t.classList.contains("list-group-item")||"-1"!==t.getAttribute("tabindex"));)t=t.previousElementSibling;return t}getItemForward(t){for(;t&&(!t.classList.contains("list-group-item")||"-1"!==t.getAttribute("tabindex"));)t=t.nextElementSibling;return t}getFirstItem(t){return this.getItemForward(t.parentElement.firstElementChild)}getLastItem(t){return this.getItemBackward(t.parentElement.lastElementChild)}openSubmenu(t,e,i){this.eventSources.push(e[0]);const n=s.default("#contentMenu"+(t+1)).html("");e.next().find(".list-group").clone(!0).appendTo(n),n.css(this.getPosition(n,i)).show(),s.default(".list-group-item[tabindex=-1]",n).first().focus()}getPosition(t,e){let i=0,n=0;if(this.eventSources.length&&(null===this.mousePos.X||e)){const t=this.eventSources[this.eventSources.length-1].getBoundingClientRect();i=this.eventSources.length>1?t.right:t.x,n=t.y}else i=this.mousePos.X,n=this.mousePos.Y;const o=s.default(window).width()-20,a=s.default(window).height(),l=t.width(),u=t.height(),r=i-s.default(document).scrollLeft(),c=n-s.default(document).scrollTop();return a-u<c&&(c>u?n-=u-10:n+=a-u-c),o-l<r&&(r>l?i-=l-10:o-l-r<s.default(document).scrollLeft()?i=s.default(document).scrollLeft():i+=o-l-r),{left:i+"px",top:n+"px"}}drawMenu(t,e){let s="";for(const i of Object.values(t))if("item"===i.type)s+=a.drawActionItem(i);else if("divider"===i.type)s+='<li role="separator" class="list-group-item list-group-item-divider"></li>';else if("submenu"===i.type||i.childItems){s+='<li role="menuitem" aria-haspopup="true" class="list-group-item list-group-item-submenu" tabindex="-1"><span class="list-group-item-icon">'+i.icon+"</span> "+i.label+'&nbsp;&nbsp;<span class="fa fa-caret-right"></span></li>';s+='<div class="context-menu contentMenu'+(e+1)+'" style="display:none;"><ul role="menu" class="list-group">'+this.drawMenu(i.childItems,1)+"</ul></div>"}return s}mouseOutFromMenu(t){const e=s.default(t);e.length>0&&e.is(":visible")&&!a.within(e,this.mousePos.X,this.mousePos.Y)?this.hide(t):e.length>0&&e.is(":visible")&&(this.delayContextMenuHide=!0)}hide(t){this.delayContextMenuHide=!1,window.setTimeout(()=>{if(!this.delayContextMenuHide){s.default(t).hide();const e=this.eventSources.pop();e&&s.default(e).focus()}},500)}hideAll(){this.hide("#contentMenu0"),this.hide("#contentMenu1")}}return new a}));
\ No newline at end of file
+var __importDefault=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};define(["require","exports","jquery","TYPO3/CMS/Core/Ajax/AjaxRequest","./ContextMenuActions","TYPO3/CMS/Core/Event/ThrottleEvent"],(function(t,e,s,i,n,o){"use strict";s=__importDefault(s);class u{constructor(){this.mousePos={X:null,Y:null},this.delayContextMenuHide=!1,this.record={uid:null,table:null},this.eventSources=[],this.closeMenuTimeout={},this.storeMousePositionEvent=t=>{this.mousePos={X:t.pageX,Y:t.pageY},this.mouseOutFromMenu("#contentMenu0"),this.mouseOutFromMenu("#contentMenu1")},s.default(document).on("click contextmenu",".t3js-contextmenutrigger",t=>{const e=s.default(t.currentTarget);e.prop("onclick")&&"click"===t.type||(t.preventDefault(),this.show(e.data("table"),e.data("uid"),e.data("context"),e.data("iteminfo"),e.data("parameters"),t.target))}),new o("mousemove",this.storeMousePositionEvent.bind(this),50).bindTo(document)}static drawActionItem(t){const e=t.additionalAttributes||{};let s="";for(const t of Object.entries(e)){const[e,i]=t;s+=" "+e+'="'+i+'"'}return'<li role="menuitem" class="list-group-item" tabindex="-1" data-callback-action="'+t.callbackAction+'"'+s+'><span class="list-group-item-icon">'+t.icon+"</span> "+t.label+"</li>"}static within(t,e,s){const i=t.offset();return s>=i.top&&s<i.top+t.height()&&e>=i.left&&e<i.left+t.width()}static initializeContextMenuContainer(){if(0===s.default("#contentMenu0").length){const t='<div id="contentMenu0" class="context-menu" style="display: none;"></div><div id="contentMenu1" class="context-menu" style="display: none;"></div>';s.default("body").append(t)}}show(t,e,s,i,n,o=null){this.hideAll(),this.closeMenuTimeout={},this.record={table:t,uid:e};const u=o.matches("a, button, [tabindex]")?o:o.closest("a, button, [tabindex]");this.eventSources.push(u);let l="";void 0!==t&&(l+="table="+encodeURIComponent(t)),void 0!==e&&(l+=(l.length>0?"&":"")+"uid="+e),void 0!==s&&(l+=(l.length>0?"&":"")+"context="+s),void 0!==i&&(l+=(l.length>0?"&":"")+"enDisItems="+i),void 0!==n&&(l+=(l.length>0?"&":"")+"addParams="+n),this.fetch(l)}fetch(t){const e=TYPO3.settings.ajaxUrls.contextmenu;new i(e).withQueryArguments(t).get().then(async t=>{const e=await t.resolve();void 0!==t&&Object.keys(t).length>0&&this.populateData(e,0)})}populateData(e,i){u.initializeContextMenuContainer();const o=s.default("#contentMenu"+i);if(o.length&&(0===i||s.default("#contentMenu"+(i-1)).is(":visible"))){const u=this.drawMenu(e,i);o.html('<ul class="list-group">'+u+"</ul>"),s.default("li.list-group-item",o).on("click",e=>{e.preventDefault();const o=s.default(e.currentTarget);if(o.hasClass("list-group-item-submenu"))return void this.openSubmenu(i,o,!1);const u=o.data("callback-action"),l=o.data("callback-module");o.data("callback-module")?t([l],t=>{t[u].bind(o)(this.record.table,this.record.uid)}):n&&"function"==typeof n[u]?n[u].bind(o)(this.record.table,this.record.uid):console.log("action: "+u+" not found"),this.hideAll()}),s.default("li.list-group-item",o).on("keydown",t=>{const e=s.default(t.currentTarget);switch(t.key){case"Down":case"ArrowDown":this.setFocusToNextItem(e.get(0));break;case"Up":case"ArrowUp":this.setFocusToPreviousItem(e.get(0));break;case"Right":case"ArrowRight":if(!e.hasClass("list-group-item-submenu"))return;this.openSubmenu(i,e,!0);break;case"Home":this.setFocusToFirstItem(e.get(0));break;case"End":this.setFocusToLastItem(e.get(0));break;case"Enter":case"Space":e.click();break;case"Esc":case"Escape":case"Left":case"ArrowLeft":this.hide("#"+e.parents(".context-menu").first().attr("id"));break;case"Tab":this.hideAll();break;default:return}t.preventDefault()}),o.css(this.getPosition(o,!1)).show(),s.default("li.list-group-item[tabindex=-1]",o).first().focus()}}setFocusToPreviousItem(t){let e=this.getItemBackward(t.previousElementSibling);e||(e=this.getLastItem(t)),e.focus()}setFocusToNextItem(t){let e=this.getItemForward(t.nextElementSibling);e||(e=this.getFirstItem(t)),e.focus()}setFocusToFirstItem(t){let e=this.getFirstItem(t);e&&e.focus()}setFocusToLastItem(t){let e=this.getLastItem(t);e&&e.focus()}getItemBackward(t){for(;t&&(!t.classList.contains("list-group-item")||"-1"!==t.getAttribute("tabindex"));)t=t.previousElementSibling;return t}getItemForward(t){for(;t&&(!t.classList.contains("list-group-item")||"-1"!==t.getAttribute("tabindex"));)t=t.nextElementSibling;return t}getFirstItem(t){return this.getItemForward(t.parentElement.firstElementChild)}getLastItem(t){return this.getItemBackward(t.parentElement.lastElementChild)}openSubmenu(t,e,i){this.eventSources.push(e[0]);const n=s.default("#contentMenu"+(t+1)).html("");e.next().find(".list-group").clone(!0).appendTo(n),n.css(this.getPosition(n,i)).show(),s.default(".list-group-item[tabindex=-1]",n).first().focus()}getPosition(t,e){let i=0,n=0;if(this.eventSources.length&&(null===this.mousePos.X||e)){const t=this.eventSources[this.eventSources.length-1].getBoundingClientRect();i=this.eventSources.length>1?t.right:t.x,n=t.y}else i=this.mousePos.X,n=this.mousePos.Y;const o=s.default(window).width()-20,u=s.default(window).height(),l=t.width(),a=t.height(),c=i-s.default(document).scrollLeft(),r=n-s.default(document).scrollTop();return u-a<r&&(r>a?n-=a-10:n+=u-a-r),o-l<c&&(c>l?i-=l-10:o-l-c<s.default(document).scrollLeft()?i=s.default(document).scrollLeft():i+=o-l-c),{left:i+"px",top:n+"px"}}drawMenu(t,e){let s="";for(const i of Object.values(t))if("item"===i.type)s+=u.drawActionItem(i);else if("divider"===i.type)s+='<li role="separator" class="list-group-item list-group-item-divider"></li>';else if("submenu"===i.type||i.childItems){s+='<li role="menuitem" aria-haspopup="true" class="list-group-item list-group-item-submenu" tabindex="-1"><span class="list-group-item-icon">'+i.icon+"</span> "+i.label+'&nbsp;&nbsp;<span class="fa fa-caret-right"></span></li>';s+='<div class="context-menu contentMenu'+(e+1)+'" style="display:none;"><ul role="menu" class="list-group">'+this.drawMenu(i.childItems,1)+"</ul></div>"}return s}mouseOutFromMenu(t){const e=s.default(t);e.length>0&&e.is(":visible")&&!u.within(e,this.mousePos.X,this.mousePos.Y)?this.hide(t):e.length>0&&e.is(":visible")&&(this.delayContextMenuHide=!0,window.clearTimeout(this.closeMenuTimeout[t]))}hide(t,e=!0){this.delayContextMenuHide=!1,window.clearTimeout(this.closeMenuTimeout[t]);const i=()=>{if(!this.delayContextMenuHide){s.default(t).hide();const e=this.eventSources.pop();e&&s.default(e).focus()}};e?this.closeMenuTimeout[t]=window.setTimeout(i,500):i()}hideAll(){this.hide("#contentMenu0",!1),this.hide("#contentMenu1",!1)}}return new u}));
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Acceptance/Backend/Impexp/ExportCest.php b/typo3/sysext/core/Tests/Acceptance/Backend/Impexp/ExportCest.php
index f3b2da3f10bc..dd9938f44fd8 100644
--- a/typo3/sysext/core/Tests/Acceptance/Backend/Impexp/ExportCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Backend/Impexp/ExportCest.php
@@ -84,8 +84,6 @@ class ExportCest
 
         $I->click($selectedPageIcon);
         $I->waitForElementVisible($contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($contextMenuMore);
         $I->waitForElementVisible($contextMenuExport, 5);
         $I->click($contextMenuExport);
@@ -155,8 +153,6 @@ class ExportCest
         $I->waitForElementNotVisible('#nprogress');
         $I->click($recordIcon, $recordTable);
         $I->waitForElementVisible($contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($contextMenuMore);
         $I->waitForElementVisible($contextMenuExport, 5);
         $I->click($contextMenuExport);
@@ -188,8 +184,6 @@ class ExportCest
 
         $I->click($pageIcon);
         $I->waitForElementVisible($contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($contextMenuMore);
         $I->waitForElementVisible($contextMenuExport, 5);
         $I->click($contextMenuExport);
@@ -291,8 +285,6 @@ class ExportCest
         $I->waitForElementNotVisible('#nprogress');
         $I->click($sysLanguageIcon, $sysLanguageTable);
         $I->waitForElementVisible($contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($contextMenuMore);
         $I->waitForText('Export');
         $I->waitForElementVisible($contextMenuExport, 5);
diff --git a/typo3/sysext/core/Tests/Acceptance/Backend/Impexp/ImportCest.php b/typo3/sysext/core/Tests/Acceptance/Backend/Impexp/ImportCest.php
index 19507fdc4a3c..ab40303436b2 100644
--- a/typo3/sysext/core/Tests/Acceptance/Backend/Impexp/ImportCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Backend/Impexp/ImportCest.php
@@ -92,8 +92,6 @@ class ImportCest
         $pageInPageTreeIcon = '//*[text()=\'' . $pageInPageTreeTitle . '\']/../*[contains(@class, \'node-icon-container\')]';
         $I->click($pageInPageTreeIcon);
         $I->waitForElementVisible($this->contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($this->contextMenuMore);
         $I->waitForElementVisible($this->contextMenuImport, 5);
         $I->click($this->contextMenuImport);
@@ -112,8 +110,6 @@ class ImportCest
 
         $I->click($page1Icon);
         $I->waitForElementVisible($this->contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($this->contextMenuMore);
         $I->waitForElementVisible($this->contextMenuImport, 5);
         $I->click($this->contextMenuImport);
@@ -170,8 +166,6 @@ class ImportCest
 
         $I->click($page1Icon);
         $I->waitForElementVisible($this->contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($this->contextMenuMore);
         $I->waitForElementVisible($this->contextMenuImport, 5);
         $I->click($this->contextMenuImport);
@@ -212,8 +206,6 @@ class ImportCest
 
         $I->click($page1Icon);
         $I->waitForElementVisible($this->contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($this->contextMenuMore);
         $I->waitForElementVisible($this->contextMenuImport, 5);
         $I->click($this->contextMenuImport);
@@ -265,8 +257,6 @@ class ImportCest
 
         $I->click($page1Icon);
         $I->waitForElementVisible($this->contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($this->contextMenuMore);
         $I->waitForElementVisible($this->contextMenuImport, 5);
         $I->click($this->contextMenuImport);
@@ -321,8 +311,6 @@ class ImportCest
 
         $I->click($page1Icon);
         $I->waitForElementVisible($this->contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($this->contextMenuMore);
         $I->waitForElementVisible($this->contextMenuImport, 5);
         $I->click($this->contextMenuImport);
@@ -380,8 +368,6 @@ class ImportCest
 
         $I->click($page1Icon);
         $I->waitForElementVisible($this->contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($this->contextMenuMore);
         $I->waitForElementVisible($this->contextMenuImport, 5);
         $I->click($this->contextMenuImport);
diff --git a/typo3/sysext/core/Tests/Acceptance/Backend/Impexp/UsersCest.php b/typo3/sysext/core/Tests/Acceptance/Backend/Impexp/UsersCest.php
index 887221813a55..0ce533febbd9 100644
--- a/typo3/sysext/core/Tests/Acceptance/Backend/Impexp/UsersCest.php
+++ b/typo3/sysext/core/Tests/Acceptance/Backend/Impexp/UsersCest.php
@@ -67,8 +67,6 @@ class UsersCest extends AbstractCest
 
         $I->click($selectedPageIcon);
         $I->waitForElementVisible($this->contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($this->contextMenuMore);
         $I->waitForElementVisible($this->contextMenuExport, 5);
         $I->seeElement($this->contextMenuExport);
@@ -92,8 +90,6 @@ class UsersCest extends AbstractCest
 
         $I->click($selectedPageIcon);
         $I->waitForElementVisible($this->contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($this->contextMenuMore);
         $I->waitForElementVisible($this->contextMenuExport, 5);
         $I->seeElement($this->contextMenuExport);
@@ -115,8 +111,6 @@ class UsersCest extends AbstractCest
 
         $I->click($selectedPageIcon);
         $I->waitForElementVisible($this->contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($this->contextMenuMore);
         $I->waitForElementVisible($this->contextMenuImport, 5);
         $I->click($this->contextMenuImport);
@@ -127,8 +121,6 @@ class UsersCest extends AbstractCest
 
         $I->click($selectedPageIcon);
         $I->waitForElementVisible($this->contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($this->contextMenuMore);
         $I->waitForElementVisible($this->contextMenuImport, 5);
         $I->click($this->contextMenuImport);
@@ -152,8 +144,6 @@ class UsersCest extends AbstractCest
 
         $I->click($selectedPageIcon);
         $I->waitForElementVisible($this->contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($this->contextMenuMore);
         $I->waitForElementVisible($this->contextMenuImport, 5);
         $I->click($this->contextMenuImport);
@@ -165,8 +155,6 @@ class UsersCest extends AbstractCest
 
         $I->click($selectedPageIcon);
         $I->waitForElementVisible($this->contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($this->contextMenuMore);
         $I->waitForElementVisible($this->contextMenuImport, 5);
         $I->click($this->contextMenuImport);
@@ -195,8 +183,6 @@ class UsersCest extends AbstractCest
 
         $I->click($this->inPageTree . ' .node.identifier-0_0 .node-icon-container');
         $I->waitForElementVisible($this->contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($this->contextMenuMore);
         $I->waitForElementVisible($this->contextMenuExport, 5);
         $I->click($this->contextMenuImport);
@@ -209,8 +195,6 @@ class UsersCest extends AbstractCest
         $I->click('List');
         $I->click($selectedPageIcon);
         $I->waitForElementVisible($this->contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($this->contextMenuMore);
         $I->waitForElementVisible($this->contextMenuExport, 5);
         $I->click($this->contextMenuImport);
@@ -224,8 +208,6 @@ class UsersCest extends AbstractCest
 
         $I->click($selectedPageIcon);
         $I->waitForElementVisible($this->contextMenuMore, 5);
-        // Give JS 2 seconds for event registration, so click on 'more' works
-        $I->wait(1);
         $I->click($this->contextMenuMore);
         $I->waitForElementVisible($this->contextMenuExport, 5);
         $I->click($this->contextMenuImport);
-- 
GitLab