diff --git a/src/zen/folders/ZenFolder.mjs b/src/zen/folders/ZenFolder.mjs index a0588c258..83cfec553 100644 --- a/src/zen/folders/ZenFolder.mjs +++ b/src/zen/folders/ZenFolder.mjs @@ -144,6 +144,10 @@ return activeGroups; } + get childActiveGroups() { + return Array.from(this.querySelectorAll('zen-folder[has-active]')); + } + rename() { if (!document.documentElement.hasAttribute('zen-sidebar-expanded')) { return; @@ -246,6 +250,7 @@ let activeGroup = folders.get(group?.id); if (!activeGroup) { tab.removeAttribute('folder-active'); + tab.style.removeProperty('--zen-folder-indent'); } } this._activeTabs = []; @@ -266,12 +271,10 @@ } async #unloadAllActiveTabs(event, noClose = false) { - for (const tab of this.tabs) { - await gZenPinnedTabManager._onCloseTabShortcut(event, tab, { - noClose, - expandSplitViewList: false, - }); - } + await gZenPinnedTabManager._onCloseTabShortcut(event, this.tabs, { + noClose, + folderToUnload: this, + }); this.activeTabs = []; } diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs index fd2e7f7ff..29062dbb4 100644 --- a/src/zen/folders/ZenFolders.mjs +++ b/src/zen/folders/ZenFolders.mjs @@ -1398,7 +1398,7 @@ const groupStart = group.querySelector('.zen-tab-group-start'); const itemsToShow = this.#normalizeGroupItems(group.childGroupsAndTabs); - const activeFolders = Array.from(group.querySelectorAll('zen-folder[has-active]')); + const activeFolders = group.childActiveGroups; for (const folder of activeFolders) { const splitViewIds = new Set(); @@ -1502,6 +1502,45 @@ this.styleCleanup(itemsToHide); } + async animateUnloadAll(group) { + const animations = []; + + const activeGroups = [group, ...group.childActiveGroups]; + for (const folder of activeGroups) { + folder.removeAttribute('has-active'); + folder.activeTabs = []; + const groupItems = this.#normalizeGroupItems(folder.allItems); + const tabsContainer = folder.querySelector('.tab-group-container'); + + this.styleCleanup(groupItems); + + const groupStart = folder.querySelector('.zen-tab-group-start'); + + // Trigger a reflow + tabsContainer.offsetHeight; + // tabsContainer.setAttribute('hidden', true); + + const heightUntilSelected = this.#calculateHeightShift(tabsContainer, []); + + // Collect animations for this specific folder becoming inactive + animations.push( + ...this.updateFolderIcon(folder, 'close', false), + ...this.#createAnimation( + groupStart, + { + marginTop: -(heightUntilSelected + 4), + }, + { duration: 0.12, ease: 'easeInOut' } + ) + ); + } + + this.#animationCount += 1; + await Promise.all(animations); + this.#animationCount -= 1; + gBrowser.tabContainer._invalidateCachedTabs(); + } + async animateUnload(group, tabToUnload, ungroup = false) { const isSplitView = tabToUnload.group?.hasAttribute('split-view-group'); if ((!group?.isZenFolder || !isSplitView) && !tabToUnload.hasAttribute('folder-active')) @@ -1513,29 +1552,34 @@ folder.activeTabs = folder.activeTabs.filter((tab) => tab !== tabToUnload); if (folder.activeTabs.length === 0) { - folder.removeAttribute('has-active'); - const groupItems = this.#normalizeGroupItems(folder.allItems); - const tabsContainer = folder.querySelector('.tab-group-container'); + animations.push(async () => { + folder.removeAttribute('has-active'); + const groupItems = this.#normalizeGroupItems(folder.allItems); + const tabsContainer = folder.querySelector('.tab-group-container'); - this.styleCleanup(groupItems); + this.styleCleanup(groupItems); - const groupStart = folder.querySelector('.zen-tab-group-start'); + const groupStart = folder.querySelector('.zen-tab-group-start'); - tabsContainer.offsetHeight; - tabsContainer.setAttribute('hidden', true); + // Trigger a reflow + tabsContainer.offsetHeight; + tabsContainer.setAttribute('hidden', true); - const heightUntilSelected = this.#calculateHeightShift(tabsContainer, []); + const heightUntilSelected = this.#calculateHeightShift(tabsContainer, []); - animations.push( - ...this.updateFolderIcon(folder, 'close', false), - ...this.#createAnimation( - groupStart, - { - marginTop: -(heightUntilSelected + 4), - }, - { duration: 0.12, ease: 'easeInOut' } - ) - ); + // Collect animations for this specific folder becoming inactive + const folderAnimation = [ + ...this.updateFolderIcon(folder, 'close', false), + ...this.#createAnimation( + groupStart, + { + marginTop: -(heightUntilSelected + 4), + }, + { duration: 0.12, ease: 'easeInOut' } + ), + ]; + await Promise.all(folderAnimation); + }); } } @@ -1546,24 +1590,27 @@ tabToUnload.style.removeProperty('--zen-folder-indent'); + let tabUnloadAnimations = []; if (!ungroup) { - animations.push( - ...this.#createAnimation( - tabToUnload, - { - opacity: 0, - height: 0, - }, - { - duration: 0.12, - ease: 'easeInOut', - } - ) + tabUnloadAnimations = this.#createAnimation( + tabToUnload, + { + opacity: 0, + height: 0, + }, + { + duration: 0.12, + ease: 'easeInOut', + } ); } + // Manage global animation count this.#animationCount += 1; - await Promise.all(animations); + + // Await the tab unload animation first + await Promise.all(tabUnloadAnimations); + await Promise.all(animations.map((item) => (typeof item === 'function' ? item() : item))); this.#animationCount -= 1; gBrowser.tabContainer._invalidateCachedTabs(); } diff --git a/src/zen/split-view/zen-decks.css b/src/zen/split-view/zen-decks.css index b311601d3..237c35d8a 100644 --- a/src/zen/split-view/zen-decks.css +++ b/src/zen/split-view/zen-decks.css @@ -182,7 +182,7 @@ position: absolute; height: 100%; border: 2px solid var(--zen-primary-color); - background: rgba(255, 255, 255, 0.01); + background: rgba(255, 255, 255, 0.1); border-radius: var(--zen-native-inner-radius); box-shadow: var(--zen-big-shadow); overflow: hidden; diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index 1b0b77e98..46c517860 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -774,14 +774,30 @@ behavior = lazy.zenPinnedTabCloseShortcutBehavior, noClose = false, closeIfPending = false, - expandSplitViewList = true, + folderToUnload = null, } = {} ) { try { - if (!selectedTab?.pinned) { + const tabs = Array.isArray(selectedTab) ? selectedTab : [selectedTab]; + const pinnedTabs = [ + ...new Set( + tabs + .flatMap((tab) => { + if (tab.group?.hasAttribute('split-view-group')) { + return tab.group.tabs; + } + return tab; + }) + .filter((tab) => tab?.pinned) + ), + ]; + + if (!pinnedTabs.length) { return; } + const selectedTabs = pinnedTabs.filter((tab) => tab.selected); + event.stopPropagation(); event.preventDefault(); @@ -791,63 +807,80 @@ switch (behavior) { case 'close': - this._removePinnedAttributes(selectedTab, true); - gBrowser.removeTab(selectedTab, { animate: true }); + for (const tab of pinnedTabs) { + this._removePinnedAttributes(tab, true); + gBrowser.removeTab(tab, { animate: true }); + } break; case 'reset-unload-switch': case 'unload-switch': case 'reset-switch': case 'switch': if (behavior.includes('unload')) { - if (selectedTab.hasAttribute('glance-id')) { - // We have a glance tab inside the tab we are trying to unload, - // before we used to just ignore it but now we need to fully close - // it as well. - gZenGlanceManager.manageTabClose(selectedTab.glanceTab); - await new Promise((resolve) => { - let hasRan = false; - const onGlanceClose = () => { - hasRan = true; - resolve(); - }; - window.addEventListener('GlanceClose', onGlanceClose, { once: true }); - // Set a timeout to resolve the promise if the event doesn't fire. - // We do this to prevent any future issues where glance woudnt close such as - // glance requering to ask for permit unload. - setTimeout(() => { - if (!hasRan) { - console.warn('GlanceClose event did not fire within 3 seconds'); + for (const tab of pinnedTabs) { + if (tab.hasAttribute('glance-id')) { + // We have a glance tab inside the tab we are trying to unload, + // before we used to just ignore it but now we need to fully close + // it as well. + gZenGlanceManager.manageTabClose(tab.glanceTab); + await new Promise((resolve) => { + let hasRan = false; + const onGlanceClose = () => { + hasRan = true; resolve(); - } - }, 3000); - }); + }; + window.addEventListener('GlanceClose', onGlanceClose, { once: true }); + // Set a timeout to resolve the promise if the event doesn't fire. + // We do this to prevent any future issues where glance woudnt close such as + // glance requering to ask for permit unload. + setTimeout(() => { + if (!hasRan) { + console.warn('GlanceClose event did not fire within 3 seconds'); + resolve(); + } + }, 3000); + }); + } + const isSpltView = tab.group?.hasAttribute('split-view-group'); + const group = isSpltView ? tab.group.group : tab.group; + + if (!folderToUnload && tab.hasAttribute('folder-active')) { + await gZenFolders.animateUnload(group, tab); + } } - const group = selectedTab.group?.hasAttribute('split-view-group') - ? selectedTab.group.group - : selectedTab.group; - await gZenFolders.animateUnload(group, selectedTab); - let tabsToUnload = [selectedTab]; - if (selectedTab.group?.hasAttribute('split-view-group') && expandSplitViewList) { - tabsToUnload = selectedTab.group.tabs; + + if (folderToUnload) { + await gZenFolders.animateUnloadAll(folderToUnload); } - const allAreUnloaded = tabsToUnload.every( + + const allAreUnloaded = pinnedTabs.every( (tab) => tab.hasAttribute('pending') && !tab.hasAttribute('zen-essential') ); - if (allAreUnloaded && closeIfPending) { - return await this._onCloseTabShortcut(event, selectedTab, { behavior: 'close' }); + + for (const tab of pinnedTabs) { + if (allAreUnloaded && closeIfPending) { + return await this._onCloseTabShortcut(event, tab, { behavior: 'close' }); + } + } + + await gBrowser.explicitUnloadTabs(pinnedTabs); + for (const tab of pinnedTabs) { + tab.removeAttribute('discarded'); } - await gBrowser.explicitUnloadTabs(tabsToUnload); - selectedTab.removeAttribute('discarded'); } - if (selectedTab.selected) { - this._handleTabSwitch(selectedTab); + if (selectedTabs.length) { + this._handleTabSwitch(selectedTabs[0]); } if (behavior.includes('reset')) { - this._resetTabToStoredState(selectedTab); + for (const tab of pinnedTabs) { + this._resetTabToStoredState(tab); + } } break; case 'reset': - this._resetTabToStoredState(selectedTab); + for (const tab of pinnedTabs) { + this._resetTabToStoredState(tab); + } break; default: return;