Bug 1458066 - Implement ability to move a selection of tabs within the same window through drag and drop. r?jaws draft mutliselect_move_within_window_drag
authorAbdoulaye O. Ly <ablayelyfondou@gmail.com>
Mon, 23 Jul 2018 19:01:17 +0000
branchmutliselect_move_within_window_drag
changeset 825428 6b7249bcfa9df3e64dd2a144bc62f3400646467d
parent 824238 dead9fcddd4a25fd36d54ab7eb782d7d9b8bb7a1
push id118106
push userbmo:ablayelyfondou@gmail.com
push dateWed, 01 Aug 2018 19:04:10 +0000
reviewersjaws
bugs1458066
milestone63.0a1
Bug 1458066 - Implement ability to move a selection of tabs within the same window through drag and drop. r?jaws MozReview-Commit-ID: A3t0CAgrI5Z
browser/base/content/browser.css
browser/base/content/tabbrowser.js
browser/base/content/tabbrowser.xml
browser/base/content/test/general/browser.ini
browser/base/content/test/general/browser_tabReorder.js
browser/base/content/test/tabs/browser.ini
browser/base/content/test/tabs/browser_multiselect_tabs_reorder.js
browser/base/content/test/tabs/browser_tabReorder.js
browser/base/content/test/tabs/browser_tabReorder_overflow.js
browser/base/content/test/tabs/head.js
browser/themes/shared/tabs.inc.css
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -211,24 +211,25 @@ panelview[mainview] > .panel-header {
 }
 %endif
 
 #tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned] {
   position: fixed !important;
   display: block; /* position:fixed already does this (bug 579776), but let's be explicit */
 }
 
-#tabbrowser-tabs[movingtab] > .tabbrowser-tab[selected] {
+#tabbrowser-tabs[movingtab] > .tabbrowser-tab[selected],
+#tabbrowser-tabs[movingtab] > .tabbrowser-tab[multiselected] {
   position: relative;
   z-index: 2;
   pointer-events: none; /* avoid blocking dragover events on scroll buttons */
 }
 
 .tabbrowser-tab[tabdrop-samewindow],
