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
This commit is contained in:
parent
1b751d675b
commit
826774b1f7
5 changed files with 62 additions and 6 deletions
|
@ -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)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -117,6 +117,10 @@
|
|||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
--width-focus-border: 1px;
|
||||
--radius-focus-border: 5px;
|
||||
@include focus-ring;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--fill-quinary);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue