diff --git a/src/actions/arrow_navigation.ts b/src/actions/arrow_navigation.ts index 0bae1c83..dd91bcbb 100644 --- a/src/actions/arrow_navigation.ts +++ b/src/actions/arrow_navigation.ts @@ -8,6 +8,7 @@ import {ASTNode, ShortcutRegistry, utils as BlocklyUtils} from 'blockly/core'; import type {Field, Toolbox, WorkspaceSvg} from 'blockly/core'; +import * as Blockly from 'blockly/core'; import * as Constants from '../constants'; import type {Navigation} from '../navigation'; @@ -47,6 +48,7 @@ export class ArrowNavigation { * Adds all arrow key navigation shortcuts to the registry. */ install() { + console.log('@@@@@ install arrow keys'); const shortcuts: { [name: string]: ShortcutRegistry.KeyboardShortcut; } = { @@ -73,12 +75,15 @@ export class ArrowNavigation { } return isHandled; case Constants.STATE.TOOLBOX: - isHandled = - toolbox && typeof toolbox.onShortcut === 'function' - ? toolbox.onShortcut(shortcut) - : false; - if (!isHandled) { - this.navigation.focusFlyout(workspace); + isHandled = toolbox.selectChild(); + // isHandled = + // toolbox && typeof toolbox.onShortcut === 'function' + // ? toolbox.onShortcut(shortcut) + // : false; + const flyout = toolbox.getFlyout(); + if (!isHandled && flyout) { + Blockly.getFocusManager().focusTree(flyout.getWorkspace()); + // this.navigation.focusFlyout(workspace); } return true; default: @@ -111,12 +116,15 @@ export class ArrowNavigation { } return isHandled; case Constants.STATE.FLYOUT: - this.navigation.focusToolbox(workspace); + Blockly.getFocusManager().focusTree(toolbox); + // this.navigation.focusToolbox(workspace); return true; case Constants.STATE.TOOLBOX: - return toolbox && typeof toolbox.onShortcut === 'function' - ? toolbox.onShortcut(shortcut) - : false; + isHandled = toolbox.selectParent(); + return isHandled; + // return toolbox && typeof toolbox.onShortcut === 'function' + // ? toolbox.onShortcut(shortcut) + // : false; default: return false; } @@ -157,9 +165,20 @@ export class ArrowNavigation { } return isHandled; case Constants.STATE.TOOLBOX: - return toolbox && typeof toolbox.onShortcut === 'function' - ? toolbox.onShortcut(shortcut) - : false; + // TODO: Move this into cursor? + if (!toolbox.getSelectedItem()) { + const firstItem = toolbox.getToolboxItems().find((item) => item.isSelectable()) ?? null; + toolbox.setSelectedItem(firstItem); + isHandled = true; + } else isHandled = toolbox.selectNext(); + const selectedItem = toolbox.getSelectedItem(); + if (selectedItem) { + Blockly.getFocusManager().focusNode(selectedItem); + } + return isHandled; + // return toolbox && typeof toolbox.onShortcut === 'function' + // ? toolbox.onShortcut(shortcut) + // : false; default: return false; } @@ -205,9 +224,16 @@ export class ArrowNavigation { } return isHandled; case Constants.STATE.TOOLBOX: - return toolbox && typeof toolbox.onShortcut === 'function' - ? toolbox.onShortcut(shortcut) - : false; + // TODO: Move this into cursor? + isHandled = toolbox.selectPrevious(); + const selectedItem = toolbox.getSelectedItem(); + if (selectedItem) { + Blockly.getFocusManager().focusNode(selectedItem); + } + return isHandled; + // return toolbox && typeof toolbox.onShortcut === 'function' + // ? toolbox.onShortcut(shortcut) + // : false; default: return false; } diff --git a/src/actions/exit.ts b/src/actions/exit.ts index 3b9666be..90840a02 100644 --- a/src/actions/exit.ts +++ b/src/actions/exit.ts @@ -6,6 +6,7 @@ import {ShortcutRegistry, utils as BlocklyUtils} from 'blockly/core'; +import * as Blockly from 'blockly/core'; import * as Constants from '../constants'; import type {Navigation} from '../navigation'; @@ -29,7 +30,11 @@ export class ExitAction { switch (this.navigation.getState(workspace)) { case Constants.STATE.FLYOUT: case Constants.STATE.TOOLBOX: - this.navigation.focusWorkspace(workspace); + Blockly.getFocusManager().focusTree(workspace); + if (!Blockly.Gesture.inProgress()) { + workspace.hideChaff(); + } + // this.navigation.focusWorkspace(workspace); return true; default: return false; diff --git a/src/index.ts b/src/index.ts index 526b9d6c..62f02c9c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -55,8 +55,8 @@ export class KeyboardNavigation { * These fields are used to preserve the workspace's initial state to restore * it when/if keyboard navigation is disabled. */ - private injectionDivTabIndex: string | null; - private workspaceParentTabIndex: string | null; + // private injectionDivTabIndex: string | null; + // private workspaceParentTabIndex: string | null; private originalTheme: Blockly.Theme; /** @@ -98,15 +98,15 @@ export class KeyboardNavigation { workspace.addChangeListener(enableBlocksOnDrag); // Ensure that only the root SVG G (group) has a tab index. - this.injectionDivTabIndex = workspace - .getInjectionDiv() - .getAttribute('tabindex'); - workspace.getInjectionDiv().removeAttribute('tabindex'); - this.workspaceParentTabIndex = workspace - .getParentSvg() - .getAttribute('tabindex'); - // We add a focus listener below so use -1 so it doesn't become focusable. - workspace.getParentSvg().setAttribute('tabindex', '-1'); + // this.injectionDivTabIndex = workspace + // .getInjectionDiv() + // .getAttribute('tabindex'); + // workspace.getInjectionDiv().removeAttribute('tabindex'); + // this.workspaceParentTabIndex = workspace + // .getParentSvg() + // .getAttribute('tabindex'); + // // We add a focus listener below so use -1 so it doesn't become focusable. + // workspace.getParentSvg().setAttribute('tabindex', '-1'); // Move the flyout for logical tab order. const flyoutElement = getFlyoutElement(workspace); @@ -157,8 +157,8 @@ export class KeyboardNavigation { this.navigationController.handleBlurWorkspace(workspace); }; - workspace.getSvgGroup().addEventListener('focus', this.focusListener); - workspace.getSvgGroup().addEventListener('blur', this.blurListener); + workspace.getSvgGroup().addEventListener('focusin', this.focusListener); + workspace.getSvgGroup().addEventListener('focusout', this.blurListener); this.widgetDropDownDivFocusOutListener = (e: Event) => { this.navigationController.handleFocusOutWidgetDropdownDiv( @@ -240,7 +240,7 @@ export class KeyboardNavigation { // Remove the event listener that enables blocks on drag this.workspace.removeChangeListener(enableBlocksOnDrag); - this.workspace.getSvgGroup().removeEventListener('blur', this.blurListener); + this.workspace.getSvgGroup().removeEventListener('focusout', this.blurListener); this.workspace .getSvgGroup() .removeEventListener('focus', this.focusListener); @@ -265,21 +265,21 @@ export class KeyboardNavigation { flyoutElement?.removeEventListener('focus', this.flyoutFocusListener); flyoutElement?.removeEventListener('blur', this.flyoutBlurListener); - if (this.workspaceParentTabIndex) { - this.workspace - .getParentSvg() - .setAttribute('tabindex', this.workspaceParentTabIndex); - } else { - this.workspace.getParentSvg().removeAttribute('tabindex'); - } - - if (this.injectionDivTabIndex) { - this.workspace - .getInjectionDiv() - .setAttribute('tabindex', this.injectionDivTabIndex); - } else { - this.workspace.getInjectionDiv().removeAttribute('tabindex'); - } + // if (this.workspaceParentTabIndex) { + // this.workspace + // .getParentSvg() + // .setAttribute('tabindex', this.workspaceParentTabIndex); + // } else { + // this.workspace.getParentSvg().removeAttribute('tabindex'); + // } + + // if (this.injectionDivTabIndex) { + // this.workspace + // .getInjectionDiv() + // .setAttribute('tabindex', this.injectionDivTabIndex); + // } else { + // this.workspace.getInjectionDiv().removeAttribute('tabindex'); + // } this.workspace.setTheme(this.originalTheme); diff --git a/src/navigation.ts b/src/navigation.ts index c9b1f83b..bd9d999c 100644 --- a/src/navigation.ts +++ b/src/navigation.ts @@ -139,7 +139,18 @@ export class Navigation { * @returns The state of the given workspace. */ getState(workspace: Blockly.WorkspaceSvg): Constants.STATE { - return this.workspaceStates[workspace.id]; + const focusedTree = Blockly.getFocusManager().getFocusedTree(); + if (focusedTree instanceof Blockly.WorkspaceSvg) { + // TODO: Is the instanceof Flyout below ever needed now? Probably not since Flyout shouldn't actually be a real IFocusableTree... + if (focusedTree.isFlyout) { + return Constants.STATE.FLYOUT; + } else return Constants.STATE.WORKSPACE; + } else if (focusedTree instanceof Blockly.Toolbox) { + return Constants.STATE.TOOLBOX; + } else if (focusedTree instanceof Blockly.Flyout) { + return Constants.STATE.FLYOUT; + } else return Constants.STATE.NOWHERE; + // return this.workspaceStates[workspace.id]; } /** @@ -381,7 +392,7 @@ export class Navigation { * @param workspace The workspace to focus. */ focusWorkspace(workspace: Blockly.WorkspaceSvg) { - getWorkspaceElement(workspace).focus(); + // getWorkspaceElement(workspace).focus(); } /** @@ -432,7 +443,7 @@ export class Navigation { if (cursor && (ignorePopUpDivs || !popUpDivsShowing)) { const curNode = cursor.getCurNode(); if (curNode) { - this.passiveFocusIndicator.show(curNode); + // this.passiveFocusIndicator.show(curNode); } // It's initially null so this is a valid state despite the types. cursor.setCurNode(null); @@ -460,9 +471,9 @@ export class Navigation { // https://github.com/google/blockly-samples/issues/2498 return; } - if (relatedTarget !== getWorkspaceElement(workspace)) { - this.handleBlurWorkspace(workspace, true); - } + // if (relatedTarget !== getWorkspaceElement(workspace)) { + // this.handleBlurWorkspace(workspace, true); + // } } /** @@ -471,7 +482,7 @@ export class Navigation { * @param workspace The workspace with the toolbox. */ focusToolbox(workspace: Blockly.WorkspaceSvg) { - getToolboxElement(workspace)?.focus(); + // getToolboxElement(workspace)?.focus(); } /** @@ -519,7 +530,7 @@ export class Navigation { * @param workspace The workspace with the flyout. */ focusFlyout(workspace: Blockly.WorkspaceSvg) { - getFlyoutElement(workspace)?.focus(); + // getFlyoutElement(workspace)?.focus(); } /** @@ -1141,6 +1152,7 @@ export class Navigation { * @returns whether keyboard navigation is currently allowed. */ canCurrentlyNavigate(workspace: Blockly.WorkspaceSvg) { + console.log('@@@@@@@ current state:', this.getState(workspace)); return ( workspace.keyboardAccessibilityMode && this.getState(workspace) !== Constants.STATE.NOWHERE diff --git a/src/navigation_controller.ts b/src/navigation_controller.ts index eb3f6bdb..e9585442 100644 --- a/src/navigation_controller.ts +++ b/src/navigation_controller.ts @@ -132,16 +132,12 @@ export class NavigationController { } switch (shortcut.name) { case Constants.SHORTCUT_NAMES.UP: - // @ts-expect-error private method return this.selectPrevious(); case Constants.SHORTCUT_NAMES.LEFT: - // @ts-expect-error private method return this.selectParent(); case Constants.SHORTCUT_NAMES.DOWN: - // @ts-expect-error private method return this.selectNext(); case Constants.SHORTCUT_NAMES.RIGHT: - // @ts-expect-error private method return this.selectChild(); default: return false; @@ -172,46 +168,46 @@ export class NavigationController { } focusWorkspace(workspace: WorkspaceSvg) { - this.navigation.focusWorkspace(workspace); + // this.navigation.focusWorkspace(workspace); } handleFocusWorkspace(workspace: Blockly.WorkspaceSvg) { - this.navigation.handleFocusWorkspace(workspace); + // this.navigation.handleFocusWorkspace(workspace); } handleBlurWorkspace(workspace: Blockly.WorkspaceSvg) { - this.navigation.handleBlurWorkspace(workspace); + // this.navigation.handleBlurWorkspace(workspace); } handleFocusOutWidgetDropdownDiv( workspace: Blockly.WorkspaceSvg, relatedTarget: EventTarget | null, ) { - this.navigation.handleFocusOutWidgetDropdownDiv(workspace, relatedTarget); + // this.navigation.handleFocusOutWidgetDropdownDiv(workspace, relatedTarget); } focusToolbox(workspace: Blockly.WorkspaceSvg) { - this.navigation.focusToolbox(workspace); + // this.navigation.focusToolbox(workspace); } handleFocusToolbox(workspace: Blockly.WorkspaceSvg) { - this.navigation.handleFocusToolbox(workspace); + // this.navigation.handleFocusToolbox(workspace); } handleBlurToolbox(workspace: Blockly.WorkspaceSvg, closeFlyout: boolean) { - this.navigation.handleBlurToolbox(workspace, closeFlyout); + // this.navigation.handleBlurToolbox(workspace, closeFlyout); } focusFlyout(workspace: Blockly.WorkspaceSvg) { - this.navigation.focusFlyout(workspace); + // this.navigation.focusFlyout(workspace); } handleFocusFlyout(workspace: Blockly.WorkspaceSvg) { - this.navigation.handleFocusFlyout(workspace); + // this.navigation.handleFocusFlyout(workspace); } handleBlurFlyout(workspace: Blockly.WorkspaceSvg, closeFlyout: boolean) { - this.navigation.handleBlurFlyout(workspace, closeFlyout); + // this.navigation.handleBlurFlyout(workspace, closeFlyout); } /** @@ -248,10 +244,27 @@ export class NavigationController { callback: (workspace) => { switch (this.navigation.getState(workspace)) { case Constants.STATE.WORKSPACE: - if (!workspace.getToolbox()) { - this.navigation.focusFlyout(workspace); + const toolbox = workspace.getToolbox(); + if (!toolbox) { + Blockly.getFocusManager().focusTree(workspace); + // this.navigation.focusFlyout(workspace); } else { - this.navigation.focusToolbox(workspace); + // The toolbox receiving focus should ensure it has at least its first item selected + // if there was no previous focus yet. + Blockly.getFocusManager().focusTree(toolbox); + // Ensure that the first item is selected. + // TODO: Retain this across contexts? + // if (!toolbox.getSelectedItem() && toolbox instanceof Blockly.Toolbox) { + // // Find the first item that is selectable. + // const toolboxItems = toolbox.getToolboxItems(); + // for (let i = 0, toolboxItem; (toolboxItem = toolboxItems[i]); i++) { + // if (toolboxItem.isSelectable()) { + // toolbox.selectItemByPosition(i); + // break; + // } + // } + // } + // this.navigation.focusToolbox(workspace); } return true; default: diff --git a/test/index.html b/test/index.html index 19399fa5..12344a78 100644 --- a/test/index.html +++ b/test/index.html @@ -34,10 +34,10 @@ --outline-width: 5px; } - .blocklyWorkspace:focus .blocklyMainBackground { + /* .blocklyWorkspace:focus .blocklyMainBackground { outline: Highlight solid var(--outline-width); outline-offset: -5px; - } + } */ .blocklyFlyout { top: var(--outline-width); diff --git a/test/index.ts b/test/index.ts index 0e7687e9..a878522e 100644 --- a/test/index.ts +++ b/test/index.ts @@ -89,6 +89,7 @@ function createWorkspace(): Blockly.WorkspaceSvg { const injectOptions = { toolbox, + comments: true, renderer, }; const blocklyDiv = document.getElementById('blocklyDiv'); @@ -103,6 +104,8 @@ function createWorkspace(): Blockly.WorkspaceSvg { new KeyboardNavigation(workspace, navigationOptions); registerRunCodeShortcut(); + Blockly.ContextMenuItems.registerCommentOptions(); + // Disable blocks that aren't inside the setup or draw loops. workspace.addChangeListener(Blockly.Events.disableOrphans);