Bug 1458018 - Add style for multiselected tabs. r=jaws
authorlayely <ablayelyfondou@gmail.com>
Sun, 03 Jun 2018 05:04:48 +0000
changeset 421738 693da404eacaaa1700274ebeff901a0acdd71af0
parent 421737 531593bacc4e9cba1bafc57a132dd6880692a69a
child 421739 db4d4b65e0803757623353445a62ace92c212ba7
push id34106
push useraciure@mozilla.com
push dateThu, 07 Jun 2018 21:48:42 +0000
treeherdermozilla-central@68114b4c0d76 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1458018
milestone62.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 1458018 - Add style for multiselected tabs. r=jaws MozReview-Commit-ID: Ead7pIfHBJP
browser/base/content/tabbrowser.css
browser/base/content/tabbrowser.js
browser/base/content/tabbrowser.xml
browser/base/content/test/tabs/browser.ini
browser/base/content/test/tabs/browser_multiselect_tabs_positional_attrs.js
browser/themes/shared/tabs.inc.css
browser/themes/windows/compacttheme.css
--- a/browser/base/content/tabbrowser.css
+++ b/browser/base/content/tabbrowser.css
@@ -31,20 +31,16 @@
 .tab-icon-overlay[crashed] {
   display: -moz-box;
 }
 
 .tab-label {
   white-space: nowrap;
 }
 
