mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-10 11:55:49 +00:00
Bug 1221384 - Scroll the Tree component when focusing new items with the arrow keys; r=jsantell
This commit is contained in:
parent
cdcf664fb3
commit
c6425cbdee
@ -14,3 +14,4 @@ support-files =
|
||||
[test_tree_08.html]
|
||||
[test_tree_09.html]
|
||||
[test_tree_10.html]
|
||||
[test_tree_11.html]
|
||||
|
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test that when an item in the Tree component is focused by arrow key, the view is scrolled.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Tree component test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
<link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 30px;
|
||||
max-height: 30px;
|
||||
min-height: 30px;
|
||||
font-size: 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<pre id="test">
|
||||
<script src="head.js" type="application/javascript;version=1.8"></script>
|
||||
<script type="application/javascript;version=1.8">
|
||||
window.onload = Task.async(function* () {
|
||||
try {
|
||||
const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
|
||||
const React = browserRequire("devtools/client/shared/vendor/react");
|
||||
const { Simulate } = React.addons.TestUtils;
|
||||
const Tree = React.createFactory(browserRequire("devtools/client/shared/components/tree"));
|
||||
|
||||
TEST_TREE.expanded = new Set("ABCDEFGHIJKLMNO".split(""));
|
||||
|
||||
const tree = ReactDOM.render(Tree(TEST_TREE_INTERFACE), window.document.body);
|
||||
|
||||
yield setProps(tree, {
|
||||
itemHeight: 10,
|
||||
onFocus: item => setProps(tree, { focused: item }),
|
||||
focused: "K",
|
||||
});
|
||||
yield setState(tree, {
|
||||
scroll: 10,
|
||||
});
|
||||
yield forceRender(tree);
|
||||
|
||||
isRenderedTree(document.body.textContent, [
|
||||
"A:false",
|
||||
"-B:false",
|
||||
"--E:false",
|
||||
"---K:true",
|
||||
"---L:false",
|
||||
], "Should render initial correctly");
|
||||
|
||||
yield new Promise(resolve => {
|
||||
const treeElem = document.querySelector(".tree");
|
||||
treeElem.addEventListener("scroll", function onScroll() {
|
||||
dumpn("Got scroll event");
|
||||
treeElem.removeEventListener("scroll", onScroll);
|
||||
resolve();
|
||||
});
|
||||
|
||||
dumpn("Sending ArrowDown key");
|
||||
Simulate.keyDown(treeElem, { key: "ArrowDown" });
|
||||
});
|
||||
|
||||
dumpn("Forcing re-render");
|
||||
yield forceRender(tree);
|
||||
|
||||
isRenderedTree(document.body.textContent, [
|
||||
"-B:false",
|
||||
"--E:false",
|
||||
"---K:false",
|
||||
"---L:true",
|
||||
"--F:false",
|
||||
], "Should have scrolled down one");
|
||||
|
||||
} catch(e) {
|
||||
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
|
||||
} finally {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
@ -61,7 +61,7 @@ const TreeNode = createFactory(createClass({
|
||||
expanded: this.props.expanded,
|
||||
visible: this.props.hasChildren,
|
||||
onExpand: this.props.onExpand,
|
||||
onCollapse: this.props.onCollapse
|
||||
onCollapse: this.props.onCollapse,
|
||||
});
|
||||
|
||||
let isOddRow = this.props.index % 2;
|
||||
@ -115,14 +115,17 @@ const TreeNode = createFactory(createClass({
|
||||
*/
|
||||
function oncePerAnimationFrame(fn) {
|
||||
let animationId = null;
|
||||
let argsToPass = null;
|
||||
return function (...args) {
|
||||
argsToPass = args;
|
||||
if (animationId !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(() => {
|
||||
fn.call(this, ...argsToPass);
|
||||
animationId = null;
|
||||
fn.call(this, ...args);
|
||||
argsToPass = null;
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -235,9 +238,11 @@ const Tree = module.exports = createClass({
|
||||
render() {
|
||||
const traversal = this._dfsFromRoots();
|
||||
|
||||
// Remove 1 from `begin` and add 2 to `end` so that the top and bottom of
|
||||
// the page are filled with the previous and next items respectively,
|
||||
// rather than whitespace if the item is not in full view.
|
||||
// Remove `NUMBER_OF_OFFSCREEN_ITEMS` from `begin` and add `2 *
|
||||
// NUMBER_OF_OFFSCREEN_ITEMS` to `end` so that the top and bottom of the
|
||||
// page are filled with the `NUMBER_OF_OFFSCREEN_ITEMS` previous and next
|
||||
// items respectively, rather than whitespace if the item is not in full
|
||||
// view.
|
||||
const begin = Math.max(((this.state.scroll / this.props.itemHeight) | 0) - NUMBER_OF_OFFSCREEN_ITEMS, 0);
|
||||
const end = begin + (2 * NUMBER_OF_OFFSCREEN_ITEMS) + ((this.state.height / this.props.itemHeight) | 0);
|
||||
const toRender = traversal.slice(begin, end);
|
||||
@ -266,7 +271,7 @@ const Tree = module.exports = createClass({
|
||||
hasChildren: !!this.props.getChildren(item).length,
|
||||
onExpand: this._onExpand,
|
||||
onCollapse: this._onCollapse,
|
||||
onFocus: () => this._focus(item)
|
||||
onFocus: () => this._focus(begin + i, item),
|
||||
}));
|
||||
}
|
||||
|
||||
@ -284,6 +289,8 @@ const Tree = module.exports = createClass({
|
||||
className: "tree",
|
||||
ref: "tree",
|
||||
onKeyDown: this._onKeyDown,
|
||||
onKeyPress: this._preventArrowKeyScrolling,
|
||||
onKeyUp: this._preventArrowKeyScrolling,
|
||||
onScroll: this._onScroll,
|
||||
style: {
|
||||
padding: 0,
|
||||
@ -294,6 +301,25 @@ const Tree = module.exports = createClass({
|
||||
);
|
||||
},
|
||||
|
||||
_preventArrowKeyScrolling(e) {
|
||||
switch (e.key) {
|
||||
case "ArrowUp":
|
||||
case "ArrowDown":
|
||||
case "ArrowLeft":
|
||||
case "ArrowRight":
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.nativeEvent) {
|
||||
if (e.nativeEvent.preventDefault) {
|
||||
e.nativeEvent.preventDefault();
|
||||
}
|
||||
if (e.nativeEvent.stopPropagation) {
|
||||
e.nativeEvent.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the state's height based on clientHeight.
|
||||
*/
|
||||
@ -377,9 +403,31 @@ const Tree = module.exports = createClass({
|
||||
/**
|
||||
* Sets the passed in item to be the focused item.
|
||||
*
|
||||
* @param {Object} item
|
||||
* @param {Number} index
|
||||
* The index of the item in a full DFS traversal (ignoring collapsed
|
||||
* nodes). Ignored if `item` is undefined.
|
||||
*
|
||||
* @param {Object|undefined} item
|
||||
* The item to be focused, or undefined to focus no item.
|
||||
*/
|
||||
_focus(item) {
|
||||
_focus(index, item) {
|
||||
if (item !== undefined) {
|
||||
const itemStartPosition = index * this.props.itemHeight;
|
||||
const itemEndPosition = (index + 1) * this.props.itemHeight;
|
||||
|
||||
// Note that if the height of the viewport (this.state.height) is less than
|
||||
// `this.props.itemHeight`, we could accidentally try and scroll both up and
|
||||
// down in a futile attempt to make both the item's start and end positions
|
||||
// visible. Instead, give priority to the start of the item by checking its
|
||||
// position first, and then using an "else if", rather than a separate "if",
|
||||
// for the end position.
|
||||
if (this.state.scroll > itemStartPosition) {
|
||||
this.refs.tree.scrollTo(0, itemStartPosition);
|
||||
} else if ((this.state.scroll + this.state.height) < itemEndPosition) {
|
||||
this.refs.tree.scrollTo(0, itemEndPosition - this.state.height);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.onFocus) {
|
||||
this.props.onFocus(item);
|
||||
}
|
||||
@ -389,7 +437,7 @@ const Tree = module.exports = createClass({
|
||||
* Sets the state to have no focused item.
|
||||
*/
|
||||
_onBlur() {
|
||||
this._focus(undefined);
|
||||
this._focus(0, undefined);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -420,20 +468,16 @@ const Tree = module.exports = createClass({
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent scrolling when pressing navigation keys. Guard against mocked
|
||||
// events received when testing.
|
||||
if (e.nativeEvent && e.nativeEvent.preventDefault) {
|
||||
ViewHelpers.preventScrolling(e.nativeEvent);
|
||||
}
|
||||
this._preventArrowKeyScrolling(e);
|
||||
|
||||
switch (e.key) {
|
||||
case "ArrowUp":
|
||||
this._focusPrevNode();
|
||||
return false;
|
||||
return;
|
||||
|
||||
case "ArrowDown":
|
||||
this._focusNextNode();
|
||||
return false;
|
||||
return;
|
||||
|
||||
case "ArrowLeft":
|
||||
if (this.props.isExpanded(this.props.focused)
|
||||
@ -442,7 +486,7 @@ const Tree = module.exports = createClass({
|
||||
} else {
|
||||
this._focusParentNode();
|
||||
}
|
||||
return false;
|
||||
return;
|
||||
|
||||
case "ArrowRight":
|
||||
if (!this.props.isExpanded(this.props.focused)) {
|
||||
@ -450,8 +494,8 @@ const Tree = module.exports = createClass({
|
||||
} else {
|
||||
this._focusNextNode();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@ -463,6 +507,7 @@ const Tree = module.exports = createClass({
|
||||
// doesn't exist, we're at the first node already.
|
||||
|
||||
let prev;
|
||||
let prevIndex;
|
||||
|
||||
const traversal = this._dfsFromRoots();
|
||||
const length = traversal.length;
|
||||
@ -472,13 +517,14 @@ const Tree = module.exports = createClass({
|
||||
break;
|
||||
}
|
||||
prev = item;
|
||||
prevIndex = i;
|
||||
}
|
||||
|
||||
if (prev === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._focus(prev);
|
||||
this._focus(prevIndex, prev);
|
||||
}),
|
||||
|
||||
/**
|
||||
@ -502,7 +548,7 @@ const Tree = module.exports = createClass({
|
||||
}
|
||||
|
||||
if (i + 1 < traversal.length) {
|
||||
this._focus(traversal[i + 1].item);
|
||||
this._focus(i + 1, traversal[i + 1].item);
|
||||
}
|
||||
}),
|
||||
|
||||
@ -512,8 +558,19 @@ const Tree = module.exports = createClass({
|
||||
*/
|
||||
_focusParentNode: oncePerAnimationFrame(function () {
|
||||
const parent = this.props.getParent(this.props.focused);
|
||||
if (parent) {
|
||||
this._focus(parent);
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const traversal = this._dfsFromRoots();
|
||||
const length = traversal.length;
|
||||
let parentIndex = 0;
|
||||
for (; parentIndex < length; parentIndex++) {
|
||||
if (traversal[parentIndex].item === parent) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this._focus(parentIndex, parent);
|
||||
}),
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user