mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-21 01:05:45 +00:00
Bug 1544710 - ensure that selected row is always visible within TreeView after update. Clean up scroll into view operations across all uses of TreeView. r=mtigley
Differential Revision: https://phabricator.services.mozilla.com/D29342 --HG-- extra : moz-landing-system : lando
This commit is contained in:
parent
87b3beffae
commit
c087faa4dc
@ -12,6 +12,7 @@
|
||||
--accessibility-horizontal-padding: 5px;
|
||||
--accessibility-horizontal-indent: 20px;
|
||||
--accessibility-properties-item-width: calc(100% - var(--accessibility-horizontal-indent));
|
||||
--accessibility-tree-height: calc(100vh - var(--accessibility-toolbar-height) * 2 - 1px);
|
||||
--accessibility-arrow-horizontal-padding: 4px;
|
||||
--accessibility-tree-row-height: 21px;
|
||||
--accessibility-unfocused-tree-focused-node-background: var(--grey-20);
|
||||
@ -71,16 +72,16 @@ body {
|
||||
fill: var(--theme-selection-color);
|
||||
}
|
||||
|
||||
.mainFrame .main-panel {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.mainFrame {
|
||||
height: 100%;
|
||||
color: var(--theme-toolbar-color);
|
||||
}
|
||||
|
||||
.main-panel {
|
||||
/* To compenstate for 1px splitter between the tree and sidebar. */
|
||||
width: var(--accessibility-full-length-minus-splitter);
|
||||
}
|
||||
|
||||
.devtools-button,
|
||||
.toggle-button {
|
||||
cursor: pointer;
|
||||
@ -225,24 +226,30 @@ body {
|
||||
}
|
||||
|
||||
/* TreeView Customization */
|
||||
.split-box:not(.horz) .main-panel {
|
||||
height: calc(100vh - var(--accessibility-toolbar-height));
|
||||
.treeTable thead, .treeTable tbody {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.treeTable > thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
/* Bug 1466806 - fix expander arrow for expanding treeview rows rendering over the
|
||||
thead */
|
||||
z-index: 1;
|
||||
.treeTable tr {
|
||||
width: 100%;
|
||||
display: table;
|
||||
}
|
||||
|
||||
.split-box:not(.horz) .treeTable {
|
||||
/* To compenstate for 1px splitter between the tree and sidebar. */
|
||||
width: var(--accessibility-full-length-minus-splitter);
|
||||
.treeTable tbody {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.split-box.horz .treeTable {
|
||||
.split-box:not(.horz) .treeTable tbody {
|
||||
height: var(--accessibility-tree-height);
|
||||
}
|
||||
|
||||
.split-box.horz .treeTable tbody {
|
||||
/* Accessibility tree height depends on the height of the controlled panel
|
||||
(sidebar) when in horz mode and also has an additional separator. */
|
||||
height: calc(var(--accessibility-tree-height) - var(--split-box-controlled-panel-size) - 1px);
|
||||
}
|
||||
|
||||
.treeTable {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -318,6 +325,7 @@ body {
|
||||
}
|
||||
|
||||
.mainFrame .treeTable .treeHeaderCell {
|
||||
width: 50%;
|
||||
border-bottom: 1px solid var(--theme-splitter-color);
|
||||
background: var(--theme-toolbar-background);
|
||||
font: message-box;
|
||||
|
@ -30,6 +30,8 @@ const { L10N } = require("../utils/l10n");
|
||||
loader.lazyRequireGetter(this, "Menu", "devtools/client/framework/menu");
|
||||
loader.lazyRequireGetter(this, "MenuItem", "devtools/client/framework/menu-item");
|
||||
|
||||
const { scrollIntoView } = require("devtools/client/shared/scroll");
|
||||
|
||||
const JSON_URL_PREFIX = "data:application/json;charset=UTF-8,";
|
||||
|
||||
const TELEMETRY_ACCESSIBLE_CONTEXT_MENU_OPENED =
|
||||
@ -71,7 +73,7 @@ class AccessibilityRow extends Component {
|
||||
const { selected, object } = this.props.member;
|
||||
if (selected) {
|
||||
this.unhighlight();
|
||||
this.updateAndScrollIntoViewIfNeeded();
|
||||
this.update();
|
||||
this.highlight(object, { duration: VALUE_HIGHLIGHT_DURATION });
|
||||
}
|
||||
|
||||
@ -89,7 +91,7 @@ class AccessibilityRow extends Component {
|
||||
// If row is selected, update corresponding accessible details.
|
||||
if (!prevProps.member.selected && selected) {
|
||||
this.unhighlight();
|
||||
this.updateAndScrollIntoViewIfNeeded();
|
||||
this.update();
|
||||
this.highlight(object, { duration: VALUE_HIGHLIGHT_DURATION });
|
||||
}
|
||||
|
||||
@ -110,17 +112,16 @@ class AccessibilityRow extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
row.scrollIntoView({ block: "center" });
|
||||
scrollIntoView(row);
|
||||
}
|
||||
|
||||
updateAndScrollIntoViewIfNeeded() {
|
||||
update() {
|
||||
const { dispatch, member: { object }, supports } = this.props;
|
||||
if (!gToolbox || !object.actorID) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(updateDetails(gToolbox.walker, object, supports));
|
||||
this.scrollIntoView();
|
||||
window.emit(EVENTS.NEW_ACCESSIBLE_FRONT_SELECTED, object);
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,8 @@ const AccessibilityRow = createFactory(require("./AccessibilityRow"));
|
||||
const AccessibilityRowValue = createFactory(require("./AccessibilityRowValue"));
|
||||
const { Provider } = require("../provider");
|
||||
|
||||
const { scrollIntoView } = require("devtools/client/shared/scroll");
|
||||
|
||||
/**
|
||||
* Renders Accessibility panel tree.
|
||||
*/
|
||||
@ -60,7 +62,16 @@ class AccessibilityTree extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
componentDidUpdate(prevProps) {
|
||||
// When filtering is toggled, make sure that the selected row remains in
|
||||
// view.
|
||||
if (this.props.filtered !== prevProps.filtered) {
|
||||
const selected = document.querySelector(".treeTable .treeRow.selected");
|
||||
if (selected) {
|
||||
scrollIntoView(selected);
|
||||
}
|
||||
}
|
||||
|
||||
window.emit(EVENTS.ACCESSIBILITY_INSPECTOR_UPDATED);
|
||||
}
|
||||
|
||||
@ -191,7 +202,10 @@ class AccessibilityTree extends Component {
|
||||
if (event.target.classList.contains("theme-twisty")) {
|
||||
this.toggle(nodePath);
|
||||
}
|
||||
this.selectRow(event.currentTarget);
|
||||
|
||||
this.selectRow(
|
||||
this.rows.find(row => row.props.member.path === nodePath),
|
||||
{ preventAutoScroll: true });
|
||||
},
|
||||
onContextMenuTree: hasContextMenu && function(e) {
|
||||
// If context menu event is triggered on (or bubbled to) the TreeView, it was
|
||||
|
@ -24,6 +24,7 @@ skip-if = (os == 'linux' && debug && bits == 64) # Bug 1511247
|
||||
[browser_accessibility_reload.js]
|
||||
[browser_accessibility_sidebar_checks.js]
|
||||
[browser_accessibility_sidebar.js]
|
||||
[browser_accessibility_tree_audit_long.js]
|
||||
[browser_accessibility_tree_audit_reset.js]
|
||||
[browser_accessibility_tree_audit_toolbar.js]
|
||||
[browser_accessibility_tree_audit.js]
|
||||
|
@ -65,6 +65,7 @@ const tests = [{
|
||||
role: "text leaf",
|
||||
name: `"Second level header "contrast`,
|
||||
badges: [ "contrast" ],
|
||||
selected: true,
|
||||
}],
|
||||
},
|
||||
}, {
|
||||
@ -90,6 +91,7 @@ const tests = [{
|
||||
role: "text leaf",
|
||||
name: `"Second level header "contrast`,
|
||||
badges: [ "contrast" ],
|
||||
selected: true,
|
||||
}],
|
||||
},
|
||||
}];
|
||||
|
@ -0,0 +1,95 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
/* global toggleFilter */
|
||||
|
||||
const header = "<h1 style=\"color:rgba(255,0,0,0.1); " +
|
||||
"background-color:rgba(255,255,255,1);\">header</h1>";
|
||||
|
||||
const TEST_URI = `<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Accessibility Panel Test</title>
|
||||
</head>
|
||||
<body>
|
||||
${header.repeat(20)}
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const docRow = {
|
||||
role: "document",
|
||||
name: `"Accessibility Panel Test"`,
|
||||
};
|
||||
const headingRow = {
|
||||
role: "heading",
|
||||
name: `"header"`,
|
||||
};
|
||||
const textLeafRow = {
|
||||
role: "text leaf",
|
||||
name: `"header"contrast`,
|
||||
badges: [ "contrast" ],
|
||||
};
|
||||
const audit = new Array(20).fill(textLeafRow);
|
||||
|
||||
const auditInitial = audit.map(check => ({ ...check }));
|
||||
auditInitial[0].selected = true;
|
||||
|
||||
const auditSecondLastSelected = audit.map(check => ({ ...check }));
|
||||
auditSecondLastSelected[19].selected = true;
|
||||
|
||||
const resetAfterAudit = [docRow];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
resetAfterAudit.push(headingRow);
|
||||
resetAfterAudit.push({ ...textLeafRow, selected: i === 19 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Test data has the format of:
|
||||
* {
|
||||
* desc {String} description for better logging
|
||||
* setup {Function} An optional setup that needs to be performed before
|
||||
* the state of the tree and the sidebar can be checked.
|
||||
* expected {JSON} An expected states for the tree and the sidebar.
|
||||
* }
|
||||
*/
|
||||
const tests = [{
|
||||
desc: "Check initial state.",
|
||||
expected: {
|
||||
tree: [{ ...docRow, selected: true }],
|
||||
},
|
||||
}, {
|
||||
desc: "Run an audit from a11y panel toolbar by activating a filter.",
|
||||
setup: async ({ doc }) => {
|
||||
await toggleFilter(doc, 0);
|
||||
},
|
||||
expected: {
|
||||
tree: auditInitial,
|
||||
},
|
||||
}, {
|
||||
desc: "Select a row that is guaranteed to have to be scrolled into view.",
|
||||
setup: async ({ doc }) => {
|
||||
selectRow(doc, 0);
|
||||
EventUtils.synthesizeKey("VK_END", {}, doc.defaultView);
|
||||
},
|
||||
expected: {
|
||||
tree: auditSecondLastSelected,
|
||||
},
|
||||
}, {
|
||||
desc: "Click on the filter again.",
|
||||
setup: async ({ doc }) => {
|
||||
await toggleFilter(doc, 0);
|
||||
},
|
||||
expected: {
|
||||
tree: resetAfterAudit,
|
||||
},
|
||||
}];
|
||||
|
||||
/**
|
||||
* Simple test that checks content of the Accessibility panel tree when the
|
||||
* audit is activated via the panel's toolbar and the selection persists when
|
||||
* the filter is toggled off.
|
||||
*/
|
||||
addA11yPanelTestsTask(tests, TEST_URI,
|
||||
"Test Accessibility panel tree with persistent selected row.");
|
@ -35,6 +35,7 @@ const tests = [{
|
||||
tree: [{
|
||||
role: "document",
|
||||
name: `"Accessibility Panel Test"`,
|
||||
selected: true,
|
||||
}],
|
||||
},
|
||||
}, {
|
||||
@ -47,6 +48,7 @@ const tests = [{
|
||||
role: "text leaf",
|
||||
name: `"Top level header "contrast`,
|
||||
badges: [ "contrast" ],
|
||||
selected: true,
|
||||
}, {
|
||||
role: "text leaf",
|
||||
name: `"Second level header "contrast`,
|
||||
@ -62,6 +64,7 @@ const tests = [{
|
||||
tree: [{
|
||||
role: "document",
|
||||
name: `"Accessibility Panel Test"`,
|
||||
selected: true,
|
||||
}, {
|
||||
role: "heading",
|
||||
name: `"Top level header"`,
|
||||
|
@ -35,6 +35,7 @@ const tests = [{
|
||||
tree: [{
|
||||
role: "document",
|
||||
name: `"Accessibility Panel Test"`,
|
||||
selected: true,
|
||||
}],
|
||||
},
|
||||
}, {
|
||||
@ -47,6 +48,7 @@ const tests = [{
|
||||
role: "text leaf",
|
||||
name: `"Top level header "contrast`,
|
||||
badges: [ "contrast" ],
|
||||
selected: true,
|
||||
}, {
|
||||
role: "text leaf",
|
||||
name: `"Second level header "contrast`,
|
||||
@ -69,6 +71,7 @@ const tests = [{
|
||||
role: "text leaf",
|
||||
name: `"Top level header "contrast`,
|
||||
badges: [ "contrast" ],
|
||||
selected: true,
|
||||
}, {
|
||||
role: "heading",
|
||||
name: `"Second level header"`,
|
||||
|
@ -176,6 +176,66 @@ function compareBadges(badges, expected = []) {
|
||||
badgeEls.every((badge, i) => badge.textContent === expected[i]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an ancestor that is scrolled for a given DOMNode.
|
||||
*
|
||||
* @param {DOMNode} node
|
||||
* DOMNode that to find an ancestor for that is scrolled.
|
||||
*/
|
||||
function closestScrolledParent(node) {
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (node.scrollHeight > node.clientHeight) {
|
||||
return node;
|
||||
}
|
||||
|
||||
return closestScrolledParent(node.parentNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given element is visible to the user and is not scrolled off
|
||||
* because of the overflow.
|
||||
*
|
||||
* @param {Element} element
|
||||
* Element to be checked whether it is visible and is not scrolled off.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* True if the element is visible.
|
||||
*/
|
||||
function isVisible(element) {
|
||||
const { top, bottom } = element.getBoundingClientRect();
|
||||
const scrolledParent = closestScrolledParent(element.parentNode);
|
||||
const scrolledParentRect = scrolledParent ? scrolledParent.getBoundingClientRect() :
|
||||
null;
|
||||
return !scrolledParent ||
|
||||
(top >= scrolledParentRect.top && bottom <= scrolledParentRect.bottom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check selected styling and visibility for a given row in the accessibility
|
||||
* tree.
|
||||
* @param {DOMNode} row
|
||||
* DOMNode for a given accessibility row.
|
||||
* @param {Boolean} expected
|
||||
* Expected selected state.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
* True if visibility and styling matches expected selected state.
|
||||
*/
|
||||
function checkSelected(row, expected) {
|
||||
if (!expected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (row.classList.contains("selected") !== expected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isVisible(row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the state of the accessibility tree.
|
||||
* @param {document} doc panel documnent.
|
||||
@ -184,11 +244,13 @@ function compareBadges(badges, expected = []) {
|
||||
async function checkTreeState(doc, expected) {
|
||||
info("Checking tree state.");
|
||||
const hasExpectedStructure = await BrowserTestUtils.waitForCondition(() =>
|
||||
[...doc.querySelectorAll(".treeRow")].every((row, i) =>
|
||||
row.querySelector(".treeLabelCell").textContent === expected[i].role &&
|
||||
row.querySelector(".treeValueCell").textContent === expected[i].name &&
|
||||
compareBadges(row.querySelector(".badges"), expected[i].badges)),
|
||||
"Wait for the right tree update.");
|
||||
[...doc.querySelectorAll(".treeRow")].every((row, i) => {
|
||||
const { role, name, badges, selected } = expected[i];
|
||||
return row.querySelector(".treeLabelCell").textContent === role &&
|
||||
row.querySelector(".treeValueCell").textContent === name &&
|
||||
compareBadges(row.querySelector(".badges"), badges) &&
|
||||
checkSelected(row, selected);
|
||||
}), "Wait for the right tree update.");
|
||||
|
||||
ok(hasExpectedStructure, "Tree structure is correct.");
|
||||
}
|
||||
|
@ -15,14 +15,24 @@ add_task(async function() {
|
||||
|
||||
is(await getElementCount(".treeRow"), numRows, "Got the expected number of rows.");
|
||||
await assertRowSelected(null);
|
||||
|
||||
// Focus the tree and select first row.
|
||||
await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
|
||||
const tree = content.document.querySelector(".treeTable");
|
||||
tree.focus();
|
||||
is(tree, content.document.activeElement, "Tree should be focused");
|
||||
content.document.querySelector(".treeRow:nth-child(1)").click();
|
||||
});
|
||||
await assertRowSelected(1);
|
||||
|
||||
await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
|
||||
const scroller = content.document.querySelector(".jsonPanelBox .panelContent");
|
||||
ok(scroller.clientHeight < scroller.scrollHeight, "There is a scrollbar.");
|
||||
is(scroller.scrollTop, 0, "Initially scrolled to the top.");
|
||||
|
||||
// Click to select last row.
|
||||
content.document.querySelector(".treeRow:last-child").click();
|
||||
});
|
||||
|
||||
// Select last row.
|
||||
await BrowserTestUtils.synthesizeKey("VK_END", {}, tab.linkedBrowser);
|
||||
await assertRowSelected(numRows);
|
||||
|
||||
await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
|
||||
@ -77,7 +87,7 @@ add_task(async function() {
|
||||
|
||||
// Create a JSON with a row taller than the panel.
|
||||
const json = JSON.stringify([0, "a ".repeat(1e4), 1]);
|
||||
await addJsonViewTab("data:application/json," + encodeURI(json));
|
||||
const tab = await addJsonViewTab("data:application/json," + encodeURI(json));
|
||||
|
||||
is(await getElementCount(".treeRow"), 3, "Got the expected number of rows.");
|
||||
await assertRowSelected(null);
|
||||
@ -88,20 +98,17 @@ add_task(async function() {
|
||||
is(scroller.scrollTop, 0, "Initially scrolled to the top.");
|
||||
|
||||
// Select the tall row.
|
||||
content.document.querySelector(".treeTable").focus();
|
||||
row.click();
|
||||
});
|
||||
await assertRowSelected(2);
|
||||
await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
|
||||
const scroller = content.document.querySelector(".jsonPanelBox .panelContent");
|
||||
const row = content.document.querySelector(".treeRow:nth-child(2)");
|
||||
is(scroller.scrollTop, row.offsetTop,
|
||||
"Scrolled to the top of the row.");
|
||||
is(scroller.scrollTop, 0, "When the row is visible, do not scroll on click.");
|
||||
});
|
||||
|
||||
// Select the last row.
|
||||
await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
|
||||
content.document.querySelector(".treeRow:last-child").click();
|
||||
});
|
||||
await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, tab.linkedBrowser);
|
||||
await assertRowSelected(3);
|
||||
await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
|
||||
const scroller = content.document.querySelector(".jsonPanelBox .panelContent");
|
||||
@ -117,8 +124,8 @@ add_task(async function() {
|
||||
const scroll = await ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
|
||||
const scroller = content.document.querySelector(".jsonPanelBox .panelContent");
|
||||
const row = content.document.querySelector(".treeRow:nth-child(2)");
|
||||
is(scroller.scrollTop + scroller.offsetHeight, row.offsetTop + row.offsetHeight,
|
||||
"Scrolled to the bottom of the row.");
|
||||
is(scroller.scrollTop + scroller.offsetHeight, scroller.scrollHeight,
|
||||
"Scrolled to the bottom. When the row is visible, do not scroll on click.");
|
||||
|
||||
// Scroll up a bit, so that both the top and bottom of the row are not visible.
|
||||
const scrollPos =
|
||||
@ -132,6 +139,7 @@ add_task(async function() {
|
||||
row.click();
|
||||
return scrollPos;
|
||||
});
|
||||
|
||||
await assertRowSelected(2);
|
||||
await ContentTask.spawn(gBrowser.selectedBrowser, scroll, function(scrollPos) {
|
||||
const scroller = content.document.querySelector(".jsonPanelBox .panelContent");
|
||||
|
@ -203,7 +203,13 @@ class SplitBox extends Component {
|
||||
const { endPanelControl, splitterSize, vert } = this.state;
|
||||
const { startPanel, endPanel, minSize, maxSize } = this.props;
|
||||
|
||||
const style = Object.assign({}, this.props.style);
|
||||
const style = Object.assign({
|
||||
// Set the size of the controlled panel (height or width depending on the
|
||||
// current state). This can be used to help with styling of dependent
|
||||
// panels.
|
||||
"--split-box-controlled-panel-size":
|
||||
`${vert ? this.state.width : this.state.height}`,
|
||||
}, this.props.style);
|
||||
|
||||
// Calculate class names list.
|
||||
let classNames = ["split-box"];
|
||||
|
@ -21,8 +21,6 @@ define(function(require, exports, module) {
|
||||
const TreeCell = createFactory(require("./TreeCell"));
|
||||
const LabelCell = createFactory(require("./LabelCell"));
|
||||
|
||||
// Scroll
|
||||
const { scrollIntoViewIfNeeded } = require("devtools/client/shared/scroll");
|
||||
const { focusableSelector } = require("devtools/client/shared/focus");
|
||||
|
||||
const UPDATE_ON_PROPS = [
|
||||
@ -123,17 +121,6 @@ define(function(require, exports, module) {
|
||||
return false;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.member.selected) {
|
||||
const row = findDOMNode(this);
|
||||
// Because this is called asynchronously, context window might be
|
||||
// already gone.
|
||||
if (row.ownerDocument.defaultView) {
|
||||
scrollIntoViewIfNeeded(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.observer.disconnect();
|
||||
this.observer = null;
|
||||
|
@ -22,6 +22,8 @@ define(function(require, exports, module) {
|
||||
const TreeRow = createFactory(require("./TreeRow"));
|
||||
const TreeHeader = createFactory(require("./TreeHeader"));
|
||||
|
||||
const { scrollIntoView } = require("devtools/client/shared/scroll");
|
||||
|
||||
const SUPPORTED_KEYS = [
|
||||
"ArrowUp",
|
||||
"ArrowDown",
|
||||
@ -240,7 +242,8 @@ define(function(require, exports, module) {
|
||||
const selected = this.getSelectedRow();
|
||||
if (!selected && this.rows.length > 0) {
|
||||
this.selectRow(this.rows[
|
||||
Math.min(this.state.lastSelectedIndex, this.rows.length - 1)]);
|
||||
Math.min(this.state.lastSelectedIndex, this.rows.length - 1)
|
||||
], { alignTo: "top" });
|
||||
}
|
||||
}
|
||||
|
||||
@ -291,39 +294,34 @@ define(function(require, exports, module) {
|
||||
const parentRow = this.rows.slice(0, index).reverse().find(
|
||||
r => r.props.member.level < row.props.member.level);
|
||||
if (parentRow) {
|
||||
this.selectRow(parentRow);
|
||||
this.selectRow(parentRow, { alignTo: "top" });
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "ArrowDown":
|
||||
const nextRow = this.rows[index + 1];
|
||||
if (nextRow) {
|
||||
this.selectRow(nextRow);
|
||||
this.selectRow(nextRow, { alignTo: "bottom" });
|
||||
}
|
||||
break;
|
||||
case "ArrowUp":
|
||||
const previousRow = this.rows[index - 1];
|
||||
if (previousRow) {
|
||||
this.selectRow(previousRow);
|
||||
this.selectRow(previousRow, { alignTo: "top" });
|
||||
}
|
||||
break;
|
||||
case "Home":
|
||||
const firstRow = this.rows[0];
|
||||
|
||||
if (firstRow) {
|
||||
// Due to the styling, the first row is sometimes overlapped by
|
||||
// the table head. So we want to force the tree to scroll to the very top.
|
||||
this.selectRow(firstRow, {
|
||||
block: "end",
|
||||
inline: "nearest",
|
||||
});
|
||||
this.selectRow(firstRow, { alignTo: "top" });
|
||||
}
|
||||
break;
|
||||
|
||||
case "End":
|
||||
const lastRow = this.rows[this.rows.length - 1];
|
||||
if (lastRow) {
|
||||
this.selectRow(lastRow);
|
||||
this.selectRow(lastRow, { alignTo: "bottom" });
|
||||
}
|
||||
break;
|
||||
|
||||
@ -367,7 +365,10 @@ define(function(require, exports, module) {
|
||||
if (cell && cell.classList.contains("treeLabelCell")) {
|
||||
this.toggle(nodePath);
|
||||
}
|
||||
this.selectRow(event.currentTarget);
|
||||
|
||||
this.selectRow(
|
||||
this.rows.find(row => row.props.member.path === nodePath),
|
||||
{ preventAutoScroll: true });
|
||||
}
|
||||
|
||||
onContextMenu(member, event) {
|
||||
@ -394,27 +395,47 @@ define(function(require, exports, module) {
|
||||
return this.rows.indexOf(row);
|
||||
}
|
||||
|
||||
selectRow(row, scrollOptions = {block: "nearest"}) {
|
||||
row = findDOMNode(row);
|
||||
_scrollIntoView(row, options = {}) {
|
||||
const treeEl = this.treeRef.current;
|
||||
if (!treeEl || !row) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.selected === row.id) {
|
||||
row.scrollIntoView(scrollOptions);
|
||||
const { props: { member: { path } = {} } = {} } = row;
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = treeEl.ownerDocument.getElementById(path);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollIntoView(element, { ...options });
|
||||
}
|
||||
|
||||
selectRow(row, options = {}) {
|
||||
const { props: { member: { path } = {} } = {} } = row;
|
||||
if (this.isSelected(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.active != null) {
|
||||
if (this.treeRef.current !== document.activeElement) {
|
||||
this.treeRef.current.focus();
|
||||
const treeEl = this.treeRef.current;
|
||||
if (treeEl && treeEl !== treeEl.ownerDocument.activeElement) {
|
||||
treeEl.focus();
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.preventAutoScroll) {
|
||||
this._scrollIntoView(row, options);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
...this.state,
|
||||
selected: row.id,
|
||||
selected: path,
|
||||
active: null,
|
||||
});
|
||||
|
||||
row.scrollIntoView(scrollOptions);
|
||||
}
|
||||
|
||||
activateRow(active) {
|
||||
|
Loading…
Reference in New Issue
Block a user