-.tab-label[multiselected] {
-  font-weight: bold;
-}
-
 .tab-label-container {
   overflow: hidden;
 }
 
 .tab-label-container[pinned] {
   width: 0;
 }
 
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -3619,23 +3619,27 @@ window._gBrowser = {
    *          Can be from a different window as well
    * @param   aRestoreTabImmediately
    *          Can defer loading of the tab contents
    */
   duplicateTab(aTab, aRestoreTabImmediately) {
     return SessionStore.duplicateTab(window, aTab, 0, aRestoreTabImmediately);
   },
 
-  addToMultiSelectedTabs(aTab) {
+  addToMultiSelectedTabs(aTab, skipPositionalAttributes) {
     if (aTab.multiselected) {
       return;
     }
 
     aTab.setAttribute("multiselected", "true");
     this._multiSelectedTabsSet.add(aTab);
+
+    if (!skipPositionalAttributes) {
+      this.tabContainer._setPositionalAttributes();
+    }
   },
 
   /**
    * Adds two given tabs and all tabs between them into the (multi) selected tabs collection
    */
   addRangeToMultiSelectedTabs(aTab1, aTab2) {
     // Let's avoid going through all the heavy process below when the same
     // tab is given as params.
@@ -3647,36 +3651,41 @@ window._gBrowser = {
     const tabs = this._visibleTabs;
     const indexOfTab1 = tabs.indexOf(aTab1);
     const indexOfTab2 = tabs.indexOf(aTab2);
 
     const [lowerIndex, higherIndex] = indexOfTab1 < indexOfTab2 ?
       [indexOfTab1, indexOfTab2] : [indexOfTab2, indexOfTab1];
 
     for (let i = lowerIndex; i <= higherIndex; i++) {
-      this.addToMultiSelectedTabs(tabs[i]);
-    }
+      this.addToMultiSelectedTabs(tabs[i], true);
+    }
+    this.tabContainer._setPositionalAttributes();
   },
 
   removeFromMultiSelectedTabs(aTab) {
     if (!aTab.multiselected) {
       return;
     }
     aTab.removeAttribute("multiselected");
+    this.tabContainer._setPositionalAttributes();
     this._multiSelectedTabsSet.delete(aTab);
   },
 
-  clearMultiSelectedTabs() {
+  clearMultiSelectedTabs(updatePositionalAttributes) {
     const selectedTabs = ChromeUtils.nondeterministicGetWeakSetKeys(this._multiSelectedTabsSet);
     for (let tab of selectedTabs) {
       if (tab.isConnected && tab.multiselected) {
         tab.removeAttribute("multiselected");
       }
     }
     this._multiSelectedTabsSet = new WeakSet();
+    if (updatePositionalAttributes) {
+      this.tabContainer._setPositionalAttributes();
+    }
   },
 
   get multiSelectedTabsCount() {
     return ChromeUtils.nondeterministicGetWeakSetKeys(this._multiSelectedTabsSet)
       .filter(tab => tab.isConnected && !tab.closing)
       .length;
   },
 
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -292,16 +292,27 @@
           let hoveredTab = this._hoveredTab;
           if (hoveredTab) {
             hoveredTab._mouseleave();
           }
           hoveredTab = this.querySelector("tab:hover");
           if (hoveredTab) {
             hoveredTab._mouseenter();
           }
+
+          // Update before-multiselected attributes.
+          // gBrowser may not be initialized yet, so avoid using it
+          for (let i = 0; i < visibleTabs.length - 1; i++) {
+            let tab = visibleTabs[i];
+            let nextTab = visibleTabs[i + 1];
+            tab.removeAttribute("before-multiselected");
+            if (nextTab.multiselected) {
+              tab.setAttribute("before-multiselected", "true");
+            }
+          }
         ]]></body>
       </method>
 
       <field name="_blockDblClick">false</field>
 
       <field name="_tabDropIndicator">
         document.getAnonymousElementByAttribute(this, "anonid", "tab-drop-indicator");
       </field>
@@ -1540,17 +1551,17 @@
   </binding>
 
   <binding id="tabbrowser-tab" display="xul:hbox"
            extends="chrome://global/content/bindings/tabbox.xml#tab">
     <content context="tabContextMenu">
       <xul:stack class="tab-stack" flex="1">
         <xul:vbox xbl:inherits="selected=visuallyselected,fadein"
                   class="tab-background">
-          <xul:hbox xbl:inherits="selected=visuallyselected"
+          <xul:hbox xbl:inherits="selected=visuallyselected,multiselected,before-multiselected"
                     class="tab-line"/>
           <xul:spacer flex="1"/>
           <xul:hbox class="tab-bottom-line"/>
         </xul:vbox>
         <xul:hbox xbl:inherits="pinned,bursting,notselectedsinceload"
                   anonid="tab-loading-burst"
                   class="tab-loading-burst"/>
         <xul:hbox xbl:inherits="pinned,selected=visuallyselected,titlechanged,attention"
@@ -1577,17 +1588,17 @@
                      class="tab-icon-overlay"
                      role="presentation"/>
           <xul:hbox class="tab-label-container"
                     xbl:inherits="pinned,selected=visuallyselected,labeldirection"
                     onoverflow="this.setAttribute('textoverflow', 'true');"
                     onunderflow="this.removeAttribute('textoverflow');"
                     flex="1">
             <xul:label class="tab-text tab-label"
-                       xbl:inherits="xbl:text=label,accesskey,fadein,pinned,selected=visuallyselected,attention,multiselected"
+                       xbl:inherits="xbl:text=label,accesskey,fadein,pinned,selected=visuallyselected,attention"
                        role="presentation"/>
           </xul:hbox>
           <xul:image xbl:inherits="soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked"
                      anonid="soundplaying-icon"
                      class="tab-icon-sound"
                      role="presentation"/>
           <xul:image anonid="close-button"
                      xbl:inherits="fadein,pinned,selected=visuallyselected"
@@ -1663,16 +1674,21 @@
           return this.getAttribute("muted") == "true";
         </getter>
       </property>
       <property name="multiselected" readonly="true">
         <getter>
           return this.getAttribute("multiselected") == "true";
         </getter>
       </property>
+      <property name="beforeMultiselected" readonly="true">
+        <getter>
+          return this.getAttribute("before-multiselected") == "true";
+        </getter>
+      </property>
       <!--
       Describes how the tab ended up in this mute state. May be any of:
 
        - undefined: The tabs mute state has never changed.
        - null: The mute state was last changed through the UI.
        - Any string: The ID was changed through an extension API. The string
                      must be the ID of the extension which changed it.
       -->
@@ -1999,17 +2015,22 @@
             }
             return;
           }
 
           const overCloseButton = event.originalTarget.getAttribute("anonid") == "close-button";
           if (gBrowser.multiSelectedTabsCount > 0 && !overCloseButton) {
             // Tabs were previously multi-selected and user clicks on a tab
             // without holding Ctrl/Cmd Key
-            gBrowser.clearMultiSelectedTabs();
+
+            // Force positional attributes to update when the
+            // target (of the click) is the "active" tab.
+            let updatePositionalAttr = gBrowser.selectedTab == this;
+
+            gBrowser.clearMultiSelectedTabs(updatePositionalAttr);
           }
         }
 
         if (this._overPlayingIcon) {
           this.toggleMuteAudio();
           return;
         }
 