-#tabbrowser-tabs[movingtab] > .tabbrowser-tab[fadein]:not([selected]) {
+#tabbrowser-tabs[movingtab] > .tabbrowser-tab[fadein]:not([selected]):not([multiselected]) {
   transition: transform 200ms var(--animation-easing-function);
 }
 
 /* The next 3 rules allow dragging tabs slightly outside of the tabstrip
  * to make it easier to drag tabs. */
 #TabsToolbar[movingtab] {
   padding-bottom: 15px;
 }
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -144,16 +144,18 @@ window._gBrowser = {
   _removingTabs: [],
 
   _multiSelectedTabsSet: new WeakSet(),
 
   _lastMultiSelectedTabRef: null,
 
   _clearMultiSelectionLocked: false,
 
+  _clearMultiSelectionLockedOnce: false,
+
   /**
    * Tab close requests are ignored if the window is closing anyway,
    * e.g. when holding Ctrl+W.
    */
   _windowIsClosing: false,
 
   /**
    * This defines a proxy which allows us to access browsers by
@@ -3689,16 +3691,20 @@ window._gBrowser = {
     }
     aTab.removeAttribute("multiselected");
     this.tabContainer._setPositionalAttributes();
     this._multiSelectedTabsSet.delete(aTab);
   },
 
   clearMultiSelectedTabs(updatePositionalAttributes) {
     if (this._clearMultiSelectionLocked) {
+      if (this._clearMultiSelectionLockedOnce) {
+        this._clearMultiSelectionLockedOnce = false;
+        this._clearMultiSelectionLocked = false;
+      }
       return;
     }
 
     let selectedTabs = this.selectedTabs;
     if (selectedTabs.length < 2) {
       return;
     }
 
@@ -3707,16 +3713,21 @@ window._gBrowser = {
     }
     this._multiSelectedTabsSet = new WeakSet();
     this._lastMultiSelectedTabRef = null;
     if (updatePositionalAttributes) {
       this.tabContainer._setPositionalAttributes();
     }
   },
 
+  lockClearMultiSelectionOnce() {
+    this._clearMultiSelectionLockedOnce = true;
+    this._clearMultiSelectionLocked = true;
+  },
+
   /**
    * Remove the active tab from the multiselection if it's the only one left there.
    */
   updateActiveTabMultiSelectState() {
     if (this.selectedTabs.length == 1) {
       this.clearMultiSelectedTabs();
     }
   },
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -552,74 +552,93 @@
       <method name="_animateTabMove">
         <parameter name="event"/>
         <body><![CDATA[
           let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
 
           if (this.getAttribute("movingtab") != "true") {
             this.setAttribute("movingtab", "true");
             this.parentNode.setAttribute("movingtab", "true");
-            this.selectedItem = draggedTab;
+            if (!draggedTab.multiselected)
+              this.selectedItem = draggedTab;
           }
 
           if (!("animLastScreenX" in draggedTab._dragData))
             draggedTab._dragData.animLastScreenX = draggedTab._dragData.screenX;
 
           let screenX = event.screenX;
           if (screenX == draggedTab._dragData.animLastScreenX)
             return;
 
+          // Direction of the mouse movement.
+          let ltrMove = screenX > draggedTab._dragData.animLastScreenX;
+
           draggedTab._dragData.animLastScreenX = screenX;
 
           let rtl = (window.getComputedStyle(this).direction == "rtl");
           let pinned = draggedTab.pinned;
           let numPinned = gBrowser._numPinnedTabs;
           let tabs = this._getVisibleTabs()
                          .slice(pinned ? 0 : numPinned,
                                 pinned ? numPinned : undefined);
+          let movingTabs = draggedTab._dragData.movingTabs;
           if (rtl) {
             tabs.reverse();
+            // Copy moving tabs array to avoid infinite reversing.
+            movingTabs = [...movingTabs].reverse();
           }
           let tabWidth = draggedTab.getBoundingClientRect().width;
+          let shiftWidth = tabWidth * movingTabs.length;
           draggedTab._dragData.tabWidth = tabWidth;
 
           // Move the dragged tab based on the mouse position.
 
           let leftTab = tabs[0];
           let rightTab = tabs[tabs.length - 1];
-          let tabScreenX = draggedTab.boxObject.screenX;
+          let rightMovingTabScreenX = movingTabs[movingTabs.length - 1].boxObject.screenX;
+          let leftMovingTabScreenX = movingTabs[0].boxObject.screenX;
           let translateX = screenX - draggedTab._dragData.screenX;
           if (!pinned) {
             translateX += this.arrowScrollbox._scrollbox.scrollLeft - draggedTab._dragData.scrollX;
           }
-          let leftBound = leftTab.boxObject.screenX - tabScreenX;
+          let leftBound = leftTab.boxObject.screenX - leftMovingTabScreenX;
           let rightBound = (rightTab.boxObject.screenX + rightTab.boxObject.width) -
-                           (tabScreenX + tabWidth);
-          translateX = Math.max(translateX, leftBound);
-          translateX = Math.min(translateX, rightBound);
-          draggedTab.style.transform = "translateX(" + translateX + "px)";
+                           (rightMovingTabScreenX + tabWidth);
+          translateX = Math.min(Math.max(translateX, leftBound), rightBound);
+
+          for (let tab of movingTabs) {
+            tab.style.transform = "translateX(" + translateX + "px)";
+          }
+
           draggedTab._dragData.translateX = translateX;
 
           // Determine what tab we're dragging over.
-          // * Point of reference is the center of the dragged tab. If that
+          // * Single tab dragging: Point of reference is the center of the dragged tab. If that
           //   point touches a background tab, the dragged tab would take that
           //   tab's position when dropped.
+          // * Multiple tabs dragging: All dragged tabs are one "giant" tab with two
+          //   points of reference (center of tabs on the extremities). When
+          //   mouse is moving from left to right, the right reference gets activated,
+          //   otherwise the left reference will be used. Everything else works the same
+          //   as single tab dragging.
           // * We're doing a binary search in order to reduce the amount of
           //   tabs we need to check.
 
-          let tabCenter = tabScreenX + translateX + tabWidth / 2;
+          tabs = tabs.filter(t => !movingTabs.includes(t) || t == draggedTab);
+          let leftTabCenter = leftMovingTabScreenX + translateX + tabWidth / 2;
+          let rightTabCenter = rightMovingTabScreenX + translateX + tabWidth / 2;
+          let tabCenter = ltrMove ? rightTabCenter : leftTabCenter;
           let newIndex = -1;
           let oldIndex = "animDropIndex" in draggedTab._dragData ?
-                         draggedTab._dragData.animDropIndex : draggedTab._tPos;
+                         draggedTab._dragData.animDropIndex : movingTabs[0]._tPos;
           let low = 0;
           let high = tabs.length - 1;
           while (low <= high) {
             let mid = Math.floor((low + high) / 2);
-            if (tabs[mid] == draggedTab &&
-                ++mid > high)
+            if (tabs[mid] == draggedTab && ++mid > high)
               break;
             let boxObject = tabs[mid].boxObject;
             screenX = boxObject.screenX + getTabShift(tabs[mid], oldIndex);
             if (screenX > tabCenter) {
               high = mid - 1;
             } else if (screenX + boxObject.width < tabCenter) {
               low = mid + 1;
             } else {
@@ -640,19 +659,19 @@
             if (tab != draggedTab) {
               let shift = getTabShift(tab, newIndex);
               tab.style.transform = shift ? "translateX(" + shift + "px)" : "";
             }
           }
 
           function getTabShift(tab, dropIndex) {
             if (tab._tPos < draggedTab._tPos && tab._tPos >= dropIndex)
-              return rtl ? -tabWidth : tabWidth;
+              return (rtl ? -shiftWidth : shiftWidth);
             if (tab._tPos > draggedTab._tPos && tab._tPos < dropIndex)
-              return rtl ? tabWidth : -tabWidth;
+              return (rtl ? shiftWidth : -shiftWidth);
             return 0;
           }
         ]]></body>
       </method>
 
       <method name="_finishAnimateTabMove">
         <body><![CDATA[
           if (this.getAttribute("movingtab") != "true") {
@@ -1163,16 +1182,39 @@
 
         // Set the cursor to an arrow during tab drags.
         dt.mozCursor = "default";
 
         // Set the tab as the source of the drag, which ensures we have a stable
         // node to deliver the `dragend` event.  See bug 1345473.
         dt.addElement(tab);
 
+        // Regroup all selected tabs around the dragged tab
+        // for multiple tabs dragging
+        if (tab.multiselected) {
+          let selectedTabs = gBrowser.selectedTabs;
+          let draggedTabPos = tab._tPos;
+
+          // Move left selected tabs
+          let insertAtPos = draggedTabPos - 1;
+          for (let i = selectedTabs.indexOf(tab) - 1; i > -1; i--) {
+            let movingTab = selectedTabs[i];
+            gBrowser.moveTabTo(movingTab, insertAtPos);
+            insertAtPos--;
+          }
+
+          // Move right selected tabs
+          insertAtPos = draggedTabPos + 1;
+          for (let i = selectedTabs.indexOf(tab) + 1; i < selectedTabs.length; i++) {
+            let movingTab = selectedTabs[i];
+            gBrowser.moveTabTo(movingTab, insertAtPos);
+            insertAtPos++;
+          }
+        }
+
         // Create a canvas to which we capture the current tab.
         // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
         // canvas size (in CSS pixels) to the window's backing resolution in order
         // to get a full-resolution drag image for use on HiDPI displays.
         let windowUtils = window.windowUtils;
         let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom;
         let canvas = this._dndCanvas;
         if (!canvas) {
@@ -1236,17 +1278,19 @@
         function clientX(ele) {
           return ele.getBoundingClientRect().left;
         }
         let tabOffsetX = clientX(tab) - clientX(this);
         tab._dragData = {
           offsetX: event.screenX - window.screenX - tabOffsetX,
           offsetY: event.screenY - window.screenY,
           scrollX: this.arrowScrollbox._scrollbox.scrollLeft,
-          screenX: event.screenX
+          screenX: event.screenX,
+          movingTabs: (tab.multiselected ? gBrowser.selectedTabs : [tab])
+                      .filter(t => t.pinned == tab.pinned)
         };
 
         event.stopPropagation();
       ]]></handler>
 
       <handler event="dragover"><![CDATA[
         var effects = this._getDropEffectForTabDrag(event);
 
@@ -1339,21 +1383,23 @@
         ind.style.transform = "translate(" + Math.round(newMargin) + "px)";
         ind.style.marginInlineStart = (-ind.clientWidth) + "px";
       ]]></handler>
 
       <handler event="drop"><![CDATA[
         var dt = event.dataTransfer;
         var dropEffect = dt.dropEffect;
         var draggedTab;
+        let movingTabs;
         if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) { // tab copy or move
           draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
           // not our drop then
           if (!draggedTab)
             return;
+          movingTabs = draggedTab._dragData.movingTabs;
         }
 
         this._tabDropIndicator.collapsed = true;
         event.stopPropagation();
         if (draggedTab && dropEffect == "copy") {
           // copy the dropped tab (wherever it's from)
           let newIndex = this._getDropIndex(event, false);
           let newTab = gBrowser.duplicateTab(draggedTab);
@@ -1369,44 +1415,55 @@
           if (oldTranslateX > 0 && translateOffset > tabWidth / 2) {
             newTranslateX += tabWidth;
           } else if (oldTranslateX < 0 && -translateOffset > tabWidth / 2) {
             newTranslateX -= tabWidth;
           }
 
           let dropIndex = "animDropIndex" in draggedTab._dragData &&
                           draggedTab._dragData.animDropIndex;
-          if (dropIndex && dropIndex > draggedTab._tPos)
+          let incrementDropIndex = true;
+          if (dropIndex && dropIndex > movingTabs[0]._tPos) {
             dropIndex--;
+            incrementDropIndex = false;
+          }
 
           let animate = gBrowser.animationsEnabled;
           if (oldTranslateX && oldTranslateX != newTranslateX && animate) {
-            draggedTab.setAttribute("tabdrop-samewindow", "true");
-            draggedTab.style.transform = "translateX(" + newTranslateX + "px)";
-            let onTransitionEnd = transitionendEvent => {
-              if (transitionendEvent.propertyName != "transform" ||
-                  transitionendEvent.originalTarget != draggedTab) {
-                return;
-              }
-              draggedTab.removeEventListener("transitionend", onTransitionEnd);
+            for (let tab of movingTabs) {
+              tab.setAttribute("tabdrop-samewindow", "true");
+              tab.style.transform = "translateX(" + newTranslateX + "px)";
+              let onTransitionEnd = transitionendEvent => {
+                if (transitionendEvent.propertyName != "transform" ||
+                    transitionendEvent.originalTarget != tab) {
+                  return;
+                }
+                tab.removeEventListener("transitionend", onTransitionEnd);
+
+                tab.removeAttribute("tabdrop-samewindow");
 
-              draggedTab.removeAttribute("tabdrop-samewindow");
+                this._finishAnimateTabMove();
+                if (dropIndex !== false) {
+                  gBrowser.moveTabTo(tab, dropIndex);
+                  if (incrementDropIndex)
+                    dropIndex++;
+                }
 
-              this._finishAnimateTabMove();
-              if (dropIndex !== false) {
-                gBrowser.moveTabTo(draggedTab, dropIndex);
-              }
-
-              gBrowser.syncThrobberAnimations(draggedTab);
-            };
-            draggedTab.addEventListener("transitionend", onTransitionEnd);
+                gBrowser.syncThrobberAnimations(tab);
+              };
+              tab.addEventListener("transitionend", onTransitionEnd);
+            }
           } else {
             this._finishAnimateTabMove();
             if (dropIndex !== false) {
-              gBrowser.moveTabTo(draggedTab, dropIndex);
+              for (let tab of movingTabs) {
+                gBrowser.moveTabTo(tab, dropIndex);
+                if (incrementDropIndex)
+                  dropIndex++;
+              }
             }
           }
         } else if (draggedTab) {
           let newIndex = this._getDropIndex(event, false);
           gBrowser.adoptTab(draggedTab, newIndex, true);
         } else {
           // Pass true to disallow dropping javascript: or data: urls
           let links;
@@ -1968,16 +2025,20 @@
       <handler event="dragstart"><![CDATA[
         if (this.mOverCloseButton) {
           event.stopPropagation();
         }
       ]]></handler>
 
       <handler event="mousedown" phase="capturing">
       <![CDATA[
+        if (event.button == 0 && !this.selected && this.multiselected) {
+          gBrowser.lockClearMultiSelectionOnce();
+        }
+
         let tabContainer = this.parentNode;
         if (tabContainer._closeTabByDblclick &&
             event.button == 0 &&
             event.detail == 1) {
           this._selectedOnFirstMouseDown = this.selected;
         }
 
         if (this.selected) {
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -405,18 +405,16 @@ support-files =
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_storagePressure_notification.js]
 skip-if = verify
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_tab_close_dependent_window.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_tabDrop.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_tabReorder.js]
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_tab_detach_restore.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_tab_drag_drop_perwindow.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_tab_dragdrop.js]
 skip-if = debug || (os == 'linux') || (os == 'mac') # Bug 1312436, Bug 1388973
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_tab_dragdrop2.js]
--- a/browser/base/content/test/tabs/browser.ini
+++ b/browser/base/content/test/tabs/browser.ini
@@ -25,16 +25,17 @@ support-files =
 [browser_multiselect_tabs_close_other_tabs.js]
 [browser_multiselect_tabs_close_using_shortcuts.js]
 [browser_multiselect_tabs_close.js]
 [browser_multiselect_tabs_move_to_new_window_contextmenu.js]
 [browser_multiselect_tabs_mute_unmute.js]
 [browser_multiselect_tabs_pin_unpin.js]
 [browser_multiselect_tabs_positional_attrs.js]
 [browser_multiselect_tabs_reload.js]
+[browser_multiselect_tabs_reorder.js]
 [browser_multiselect_tabs_using_Ctrl.js]
 [browser_multiselect_tabs_using_selectedTabs.js]
 [browser_multiselect_tabs_using_Shift_and_Ctrl.js]
 [browser_multiselect_tabs_using_Shift.js]
 [browser_navigatePinnedTab.js]
 [browser_new_file_whitelisted_http_tab.js]
 skip-if = !e10s # Test only relevant for e10s.
 [browser_new_tab_insert_position.js]
@@ -53,16 +54,17 @@ skip-if = !e10s # Pref and test only rel
 [browser_pinnedTabs.js]
 [browser_positional_attributes.js]
 skip-if = (verify && (os == 'win' || os == 'mac'))
 [browser_preloadedBrowser_zoom.js]
 [browser_reload_deleted_file.js]
 skip-if = (debug && os == 'mac') || (debug && os == 'linux' && bits == 64) #Bug 1421183, disabled on Linux/OSX for leaked windows
 [browser_tabCloseProbes.js]
 [browser_tabReorder_overflow.js]
+[browser_tabReorder.js]
 [browser_tabSpinnerProbe.js]
 skip-if = !e10s # Tab spinner is e10s only.
 [browser_tabSwitchPrintPreview.js]
 skip-if = os == 'mac'
 [browser_tabswitch_updatecommands.js]
 [browser_viewsource_of_data_URI_in_file_process.js]
 [browser_visibleTabs_bookmarkAllTabs.js]
 [browser_visibleTabs_contextMenu.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_reorder.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function() {
+  let tab0 = gBrowser.selectedTab;
+  let tab1 = await addTab();
+  let tab2 = await addTab();
+  let tab3 = await addTab();
+  let tab4 = await addTab();
+  let tab5 = await addTab();
+  let tabs = [tab0, tab1, tab2, tab3, tab4, tab5];
+
+  await BrowserTestUtils.switchTab(gBrowser, tab1);
+  await triggerClickOn(tab3, { ctrlKey: true });
+  await triggerClickOn(tab5, { ctrlKey: true });
+
+  is(gBrowser.selectedTab, tab1, "Tab1 is active");
+  is(gBrowser.selectedTabs.length, 3, "Three selected tabs");
+
+  for (let i of [1, 3, 5]) {
+    ok(tabs[i].multiselected, "Tab" + i + " is multiselected");
+  }
+  for (let i of [0, 2, 4]) {
+    ok(!tabs[i].multiselected, "Tab" + i + " is not multiselected");
+  }
+  for (let i of [0, 1, 2, 3, 4, 5]) {
+    is(tabs[i]._tPos, i, "Tab" + i + " position is :" + i);
+  }
+
+  await dragAndDrop(tab3, tab4, false);
+
+  is(gBrowser.selectedTab, tab3, "Dragged tab (tab3) is now active");
+  is(gBrowser.selectedTabs.length, 3, "Three selected tabs");
+
+  for (let i of [1, 3, 5]) {
+    ok(tabs[i].multiselected, "Tab" + i + " is still multiselected");
+  }
+  for (let i of [0, 2, 4]) {
+    ok(!tabs[i].multiselected, "Tab" + i + " is still not multiselected");
+  }
+
+  is(tab0._tPos, 0, "Tab0 position (0) doesn't change");
+
+  // Multiselected tabs gets grouped at the start of the slide.
+  is(tab1._tPos, tab3._tPos - 1, "Tab1 is located right at the left of the dragged tab (tab3)");
+  is(tab5._tPos, tab3._tPos + 1, "Tab5 is located right at the right of the dragged tab (tab3)");
+  is(tab3._tPos, 4, "Dragged tab (tab3) position is 4");
+
+  is(tab4._tPos, 2, "Drag target (tab4) has shifted to position 2");
+
+  for (let tab of tabs.filter(t => t != tab0))
+    BrowserTestUtils.removeTab(tab);
+});
+
rename from browser/base/content/test/general/browser_tabReorder.js
rename to browser/base/content/test/tabs/browser_tabReorder.js
--- a/browser/base/content/test/general/browser_tabReorder.js
+++ b/browser/base/content/test/tabs/browser_tabReorder.js
@@ -13,33 +13,16 @@ add_task(async function() {
     }
   });
 
   is(gBrowser.tabs.length, initialTabsLength + 3, "new tabs are opened");
   is(gBrowser.tabs[initialTabsLength], newTab1, "newTab1 position is correct");
   is(gBrowser.tabs[initialTabsLength + 1], newTab2, "newTab2 position is correct");
   is(gBrowser.tabs[initialTabsLength + 2], newTab3, "newTab3 position is correct");
 
-  async function dragAndDrop(tab1, tab2, copy) {
-    let rect = tab2.getBoundingClientRect();
-    let event = {
-      ctrlKey: copy,
-      altKey: copy,
-      clientX: rect.left + rect.width / 2 + 10,
-      clientY: rect.top + rect.height / 2,
-    };
-
-    let originalTPos = tab1._tPos;
-    EventUtils.synthesizeDrop(tab1, tab2, null, copy ? "copy" : "move", window, window, event);
-    if (!copy) {
-      await BrowserTestUtils.waitForCondition(() => tab1._tPos != originalTPos,
-        "Waiting for tab position to be updated");
-    }
-  }
-
   await dragAndDrop(newTab1, newTab2, false);
   is(gBrowser.tabs.length, initialTabsLength + 3, "tabs are still there");
   is(gBrowser.tabs[initialTabsLength], newTab2, "newTab2 and newTab1 are swapped");
   is(gBrowser.tabs[initialTabsLength + 1], newTab1, "newTab1 and newTab2 are swapped");
   is(gBrowser.tabs[initialTabsLength + 2], newTab3, "newTab3 stays same place");
 
   await dragAndDrop(newTab2, newTab1, true);
   is(gBrowser.tabs.length, initialTabsLength + 4, "a tab is duplicated");
--- a/browser/base/content/test/tabs/browser_tabReorder_overflow.js
+++ b/browser/base/content/test/tabs/browser_tabReorder_overflow.js
@@ -7,21 +7,17 @@ requestLongerTimeout(2);
 
 add_task(async function() {
   let initialTabsLength = gBrowser.tabs.length;
 
   let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox;
   let tabs = gBrowser.tabs;
   let tabMinWidth = parseInt(getComputedStyle(gBrowser.selectedTab, null).minWidth);
 
-  let rect = ele => ele.getBoundingClientRect();
-  let width = ele => rect(ele).width;
-  let height = ele => rect(ele).height;
-  let left = ele => rect(ele).left;
-  let top = ele => rect(ele).top;
+  let width = ele => ele.getBoundingClientRect().width;
 
   let tabCountForOverflow = Math.ceil(width(arrowScrollbox) / tabMinWidth);
 
   let newTab1 = gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:robots", {skipAnimation: true});
   let newTab2 = gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:about", {skipAnimation: true});
   let newTab3 = gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:config", {skipAnimation: true});
 
   while (tabs.length < tabCountForOverflow) {
@@ -34,26 +30,14 @@ add_task(async function() {
     }
   });
 
   is(gBrowser.tabs.length, tabCountForOverflow, "new tabs are opened");
   is(gBrowser.tabs[initialTabsLength], newTab1, "newTab1 position is correct");
   is(gBrowser.tabs[initialTabsLength + 1], newTab2, "newTab2 position is correct");
   is(gBrowser.tabs[initialTabsLength + 2], newTab3, "newTab3 position is correct");
 
-  async function dragAndDrop(tab1, tab2) {
-    let event = {
-      clientX: left(tab2) + width(tab2) / 2 + 10,
-      clientY: top(tab2) + height(tab2) / 2,
-    };
-
-    let originalTPos = tab1._tPos;
-    EventUtils.synthesizeDrop(tab1, tab2, null, "move", window, window, event);
-    await BrowserTestUtils.waitForCondition(() => tab1._tPos != originalTPos,
-     "Waiting for tab position to be updated");
-  }
-
-  await dragAndDrop(newTab1, newTab2);
+  await dragAndDrop(newTab1, newTab2, false);
   is(gBrowser.tabs.length, tabCountForOverflow, "tabs are still there");
   is(gBrowser.tabs[initialTabsLength], newTab2, "newTab2 and newTab1 are swapped");
   is(gBrowser.tabs[initialTabsLength + 1], newTab1, "newTab1 and newTab2 are swapped");
   is(gBrowser.tabs[initialTabsLength + 2], newTab3, "newTab3 stays same place");
 });
--- a/browser/base/content/test/tabs/head.js
+++ b/browser/base/content/test/tabs/head.js
@@ -152,8 +152,25 @@ async function test_mute_tab(tab, icon, 
   // the media element's playing state.
   let isAudioPlaying = await is_audio_playing(tab);
   if (isAudioPlaying) {
     await wait_for_tab_playing_event(tab, !expectMuted);
   }
 
   return mutedPromise;
 }
+
+async function dragAndDrop(tab1, tab2, copy) {
+  let rect = tab2.getBoundingClientRect();
+  let event = {
+    ctrlKey: copy,
+    altKey: copy,
+    clientX: rect.left + rect.width / 2 + 10,
+    clientY: rect.top + rect.height / 2,
+  };
+
+  let originalTPos = tab1._tPos;
+  EventUtils.synthesizeDrop(tab1, tab2, null, copy ? "copy" : "move", window, window, event);
+  if (!copy) {
+    await BrowserTestUtils.waitForCondition(() => tab1._tPos != originalTPos,
+      "Waiting for tab position to be updated");
+  }
+}
--- a/browser/themes/shared/tabs.inc.css
+++ b/browser/themes/shared/tabs.inc.css
@@ -680,17 +680,17 @@
 :root[tabsintitlebar]:not([extradragspace]) #toolbar-menubar[autohide=true] + #TabsToolbar > #tabbrowser-tabs > .tabbrowser-tab::after,
 %else
 :root[tabsintitlebar]:not([extradragspace]) .tabbrowser-tab::after,
 %endif
 /* Show full height tab separators on hover and multiselection. */
 .tabbrowser-tab:hover::after,
 #tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab[beforehovered]::after,
 .tabbrowser-tab[multiselected]::after,
-.tabbrowser-tab[before-multiselected]::after {
+#tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab[before-multiselected]::after {
   margin-top: var(--tabs-top-border-width);
   margin-bottom: 0;
 }
 
 /* Show full height tab separators on selected tabs. */
 #tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab[beforeselected-visible]::after,
 #tabbrowser-tabs[movingtab] > .tabbrowser-tab[visuallyselected]::before,
 .tabbrowser-tab[visuallyselected]::after {