Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 44 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@
import * as Blockly from 'blockly/core';
import {NavigationController} from './navigation_controller';
import {CursorOptions, LineCursor} from './line_cursor';
import * as Constants from './constants';

export interface ExternalToolbox {
focus(): void;
}

/** Options object for KeyboardNavigation instances. */
export type NavigationOptions = {
cursor: Partial<CursorOptions>;
cursor?: Partial<CursorOptions>;
externalToolbox?: ExternalToolbox;
};

/** Default options for LineCursor instances. */
Expand All @@ -22,6 +28,12 @@ const defaultOptions: NavigationOptions = {
export class KeyboardNavigation {
/** The workspace. */
protected workspace: Blockly.WorkspaceSvg;
/**
* Workaround: Tracks when we're focusing the workspace just to get flyout
* keyboard navigation working. In this case we need to avoid resetting the
* flyout. In future, we hope the flyout can take focus.
*/
private focusingWorkspaceForFlyoutKeyboardNavigation = false;

/** Event handler run when the workspace gains focus. */
private focusListener: () => void;
Expand All @@ -48,17 +60,15 @@ export class KeyboardNavigation {
*
* @param workspace The workspace that the plugin will
* be added to.
* @param options Options.
*/
constructor(
workspace: Blockly.WorkspaceSvg,
options: Partial<NavigationOptions>,
) {
constructor(workspace: Blockly.WorkspaceSvg, options: NavigationOptions) {
this.workspace = workspace;

// Regularise options and apply defaults.
options = {...defaultOptions, ...options};

this.navigationController = new NavigationController();
this.navigationController = new NavigationController(options);
this.navigationController.init();
this.navigationController.addWorkspace(workspace);
this.navigationController.enable(workspace);
Expand All @@ -82,7 +92,11 @@ export class KeyboardNavigation {
workspace.getParentSvg().setAttribute('tabindex', '-1');

this.focusListener = () => {
this.navigationController.setHasFocus(workspace, true);
this.navigationController.setHasFocus(
workspace,
true,
this.focusingWorkspaceForFlyoutKeyboardNavigation,
);
};
this.blurListener = () => {
this.navigationController.setHasFocus(workspace, false);
Expand Down Expand Up @@ -137,6 +151,29 @@ export class KeyboardNavigation {
this.navigationController.dispose();
}

/**
* Focus the flyout if open.
*
* Generally not required if Blockly manages your toolbox.
*/
focusFlyout(): void {
this.navigationController.navigation.focusFlyout(this.workspace);
this.focusingWorkspaceForFlyoutKeyboardNavigation = true;
(this.workspace.getSvgGroup() as SVGElement).focus();
this.focusingWorkspaceForFlyoutKeyboardNavigation = false;
}

/**
* Called when an external toolbox loses the focus.
*/
onExternalToolboxBlur(): void {
if (
this.navigationController.navigation.getState(this.workspace) !==
Constants.STATE.FLYOUT
) {
this.navigationController.navigation.resetFlyout(this.workspace, true);
}

/**
* Toggle visibility of a help dialog for the keyboard shortcuts.
*/
Expand Down
8 changes: 6 additions & 2 deletions src/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
registrationType as cursorRegistrationType,
FlyoutCursor,
} from './flyout_cursor';
import {NavigationOptions} from './index';
import {PassiveFocus} from './passive_focus';

/**
Expand Down Expand Up @@ -76,8 +77,9 @@ export class Navigation {

/**
* Constructor for keyboard navigation.
* @param options Options.
*/
constructor() {
constructor(private options: NavigationOptions) {
this.wsChangeWrapper = this.workspaceChangeListener.bind(this);
this.flyoutChangeWrapper = this.flyoutChangeListener.bind(this);
}
Expand Down Expand Up @@ -419,7 +421,9 @@ export class Navigation {
this.setState(workspace, Constants.STATE.TOOLBOX);
this.resetFlyout(workspace, false /* shouldHide */);

if (!toolbox.getSelectedItem()) {
if (this.options.externalToolbox) {
this.options.externalToolbox.focus();
} else if (!toolbox.getSelectedItem()) {
// Find the first item that is selectable.
const toolboxItems = (toolbox as any).getToolboxItems();
for (let i = 0, toolboxItem; (toolboxItem = toolboxItems[i]); i++) {
Expand Down
46 changes: 31 additions & 15 deletions src/navigation_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {

import * as Constants from './constants';
import {Navigation} from './navigation';
import {NavigationOptions} from './index';
import {Announcer} from './announcer';
import {LineCursor} from './line_cursor';
import {ShortcutDialog} from './shortcut_dialog';
Expand All @@ -40,26 +41,17 @@ const createSerializedKey = ShortcutRegistry.registry.createSerializedKey.bind(
* Class for registering shortcuts for keyboard navigation.
*/
export class NavigationController {
navigation: Navigation = new Navigation();
navigation: Navigation;
announcer: Announcer = new Announcer();
shortcutDialog: ShortcutDialog = new ShortcutDialog();

/** Context menu and keyboard action for deletion. */
deleteAction: DeleteAction = new DeleteAction(
this.navigation,
this.canCurrentlyEdit.bind(this),
);
deleteAction: DeleteAction;

/** Context menu and keyboard action for insertion. */
insertAction: InsertAction = new InsertAction(
this.navigation,
this.canCurrentlyEdit.bind(this),
);
insertAction: InsertAction;

clipboard: Clipboard = new Clipboard(
this.navigation,
this.canCurrentlyEdit.bind(this),
);
clipboard: Clipboard;

workspaceMovement: WorkspaceMovement = new WorkspaceMovement(
this.canCurrentlyEdit.bind(this),
Expand All @@ -75,6 +67,25 @@ export class NavigationController {
| typeof Blockly.Toolbox.prototype.onShortcut
| null = null;

constructor(options: NavigationOptions) {
this.navigation = new Navigation(options);

this.deleteAction = new DeleteAction(
this.navigation,
this.canCurrentlyEdit.bind(this),
);

this.insertAction = new InsertAction(
this.navigation,
this.canCurrentlyEdit.bind(this),
);

this.clipboard = new Clipboard(
this.navigation,
this.canCurrentlyEdit.bind(this),
);
}

/**
* Registers the default keyboard shortcuts for keyboard navigation.
*/
Expand Down Expand Up @@ -162,10 +173,15 @@ export class NavigationController {
*
* @param workspace the workspace that now has input focus.
* @param isFocused whether the environment has browser focus.
* @param isFocusedForFlyoutKeyboardNavigation avoid workspace modifications.
*/
setHasFocus(workspace: WorkspaceSvg, isFocused: boolean) {
setHasFocus(
workspace: WorkspaceSvg,
isFocused: boolean,
isFocusedForFlyoutKeyboardNavigation = false,
) {
this.hasNavigationFocus = isFocused;
if (isFocused) {
if (isFocused && !isFocusedForFlyoutKeyboardNavigation) {
this.navigation.focusWorkspace(workspace, true);
}
}
Expand Down