--- a/browser/base/content/test/tabs/browser.ini
+++ b/browser/base/content/test/tabs/browser.ini
@@ -40,8 +40,9 @@ skip-if = (debug && os == 'mac') || (deb
 [browser_viewsource_of_data_URI_in_file_process.js]
 [browser_visibleTabs_bookmarkAllTabs.js]
 [browser_visibleTabs_contextMenu.js]
 [browser_open_newtab_start_observer_notification.js]
 [browser_bug_1387976_restore_lazy_tab_browser_muted_state.js]
 [browser_multiselect_tabs_using_Ctrl.js]
 [browser_multiselect_tabs_using_Shift.js]
 [browser_multiselect_tabs_close.js]
+[browser_multiselect_tabs_positional_attrs.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_positional_attrs.js
@@ -0,0 +1,54 @@
+const PREF_MULTISELECT_TABS = "browser.tabs.multiselect";
+const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs";
+
+add_task(async function setPref() {
+    await SpecialPowers.pushPrefEnv({
+        set: [
+            [PREF_MULTISELECT_TABS, true],
+            [PREF_WARN_ON_CLOSE, false]
+        ]
+    });
+});
+
+add_task(async function checkBeforeMultiselectedAttributes() {
+    let tab1 = await addTab();
+    let tab2 = await addTab();
+    let tab3 = await addTab();
+
+    let visibleTabs = gBrowser._visibleTabs;
+
+    await triggerClickOn(tab3, { ctrlKey: true });
+
+    is(visibleTabs.indexOf(tab1), 1, "The index of Tab1 is one");
+    is(visibleTabs.indexOf(tab2), 2, "The index of Tab2 is two");
+    is(visibleTabs.indexOf(tab3), 3, "The index of Tab3 is three");
+
+    ok(!tab1.multiselected, "Tab1 is not multi-selected");
+    ok(!tab2.multiselected, "Tab2 is not multi-selected");
+    ok(tab3.multiselected, "Tab3 is multi-selected");
+
+    ok(!tab1.beforeMultiselected, "Tab1 is not before-multiselected");
+    ok(tab2.beforeMultiselected, "Tab2 is before-multiselected");
+
+    info("Close Tab2");
+    let tab2Closing = BrowserTestUtils.waitForTabClosing(tab2);
+    BrowserTestUtils.removeTab(tab2);
+    await tab2Closing;
+
+    // Cache invalidated, so we need to update the collection
+    visibleTabs = gBrowser._visibleTabs;
+
+    is(visibleTabs.indexOf(tab1), 1, "The index of Tab1 is one");
+    is(visibleTabs.indexOf(tab3), 2, "The index of Tab3 is two");
+    ok(tab1.beforeMultiselected, "Tab1 is before-multiselected");
+
+    // Checking if positional attributes are updated when "active" tab is clicked.
+    info("Click on the active tab to clear multiselect");
+    await triggerClickOn(gBrowser.selectedTab, {});
+
+    is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+    ok(!tab1.beforeMultiselected, "Tab1 is not before-multiselected anymore");
+
+    BrowserTestUtils.removeTab(tab1);
+    BrowserTestUtils.removeTab(tab3);
+});
--- a/browser/themes/shared/tabs.inc.css
+++ b/browser/themes/shared/tabs.inc.css
@@ -530,16 +530,17 @@
 
 .tab-background[selected=true] {
   border-top-color: var(--tabs-border-color);
   background-color: var(--toolbar-bgcolor);
   background-image: var(--toolbar-bgimage);
   background-repeat: repeat-x;
 }
 
+.tab-line[multiselected],
 .tab-line[selected=true] {
   background-color: var(--tab-line-color);
 }
 
 /*
  * LightweightThemeConsumer will set the current lightweight theme's header
  * image to the lwt-header-image variable, used in each of the following rulesets.
  */
@@ -561,32 +562,42 @@
 .tabbrowser-tab:hover > .tab-stack > .tab-background:not([selected=true]) {
   background-color: rgba(0,0,0,.1);
 }
 
 #TabsToolbar[brighttext] > #tabbrowser-tabs > .tabbrowser-tab:hover > .tab-stack > .tab-background:not([selected=true]) {
   background-color: rgba(255,255,255,.1);
 }
 
-.tab-line:not([selected=true]) {
+.tab-line:not([selected=true]):not([multiselected]) {
   opacity: 0;
   transform: scaleX(0);
   transition: transform 250ms var(--animation-easing-function), opacity 250ms var(--animation-easing-function);
 }
 
 .tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-line:not([selected=true]) {
   background-color: rgba(0,0,0,.2);
   opacity: 1;
   transform: none;
 }
 
 #TabsToolbar[brighttext] > #tabbrowser-tabs > .tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-line:not([selected=true]) {
   background-color: rgba(255,255,255,.2);
 }
 
+/* Tab multi-selected */
+
+.tabbrowser-tab[multiselected] > .tab-stack > .tab-background:not([selected=true]) {
+  background-color: rgba(0,0,0,.1);
+}
+
+#TabsToolbar[brighttext] > #tabbrowser-tabs > .tabbrowser-tab[multiselected] > .tab-stack > .tab-background:not([selected=true]) {
+  background-color: rgba(255,255,255,.1);
+}
+
 /* Pinned tabs */
 
 /* Pinned tab separators need position: absolute when positioned (during overflow). */
 #tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned]::after {
   position: absolute;
   top: 0;
   bottom: 0;
   right: 0;
