diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index bf86cf5f4c..7b75e60bed 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -174,6 +174,11 @@ var ZoteroPane = new function () { function setUpKeyboardNavigation() { let collectionTreeToolbar = document.getElementById("zotero-toolbar-collection-tree"); let itemTreeToolbar = document.getElementById("zotero-toolbar-item-tree"); + // Plugins can opt into keyboard focus with tabindex=0 on an appended + // toolbarbutton. Read the current buttons for each key event. + let pluginButtons = () => [...document.querySelectorAll( + '#zotero-items-toolbar > toolbarbutton[tabindex="0"]' + )]; let titleBar = document.getElementById("zotero-title-bar"); let itemTree = document.getElementById("zotero-items-tree"); let collectionsTree = document.getElementById("zotero-collections-tree"); @@ -367,6 +372,11 @@ var ZoteroPane = new function () { }); itemTreeToolbar.addEventListener("keydown", (event) => { + let adjacentPluginButton = (button, direction) => { + let buttons = pluginButtons(); + let index = buttons.indexOf(button); + return index === -1 ? null : buttons[index + direction]; + }; let actionsMap = { 'zotero-tb-add': { ArrowNext: () => document.getElementById("zotero-tb-lookup"), @@ -420,10 +430,20 @@ var ZoteroPane = new function () { ShiftTab: () => document.getElementById("zotero-tb-search-textbox") }, 'zotero-tb-toggle-item-pane-stacked': { - Tab: () => itemTree.querySelector(".virtualized-table"), + Tab: () => pluginButtons()[0] || itemTree.querySelector(".virtualized-table"), ShiftTab: () => document.getElementById("zotero-tb-search-advanced-button") }, }; + if (pluginButtons().includes(event.target)) { + actionsMap[event.target.id] = { + ArrowNext: () => adjacentPluginButton(event.target, 1) || null, + ArrowPrevious: () => adjacentPluginButton(event.target, -1) || null, + Tab: () => adjacentPluginButton(event.target, 1) + || itemTree.querySelector(".virtualized-table"), + ShiftTab: () => adjacentPluginButton(event.target, -1) + || document.getElementById('zotero-tb-toggle-item-pane-stacked') + }; + } moveFocus(actionsMap, event, true); }); @@ -468,7 +488,8 @@ var ZoteroPane = new function () { if (advancedSearchDeck?.state === 'collapsed') { return document.querySelector('#zotero-tb-search .advanced-close-button'); } - return document.getElementById('zotero-tb-toggle-item-pane-stacked'); + return pluginButtons().at(-1) + || document.getElementById('zotero-tb-toggle-item-pane-stacked'); } } }; diff --git a/test/tests/zoteroPaneTest.js b/test/tests/zoteroPaneTest.js index 1b5a379f05..a3f92d272b 100644 --- a/test/tests/zoteroPaneTest.js +++ b/test/tests/zoteroPaneTest.js @@ -1898,6 +1898,82 @@ describe("ZoteroPane", function () { doc.activeElement.dispatchEvent(leftArrow); assert.equal(doc.activeElement.id, "zotero-tb-add"); }); + + it("should include plugin toolbarbuttons in the Tab and arrow routes", async function () { + if (!await activatePaneWindow()) { + this.skip(); + } + let toolbar = doc.getElementById('zotero-items-toolbar'); + let buttons = ['first', 'second'].map(name => { + let button = doc.createXULElement('toolbarbutton'); + button.id = `test-plugin-${name}`; + button.setAttribute('tabindex', '0'); + toolbar.append(button); + return button; + }); + try { + // The plugin buttons are appended after the search and pane-toggle + // controls, so keyboard traversal must reach them in that order. + doc.getElementById('zotero-tb-add').focus(); + doc.activeElement.dispatchEvent(tab); + assert.equal(doc.activeElement.id, 'zotero-tb-search-textbox'); + let paneToggle = doc.getElementById('zotero-tb-toggle-item-pane-stacked'); + paneToggle.dispatchEvent(tab); + assert.equal(doc.activeElement, buttons[0]); + doc.activeElement.dispatchEvent(tab); + assert.equal(doc.activeElement, buttons[1]); + doc.activeElement.dispatchEvent(tab); + assert.equal(doc.activeElement, + doc.getElementById('zotero-items-tree').querySelector('.virtualized-table')); + doc.activeElement.dispatchEvent(shiftTab); + assert.equal(doc.activeElement, buttons[1]); + doc.activeElement.dispatchEvent(shiftTab); + assert.equal(doc.activeElement, buttons[0]); + doc.activeElement.dispatchEvent(shiftTab); + let toggleHidden = paneToggle.disabled || paneToggle.hidden + || paneToggle.parentNode.hidden + || win.getComputedStyle(paneToggle).display === 'none'; + assert.equal(doc.activeElement.id, toggleHidden + ? 'zotero-tb-search-advanced-button' : paneToggle.id); + doc.getElementById('zotero-tb-lookup').focus(); + doc.activeElement.dispatchEvent(tab); + assert.equal(doc.activeElement.id, 'zotero-tb-search-textbox'); + doc.getElementById('zotero-tb-attachment-add').focus(); + doc.activeElement.dispatchEvent(tab); + assert.equal(doc.activeElement.id, 'zotero-tb-search-textbox'); + + doc.getElementById('zotero-tb-note-add').focus(); + doc.activeElement.dispatchEvent(rightArrow); + assert.equal(doc.activeElement.id, 'zotero-tb-note-add'); + buttons[0].focus(); + doc.activeElement.dispatchEvent(leftArrow); + assert.equal(doc.activeElement, buttons[0]); + doc.activeElement.dispatchEvent(rightArrow); + assert.equal(doc.activeElement, buttons[1]); + doc.activeElement.dispatchEvent(leftArrow); + assert.equal(doc.activeElement, buttons[0]); + + buttons[0].hidden = true; + paneToggle.dispatchEvent(tab); + assert.equal(doc.activeElement, buttons[1]); + buttons[0].hidden = false; + buttons[0].disabled = true; + paneToggle.dispatchEvent(tab); + assert.equal(doc.activeElement, buttons[1]); + buttons[0].disabled = false; + buttons[0].style.display = 'none'; + paneToggle.dispatchEvent(tab); + assert.equal(doc.activeElement, buttons[1]); + buttons[0].style.display = ''; + buttons[0].disabled = true; + doc.getElementById('zotero-tb-add').focus(); + buttons[0].dispatchEvent(tab); + assert.equal(doc.activeElement, buttons[1]); + } + finally { + buttons.forEach(button => button.remove()); + } + }); }); describe("#addAttachmentFromDialog()", function () {