Improve tabs menu keyboard handle (#5319)

Use space to close tab if the close button is active
Focus next close button after closing tab with keyboard
This commit is contained in:
windingwind 2025-06-05 08:11:53 +02:00 committed by GitHub
parent 0b7340a651
commit 51a62022e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 73 additions and 18 deletions

View file

@ -57,6 +57,8 @@
_filterInput;
_prevFocusClass = null;
get visible() {
return ["showing", "open"].includes(this.state);
}
@ -91,15 +93,21 @@
/**
* Create the list of opened tabs in tabs menu.
* @param {Object} [options] - Options for refreshing the list
* @param {boolean} [options.ignorePreviousSelection=false] - If true, ignore the previous selection and select the currently selected tab
* @returns {void}
*/
refreshList() {
refreshList(options = {}) {
if (!this.visible) {
return;
}
this._selectedIndex = null;
// Empty existing nodes
this._tabsList.replaceChildren();
let activeElement = document.activeElement;
if (this.contains(activeElement) && !activeElement.classList.contains(this._prevFocusClass)) {
this._prevFocusClass = null;
}
let selectedIndex = null;
let index = 0;
let validTabs = Zotero_Tabs._tabs.filter(
@ -112,6 +120,17 @@
);
let tabsCount = validTabs.length;
let selectedTabID = Zotero_Tabs.selectedID;
if (!options.ignorePreviousSelection) {
let prevTabID = this._tabsList.querySelector(".selected")?.dataset.tabId;
if (prevTabID && validTabs.some(tab => tab.id == prevTabID)) {
selectedTabID = prevTabID;
}
}
// Empty existing nodes
this._tabsList.replaceChildren();
// If no tabs are open, show an empty row with a message
if (tabsCount === 0) {
let row = document.createElement('div');
@ -132,7 +151,6 @@
return;
}
let selectedTabID = Zotero_Tabs.selectedID;
for (let tab of validTabs) {
// Top-level entry of the opened tabs array
let row = document.createElement('div');
@ -219,6 +237,10 @@
if (selectedIndex !== null) {
this._selectRow(selectedIndex);
if (this._prevFocusClass) {
this._tabsList.querySelector(`.selected > .${this._prevFocusClass}`).focus();
}
}
else {
this.moveSelection("first");
@ -333,11 +355,21 @@
row.querySelector(".close").setAttribute("tabindex", "0");
this._filterInput.setAttribute("aria-activedescendant", row.id);
}
this._focusInput();
this.resetFocus();
}
_focusInput() {
resetFocus() {
let row = this._tabsList.querySelector(".selected");
if (this._prevFocusClass) {
let focusTarget = row.querySelector(`.${this._prevFocusClass}`);
if (focusTarget && !focusTarget.hidden) {
focusTarget.focus();
}
else {
this._filterInput.focus();
}
return;
}
if (document.activeElement == this._filterInput) {
return;
}
@ -347,7 +379,7 @@
_handleShowing = (event) => {
if (event.originalTarget !== this) return;
this.refreshList();
this.refreshList({ ignorePreviousSelection: true });
// Make sure that if the menu is very long, there is a small
// gap left between the top/bottom of the menu and the edge of the screen
@ -398,7 +430,7 @@
};
_handleShown = () => {
this._focusInput();
this.resetFocus();
};
_handleHiding = (event) => {
@ -411,6 +443,9 @@
this._filterInput.removeEventListener("input", this._handleFilterInput);
this._filterInput.removeEventListener("blur", this._handleFilterBlur);
this._prevFocusClass = null;
this._selectedIndex = null;
};
/**
@ -426,6 +461,7 @@
* - Home/PageUp focuses the filter field
* - End/PageDown focues the last tab title
* - CMD-f will focus the input field
* - Space on the close button will close the tab and select the next tab's close button
*/
_handleKeydown = (event) => {
if (["Home", "PageUp"].includes(event.key)) {
@ -458,6 +494,16 @@
event.preventDefault();
event.stopPropagation();
}
else if (event.key === " ") {
if (document.activeElement.classList.contains("close")) {
event.preventDefault();
event.stopPropagation();
// Move selection to the next tab to make sure the next tab's close button is focused
this.moveSelection("next");
this._prevFocusClass = "close";
this._handleCloseClick(event);
}
}
};
/**
@ -481,7 +527,7 @@
// If not title or close button, prevent the blur
event.preventDefault();
event.stopPropagation();
this._focusInput();
this.resetFocus();
});
};
@ -504,8 +550,9 @@
if (!tabID) {
return;
}
this.resetFocus();
Zotero_Tabs.close(tabID);
this._focusInput();
this._prevFocusClass = null;
};
_handleRowDragStart = (event) => {
@ -568,7 +615,7 @@
if (document.getElementById("zotero-tabs-menu-dragged")) {
this.refreshList();
}
this._focusInput();
this.resetFocus();
};
}

View file

@ -144,9 +144,6 @@ var Zotero_Tabs = new function () {
let panel = this.tabsMenuPanel;
if (panel.visible) {
panel.refreshList();
if (document.activeElement.id !== "zotero-tabs-menu-filter") {
panel.focusEntry();
}
}
};
@ -346,7 +343,15 @@ var Zotero_Tabs = new function () {
continue;
}
if (tab.id == this._selectedID) {
this.select(this._prevSelectedID || (this._tabs[tabIndex + 1] || this._tabs[tabIndex - 1]).id);
let selectOptions = {};
// If the tabs menu is visible, let the tab bar handle focus
if (this.tabsMenuPanel.visible) {
selectOptions.keepTabFocused = true;
}
this.select(
this._prevSelectedID || (this._tabs[tabIndex + 1] || this._tabs[tabIndex - 1]).id,
false, selectOptions
);
}
if (tab.id == this._prevSelectedID) {
this._prevSelectedID = null;
@ -453,10 +458,13 @@ var Zotero_Tabs = new function () {
// Small delay to make sure the focus does not remain on the actual
// tab after mouse click
setTimeout(() => {
if (tab.lastFocusedElement) {
if (this.tabsMenuPanel.visible) {
this.tabsMenuPanel.resetFocus();
}
else if (tab.lastFocusedElement) {
tab.lastFocusedElement.focus();
}
if (document.activeElement !== tab.lastFocusedElement) {
else if (document.activeElement !== tab.lastFocusedElement) {
ZoteroPane_Local.itemsView.focus();
}
tab.lastFocusedElement = null;