Bug 1538281 - make tree view row keyboard navigation consistent with other shared components. r=nchevobbe

Differential Revision: https://phabricator.services.mozilla.com/D24538
This commit is contained in:
Yura Zenevich
2019-04-03 00:36:03 +00:00
parent 3b5095f7ae
commit 27d11f65d7
5 changed files with 526 additions and 13 deletions

View File

@@ -7,8 +7,12 @@
// Make this available to both AMD and CJS environments
define(function(require, exports, module) {
const { cloneElement, Component, createFactory } =
require("devtools/client/shared/vendor/react");
const {
cloneElement,
Component,
createFactory,
createRef,
} = require("devtools/client/shared/vendor/react");
const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
@@ -25,6 +29,9 @@ define(function(require, exports, module) {
"ArrowRight",
"End",
"Home",
"Enter",
" ",
"Escape",
];
const defaultProps = {
@@ -33,6 +40,7 @@ define(function(require, exports, module) {
provider: ObjectProvider,
expandedNodes: new Set(),
selected: null,
active: null,
expandableStrings: true,
columns: [],
};
@@ -111,6 +119,8 @@ define(function(require, exports, module) {
expandedNodes: PropTypes.object,
// Selected node
selected: PropTypes.string,
// The currently active (keyboard) item, if any such item exists.
active: PropTypes.string,
// Custom filtering callback
onFilter: PropTypes.func,
// Custom sorting callback
@@ -190,15 +200,19 @@ define(function(require, exports, module) {
expandedNodes: props.expandedNodes,
columns: ensureDefaultColumn(props.columns),
selected: props.selected,
active: props.active,
lastSelectedIndex: 0,
};
this.treeRef = createRef();
this.toggle = this.toggle.bind(this);
this.isExpanded = this.isExpanded.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.onClickRow = this.onClickRow.bind(this);
this.getSelectedRow = this.getSelectedRow.bind(this);
this.selectRow = this.selectRow.bind(this);
this.activateRow = this.activateRow.bind(this);
this.isSelected = this.isSelected.bind(this);
this.onFilter = this.onFilter.bind(this);
this.onSort = this.onSort.bind(this);
@@ -310,10 +324,31 @@ define(function(require, exports, module) {
this.selectRow(lastRow);
}
break;
case "Enter":
case " ":
// On space or enter make selected row active. This means keyboard
// focus handling is passed on to the tree row itself.
if (this.treeRef.current === document.activeElement) {
event.stopPropagation();
event.preventDefault();
if (this.state.active !== this.state.selected) {
this.activateRow(this.state.selected);
}
return;
}
break;
case "Escape":
event.stopPropagation();
if (this.state.active != null) {
this.activateRow(null);
}
break;
}
// Focus should always remain on the tree container itself.
this.tree.focus();
this.treeRef.current.focus();
event.preventDefault();
}
@@ -358,17 +393,36 @@ define(function(require, exports, module) {
return;
}
this.setState(Object.assign({}, this.state, {
if (this.state.active != null) {
if (this.treeRef.current !== document.activeElement) {
this.treeRef.current.focus();
}
}
this.setState({
...this.state,
selected: row.id,
}));
active: null,
});
row.scrollIntoView(scrollOptions);
}
activateRow(active) {
this.setState({
...this.state,
active,
});
}
isSelected(nodePath) {
return nodePath === this.state.selected;
}
isActive(nodePath) {
return nodePath === this.state.active;
}
// Filtering & Sorting
/**
@@ -450,6 +504,8 @@ define(function(require, exports, module) {
hidden: !this.onFilter(child),
// True if the node is selected with keyboard
selected: this.isSelected(nodePath),
// True if the node is activated with keyboard
active: this.isActive(nodePath),
};
});
}
@@ -477,7 +533,7 @@ define(function(require, exports, module) {
}
const props = Object.assign({}, this.props, {
key: member.path,
key: `${member.path}-${member.active ? "active" : "inactive"}`,
member: member,
columns: this.state.columns,
id: member.path,
@@ -538,12 +594,22 @@ define(function(require, exports, module) {
dom.table({
className: classNames.join(" "),
role: "tree",
ref: tree => {
this.tree = tree;
},
ref: this.treeRef,
tabIndex: 0,
onKeyDown: this.onKeyDown,
onContextMenu: onContextMenuTree && onContextMenuTree.bind(this),
onClick: () => {
// Focus should always remain on the tree container itself.
this.treeRef.current.focus();
},
onBlur: event => {
if (this.state.active != null) {
const { relatedTarget } = event;
if (!this.treeRef.current.contains(relatedTarget)) {
this.activateRow(null);
}
}
},
"aria-label": this.props.label || "",
"aria-activedescendant": this.state.selected,
cellPadding: 0,