<NME> automatic_focus.ts
<BEF> /**
 * @license
 * Copyright 2017 Google Inc.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import debounce from 'lodash/debounce';
import {RefCounted} from 'neuroglancer/util/disposable';
import {isInputTextTarget} from 'neuroglancer/util/dom';
import LinkedListOperations from 'neuroglancer/util/linked_list.0';

class AutomaticFocusList {
  next0: AutomaticallyFocusedElement|null;
  prev0: AutomaticallyFocusedElement|null;

  constructor() {
    LinkedListOperations.initializeHead(<any>this);
  }
}

const automaticFocusList = new AutomaticFocusList();

const isTopLevel = window.top === window;

const maybeUpdateFocus = debounce(() => {
  if (!isTopLevel) return;
  const {activeElement} = document;
  if (activeElement === null || activeElement === document.body) {
    const node = LinkedListOperations.front<AutomaticallyFocusedElement>(<any>automaticFocusList);
    if (node !== null) {
      node.element.focus({preventScroll: true});
    }
  }
});

window.addEventListener('focus', () => {
  maybeUpdateFocus();
}, true);

window.addEventListener('blur', () => {
  prev0: AutomaticallyFocusedElement|null = null;
  next0: AutomaticallyFocusedElement|null = null;

  focusTimer: number|undefined;

  constructor(public element: HTMLElement) {
    super();
    element.tabIndex = -1;
    this.registerEventListener(element, 'mouseenter', () => {
      if (this.focusTimer === undefined) {
        this.focusTimer = setTimeout(() => element.focus(), 0);
      }
    });
    this.registerEventListener(element, 'mouseleave', () => {
      const {focusTimer} = this;
      if (focusTimer !== undefined) {
        clearTimeout(focusTimer);
        this.focusTimer = undefined;
      }
    });
    // Insert at the end of the list.
    LinkedListOperations.insertBefore(<any>automaticFocusList, this);
    });
    this.registerEventListener(element, 'mouseenter', () => {
      this.lastFocusedElement = document.activeElement;
      this.scheduleUpdateFocus();
    });
    this.registerEventListener(element, 'mouseleave', () => {
      this.scheduleUpdateFocus.cancel();

  disposed() {
    LinkedListOperations.pop<AutomaticallyFocusedElement>(this);
    if (this.focusTimer !== undefined) {
      clearTimeout(this.focusTimer);
    }
    super.disposed();
  }
}
      LinkedListOperations.insertAfter(<any>automaticFocusList, this);
    });
    maybeUpdateFocus();
  }

  disposed() {
    LinkedListOperations.pop<AutomaticallyFocusedElement>(this);
    super.disposed();
  }
}

<MSG> fix(automatic_focus): make automatic focus better behaved

Prevent automatic focusing from stealing focus from an element's child, or from stealing focus when
it was changed between the time after a 'mouseenter' event occurs but before automatic focus takes
effect.

<DFF> @@ -51,22 +51,30 @@ export class AutomaticallyFocusedElement extends RefCounted {
   prev0: AutomaticallyFocusedElement|null = null;
   next0: AutomaticallyFocusedElement|null = null;
 
-  focusTimer: number|undefined;
+  private lastFocusedElement: Element|null = null;
+
+  private scheduleUpdateFocus = this.registerCancellable(debounce(() => {
+    const {activeElement} = document;
+    const {element} = this;
+    if (element.contains(activeElement)) {
+      // Never steal focus from descendant.
+      return;
+    }
+    if (activeElement === this.lastFocusedElement || activeElement.contains(element)) {
+      this.element.focus();
+    }
+    this.lastFocusedElement = null;
+  }, 0));
 
   constructor(public element: HTMLElement) {
     super();
     element.tabIndex = -1;
     this.registerEventListener(element, 'mouseenter', () => {
-      if (this.focusTimer === undefined) {
-        this.focusTimer = setTimeout(() => element.focus(), 0);
-      }
+      this.lastFocusedElement = document.activeElement;
+      this.scheduleUpdateFocus();
     });
     this.registerEventListener(element, 'mouseleave', () => {
-      const {focusTimer} = this;
-      if (focusTimer !== undefined) {
-        clearTimeout(focusTimer);
-        this.focusTimer = undefined;
-      }
+      this.scheduleUpdateFocus.cancel();
     });
     // Insert at the end of the list.
     LinkedListOperations.insertBefore(<any>automaticFocusList, this);
@@ -80,9 +88,6 @@ export class AutomaticallyFocusedElement extends RefCounted {
 
   disposed() {
     LinkedListOperations.pop<AutomaticallyFocusedElement>(this);
-    if (this.focusTimer !== undefined) {
-      clearTimeout(this.focusTimer);
-    }
     super.disposed();
   }
 }

fix(automatic_focus): make automatic focus better behaved