Merge mozilla-inbound to mozilla-central a=merge
authorRazvan Maries <rmaries@mozilla.com>
Thu, 14 Mar 2019 23:41:17 +0200
changeset 464031 49d94c83bb220524a370fc7f7734de7e7307016b
parent 464026 495029aae21bbac7d75caafb3d67082715c6c63b (current diff)
parent 464030 25f9343a6480f39a240f1862b588db7dba99efde (diff)
child 464075 fe34e61a586cd0b8b74abccfdb1e59f8880eb525
child 464096 afd8d968a200f3b8de05f4e626fee206cb67e154
push id35706
push userrmaries@mozilla.com
push dateThu, 14 Mar 2019 21:42:19 +0000
treeherdermozilla-central@49d94c83bb22 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone67.0a1
first release with
nightly linux32
49d94c83bb22 / 67.0a1 / 20190314214219 / files
nightly linux64
49d94c83bb22 / 67.0a1 / 20190314214219 / files
nightly mac
49d94c83bb22 / 67.0a1 / 20190314214219 / files
nightly win32
49d94c83bb22 / 67.0a1 / 20190314214219 / files
nightly win64
49d94c83bb22 / 67.0a1 / 20190314214219 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-inbound to mozilla-central a=merge
dom/base/nsFocusManager.cpp
--- a/devtools/client/debugger/new/src/actions/breakpoints/addBreakpoint.js
+++ b/devtools/client/debugger/new/src/actions/breakpoints/addBreakpoint.js
@@ -11,17 +11,17 @@ import {
   getASTLocation,
   makeBreakpointId,
   makeBreakpointLocation,
   findPosition
 } from "../../utils/breakpoint";
 import { PROMISE } from "../utils/middleware/promise";
 import {
   getSymbols,
-  getFirstVisibleBreakpointPosition,
+  getFirstBreakpointPosition,
   getBreakpointPositionsForSource,
   getSourceFromId
 } from "../../selectors";
 
 import { getTextAtPosition } from "../../utils/source";
 import { recordEvent } from "../../utils/telemetry";
 
 import type {
@@ -100,17 +100,17 @@ export function addBreakpoint(
   options: BreakpointOptions = {}
 ) {
   return async ({ dispatch, getState, sourceMaps, client }: ThunkArgs) => {
     recordEvent("add_breakpoint");
     let position;
     const { sourceId, column } = location;
 
     if (column === undefined) {
-      position = getFirstVisibleBreakpointPosition(getState(), location);
+      position = getFirstBreakpointPosition(getState(), location);
     } else {
       const positions = getBreakpointPositionsForSource(getState(), sourceId);
       position = findPosition(positions, location);
     }
 
     if (!position) {
       return;
     }
--- a/devtools/client/debugger/new/src/actions/breakpoints/syncBreakpoint.js
+++ b/devtools/client/debugger/new/src/actions/breakpoints/syncBreakpoint.js
@@ -13,17 +13,18 @@ import {
   findPosition,
   makeBreakpointLocation
 } from "../../utils/breakpoint";
 
 import { getTextAtPosition } from "../../utils/source";
 import { comparePosition } from "../../utils/location";
 
 import { originalToGeneratedId, isOriginalId } from "devtools-source-map";
-import { getSource } from "../../selectors";
+import { getSource, getBreakpointsList } from "../../selectors";
+import { removeBreakpoint } from ".";
 
 import type { ThunkArgs, Action } from "../types";
 
 import type {
   SourceLocation,
   ASTLocation,
   PendingBreakpoint,
   SourceId,
@@ -82,24 +83,37 @@ function createSyncData(
     { generatedLocation, location },
     overrides
   );
 
   assertBreakpoint(breakpoint);
   return { breakpoint, previousLocation };
 }
 
+// Look for an existing breakpoint at the specified generated location.
+function findExistingBreakpoint(state, generatedLocation) {
+  const breakpoints = getBreakpointsList(state);
+
+  return breakpoints.find(bp => {
+    return (
+      bp.generatedLocation.sourceUrl == generatedLocation.sourceUrl &&
+      bp.generatedLocation.line == generatedLocation.line &&
+      bp.generatedLocation.column == generatedLocation.column
+    );
+  });
+}
+
 // we have three forms of syncing: disabled syncing, existing server syncing
 // and adding a new breakpoint
 export async function syncBreakpointPromise(
   thunkArgs: ThunkArgs,
   sourceId: SourceId,
   pendingBreakpoint: PendingBreakpoint
 ): Promise<?BreakpointSyncData> {
-  const { getState, client } = thunkArgs;
+  const { getState, client, dispatch } = thunkArgs;
   assertPendingBreakpoint(pendingBreakpoint);
 
   const source = getSource(getState(), sourceId);
 
   const generatedSourceId = isOriginalId(sourceId)
     ? originalToGeneratedId(sourceId)
     : sourceId;
 
@@ -147,18 +161,21 @@ export async function syncBreakpointProm
       newLocation,
       newGeneratedLocation,
       previousLocation,
       text,
       originalText
     );
   }
 
-  // clear server breakpoints if they exist and we have moved
-  await client.removeBreakpoint(generatedLocation);
+  // Clear any breakpoint for the generated location.
+  const bp = findExistingBreakpoint(getState(), generatedLocation);
+  if (bp) {
+    await dispatch(removeBreakpoint(bp));
+  }
 
   if (!newGeneratedLocation) {
     return { previousLocation, breakpoint: null };
   }
 
   /** ******* Case 2: Add New Breakpoint ***********/
   // If we are not disabled, set the breakpoint on the server and get
   // that info so we can set it on our breakpoints.
--- a/devtools/client/debugger/new/src/actions/sources/newSources.js
+++ b/devtools/client/debugger/new/src/actions/sources/newSources.js
@@ -8,17 +8,17 @@
  * Redux actions for the sources state
  * @module actions/sources
  */
 
 import { generatedToOriginalId } from "devtools-source-map";
 import { flatten } from "lodash";
 
 import { toggleBlackBox } from "./blackbox";
-import { syncBreakpoint } from "../breakpoints";
+import { syncBreakpoint, addBreakpoint } from "../breakpoints";
 import { loadSourceText } from "./loadSourceText";
 import { togglePrettyPrint } from "./prettyPrint";
 import { selectLocation } from "../sources";
 import { getRawSourceURL, isPrettyURL, isOriginal } from "../../utils/source";
 import {
   getBlackBoxList,
   getSource,
   getPendingSelectedLocation,
@@ -181,18 +181,29 @@ function checkPendingBreakpoints(sourceI
 
     if (pendingBreakpoints.length === 0) {
       return;
     }
 
     // load the source text if there is a pending breakpoint for it
     await dispatch(loadSourceText(source));
 
+    // Matching pending breakpoints could have either the same generated or the
+    // same original source. We expect the generated source to appear first and
+    // will add a breakpoint at that location initially. If the original source
+    // appears later then we use syncBreakpoint to see if the generated location
+    // changed and we need to remove the breakpoint we added earlier.
     await Promise.all(
-      pendingBreakpoints.map(bp => dispatch(syncBreakpoint(sourceId, bp)))
+      pendingBreakpoints.map(bp => {
+        if (source.url == bp.location.sourceUrl) {
+          return dispatch(syncBreakpoint(sourceId, bp));
+        }
+        const { line, column } = bp.generatedLocation;
+        return dispatch(addBreakpoint({ sourceId, line, column }, bp.options));
+      })
     );
   };
 }
 
 function restoreBlackBoxedSources(sources: Source[]) {
   return async ({ dispatch }: ThunkArgs) => {
     const tabs = getBlackBoxList();
     if (tabs.length == 0) {
--- a/devtools/client/debugger/new/src/reducers/pending-breakpoints.js
+++ b/devtools/client/debugger/new/src/reducers/pending-breakpoints.js
@@ -156,14 +156,17 @@ export function getPendingBreakpointsFor
   source: Source
 ): PendingBreakpoint[] {
   const sources = getSourcesByURL(state, source.url);
   if (sources.length > 1 && isGenerated(source)) {
     // Don't return pending breakpoints for duplicated generated sources
     return [];
   }
 
-  return getPendingBreakpointList(state).filter(
-    pendingBreakpoint => pendingBreakpoint.location.sourceUrl === source.url
-  );
+  return getPendingBreakpointList(state).filter(pendingBreakpoint => {
+    return (
+      pendingBreakpoint.location.sourceUrl === source.url ||
+      pendingBreakpoint.generatedLocation.sourceUrl == source.url
+    );
+  });
 }
 
 export default update;
--- a/devtools/client/debugger/new/src/selectors/visibleColumnBreakpoints.js
+++ b/devtools/client/debugger/new/src/selectors/visibleColumnBreakpoints.js
@@ -168,24 +168,8 @@ export function getFirstBreakpointPositi
   if (!source || !positions) {
     return;
   }
 
   return positions.find(
     position => getSelectedLocation(position, source).line == line
   );
 }
-
-export function getFirstVisibleBreakpointPosition(
-  state: State,
-  { line }: SourceLocation
-) {
-  const positions = getVisibleBreakpointPositions(state);
-  const selectedSource = getSelectedSource(state);
-
-  if (!selectedSource || !positions) {
-    return;
-  }
-
-  return positions.find(
-    position => getSelectedLocation(position, selectedSource).line == line
-  );
-}
--- a/devtools/client/shared/components/SearchBox.js
+++ b/devtools/client/shared/components/SearchBox.js
@@ -1,89 +1,95 @@
 /* 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/. */
 
 /* global window */
 
 "use strict";
 
-const { Component, createFactory } = require("devtools/client/shared/vendor/react");
+const { createFactory, createRef, PureComponent } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
-const KeyShortcuts = require("devtools/client/shared/key-shortcuts");
-const AutocompletePopup = createFactory(require("devtools/client/shared/components/AutoCompletePopup"));
 
+loader.lazyGetter(this, "AutocompletePopup", function() {
+  return createFactory(require("devtools/client/shared/components/AutoCompletePopup"));
+});
 loader.lazyGetter(this, "MDNLink", function() {
   return createFactory(require("./MdnLink"));
 });
 
-class SearchBox extends Component {
+loader.lazyRequireGetter(this, "KeyShortcuts", "devtools/client/shared/key-shortcuts");
+
+class SearchBox extends PureComponent {
   static get propTypes() {
     return {
+      autocompleteProvider: PropTypes.func,
       delay: PropTypes.number,
       keyShortcut: PropTypes.string,
-      onChange: PropTypes.func,
+      learnMoreTitle: PropTypes.string,
+      learnMoreUrl: PropTypes.string,
+      onBlur: PropTypes.func,
+      onChange: PropTypes.func.isRequired,
       onFocus: PropTypes.func,
-      onBlur: PropTypes.func,
       onKeyDown: PropTypes.func,
-      placeholder: PropTypes.string,
+      placeholder: PropTypes.string.isRequired,
       plainStyle: PropTypes.bool,
-      type: PropTypes.string,
-      autocompleteProvider: PropTypes.func,
-      learnMoreUrl: PropTypes.string,
-      learnMoreTitle: PropTypes.string,
+      type: PropTypes.string.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
       value: "",
       focused: false,
     };
 
+    this.autocompleteRef = createRef();
+    this.inputRef = createRef();
+
+    this.onBlur = this.onBlur.bind(this);
     this.onChange = this.onChange.bind(this);
     this.onClearButtonClick = this.onClearButtonClick.bind(this);
     this.onFocus = this.onFocus.bind(this);
-    this.onBlur = this.onBlur.bind(this);
     this.onKeyDown = this.onKeyDown.bind(this);
   }
 
   componentDidMount() {
     if (!this.props.keyShortcut) {
       return;
     }
 
     this.shortcuts = new KeyShortcuts({
       window,
     });
     this.shortcuts.on(this.props.keyShortcut, event => {
       event.preventDefault();
-      this.refs.input.focus();
+      this.inputRef.current.focus();
     });
   }
 
   componentWillUnmount() {
     if (this.shortcuts) {
       this.shortcuts.destroy();
     }
 
     // Clean up an existing timeout.
     if (this.searchTimeout) {
       clearTimeout(this.searchTimeout);
     }
   }
 
   onChange() {
-    if (this.state.value !== this.refs.input.value) {
+    if (this.state.value !== this.inputRef.current.value) {
       this.setState({
         focused: true,
-        value: this.refs.input.value,
+        value: this.inputRef.current.value,
       });
     }
 
     if (!this.props.delay) {
       this.props.onChange(this.state.value);
       return;
     }
 
@@ -96,17 +102,17 @@ class SearchBox extends Component {
     // smoother if the user is typing quickly.
     this.searchTimeout = setTimeout(() => {
       this.searchTimeout = null;
       this.props.onChange(this.state.value);
     }, this.props.delay);
   }
 
   onClearButtonClick() {
-    this.refs.input.value = "";
+    this.setState({ value: "" });
     this.onChange();
   }
 
   onFocus() {
     if (this.props.onFocus) {
       this.props.onFocus();
     }
 
@@ -121,17 +127,17 @@ class SearchBox extends Component {
     this.setState({ focused: false });
   }
 
   onKeyDown(e) {
     if (this.props.onKeyDown) {
       this.props.onKeyDown();
     }
 
-    const { autocomplete } = this.refs;
+    const autocomplete = this.autocompleteRef.current;
     if (!autocomplete || autocomplete.state.list.length <= 0) {
       return;
     }
 
     switch (e.key) {
       case "ArrowDown":
         autocomplete.jumpBy(1);
         break;
@@ -159,63 +165,62 @@ class SearchBox extends Component {
       case "End":
         autocomplete.jumpToBottom();
         break;
     }
   }
 
   render() {
     let {
-      type = "search",
+      autocompleteProvider,
+      learnMoreTitle,
+      learnMoreUrl,
       placeholder,
-      autocompleteProvider,
       plainStyle,
-      learnMoreUrl,
-      learnMoreTitle,
+      type = "search",
     } = this.props;
     const { value } = this.state;
-    const divClassList = ["devtools-searchbox", "has-clear-btn"];
+    const showAutocomplete = autocompleteProvider && this.state.focused && value !== "";
+
     const inputClassList = [`devtools-${type}input`];
     if (plainStyle) {
       inputClassList.push("devtools-plaininput");
     }
-    const showAutocomplete = autocompleteProvider && this.state.focused && value !== "";
-
     if (value !== "") {
       inputClassList.push("filled");
       learnMoreUrl = false;
     }
 
     return dom.div(
-      { className: divClassList.join(" ") },
+      { className: "devtools-searchbox has-clear-btn" },
       dom.input({
         className: inputClassList.join(" "),
+        onBlur: this.onBlur,
         onChange: this.onChange,
         onFocus: this.onFocus,
-        onBlur: this.onBlur,
         onKeyDown: this.onKeyDown,
         placeholder,
-        ref: "input",
+        ref: this.inputRef,
         value,
       }),
       dom.button({
         className: "devtools-searchinput-clear",
-        hidden: value == "",
+        hidden: value === "",
         onClick: this.onClearButtonClick,
       }),
       learnMoreUrl && MDNLink({
+        title: learnMoreTitle,
         url: learnMoreUrl,
-        title: learnMoreTitle,
       }),
       showAutocomplete && AutocompletePopup({
         autocompleteProvider,
         filter: value,
-        ref: "autocomplete",
         onItemSelected: (itemValue) => {
           this.setState({ value: itemValue });
           this.onChange();
         },
+        ref: this.autocompleteRef,
       })
     );
   }
 }
 
 module.exports = SearchBox;
--- a/dom/base/nsFocusManager.cpp
+++ b/dom/base/nsFocusManager.cpp
@@ -3123,16 +3123,22 @@ nsIContent* nsFocusManager::GetNextTabba
     };
 
     // If already at lowest priority tab (0), end search completely.
     // A bit counterintuitive but true, tabindex order goes 1, 2, ... 32767, 0
     if (aCurrentTabIndex == (aForward ? 0 : 1)) {
       break;
     }
 
+    // We've been just trying to find some focusable element, and haven't, so
+    // bail out.
+    if (aIgnoreTabIndex) {
+      break;
+    }
+
     // Continue looking for next highest priority tabindex
     aCurrentTabIndex = GetNextTabIndex(aOwner, aCurrentTabIndex, aForward);
     contentTraversal.Reset();
   }
 
   // Return scope owner at last for backward navigation if its tabindex
   // is non-negative
   if (!aSkipOwner && !aForward) {
--- a/dom/base/test/file_bug1453693.html
+++ b/dom/base/test/file_bug1453693.html
@@ -1,15 +1,26 @@
 <html>
   <head>
     <title>Test for Bug 1453693</title>
     <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
     <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
     <script>
 
+      class TestNode extends HTMLElement {
+      constructor() {
+        super();
+        const styles = "<style>:focus{background-color:yellow;}</style>";
+        this.attachShadow({ mode: 'open' });
+        this.shadowRoot.innerHTML =
+          `${styles}<div tabindex='-1'>test node</div> <slot></slot>`;
+      }}
+
+      window.customElements.define('test-node', TestNode);
+
       var lastFocusTarget;
       function focusLogger(event) {
         lastFocusTarget = event.target;
         console.log(event.target + " under " + event.target.parentNode);
         event.stopPropagation();
       }
 
       function testTabbingThroughShadowDOMWithTabIndexes() {
@@ -769,29 +780,76 @@
         synthesizeKey("KEY_Tab", {shiftKey: true});
         opener.is(document.activeElement, document.body.firstChild,
                   "body's first child should have focus.");
 
         host0.remove();
         input1.remove();
       }
 
+      function testDeeplyNestedShadowTree() {
+        opener.is(document.activeElement, document.body.firstChild, "body's first child should have focus.");
+        var host1 = document.createElement("test-node");
+        var lastHost = host1;
+        for (var i = 0; i < 20; ++i) {
+          lastHost.appendChild(document.createElement("test-node"));
+          lastHost = lastHost.firstChild;
+        }
+
+        var input = document.createElement("input");
+        document.body.appendChild(host1);
+        document.body.appendChild(input);
+        document.body.offsetLeft;
+
+        // Test shadow tree which doesn't have anything tab-focusable.
+        host1.shadowRoot.getElementsByTagName("div")[0].focus();
+        synthesizeKey("KEY_Tab");
+        is(document.activeElement, input, "Should have focused input element.");
+        synthesizeKey("KEY_Tab", {shiftKey: true});
+        opener.is(document.activeElement, document.body.firstChild, "body's first child should have focus.");
+
+        // Same test but with focusable elements in the tree...
+        var input2 = document.createElement("input");
+        var host2 = host1.firstChild;
+        var host3 = host2.firstChild;
+        host2.insertBefore(input2, host3);
+        var input3 = document.createElement("input");
+        lastHost.appendChild(input3);
+        document.body.offsetLeft;
+        host3.shadowRoot.getElementsByTagName("div")[0].focus();
+        synthesizeKey("KEY_Tab");
+        is(document.activeElement, input3, "Should have focused input3 element.");
+
+        // ...and backwards
+        host3.shadowRoot.getElementsByTagName("div")[0].focus();
+        synthesizeKey("KEY_Tab", {shiftKey: true});
+        is(document.activeElement, input2, "Should have focused input2 element.");
+
+        // Remove elements added to body element.
+        host1.remove();
+        input.remove();
+
+        // Tests expect body.firstChild to have focus.
+        document.body.firstChild.focus();
+      }
+
       function runTest() {
 
         testTabbingThroughShadowDOMWithTabIndexes();
         testTabbingThroughSimpleShadowDOM();
         testTabbingThroughNestedShadowDOM();
         testTabbingThroughDisplayContentsHost();
         testTabbingThroughLightDOMShadowDOMLightDOM();
         testFocusableHost();
         testShiftTabbingThroughFocusableHost();
         testTabbingThroughNestedSlot();
         testTabbingThroughSlotInLightDOM();
         testTabbingThroughFocusableSlotInLightDOM();
         testTabbingThroughScrollableShadowDOM();
+        testDeeplyNestedShadowTree();
 
         opener.didRunTests();
         window.close();
       }
 
       function init() {
         SimpleTest.waitForFocus(runTest);
       }