diff --git a/src/index.ts b/src/index.ts index 6f8c0e49..a67d96b9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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; + cursor?: Partial; + externalToolbox?: ExternalToolbox; }; /** Default options for LineCursor instances. */ @@ -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; @@ -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, - ) { + 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); @@ -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); @@ -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. */ diff --git a/src/navigation.ts b/src/navigation.ts index 1d44c9c6..5f002874 100644 --- a/src/navigation.ts +++ b/src/navigation.ts @@ -17,6 +17,7 @@ import { registrationType as cursorRegistrationType, FlyoutCursor, } from './flyout_cursor'; +import {NavigationOptions} from './index'; import {PassiveFocus} from './passive_focus'; /** @@ -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); } @@ -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++) { diff --git a/src/navigation_controller.ts b/src/navigation_controller.ts index 724799c3..d369e954 100644 --- a/src/navigation_controller.ts +++ b/src/navigation_controller.ts @@ -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'; @@ -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), @@ -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. */ @@ -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); } }