Skip to content

Commit

Permalink
refactor: rely on DOM focus to dispatch keyboard events
Browse files Browse the repository at this point in the history
Previously, a single set of keyboard event handlers were added to the window object, and keyboard
events were dispatched using a custom mechanism based on a global stack of keyboard handlers.  With
this change, the builtin focus mechanism is used instead, which is both simpler and more general.
In order to ensure that the builtin keyboard focus remains on what the user expects, this change
also introduces a a new mechanism for automatic focusing of elements.
  • Loading branch information
jbms committed Oct 11, 2017
1 parent 3bca87e commit bd511bb
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 111 deletions.
9 changes: 6 additions & 3 deletions src/neuroglancer/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
*/

import {RefCounted} from 'neuroglancer/util/disposable';
import {globalKeyboardHandlerStack, KeySequenceMap} from 'neuroglancer/util/keyboard_shortcut_handler';
import {KeySequenceMap, KeyboardShortcutHandler} from 'neuroglancer/util/keyboard_shortcut_handler';
import {AutomaticallyFocusedElement} from 'neuroglancer/util/automatic_focus';

export const overlayKeyboardHandlerPriority = 100;

Expand All @@ -35,11 +36,13 @@ export class Overlay extends RefCounted {
let container = this.container = document.createElement('div');
container.className = 'overlay';
let content = this.content = document.createElement('div');
this.registerDisposer(new AutomaticallyFocusedElement(content));
content.className = 'overlay-content';
container.appendChild(content);
document.body.appendChild(container);
this.registerDisposer(globalKeyboardHandlerStack.push(
keySequenceMap, this.commandReceived.bind(this), overlayKeyboardHandlerPriority));
this.registerDisposer(new KeyboardShortcutHandler(
this.container, keySequenceMap, this.commandReceived.bind(this)));
content.focus();
}

commandReceived(action: string) {
Expand Down
74 changes: 74 additions & 0 deletions src/neuroglancer/util/automatic_focus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* @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 LinkedListOperations from 'neuroglancer/util/linked_list.0.ts';

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

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

const automaticFocusList = new AutomaticFocusList();

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

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

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

export class AutomaticallyFocusedElement extends RefCounted {
prev0: AutomaticallyFocusedElement|null = null;
next0: AutomaticallyFocusedElement|null = null;

constructor(public element: HTMLElement) {
super();
element.tabIndex = -1;
this.registerEventListener(element, 'mouseenter', () => {
element.focus();
});
// Insert at the end of the list.
LinkedListOperations.insertBefore(<any>automaticFocusList, this);
this.registerEventListener(element, 'focus', () => {
// Move to the beginning of the list.
LinkedListOperations.pop<AutomaticallyFocusedElement>(this);
LinkedListOperations.insertAfter(<any>automaticFocusList, this);
});
maybeUpdateFocus();
}

disposed() {
super.disposed();
LinkedListOperations.pop<AutomaticallyFocusedElement>(this);
}
}
83 changes: 0 additions & 83 deletions src/neuroglancer/util/keyboard_shortcut_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,86 +293,3 @@ export class KeySequenceMap {
return keySequenceMapEntries(this.root);
}
}

interface HandlerStackEntry {
keySequenceMap: KeySequenceMap;
handler: Handler;
identifier: any;
priority: number;
}

export class KeyboardHandlerStack extends RefCounted {
keyboardHandler: KeyboardShortcutHandler|undefined;
stack = new Array<HandlerStackEntry>();
constructor(public target: EventTarget) {
super();
}

push(keySequenceMap: KeySequenceMap, handler: Handler, priority: number = 0) {
const identifier = {};
const entry = {keySequenceMap, handler, identifier, priority};
const {stack} = this;
let insertionIndex = stack.length;
while (insertionIndex > 0 && stack[insertionIndex - 1].priority > priority) {
--insertionIndex;
}
this.stack.splice(insertionIndex, 0, entry);
if (insertionIndex === stack.length - 1) {
this.updateHandler();
}

const disposer = () => {
this.delete(identifier);
};
return disposer;
}

private delete(identifier: any) {
const {stack} = this;
const index = stack.findIndex(entry => entry.identifier === identifier);
if (index === -1) {
throw new Error('Attempt to delete keyboard handler that does not exist.');
}
stack.splice(index, 1);
if (index === stack.length) {
this.updateHandler();
}
}

/**
* Update this.keyboardHandler to reflect top of stack.
*/
private updateHandler() {
const {stack} = this;
let {keyboardHandler} = this;
if (stack.length === 0) {
if (keyboardHandler !== undefined) {
keyboardHandler.dispose();
this.keyboardHandler = undefined;
return;
}
}

const {keySequenceMap, handler} = stack[stack.length - 1];

if (keyboardHandler === undefined) {
this.keyboardHandler = new KeyboardShortcutHandler(window, keySequenceMap, handler);
return;
}

keyboardHandler.setKeySequenceMap(keySequenceMap);
keyboardHandler.handler = handler;
}

disposed() {
const {keyboardHandler} = this;
if (keyboardHandler !== undefined) {
keyboardHandler.dispose();
}
this.keyboardHandler = undefined;
this.stack.length = 0;
super.disposed();
}
}

