From 638efad3eb65e23e311e5d9dfd884f1af6383617 Mon Sep 17 00:00:00 2001 From: abaevbog Date: Tue, 30 Apr 2024 10:31:55 -0400 Subject: [PATCH] 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 --- .../tagSelector/tagSelectorList.jsx | 43 ++++++++++++++----- .../containers/tagSelectorContainer.jsx | 15 ++++++- chrome/content/zotero/zoteroPane.js | 16 +++---- 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/chrome/content/zotero/components/tagSelector/tagSelectorList.jsx b/chrome/content/zotero/components/tagSelector/tagSelectorList.jsx index af642672d8..0cc125ad47 100644 --- a/chrome/content/zotero/components/tagSelector/tagSelectorList.jsx +++ b/chrome/content/zotero/components/tagSelector/tagSelectorList.jsx @@ -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 ( -
+
{tagList}
); diff --git a/chrome/content/zotero/containers/tagSelectorContainer.jsx b/chrome/content/zotero/containers/tagSelectorContainer.jsx index 1159d3936d..c3125ee725 100644 --- a/chrome/content/zotero/containers/tagSelectorContainer.jsx +++ b/chrome/content/zotero/containers/tagSelectorContainer.jsx @@ -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)} diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index 9897af0b4f..6f84533bd6 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -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': {