Prevent focus from being lost in itemBox (#4287)

- always render options and link buttons - just do
not display them if their respective field is empty.
That allows us to easily handle focus after refresh
because otherwise, the node may does not exist.
If we try to restore focus to such hidden component,
simulate a "tab" from it to focus the next possible node.
This fixes the issue of focus being lost on tab after all
content of editable-text is cleared.
- add tabindex=0 to itemType menulist, otherwise it was
not perceived as a candidate to return focus to.
- somewhat special treatment for restoring focus to
creator rows. If the desired node is not found, we'll
try to focus the respective node in the last creator row.
It prevents focus from being lost on tab after clearing
the very last creator.

Fixes: #4241
This commit is contained in:
abaevbog 2024-07-01 00:01:33 -07:00 committed by GitHub
parent 7dc68209a5
commit a054831a3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -621,11 +621,15 @@
.replace(/\?/g, '%3f')
.replace(/%/g, '%25')
.replace(/"/g, '%22');
openLinkButton = this.createOpenLinkIcon(doi);
openLinkButton = this.createOpenLinkIcon(doi, fieldName);
link = doi;
addLinkContextMenu = true;
}
}
// Hidden open-link button just for focus management
else if (['url', 'homepage', 'DOI'].includes(fieldName)) {
openLinkButton = this.createOpenLinkIcon(null, fieldName);
}
let rowData = document.createElement('div');
rowData.className = "meta-data";
rowData.appendChild(valueElement);
@ -645,7 +649,7 @@
}
// Add options button for title fields
if (this.editable && fieldID && val && (fieldName == 'seriesTitle' || fieldName == 'shortTitle'
if (this.editable && fieldID && (fieldName == 'seriesTitle' || fieldName == 'shortTitle'
|| Zotero.ItemFields.isFieldOfBase(fieldID, 'title')
|| Zotero.ItemFields.isFieldOfBase(fieldID, 'publicationTitle'))) {
let optionsButton = document.createXULElement("toolbarbutton");
@ -676,6 +680,8 @@
optionsButton.addEventListener("click", triggerPopup);
rowData.appendChild(optionsButton);
rowData.oncontextmenu = triggerPopup;
// Options button is always created for focus management but if the field is empty, it is hidden
if (!val) optionsButton.hidden = true;
}
this.addDynamicRow(rowLabel, rowData);
@ -888,6 +894,8 @@
var menulist = document.createXULElement("menulist", { is: "menulist-item-types" });
menulist.id = "item-type-menu";
menulist.className = "zotero-clicky keyboard-clickable";
// This is to make it easier to identify the itemType menu in _saveFieldFocus
menulist.setAttribute("tabindex", 0);
menulist.addEventListener('command', (event) => {
this.changeTypeTo(event.target.value, menulist);
});
@ -1413,6 +1421,7 @@
openLink.className = "zotero-clicky zotero-clicky-open-link show-on-hover no-display";
openLink.addEventListener("click", event => ZoteroPane.loadURI(value, event));
openLink.setAttribute('data-l10n-id', "item-button-view-online");
if (!value) openLink.hidden = true;
return openLink;
}
@ -2249,11 +2258,24 @@
return;
}
let refocusField = this.querySelector(`#${CSS.escape(this._selectField)}`);
let refocusField = this.querySelector(`#${CSS.escape(this._selectField)}:not([disabled="true"])`);
// For creator rows, if a focusable node with desired id does not exist, try to focus
// the same component from the last available creator row
if (!refocusField && this._selectField.includes("creator")) {
let maybeLastCreatorID = this._selectField.replace(/\d+/g, Math.max(this._creatorCount - 1, 0));
refocusField = this.querySelector(`#${CSS.escape(maybeLastCreatorID)}`);
}
if (!refocusField) {
this._clearSavedFieldFocus();
return;
}
refocusField.focus();
// If the node did not receive focus (e.g. disabled or hidden), focus the next focusable node
if (!(refocusField.contains(document.activeElement) || document.activeElement == refocusField)) {
// Note: typically, a node contains itself, so node.contains(node) is true.
// Somehow, it is not true for itemType menulist, which explains the seemingly redundant || condition.
Services.focus.moveFocus(window, refocusField, Services.focus.MOVEFOCUS_FORWARD, 0);
}
if (this._selectFieldSelection) {
let input = refocusField.querySelector("input, textarea");