export const globalKeyboardHandlerStack = new KeyboardHandlerStack(window);
33 changes: 8 additions & 25 deletions src/neuroglancer/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@ import {TrackableValue} from 'neuroglancer/trackable_value';
import {RefCounted} from 'neuroglancer/util/disposable';
import {removeFromParent} from 'neuroglancer/util/dom';
import {vec3} from 'neuroglancer/util/geom';
import {globalKeyboardHandlerStack, KeySequenceMap} from 'neuroglancer/util/keyboard_shortcut_handler';
import {KeySequenceMap, KeyboardShortcutHandler} from 'neuroglancer/util/keyboard_shortcut_handler';
import {NullarySignal} from 'neuroglancer/util/signal';
import {CompoundTrackable} from 'neuroglancer/util/trackable';
import {DataDisplayLayout, LAYOUTS} from 'neuroglancer/viewer_layouts';
import {ViewerState, VisibilityPrioritySpecification} from 'neuroglancer/viewer_state';
import {WatchableVisibilityPriority} from 'neuroglancer/visibility_priority/frontend';
import {GL} from 'neuroglancer/webgl/context';
import {RPC} from 'neuroglancer/worker_rpc';
import {AutomaticallyFocusedElement} from 'neuroglancer/util/automatic_focus';

require('./viewer.css');
require('./help_button.css');
Expand Down Expand Up @@ -262,6 +263,9 @@ export class Viewer extends RefCounted implements ViewerState {
let {display, options} = this;
let gridContainer = document.createElement('div');
gridContainer.setAttribute('class', 'gllayoutcontainer noselect');
this.registerDisposer(
new KeyboardShortcutHandler(gridContainer, this.keyMap, this.onKeyCommand.bind(this)));
this.registerDisposer(new AutomaticallyFocusedElement(gridContainer));
let {container} = display;
container.appendChild(gridContainer);
this.registerDisposer(() => removeFromParent(gridContainer));
Expand Down Expand Up @@ -301,35 +305,14 @@ export class Viewer extends RefCounted implements ViewerState {
L.box('column', uiElements)(gridContainer);
this.display.onResize();

let keyboardHandlerDisposer: (() => void)|undefined;

const updateVisibility = () => {
const shouldBeVisible = this.visibility.visible;
if (shouldBeVisible) {
if (keyboardHandlerDisposer === undefined) {
keyboardHandlerDisposer =
globalKeyboardHandlerStack.push(this.keyMap, this.onKeyCommand.bind(this));
}
if (!this.visible) {
gridContainer.style.visibility = 'inherit';
}
} else if (!shouldBeVisible && this.visible) {
if (keyboardHandlerDisposer !== undefined) {
keyboardHandlerDisposer!();
keyboardHandlerDisposer = undefined;
}
if (this.visible) {
gridContainer.style.visibility = 'hidden';
}
if (shouldBeVisible !== this.visible) {
gridContainer.style.visibility = shouldBeVisible ? 'inherit' : 'hidden';
this.visible = shouldBeVisible;
}
this.visible = shouldBeVisible;
};
updateVisibility();
this.registerDisposer(() => {
if (keyboardHandlerDisposer !== undefined) {
keyboardHandlerDisposer();
}
});
this.registerDisposer(this.visibility.changed.add(updateVisibility));
}

Expand Down

0 comments on commit bd511bb

Please sign in to comment.