From bb17ac2c741f80c05b56ac18301f0dea0bc6a038 Mon Sep 17 00:00:00 2001 From: windingwind <33902321+windingwind@users.noreply.github.com> Date: Mon, 5 Jan 2026 16:15:21 +0100 Subject: [PATCH] Fix tab focus control (#5689) fix: #5683 fix: #5684 fix: #5688 Rename tab hook focus -> focusFirst Replace tab-focus custom event with tab hook refocus Move context pane _handleTabReady to Zotero_Tabs Focus note editor after opening tab Use setTimeout to ensure the focus does not stay in tab element --- chrome/content/zotero/elements/contextPane.js | 22 ------ chrome/content/zotero/elements/tabContent.js | 12 --- chrome/content/zotero/tabs.js | 78 ++++++++++++++----- chrome/content/zotero/xpcom/data/notes.js | 5 +- chrome/content/zotero/xpcom/reader.js | 4 - chrome/content/zotero/zoteroPane.js | 2 +- 6 files changed, 59 insertions(+), 64 deletions(-) diff --git a/chrome/content/zotero/elements/contextPane.js b/chrome/content/zotero/elements/contextPane.js index 5768ca0bc8..2728d55c8f 100644 --- a/chrome/content/zotero/elements/contextPane.js +++ b/chrome/content/zotero/elements/contextPane.js @@ -218,7 +218,6 @@ // updated by another select tab event. && (action === 'select' || (action === 'load' && Zotero_Tabs.selectedID == tabID))) { - this._handleTabReady(tabID); if (Zotero_Tabs.hasNoteContext(tabType)) { this._setupNotesContext(tabID); } @@ -260,27 +259,6 @@ this.sidenav.contextNotesPaneEnabled = false; } - async _handleTabReady(tabID) { - let tabContent = Zotero_Tabs.getTabContent(tabID); - if (!tabContent) { - return; - } - // Focus tab content (e.g. reader pages view) if context pane note editor is not selected - if (Zotero_Tabs.selectedID == tabID - && !Zotero_Tabs.tabsMenuPanel.visible - && (!document.activeElement - || !document.activeElement.closest('.context-node iframe[id="editor-view"]'))) { - if (!Zotero_Tabs.focusOptions?.keepTabFocused) { - // Do not move focus to the reader during keyboard navigation - setTimeout(() => { - // Timeout to make sure focus does not stick to the tab - // after click on windows - tabContent.setFocus(); - }); - } - } - } - _getCurrentNotesContext() { return this._notesPaneDeck.selectedPanel; } diff --git a/chrome/content/zotero/elements/tabContent.js b/chrome/content/zotero/elements/tabContent.js index 6189bf761d..e5240b6578 100644 --- a/chrome/content/zotero/elements/tabContent.js +++ b/chrome/content/zotero/elements/tabContent.js @@ -97,18 +97,6 @@ } })); } - - /** - * Notify the tab content that it has received focus. - * Used by the context pane to move focus. - */ - setFocus() { - this.dispatchEvent(new CustomEvent("tab-focus", { - detail: { - tabID: this.tabID - } - })); - } } customElements.define("tab-content", TabContent); diff --git a/chrome/content/zotero/tabs.js b/chrome/content/zotero/tabs.js index fbf69d4970..f7d02981d8 100644 --- a/chrome/content/zotero/tabs.js +++ b/chrome/content/zotero/tabs.js @@ -120,7 +120,7 @@ var Zotero_Tabs = new function () { await noteEditor; } }, - focus: { + focusFirst: { library: async () => { let collectionsPane = document.getElementById("zotero-collections-pane"); if (collectionsPane.getAttribute("collapsed")) { @@ -811,17 +811,41 @@ var Zotero_Tabs = new function () { * * @param {String} id * @param {Boolean} reopening + * @param {Object} options - Additional options for selecting the tab. Passed to tab hooks + * @param {Boolean} options.keepTabFocused + * @return {Promise} */ this.select = function (id, reopening, options = {}) { let { tab, tabIndex } = this._getTab(id); let { tabContentType, tabState } = this.parseTabType(tab.type); + let isEditorFocused = false; + + if ( + // Tab focus can change + !options.keepTabFocused + // Has note context + && this.hasNoteContext(tabContentType) + // Tabs menu popup is not open + && !this.tabsMenuPanel.visible + // Note context editor is active + && ZoteroContextPane.activeEditor?.contains(document.activeElement) + // Opened as a child note of the tab + && ZoteroContextPane.activeEditor?.closest(".context-note-standalone")) { + let currentItem = Zotero.Items.get(tab.data.itemID); + let editorItem = ZoteroContextPane.activeEditor.item; + // In the same library + if (!currentItem || editorItem.libraryID === currentItem.libraryID) { + isEditorFocused = true; + } + } + if (!tab || tab.id === this._selectedID) { // Focus on reader or zotero pane when keepTabFocused is explicitly false // E.g. when a tab is selected via Space or Enter - if (options.keepTabFocused === false && tab?.id === this._selectedID) { - let focusHook = this._getHook(tabContentType, 'focus'); - focusHook(tab, tabIndex, options); + if (!isEditorFocused && options.keepTabFocused === false && tab?.id === this._selectedID) { + let refocusHook = this._getHook(tabContentType, 'refocus'); + setTimeout(() => refocusHook(tab, tabIndex, options), 0); } return; } @@ -874,29 +898,41 @@ var Zotero_Tabs = new function () { }); } // Notify previously selected tab content about selection change - this.getTabContent(this._selectedID)?.onTabSelectionChanged(false); + let prevTabContent = this.getTabContent(this._selectedID); + prevTabContent?.onTabSelectionChanged(false); this._prevSelectedID = reopening ? this._selectedID : null; this._selectedID = id; this.deck.selectedIndex = Array.from(this.deck.children).findIndex(x => x.id == id); this._update(); Zotero.Notifier.trigger('select', 'tab', [tab.id], { [tab.id]: { type: tab.type } }, true); - if (options.keepTabFocused !== true) { - let focusHook = this._getHook(tabContentType, 'focus'); - focusHook(tab, tabIndex, options); - } - let tabNode = document.querySelector(`#tab-bar-container .tab[data-id="${tab.id}"]`); - if (this._focusOptions.keepTabFocused && document.activeElement.getAttribute('data-id') != tabNode.getAttribute('data-id')) { - // Keep focus on the currently selected tab during keyboard navigation - if (tab.id == 'zotero-pane') { - // Since there is more than one zotero-pane tab (pinned and not pinned), - // use moveFocus() to focus on the visible one - this.moveFocus('first'); + + let currentTabContent = this.getTabContent(id); + + if (!isEditorFocused) { + if (options.keepTabFocused !== true) { + let refocusHook = this._getHook(tabContentType, 'refocus'); + setTimeout(() => refocusHook(tab, tabIndex, options), 0); } else { - tabNode.focus(); + let tabNode = document.querySelector(`#tab-bar-container .tab[data-id="${tab.id}"]`); + if (document.activeElement.getAttribute('data-id') != tabNode.getAttribute('data-id')) { + // Keep focus on the currently selected tab during keyboard navigation + if (tab.id == 'zotero-pane') { + // Since there is more than one zotero-pane tab (pinned and not pinned), + // use moveFocus() to focus on the visible one + this.moveFocus('first'); + } + else { + tabNode.focus(); + } + } } } + else { + setTimeout(() => ZoteroContextPane.activeEditor?.focus(), 0); + } + tab.timeSelected = Zotero.Date.getUnixTimestamp(); // Without `setTimeout` the tab closing that happens in `unloadUnusedTabs` results in // tabs deck selection index bigger than the deck children count. It feels like something @@ -904,7 +940,7 @@ var Zotero_Tabs = new function () { setTimeout(() => this.unloadUnusedTabs()); // Notify tab content about selection change - this.getTabContent(this._selectedID)?.onTabSelectionChanged(true); + currentTabContent?.onTabSelectionChanged(true); }; this.unload = function (id) { @@ -990,13 +1026,13 @@ var Zotero_Tabs = new function () { * Move focus into the first element in content of the selected tab. * Required to move focus from the outside into the tab content. */ - this.focusContent = function (id) { + this.focusFirst = function (id) { if (!id) id = this._selectedID; let { tab, tabIndex } = this._getTab(id); if (!tab) return; let { tabContentType } = this.parseTabType(tab.type); - let focusHook = this._getHook(tabContentType, 'focus'); - focusHook(tab, tabIndex); + let focusFirstHook = this._getHook(tabContentType, 'focusFirst'); + focusFirstHook(tab, tabIndex); }; /** diff --git a/chrome/content/zotero/xpcom/data/notes.js b/chrome/content/zotero/xpcom/data/notes.js index 9a44ed2a4e..0cb8210e9a 100644 --- a/chrome/content/zotero/xpcom/data/notes.js +++ b/chrome/content/zotero/xpcom/data/notes.js @@ -148,10 +148,6 @@ Zotero.Notes = new function () { this.setContextPaneOpen(noteEditor, event.detail.open); }); - container.addEventListener('tab-focus', () => { - noteEditor.focus(); - }); - container.addEventListener('tab-selection-change', (event) => { if (event.detail.selected) { this._updateLayout(); @@ -161,6 +157,7 @@ Zotero.Notes = new function () { if (select) { this._updateLayout(); + noteEditor.focus(); } } return noteEditor; diff --git a/chrome/content/zotero/xpcom/reader.js b/chrome/content/zotero/xpcom/reader.js index af816088e4..23a0e15d5e 100644 --- a/chrome/content/zotero/xpcom/reader.js +++ b/chrome/content/zotero/xpcom/reader.js @@ -1494,10 +1494,6 @@ class ReaderTab extends ReaderInstance { this.setContextPaneOpen(event.detail.open); }); - this._tabContainer.addEventListener('tab-focus', () => { - this.focus(); - }); - this._tabContainer.addEventListener('tab-selection-change', (event) => { if (event.detail.selected) { this._updateLayout(); diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index adeae44cc1..355399dca0 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -268,7 +268,7 @@ var ZoteroPane = new function () { ArrowNext: () => null, ArrowPrevious: () => null, Tab: () => { - Zotero_Tabs.focusContent(); + Zotero_Tabs.focusFirst(); return null; }, ShiftTab: () => document.getElementById('zotero-tb-sync-error')