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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user