From 4a80889ef10aeb2d9d456a61034faab41775aa40 Mon Sep 17 00:00:00 2001 From: alschmiedt Date: Mon, 9 Sep 2019 16:41:06 -0700 Subject: [PATCH] Enter accessibility (#2982) * Fix shift clicking on a block * Add tests for toggle keyboar nav --- core/blockly.js | 7 ++--- core/gesture.js | 6 ++-- core/keyboard_nav/key_map.js | 40 +++++++++++++++++++++---- core/keyboard_nav/navigation.js | 52 +++++++++++++++++++++------------ tests/mocha/navigation_test.js | 31 ++++++++++++++++++++ tests/playground.html | 14 +++++++++ 6 files changed, 119 insertions(+), 31 deletions(-) diff --git a/core/blockly.js b/core/blockly.js index b075c751e..2e793b1a3 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -216,9 +216,7 @@ Blockly.onKeyDown_ = function(e) { if (mainWorkspace.options.readOnly) { // When in read only mode handle key actions for keyboard navigation. - if (Blockly.keyboardAccessibilityMode) { - Blockly.navigation.onKeyPress(e); - } + Blockly.navigation.onKeyPress(e); return; } @@ -227,8 +225,7 @@ Blockly.onKeyDown_ = function(e) { // Pressing esc closes the context menu. Blockly.hideChaff(); Blockly.navigation.onBlocklyAction(Blockly.navigation.ACTION_EXIT); - } else if (Blockly.keyboardAccessibilityMode && - Blockly.navigation.onKeyPress(e)) { + } else if (Blockly.navigation.onKeyPress(e)) { // If the keyboard or field handled the key press return. return; } else if (e.keyCode == Blockly.utils.KeyCodes.BACKSPACE || diff --git a/core/gesture.js b/core/gesture.js index d9ad6c8b4..ed2899132 100644 --- a/core/gesture.js +++ b/core/gesture.js @@ -498,9 +498,12 @@ Blockly.Gesture.prototype.doStart = function(e) { Blockly.Tooltip.block(); if (this.targetBlock_) { - this.targetBlock_.select(); if (!this.targetBlock_.isInFlyout && e.shiftKey) { Blockly.navigation.enableKeyboardAccessibility(); + this.creatorWorkspace_.cursor.setLocation( + Blockly.navigation.getTopNode(this.targetBlock_)); + } else { + this.targetBlock_.select(); } } @@ -760,7 +763,6 @@ Blockly.Gesture.prototype.doBlockClick_ = function() { * @private */ Blockly.Gesture.prototype.doWorkspaceClick_ = function(e) { - Blockly.navigation.disableKeyboardAccessibility(); var ws = this.creatorWorkspace_; if (e.shiftKey) { Blockly.navigation.enableKeyboardAccessibility(); diff --git a/core/keyboard_nav/key_map.js b/core/keyboard_nav/key_map.js index 6672ab1f7..9bb163b28 100644 --- a/core/keyboard_nav/key_map.js +++ b/core/keyboard_nav/key_map.js @@ -37,10 +37,15 @@ goog.require('Blockly.utils.KeyCodes'); Blockly.user.keyMap.map_ = {}; /** - * List of modifier keys checked when serializing the key event. - * @type {Array} + * Object holding valid modifiers. + * @enum {string} */ -Blockly.user.keyMap.modifierKeys = ['Shift', 'Control', 'Alt', 'Meta']; +Blockly.user.keyMap.modifierKeys = { + SHIFT: 'Shift', + CONTROL: 'Control', + ALT: 'Alt', + META: 'Meta' +}; /** * Update the key map to contain the new action. @@ -113,9 +118,9 @@ Blockly.user.keyMap.getKeyByAction = function(action) { * @return {!string} A string containing the serialized key event. */ Blockly.user.keyMap.serializeKeyEvent = function(e) { - var modifierKeys = Blockly.user.keyMap.modifierKeys; + var modifiers = Object.values(Blockly.user.keyMap.modifierKeys); var key = ''; - for (var i = 0, keyName; keyName = modifierKeys[i]; i++) { + for (var i = 0, keyName; keyName = modifiers[i]; i++) { if (e.getModifierState(keyName)) { key += keyName; } @@ -124,6 +129,27 @@ Blockly.user.keyMap.serializeKeyEvent = function(e) { return key; }; +/** + * Create the serialized key code that will be used in the key map. + * @param {!number} keyCode Number code representing the key. + * @param {!Array} modifiers List of modifiers to be used with the key. + * All valid modifiers can be found in the Blockly.user.keyMap.modifierKeys. + * @return {string} The serialized key code for the given modifiers and key. + */ +Blockly.user.keyMap.createSerializedKey = function(keyCode, modifiers) { + var key = ''; + var validModifiers = Object.values(Blockly.user.keyMap.modifierKeys); + for (var i = 0, keyName; keyName = modifiers[i]; i++) { + if (validModifiers.indexOf(keyName) > -1) { + key += keyName; + } else { + throw Error(keyName + ' is not a valid modifier key.'); + } + } + key += keyCode; + return key; +}; + /** * Creates the default key map. * @return {!Object} An object holding the default key @@ -131,6 +157,9 @@ Blockly.user.keyMap.serializeKeyEvent = function(e) { */ Blockly.user.keyMap.createDefaultKeyMap = function() { var map = {}; + var controlK = Blockly.user.keyMap.createSerializedKey( + Blockly.utils.KeyCodes.K, [Blockly.user.keyMap.modifierKeys.CONTROL]); + map[Blockly.utils.KeyCodes.W] = Blockly.navigation.ACTION_PREVIOUS; map[Blockly.utils.KeyCodes.A] = Blockly.navigation.ACTION_OUT; map[Blockly.utils.KeyCodes.S] = Blockly.navigation.ACTION_NEXT; @@ -141,5 +170,6 @@ Blockly.user.keyMap.createDefaultKeyMap = function() { map[Blockly.utils.KeyCodes.T] = Blockly.navigation.ACTION_TOOLBOX; map[Blockly.utils.KeyCodes.E] = Blockly.navigation.ACTION_EXIT; map[Blockly.utils.KeyCodes.ESC] = Blockly.navigation.ACTION_EXIT; + map[controlK] = Blockly.navigation.ACTION_TOGGLE_KEYBOARD_NAV; return map; }; diff --git a/core/keyboard_nav/navigation.js b/core/keyboard_nav/navigation.js index a236f0f6a..ad85e5593 100644 --- a/core/keyboard_nav/navigation.js +++ b/core/keyboard_nav/navigation.js @@ -105,7 +105,8 @@ Blockly.navigation.actionNames = { MARK: 'mark', DISCONNECT: 'disconnect', TOOLBOX: 'toolbox', - EXIT: 'exit' + EXIT: 'exit', + TOGGLE_KEYBOARD_NAV: 'toggle_keyboard_nav' }; /** * Set the navigation cursor. @@ -153,7 +154,7 @@ Blockly.navigation.removeMark = function() { * block. * @package */ -Blockly.navigation.getTopNode_ = function(block) { +Blockly.navigation.getTopNode = function(block) { var prevConnection = block.previousConnection; var outConnection = block.outputConnection; var topConnection = prevConnection ? prevConnection : outConnection; @@ -390,7 +391,7 @@ Blockly.navigation.insertFromFlyout = function() { } Blockly.navigation.focusWorkspace(); - Blockly.navigation.cursor_.setLocation(Blockly.navigation.getTopNode_(newBlock)); + Blockly.navigation.cursor_.setLocation(Blockly.navigation.getTopNode(newBlock)); Blockly.navigation.removeMark(); }; @@ -724,9 +725,8 @@ Blockly.navigation.disconnectBlocks = function() { /*************************/ /** - * Sets the cursor to the previous or output connection of the selected block - * on the workspace. - * If no block is selected, places the cursor at a fixed point on the workspace. + * Finds where the cursor should go on the workspace. This is either the top + * block or a set position on the workspace. */ Blockly.navigation.focusWorkspace = function() { var cursor = Blockly.navigation.cursor_; @@ -735,11 +735,8 @@ Blockly.navigation.focusWorkspace = function() { Blockly.navigation.resetFlyout(reset); Blockly.navigation.currentState_ = Blockly.navigation.STATE_WS; - if (Blockly.selected) { - cursor.setLocation(Blockly.navigation.getTopNode_(Blockly.selected)); - Blockly.selected.unselect(); - } else if (topBlocks.length > 0) { - cursor.setLocation(Blockly.navigation.getTopNode_(topBlocks[0])); + if (topBlocks.length > 0) { + cursor.setLocation(Blockly.navigation.getTopNode(topBlocks[0])); } else { var ws = cursor.workspace_; // TODO: Find the center of the visible workspace. @@ -860,15 +857,22 @@ Blockly.navigation.onKeyPress = function(e) { var actionHandled = false; if (action) { - if (!readOnly) { - if (curNode && curNode.getType() === Blockly.ASTNode.types.FIELD) { - actionHandled = curNode.getLocation().onBlocklyAction(action); - } - if (!actionHandled) { + if (Blockly.keyboardAccessibilityMode) { + if (!readOnly) { + if (curNode && curNode.getType() === Blockly.ASTNode.types.FIELD) { + actionHandled = curNode.getLocation().onBlocklyAction(action); + } + if (!actionHandled) { + actionHandled = Blockly.navigation.onBlocklyAction(action); + } + // If in readonly mode only handle valid actions. + } else if (Blockly.navigation.READONLY_ACTION_LIST.indexOf(action) > -1) { actionHandled = Blockly.navigation.onBlocklyAction(action); } - } else if (Blockly.navigation.READONLY_ACTION_LIST.indexOf(action) > -1) { - actionHandled = Blockly.navigation.onBlocklyAction(action); + // If not in accessibility mode only hanlde turning on keyboard navigation. + } else if (action.name === Blockly.navigation.actionNames.TOGGLE_KEYBOARD_NAV) { + Blockly.navigation.enableKeyboardAccessibility(); + actionHandled = true; } } return actionHandled; @@ -882,7 +886,10 @@ Blockly.navigation.onKeyPress = function(e) { * @package */ Blockly.navigation.onBlocklyAction = function(action) { - if (Blockly.navigation.currentState_ === Blockly.navigation.STATE_WS) { + if (action.name === Blockly.navigation.actionNames.TOGGLE_KEYBOARD_NAV) { + Blockly.navigation.disableKeyboardAccessibility(); + return true; + } else if (Blockly.navigation.currentState_ === Blockly.navigation.STATE_WS) { return Blockly.navigation.workspaceOnAction_(action); } else if (Blockly.navigation.currentState_ === Blockly.navigation.STATE_FLYOUT) { return Blockly.navigation.flyoutOnAction_(action); @@ -1116,6 +1123,13 @@ Blockly.navigation.ACTION_TOOLBOX = new Blockly.Action( Blockly.navigation.ACTION_EXIT = new Blockly.Action( Blockly.navigation.actionNames.EXIT, 'Close the current modal, such as a toolbox or field editor.'); +/** + * The action to toggle keyboard navigation mode on and off. + * @type {!Blockly.Action} + */ +Blockly.navigation.ACTION_TOGGLE_KEYBOARD_NAV = new Blockly.Action( + Blockly.navigation.actionNames.TOGGLE_KEYBOARD_NAV, 'Turns on and off keyboard navigation.'); + /** * List of actions that can be performed in read only mode. * @type {!Array} diff --git a/tests/mocha/navigation_test.js b/tests/mocha/navigation_test.js index 6d3b61a22..e6b9f29cb 100644 --- a/tests/mocha/navigation_test.js +++ b/tests/mocha/navigation_test.js @@ -370,6 +370,37 @@ suite('Navigation', function() { field.onBlocklyAction.restore(); Blockly.navigation.onBlocklyAction.restore(); }); + + test('Toggle Action Off', function() { + var cursor = new Blockly.Cursor(); + Blockly.navigation.setCursor(cursor); + this.mockEvent.keyCode = 'Control75'; + sinon.spy(Blockly.navigation, 'onBlocklyAction'); + Blockly.keyboardAccessibilityMode = true; + + var isHandled = Blockly.navigation.onKeyPress(this.mockEvent); + chai.assert.isTrue(isHandled); + chai.assert.isTrue(Blockly.navigation.onBlocklyAction.calledOnce); + chai.assert.isFalse(Blockly.keyboardAccessibilityMode); + Blockly.navigation.onBlocklyAction.restore(); + }); + + test('Toggle Action On', function() { + var cursor = new Blockly.Cursor(); + Blockly.navigation.setCursor(cursor); + this.workspace = Blockly.inject('blocklyDiv', {readOnly: false}); + this.mockEvent.keyCode = 'Control75'; + sinon.stub(Blockly.navigation, 'focusWorkspace'); + Blockly.keyboardAccessibilityMode = false; + + var isHandled = Blockly.navigation.onKeyPress(this.mockEvent); + chai.assert.isTrue(isHandled); + chai.assert.isTrue(Blockly.navigation.focusWorkspace.calledOnce); + chai.assert.isTrue(Blockly.keyboardAccessibilityMode); + Blockly.navigation.focusWorkspace.restore(); + this.workspace.dispose(); + }); + suite('Test key press in read only mode', function() { setup(function() { Blockly.defineBlocksWithJsonArray([{ diff --git a/tests/playground.html b/tests/playground.html index 30b23d5b7..a4098efb6 100644 --- a/tests/playground.html +++ b/tests/playground.html @@ -351,6 +351,14 @@ function toggleRenderingDebug(state) { } } +function toggleAccessibilityMode(state) { + if (state) { + Blockly.navigation.enableKeyboardAccessibility(); + } else { + Blockly.navigation.disableKeyboardAccessibility(); + } +} + function logger(e) { console.log(e); } @@ -520,6 +528,12 @@ h1 {

+

+ Enable Accessibility Mode:   + +

+ +