Bug 1458018 - Add style for multiselected tabs. r?jaws
MozReview-Commit-ID: Ead7pIfHBJP
--- 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 {