Bug 1582865 - Don't use document/window in tree component. r=yzen.

When using the ObjectInspector in an inspector extension sidebar,
document is not an HTMLDocument but a Sandbox, which means we
don't have access to the same properties than in a document.
Since the Tree and the ObjectInspector components do use `document`
and `window`, this means some interaction would lead to the sidebar
crashing.
To prevent this, we retrieve the document and/or the window from
a node reference instead.

Differential Revision: https://phabricator.services.mozilla.com/D47271

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Nicolas Chevobbe 2019-09-27 12:06:20 +00:00
Родитель 836f56e2ad
Коммит 97ff4e89aa
4 изменённых файлов: 81 добавлений и 41 удалений

Просмотреть файл

@ -90,7 +90,8 @@ class TreeNode extends Component {
// focusable element inside.
const elms = this.getFocusableElements();
if (this.props.active) {
if (elms.length > 0 && !elms.includes(document.activeElement)) {
const doc = this.treeNodeRef.current.ownerDocument;
if (elms.length > 0 && !elms.includes(doc.activeElement)) {
elms[0].focus();
}
} else {
@ -232,9 +233,12 @@ const TreeNodeFactory = createFactory(TreeNode);
* frame.
*
* @param {Function} fn
* @param {Object} options: object that contains the following properties:
* - {Function} getDocument: A function that return the document
* the component is rendered in.
* @returns {Function}
*/
function oncePerAnimationFrame(fn) {
function oncePerAnimationFrame(fn, { getDocument }) {
let animationId = null;
let argsToPass = null;
return function(...args) {
@ -243,7 +247,12 @@ function oncePerAnimationFrame(fn) {
return;
}
animationId = requestAnimationFrame(() => {
const doc = getDocument();
if (!doc) {
return;
}
animationId = doc.defaultView.requestAnimationFrame(() => {
fn.call(this, ...argsToPass);
animationId = null;
argsToPass = null;
@ -493,17 +502,19 @@ class Tree extends Component {
this.treeRef = React.createRef();
this._onExpand = oncePerAnimationFrame(this._onExpand).bind(this);
this._onCollapse = oncePerAnimationFrame(this._onCollapse).bind(this);
this._focusPrevNode = oncePerAnimationFrame(this._focusPrevNode).bind(this);
this._focusNextNode = oncePerAnimationFrame(this._focusNextNode).bind(this);
this._focusParentNode = oncePerAnimationFrame(this._focusParentNode).bind(
this
);
this._focusFirstNode = oncePerAnimationFrame(this._focusFirstNode).bind(
this
);
this._focusLastNode = oncePerAnimationFrame(this._focusLastNode).bind(this);
const opaf = fn =>
oncePerAnimationFrame(fn, {
getDocument: () =>
this.treeRef.current && this.treeRef.current.ownerDocument,
});
this._onExpand = opaf(this._onExpand).bind(this);
this._onCollapse = opaf(this._onCollapse).bind(this);
this._focusPrevNode = opaf(this._focusPrevNode).bind(this);
this._focusNextNode = opaf(this._focusNextNode).bind(this);
this._focusParentNode = opaf(this._focusParentNode).bind(this);
this._focusFirstNode = opaf(this._focusFirstNode).bind(this);
this._focusLastNode = opaf(this._focusLastNode).bind(this);
this._autoExpand = this._autoExpand.bind(this);
this._preventArrowKeyScrolling = this._preventArrowKeyScrolling.bind(this);
@ -711,7 +722,8 @@ class Tree extends Component {
if (this.props.active != undefined) {
this._activate(undefined);
if (this.treeRef.current !== document.activeElement) {
const doc = this.treeRef.current && this.treeRef.current.ownerDocument;
if (this.treeRef.current !== doc.activeElement) {
this.treeRef.current.focus();
}
}
@ -748,7 +760,8 @@ class Tree extends Component {
_scrollNodeIntoView(item, options = {}) {
if (item !== undefined) {
const treeElement = this.treeRef.current;
const element = document.getElementById(this.props.getKey(item));
const doc = treeElement && treeElement.ownerDocument;
const element = doc.getElementById(this.props.getKey(item));
if (element) {
const { top, bottom } = element.getBoundingClientRect();
@ -813,6 +826,7 @@ class Tree extends Component {
}
this._preventArrowKeyScrolling(e);
const doc = this.treeRef.current && this.treeRef.current.ownerDocument;
switch (e.key) {
case "ArrowUp":
@ -855,7 +869,7 @@ class Tree extends Component {
case "Enter":
case " ":
if (this.treeRef.current === document.activeElement) {
if (this.treeRef.current === doc.activeElement) {
this._preventEvent(e);
if (this.props.active !== this.props.focused) {
this._activate(this.props.focused);
@ -869,7 +883,7 @@ class Tree extends Component {
this._activate(undefined);
}
if (this.treeRef.current !== document.activeElement) {
if (this.treeRef.current !== doc.activeElement) {
this.treeRef.current.focus();
}
}

Просмотреть файл

@ -246,8 +246,9 @@ class ObjectInspectorItem extends Component<Props> {
// image, clicking on it does not remove any existing text selection.
// So we need to also check if the arrow was clicked.
if (
Utils.selection.documentHasSelection() &&
!(e.target && e.target.matches && e.target.matches(".arrow"))
e.target &&
Utils.selection.documentHasSelection(e.target.ownerDocument) &&
!(e.target.matches && e.target.matches(".arrow"))
) {
e.stopPropagation();
}
@ -283,7 +284,9 @@ class ObjectInspectorItem extends Component<Props> {
event.stopPropagation();
// If the user selected text, bail out.
if (Utils.selection.documentHasSelection()) {
if (
Utils.selection.documentHasSelection(event.target.ownerDocument)
) {
return;
}

Просмотреть файл

@ -2,8 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
function documentHasSelection(): boolean {
const selection = getSelection();
function documentHasSelection(doc = document): boolean {
const selection = doc.defaultView.getSelection();
if (!selection) {
return false;
}

Просмотреть файл

@ -214,7 +214,9 @@ class TreeNode extends Component {
const elms = this.getFocusableElements();
if (this.props.active) {
if (elms.length > 0 && !elms.includes(document.activeElement)) {
const doc = this.treeNodeRef.current.ownerDocument;
if (elms.length > 0 && !elms.includes(doc.activeElement)) {
elms[0].focus();
}
} else {
@ -345,10 +347,15 @@ const TreeNodeFactory = createFactory(TreeNode);
* frame.
*
* @param {Function} fn
* @param {Object} options: object that contains the following properties:
* - {Function} getDocument: A function that return the document
* the component is rendered in.
* @returns {Function}
*/
function oncePerAnimationFrame(fn) {
function oncePerAnimationFrame(fn, {
getDocument
}) {
let animationId = null;
let argsToPass = null;
return function (...args) {
@ -358,7 +365,13 @@ function oncePerAnimationFrame(fn) {
return;
}
animationId = requestAnimationFrame(() => {
const doc = getDocument();
if (!doc) {
return;
}
animationId = doc.defaultView.requestAnimationFrame(() => {
fn.call(this, ...argsToPass);
animationId = null;
argsToPass = null;
@ -592,13 +605,18 @@ class Tree extends Component {
autoExpanded: new Set()
};
this.treeRef = _react.default.createRef();
this._onExpand = oncePerAnimationFrame(this._onExpand).bind(this);
this._onCollapse = oncePerAnimationFrame(this._onCollapse).bind(this);
this._focusPrevNode = oncePerAnimationFrame(this._focusPrevNode).bind(this);
this._focusNextNode = oncePerAnimationFrame(this._focusNextNode).bind(this);
this._focusParentNode = oncePerAnimationFrame(this._focusParentNode).bind(this);
this._focusFirstNode = oncePerAnimationFrame(this._focusFirstNode).bind(this);
this._focusLastNode = oncePerAnimationFrame(this._focusLastNode).bind(this);
const opaf = fn => oncePerAnimationFrame(fn, {
getDocument: () => this.treeRef.current && this.treeRef.current.ownerDocument
});
this._onExpand = opaf(this._onExpand).bind(this);
this._onCollapse = opaf(this._onCollapse).bind(this);
this._focusPrevNode = opaf(this._focusPrevNode).bind(this);
this._focusNextNode = opaf(this._focusNextNode).bind(this);
this._focusParentNode = opaf(this._focusParentNode).bind(this);
this._focusFirstNode = opaf(this._focusFirstNode).bind(this);
this._focusLastNode = opaf(this._focusLastNode).bind(this);
this._autoExpand = this._autoExpand.bind(this);
this._preventArrowKeyScrolling = this._preventArrowKeyScrolling.bind(this);
this._preventEvent = this._preventEvent.bind(this);
@ -823,7 +841,9 @@ class Tree extends Component {
if (this.props.active != undefined) {
this._activate(undefined);
if (this.treeRef.current !== document.activeElement) {
const doc = this.treeRef.current && this.treeRef.current.ownerDocument;
if (this.treeRef.current !== doc.activeElement) {
this.treeRef.current.focus();
}
}
@ -862,7 +882,8 @@ class Tree extends Component {
_scrollNodeIntoView(item, options = {}) {
if (item !== undefined) {
const treeElement = this.treeRef.current;
const element = document.getElementById(this.props.getKey(item));
const doc = treeElement && treeElement.ownerDocument;
const element = doc.getElementById(this.props.getKey(item));
if (element) {
const {
@ -934,6 +955,8 @@ class Tree extends Component {
this._preventArrowKeyScrolling(e);
const doc = this.treeRef.current && this.treeRef.current.ownerDocument;
switch (e.key) {
case "ArrowUp":
this._focusPrevNode();
@ -975,7 +998,7 @@ class Tree extends Component {
case "Enter":
case " ":
if (this.treeRef.current === document.activeElement) {
if (this.treeRef.current === doc.activeElement) {
this._preventEvent(e);
if (this.props.active !== this.props.focused) {
@ -992,7 +1015,7 @@ class Tree extends Component {
this._activate(undefined);
}
if (this.treeRef.current !== document.activeElement) {
if (this.treeRef.current !== doc.activeElement) {
this.treeRef.current.focus();
}
@ -8250,7 +8273,7 @@ class ObjectInspectorItem extends Component {
// So we need to also check if the arrow was clicked.
if (Utils.selection.documentHasSelection() && !(e.target && e.target.matches && e.target.matches(".arrow"))) {
if (e.target && Utils.selection.documentHasSelection(e.target.ownerDocument) && !(e.target.matches && e.target.matches(".arrow"))) {
e.stopPropagation();
}
},
@ -8288,7 +8311,7 @@ class ObjectInspectorItem extends Component {
onClick: onLabelClick ? event => {
event.stopPropagation(); // If the user selected text, bail out.
if (Utils.selection.documentHasSelection()) {
if (Utils.selection.documentHasSelection(event.target.ownerDocument)) {
return;
}
@ -8347,8 +8370,8 @@ module.exports = ObjectInspectorItem;
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
function documentHasSelection() {
const selection = getSelection();
function documentHasSelection(doc = document) {
const selection = doc.defaultView.getSelection();
if (!selection) {
return false;