properly refocus tag from tag selector on re-enter (#4035)

- do not erase the last recorded tag on blur of the tag selector
- on tab from collectionTree or shift-tab from tag selector input,
try to refocus that last focused tag. If one does not exist,
try to focus the first non-disabled tag.

Fixes: #4008
This commit is contained in:
abaevbog 2024-04-30 10:31:55 -04:00 committed by GitHub
parent d5194f234d
commit 638efad3eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 55 additions and 19 deletions

View file

@ -224,6 +224,36 @@ class TagList extends React.PureComponent {
);
};
tagSelectorList() {
return document.querySelector('.tag-selector-list');
}
isEmpty() {
return !this.tagSelectorList().querySelector('.tag-selector-item:not(.disabled)');
}
clearRecordedFocusedTag() {
this.lastFocusedTagIndex = null;
this.focusedTagIndex = null;
}
// Focus the last focused tag from the list. If there is none, focus the first
// non-disabled tag.
async focus() {
if (this.focusedTagIndex === null) {
let enabledTag = this.tagSelectorList().querySelector('.tag-selector-item:not(.disabled)');
if (!enabledTag) return;
enabledTag.focus();
return;
}
let tagRefocused = this.refocusTag();
if (tagRefocused) return;
// If the tag could not be refocused, it means it was removed due to windowing,
// so we need to scroll to it.
this.setState({ scrollToCell: this.focusedTagIndex });
await this.waitForSectionRender();
}
// Try to refocus a focused tag that was removed due to windowing
refocusTag() {
let tagsList = document.querySelector('.tag-selector-list');
@ -232,7 +262,9 @@ class TagList extends React.PureComponent {
let nodeToFocus = tagsNodes.find(node => node.textContent == tagToFocus.name);
if (nodeToFocus) {
nodeToFocus.focus();
return true;
}
return false;
}
waitForSectionRender() {
@ -266,15 +298,6 @@ class TagList extends React.PureComponent {
}
};
handleBlur = (event) => {
// If the focus leaves the tags list, clear the last focused tag index
let tagsList = document.querySelector('.tag-selector-list');
if (!tagsList.contains(event.relatedTarget)) {
this.focusedTagIndex = null;
this.lastFocusedTagIndex = null;
}
};
async handleKeyDown(e) {
if (!["ArrowRight", "ArrowLeft"].includes(e.key)) return;
// If the windowing kicks in, the node of the initially-focused tag may not
@ -346,7 +369,7 @@ class TagList extends React.PureComponent {
}
return (
<div className="tag-selector-list-container" onBlur={this.handleBlur} onKeyDown={this.handleKeyDown.bind(this)}>
<div className="tag-selector-list-container" onKeyDown={this.handleKeyDown.bind(this)}>
{tagList}
</div>
);

View file

@ -81,6 +81,14 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
this.searchBoxRef.current.focus();
}
focusTagList() {
this.tagListRef.current.focus();
}
isTagListEmpty() {
return this.tagListRef.current.isEmpty();
}
componentDidCatch(error, info) {
// Async operations might attempt to update the react components
// after window close in tests, which will cause unnecessary crashing.
@ -100,6 +108,12 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
this.prevTreeViewID = this.collectionTreeRow.id;
}
}
getSnapshotBeforeUpdate(_) {
// Clear the focused tag's record if the props change
this.tagListRef.current.clearRecordedFocusedTag();
return null;
}
// Update trigger #1 (triggered by ZoteroPane)
async onItemViewChanged({ collectionTreeRow, libraryID }) {
@ -528,7 +542,6 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
searchString={this.state.searchString}
dragObserver={this.dragObserver}
onSelect={this.handleTagSelected}
onKeyDown={this.handleKeyDown}
onTagContext={this.handleTagContext}
onSearch={this.handleSearch}
onSettings={this.handleSettings.bind(this)}

View file

@ -420,11 +420,11 @@ var ZoteroPane = new function()
return document.getElementById('zotero-tb-add');
}
// If tag selector is collapsed, go to "New item" button, otherwise focus tag selector
let firstNonDisabledTag = tagSelector.querySelector('.tag-selector-item:not(.disabled)');
if (firstNonDisabledTag) {
return firstNonDisabledTag;
if (ZoteroPane.tagSelector.isTagListEmpty()) {
return tagSelector.querySelector(".search-input");
}
return tagSelector.querySelector(".search-input");
ZoteroPane.tagSelector.focusTagList();
return null;
},
Escape: clearCollectionSearch
}
@ -446,11 +446,11 @@ var ZoteroPane = new function()
'search-input': {
Tab: () => tagSelector.querySelector('.tag-selector-actions'),
ShiftTab: () => {
let firstNonDisabledTag = tagSelector.querySelector('.tag-selector-item:not(.disabled)');
if (firstNonDisabledTag) {
return firstNonDisabledTag;
if (ZoteroPane.tagSelector.isTagListEmpty()) {
return document.getElementById("collection-tree");
}
return document.getElementById("collection-tree");
ZoteroPane.tagSelector.focusTagList();
return null;
},
},
'tag-selector-item': {