From 826774b1f72cfaacb135832ea5cd42bee96bacb9 Mon Sep 17 00:00:00 2001 From: abaevbog Date: Mon, 19 Feb 2024 04:57:03 -0500 Subject: [PATCH] keyboard navigation for tag selector (#3615) - Tab from the tags list or shift-tab from the tags filter field focuses the first non-disabled tag. If there are none, the tags are skipped and the focus moves directly to the input field or the tags list. - Arrow Right/Left move focus between tags skipping over disabled tags - Space/Enter clicks on the selected tag - Space/Enter click on the search button when focused --- .../content/zotero/components/tagSelector.jsx | 4 ++- .../tagSelector/tagSelectorList.jsx | 5 +++- .../containers/tagSelectorContainer.jsx | 29 +++++++++++++++++++ chrome/content/zotero/zoteroPane.js | 26 ++++++++++++++--- scss/components/_tagSelector.scss | 4 +++ 5 files changed, 62 insertions(+), 6 deletions(-) diff --git a/chrome/content/zotero/components/tagSelector.jsx b/chrome/content/zotero/components/tagSelector.jsx index e98f43067f..a2059effec 100644 --- a/chrome/content/zotero/components/tagSelector.jsx +++ b/chrome/content/zotero/components/tagSelector.jsx @@ -41,6 +41,7 @@ class TagSelector extends React.PureComponent { tags={this.props.tags} dragObserver={this.props.dragObserver} onSelect={this.props.onSelect} + onKeyDown={this.props.onKeyDown} onTagContext={this.props.onTagContext} loaded={this.props.loaded} width={this.props.width} @@ -63,7 +64,7 @@ class TagSelector extends React.PureComponent { title="zotero.toolbar.actions.label" className="tag-selector-actions" isMenu - onMouseDown={ev => this.props.onSettings(ev)} + onClick={ev => this.props.onSettings(ev)} /> @@ -88,6 +89,7 @@ TagSelector.propTypes = { onDrop: PropTypes.func }), onSelect: PropTypes.func, + onKeyDown: PropTypes.func, onTagContext: PropTypes.func, loaded: PropTypes.bool, width: PropTypes.number.isRequired, diff --git a/chrome/content/zotero/components/tagSelector/tagSelectorList.jsx b/chrome/content/zotero/components/tagSelector/tagSelectorList.jsx index 8cde6dea32..92767475a8 100644 --- a/chrome/content/zotero/components/tagSelector/tagSelectorList.jsx +++ b/chrome/content/zotero/components/tagSelector/tagSelectorList.jsx @@ -177,14 +177,16 @@ class TagList extends React.PureComponent { className, onClick: ev => !tag.disabled && this.props.onSelect(tag.name, ev), onContextMenu: ev => this.props.onTagContext(tag, ev), + onKeyDown: ev => this.props.onKeyDown(ev), onDragOver, onDragExit, - onDrop + onDrop, }; props.style = { ...style }; + props.tabIndex = "0"; // Don't specify explicit width unless we're truncating, because for some reason the width // from canvas can sometimes be slightly smaller than the actual width, resulting in an @@ -274,6 +276,7 @@ class TagList extends React.PureComponent { onDrop: PropTypes.func }), onSelect: PropTypes.func, + onKeyDown: PropTypes.func, onTagContext: PropTypes.func, loaded: PropTypes.bool, width: PropTypes.number.isRequired, diff --git a/chrome/content/zotero/containers/tagSelectorContainer.jsx b/chrome/content/zotero/containers/tagSelectorContainer.jsx index 296f366dc5..875623290d 100644 --- a/chrome/content/zotero/containers/tagSelectorContainer.jsx +++ b/chrome/content/zotero/containers/tagSelectorContainer.jsx @@ -528,6 +528,7 @@ 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)} @@ -579,6 +580,34 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent { } } + handleKeyDown = (e) => { + if (["ArrowRight", "ArrowLeft"].includes(e.key)) { + let nextTag = (node) => { + if (e.key == "ArrowRight") return node.nextElementSibling; + return node.previousElementSibling; + }; + let nextOne = nextTag(e.target); + // Skip disabled tags + while (nextOne && nextOne.classList.contains("disabled")) { + nextOne = nextTag(nextOne); + } + if (nextOne) { + nextOne.focus(); + } + } + else if (e.key == "Tab" && !e.shiftKey) { + this.focusTextbox(); + e.preventDefault(); + } + else if (e.key == "Tab" && e.shiftKey) { + document.querySelector('.tag-selector-list').focus(); + e.preventDefault(); + } + if ([" ", "Enter"].includes(e.key)) { + e.target.click(); + } + } + handleSearch = (searchString) => { this.setState({searchString}); } diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index c1face74c0..517c198860 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -428,12 +428,30 @@ var ZoteroPane = new function() moveFocus(actionsMap, event); }); - tagSelector.addEventListener("keydown", (event) => { + tagSelector.addEventListener("keydown", (e) => { + // Tab from the scrollable tag list or Shift-Tab from the input field focuses the first + // non-disabled tag. If there are none, the tags are skipped + if ((e.target.classList.contains("tag-selector-list") && e.key == "Tab" && !e.shiftKey) + || e.target.tagName == "input" && e.key == "Tab" && e.shiftKey) { + let firstNonDisabledTag = document.querySelector('.tag-selector-item:not(.disabled)'); + if (firstNonDisabledTag) { + firstNonDisabledTag.focus(); + } + else if (e.target.classList.contains("tag-selector-list")) { + tagSelector.querySelector("input").focus(); + } + else { + tagSelector.querySelector(".tag-selector-list").focus(); + } + + e.preventDefault(); + e.stopPropagation(); + } // Special treatment for tag selector button because it has no id - if (event.target.tagName == "button" && event.key == "Tab" && !event.shiftKey) { + if (e.target.tagName == "button" && e.key == "Tab" && !e.shiftKey) { document.getElementById('item-tree-main-default').focus(); - event.preventDefault(); - event.stopPropagation(); + e.preventDefault(); + e.stopPropagation(); } }); } diff --git a/scss/components/_tagSelector.scss b/scss/components/_tagSelector.scss index 2104426d1d..d7d6b104c4 100644 --- a/scss/components/_tagSelector.scss +++ b/scss/components/_tagSelector.scss @@ -117,6 +117,10 @@ padding: 2px 4px; } + --width-focus-border: 1px; + --radius-focus-border: 5px; + @include focus-ring; + &:hover { background-color: var(--fill-quinary); }