diff --git a/accessible/app.component.js b/accessible/app.component.js index 3a79c328c..9d8e7e494 100644 --- a/accessible/app.component.js +++ b/accessible/app.component.js @@ -33,6 +33,7 @@ blocklyApp.AppView = ng.core.Component({ +
@@ -47,7 +48,8 @@ blocklyApp.AppView = ng.core.Component({ `, directives: [ blocklyApp.ToolboxComponent, blocklyApp.WorkspaceComponent, - blocklyApp.BlockOptionsModalComponent, blocklyApp.SidebarComponent], + blocklyApp.BlockOptionsModalComponent, blocklyApp.SidebarComponent, + blocklyApp.ToolboxModalComponent], pipes: [blocklyApp.TranslatePipe], // All services are declared here, so that all components in the // application use the same instance of the service. @@ -56,7 +58,7 @@ blocklyApp.AppView = ng.core.Component({ blocklyApp.ClipboardService, blocklyApp.NotificationsService, blocklyApp.TreeService, blocklyApp.UtilsService, blocklyApp.AudioService, blocklyApp.BlockOptionsModalService, - blocklyApp.KeyboardInputService] + blocklyApp.KeyboardInputService, blocklyApp.ToolboxModalService] }) .Class({ constructor: [ diff --git a/accessible/block-options-modal.component.js b/accessible/block-options-modal.component.js index 7bbeb946d..0aeb69969 100644 --- a/accessible/block-options-modal.component.js +++ b/accessible/block-options-modal.component.js @@ -51,36 +51,7 @@ blocklyApp.BlockOptionsModalComponent = ng.core.Component({
`, - pipes: [blocklyApp.TranslatePipe], - styles: [ - `.blocklyModalCurtain { - background-color: rgba(0,0,0,0.4); - height: 100%; - left: 0; - overflow: auto; - position: fixed; - top: 0; - width: 100%; - z-index: 1; - } - `, ` - .blocklyModal { - background-color: #fefefe; - border: 1px solid #888; - margin: 15% auto; - max-width: 600px; - padding: 20px; - width: 60%; - } - `, ` - .blocklyModalButtonContainer { - margin: 10px 0; - } - `, ` - .blocklyModal .activeButton { - border: 1px solid blue; - } - `] + pipes: [blocklyApp.TranslatePipe] }) .Class({ constructor: [ diff --git a/accessible/block-options-modal.service.js b/accessible/block-options-modal.service.js index b064766a8..7411acb96 100644 --- a/accessible/block-options-modal.service.js +++ b/accessible/block-options-modal.service.js @@ -26,7 +26,11 @@ blocklyApp.BlockOptionsModalService = ng.core.Class({ constructor: [function() { this.actionButtonsInfo = []; - this.preShowHook = null; + this.preShowHook = function() { + throw Error( + 'A pre-show hook must be defined for the block options modal ' + + 'before it can be shown.'); + }; this.modalIsShown = false; this.onHideCallback = null; }], diff --git a/accessible/media/accessible.css b/accessible/media/accessible.css index 038668126..77918fa90 100644 --- a/accessible/media/accessible.css +++ b/accessible/media/accessible.css @@ -10,7 +10,7 @@ float: left; margin-left: 10px; margin-top: 20px; - width: 150px; + width: 200px; } .blocklyAriaLiveStatus { @@ -35,3 +35,28 @@ .blocklyDropdownListItem[aria-selected="true"] button { font-weight: bold; } + +.blocklyModalCurtain { + background-color: rgba(0,0,0,0.4); + height: 100%; + left: 0; + overflow: auto; + position: fixed; + top: 0; + width: 100%; + z-index: 1; +} +.blocklyModal { + background-color: #fefefe; + border: 1px solid #888; + margin: 15% auto; + max-width: 600px; + padding: 20px; + width: 60%; +} +.blocklyModalButtonContainer { + margin: 10px 0; +} +.blocklyModal .activeButton { + border: 1px solid blue; +} diff --git a/accessible/sidebar.component.js b/accessible/sidebar.component.js index 2c259ea70..9d70afbbe 100644 --- a/accessible/sidebar.component.js +++ b/accessible/sidebar.component.js @@ -37,6 +37,10 @@ blocklyApp.SidebarComponent = ng.core.Component({ {{buttonConfig.text}} + + + +
+
+ +
+ + + + `, + pipes: [blocklyApp.TranslatePipe] +}) +.Class({ + constructor: [ + blocklyApp.ToolboxModalService, blocklyApp.KeyboardInputService, + blocklyApp.AudioService, blocklyApp.UtilsService, blocklyApp.TreeService, + blocklyApp.NotificationsService, + function( + toolboxModalService_, keyboardInputService_, + audioService_, utilsService_, treeService_, notificationsService_) { + this.toolboxModalService = toolboxModalService_; + this.keyboardInputService = keyboardInputService_; + this.audioService = audioService_; + this.utilsService = utilsService_; + this.treeService = treeService_; + this.notificationsService = notificationsService_; + + this.modalIsVisible = false; + this.toolboxCategories = []; + this.firstBlockIndexes = []; + this.activeButtonIndex = 0; + this.totalNumBlocks = null; + + var that = this; + this.toolboxModalService.registerPreShowHook( + function(toolboxCategories) { + that.modalIsVisible = true; + that.toolboxCategories = toolboxCategories; + + var cumulativeIndex = 0; + that.toolboxCategories.forEach(function(category) { + that.firstBlockIndexes.push(cumulativeIndex); + cumulativeIndex += category.blocks.length; + }); + that.firstBlockIndexes.push(cumulativeIndex); + that.totalNumBlocks = cumulativeIndex; + + that.activeButtonIndex = 0; + that.keyboardInputService.setOverride({ + // Tab key: no-op. + '9': function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + }, + // Enter key: selects an action, performs it, and closes the + // modal. + '13': function(evt) { + var button = document.getElementById( + that.getOptionId(that.activeButtonIndex)); + + for (var i = 0; i < that.toolboxCategories.length; i++) { + if (that.firstBlockIndexes[i + 1] > that.activeButtonIndex) { + var categoryIndex = i; + var blockIndex = + that.activeButtonIndex - that.firstBlockIndexes[i]; + + that.selectBlock(categoryIndex, blockIndex); + break; + } + } + + that.hideModal(); + }, + // Escape key: closes the modal. + '27': function() { + that.hideModal(); + }, + // Up key: navigates to the previous item in the list. + '38': function(evt) { + evt.preventDefault(); + if (that.activeButtonIndex == 0) { + that.audioService.playOopsSound(); + } else { + that.activeButtonIndex--; + } + that.focusOnOptionIfPossible(that.activeButtonIndex); + }, + // Down key: navigates to the next item in the list. + '40': function(evt) { + evt.preventDefault(); + if (that.activeButtonIndex == that.totalNumBlocks) { + that.audioService.playOopsSound(); + } else { + that.activeButtonIndex++; + } + that.focusOnOptionIfPossible(that.activeButtonIndex); + } + }); + + setTimeout(function() { + document.getElementById('toolboxModal').focus(); + }, 150); + } + ); + } + ], + getOverallIndex: function(categoryIndex, blockIndex) { + return this.firstBlockIndexes[categoryIndex] + blockIndex; + }, + selectBlock: function(categoryIndex, blockIndex) { + var block = this.toolboxCategories[categoryIndex].blocks[blockIndex]; + var blockDescription = this.getBlockDescription(block); + var xml = Blockly.Xml.blockToDom(block); + var newBlockId = Blockly.Xml.domToBlock(blocklyApp.workspace, xml).id; + + var that = this; + setTimeout(function() { + that.treeService.focusOnBlock(newBlockId); + that.notificationsService.setStatusMessage( + blockDescription + ' added to workspace. ' + + 'Now on added block in workspace.'); + }); + }, + getBlockDescription: function(block) { + return this.utilsService.getBlockDescription(block); + }, + // Focuses on the button represented by the given index. + focusOnOptionIfPossible: function(index) { + var button = document.getElementById(this.getOptionId(index)); + if (!button.disabled) { + button.focus(); + } else { + document.activeElement.blur(); + } + }, + // Returns the ID for the corresponding option button. + getOptionId: function(index) { + return 'toolbox-modal-option-' + index; + }, + // Returns the ID for the "cancel" option button. + getCancelOptionId: function() { + return 'toolbox-modal-option-' + this.totalNumBlocks; + }, + // Closes the modal. + hideModal: function() { + this.modalIsVisible = false; + this.keyboardInputService.clearOverride(); + this.toolboxModalService.hideModal(); + } +}); diff --git a/accessible/toolbox-modal.service.js b/accessible/toolbox-modal.service.js new file mode 100644 index 000000000..7bf2c0bed --- /dev/null +++ b/accessible/toolbox-modal.service.js @@ -0,0 +1,84 @@ +/** + * AccessibleBlockly + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * 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. + */ + +/** + * @fileoverview Angular2 Service for the toolbox modal. + * + * @author sll@google.com (Sean Lip) + */ + +blocklyApp.ToolboxModalService = ng.core.Class({ + constructor: [function() { + this.modalIsShown = false; + this.onHideCallback = null; + this.preShowHook = function() { + throw Error( + 'A pre-show hook must be defined for the toolbox modal before it ' + + 'can be shown.'); + }; + this.toolboxCategories = []; + + // Populate the toolbox categories. + var toolboxXmlElt = document.getElementById('blockly-toolbox-xml'); + var toolboxCategoryElts = toolboxXmlElt.getElementsByTagName('category'); + if (toolboxCategoryElts.length) { + this.toolboxCategories = Array.from(toolboxCategoryElts).map( + function(categoryElt) { + var workspace = new Blockly.Workspace(); + Blockly.Xml.domToWorkspace(categoryElt, workspace); + return { + categoryName: categoryElt.attributes.name.value, + blocks: workspace.topBlocks_ + }; + } + ); + } else { + // If there are no top-level categories, we create a single category + // containing all the top-level blocks. + var workspace = new Blockly.Workspace(); + Array.from(xmlToolboxElt.children).forEach(function(topLevelNode) { + Blockly.Xml.domToBlock(workspace, topLevelNode); + }); + + this.toolboxCategories = [{ + categoryName: 'Available Blocks', + blocks: workspace.topBlocks_ + }]; + } + }], + registerPreShowHook: function(preShowHook) { + this.preShowHook = function() { + preShowHook(this.toolboxCategories); + }; + }, + isModalShown: function() { + return this.modalIsShown; + }, + showModal: function(onHideCallback) { + this.onHideCallback = onHideCallback; + this.preShowHook(); + this.modalIsShown = true; + }, + hideModal: function() { + this.modalIsShown = false; + if (this.onHideCallback) { + this.onHideCallback(); + } + } +}); diff --git a/demos/accessible/index.html b/demos/accessible/index.html index 1b6e7f9d3..ae0d20cd5 100644 --- a/demos/accessible/index.html +++ b/demos/accessible/index.html @@ -21,6 +21,7 @@ + @@ -29,6 +30,7 @@ +