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
This commit is contained in:
windingwind 2026-01-05 16:15:21 +01:00 • committed by GitHub
parent 47e6a0f7ab
commit bb17ac2c74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 59 additions and 64 deletions

View file

@ -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;
}

View file

@ -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);

View file

@ -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);
};
/**

View file

@ -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;

View file

@ -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();

View file

@ -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')