@@ -660,19 +671,21 @@
   margin-inline-start: -1px;
 }
 
 %ifdef MENUBAR_CAN_AUTOHIDE
 :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. */
+/* Show full height tab separators on hover and multiselection. */
 .tabbrowser-tab:hover::after,
-#tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab[beforehovered]::after {
+#tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab[beforehovered]::after,
+.tabbrowser-tab[multiselected]::after,
+.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 {
--- a/browser/themes/windows/compacttheme.css
+++ b/browser/themes/windows/compacttheme.css
@@ -35,21 +35,22 @@
 
     /* Keep showing the correct color inside the tabs. */
     .tabbrowser-tab {
       color: var(--chrome-color) !important;
     }
 
     /* Because we're forcing the tabs toolbar to be [brighttext] to
      * get white toolbar button icons, we need to manually set the
-     * correct color for the tab hover state for the light theme. */
-    .tabbrowser-tab:hover > .tab-stack > .tab-background:not([selected=true]):-moz-lwtheme-darktext {
+     * correct color for the tab hover and multiselect state for the light theme. */
+    .tabbrowser-tab:hover > .tab-stack > .tab-background:not([selected=true]):-moz-lwtheme-darktext,
+    .tabbrowser-tab[multiselected] > .tab-stack > .tab-background:not([selected=true]):-moz-lwtheme-darktext {
       background-color: rgba(0,0,0,.1) !important;
     }
-    .tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-line:not([selected=true]):-moz-lwtheme-darktext {
+    .tabbrowser-tab:hover > .tab-stack > .tab-background > .tab-line:not([selected=true]):not([multiselected]):-moz-lwtheme-darktext {
       background-color: rgba(0,0,0,.2) !important;
     }
   }
 }
 
 @media (-moz-windows-glass) {
   /* Set to full fill-opacity to improve visibility of toolbar buttons on aero glass. */
   #TabsToolbar {