From 3ca593273add30aa19d0aabc829cff406d52d01d Mon Sep 17 00:00:00 2001 From: Sean Lip Date: Fri, 17 Jun 2016 17:42:51 -0700 Subject: [PATCH] Refactor how activeDescendant is set. Introduce helper functions to ensure that calls like pasteAbove() preserve the focus. --- accessible/toolbox-tree.component.js | 15 +----- accessible/toolbox.component.js | 31 ++++++------ accessible/tree.service.js | 65 ++++++++++++++++---------- accessible/workspace-tree.component.js | 63 +++++++++++++++++-------- accessible/workspace.component.js | 6 ++- 5 files changed, 106 insertions(+), 74 deletions(-) diff --git a/accessible/toolbox-tree.component.js b/accessible/toolbox-tree.component.js index 364cf586e..cc701fc35 100644 --- a/accessible/toolbox-tree.component.js +++ b/accessible/toolbox-tree.component.js @@ -103,7 +103,7 @@ blocklyApp.ToolboxTreeComponent = ng.core return blocklyApp.ToolboxTreeComponent; })], inputs: [ - 'block', 'displayBlockMenu', 'level', 'index', 'tree', 'noCategories'], + 'block', 'displayBlockMenu', 'level', 'index', 'tree', 'noCategories', 'isTopLevel'], pipes: [blocklyApp.TranslatePipe] }) .Class({ @@ -128,23 +128,12 @@ blocklyApp.ToolboxTreeComponent = ng.core elementsNeedingIds.push('listItem' + i, 'listItem' + i + 'Label') } this.idMap = this.utilsService.generateIds(elementsNeedingIds); - if (this.index == 0 && this.noCategories) { + if (this.isTopLevel) { this.idMap['parentList'] = 'blockly-toolbox-tree-node0'; } else { this.idMap['parentList'] = this.utilsService.generateUniqueId(); } }, - ngAfterViewInit: function() { - // If this is a top-level tree in the toolbox, set its active - // descendant after the ids have been computed. - if (this.index == 0 && - this.tree.getAttribute('aria-activedescendant') == - 'blockly-toolbox-tree-node0') { - this.treeService.setActiveDesc( - document.getElementById(this.idMap['parentList']), - this.tree); - } - }, generateAriaLabelledByAttr: function(mainLabel, secondLabel, isDisabled) { return this.utilsService.generateAriaLabelledByAttr( mainLabel, secondLabel, isDisabled); diff --git a/accessible/toolbox.component.js b/accessible/toolbox.component.js index 5240812f4..589a73712 100644 --- a/accessible/toolbox.component.js +++ b/accessible/toolbox.component.js @@ -39,17 +39,17 @@ blocklyApp.ToolboxComponent = ng.core [id]="idMap['Parent' + i]" role="treeitem" [ngClass]="{blocklyHasChildren: true, blocklyActiveDescendant: tree.getAttribute('aria-activedescendant') == idMap['Parent' + i]}" *ngFor="#category of toolboxCategories; #i=index" - aria-level="1" aria-selected=false> + aria-level="1" aria-selected=false + [attr.aria-label]="category.attributes.name.value">
- {{labelCategory(name, i, tree)}}
    + [tree]="tree">
@@ -59,9 +59,9 @@ blocklyApp.ToolboxComponent = ng.core + [noCategories]="true" + [isTopLevel]="true"> @@ -94,23 +94,22 @@ blocklyApp.ToolboxComponent = ng.core elementsNeedingIds.push('Parent' + i, 'Label' + i); } this.idMap = this.utilsService.generateIds(elementsNeedingIds); - this.idMap['Parent0'] = 'blockly-toolbox-tree-node0'; + for (var i = 0; i < this.toolboxCategories.length; i++) { + this.idMap['Parent' + i] = 'blockly-toolbox-tree-node' + i; + } } else { // Create a single category is created with all the top-level blocks. this.xmlHasCategories = false; this.toolboxCategories = [Array.from(xmlToolboxElt.children)]; } }, - labelCategory: function(label, i, tree) { - var parent = label.parentNode; - while (parent && parent.tagName != 'LI') { - parent = parent.parentNode; - } - parent.setAttribute('aria-label', label.innerText); - parent.id = 'blockly-toolbox-tree-node' + i; - if (i == 0 && tree.getAttribute('aria-activedescendant') == - 'blockly-toolbox-tree-node0') { - this.treeService.setActiveDesc(parent, tree); + ngAfterViewInit: function() { + // If this is a top-level tree in the toolbox, set its active + // descendant after the ids have been computed. + if (this.xmlHasCategories) { + this.treeService.setActiveDesc( + document.getElementById('blockly-toolbox-tree-node0'), + document.getElementById('blockly-toolbox-tree')); } }, getToolboxWorkspace: function(categoryNode) { diff --git a/accessible/tree.service.js b/accessible/tree.service.js index a7be7aa9e..c7ad06b55 100644 --- a/accessible/tree.service.js +++ b/accessible/tree.service.js @@ -33,6 +33,8 @@ blocklyApp.TreeService = ng.core // navigates away from the element using the arrow keys, we want // to shift focus back to the tree as a whole. this.previousKey_ = null; + // Stores active descendant ids for each tree in the page. + this.activeDescendantIds_ = {}; }, getToolboxTreeNode_: function() { return document.getElementById('blockly-toolbox-tree'); @@ -94,30 +96,43 @@ blocklyApp.TreeService = ng.core } return false; }, - // Make a given node the active descendant of a given tree. - setActiveDesc: function(node, tree, keepFocus) { - blocklyApp.debug && console.log('setting activeDesc for tree ' + tree.id); - - var activeDesc = this.getActiveDesc(tree.id); + getActiveDescId: function(treeId) { + return this.activeDescendantIds_[treeId] || ''; + }, + unmarkActiveDesc_: function(activeDescId) { + var activeDesc = document.getElementById(activeDescId); if (activeDesc) { activeDesc.classList.remove('blocklyActiveDescendant'); activeDesc.setAttribute('aria-selected', 'false'); } - - node.classList.add('blocklyActiveDescendant'); - node.setAttribute('aria-selected', 'true'); - tree.setAttribute('aria-activedescendant', node.id); - - // Make sure keyboard focus is on the entire tree in the case where the - // focus was previously on a button or input element. - if (keepFocus) { - tree.focus(); - } }, - getActiveDesc: function(treeId) { - var activeDescendantId = document.getElementById( - treeId).getAttribute('aria-activedescendant'); - return document.getElementById(activeDescendantId); + markActiveDesc_: function(activeDescId) { + var newActiveDesc = document.getElementById(activeDescId); + newActiveDesc.classList.add('blocklyActiveDescendant'); + newActiveDesc.setAttribute('aria-selected', 'true'); + }, + // Runs the given function while preserving the focus and active descendant + // for the given tree. + runWhilePreservingFocus: function(func, treeId) { + var activeDescId = this.getActiveDescId(treeId); + this.unmarkActiveDesc_(activeDescId); + func(); + + // The timeout is needed in order to give the DOM time to stabilize + // before setting the new active descendant, especially in cases like + // pasteAbove(). + var that = this; + setTimeout(function() { + that.markActiveDesc_(activeDescId); + that.activeDescendantIds_[treeId] = activeDescId; + document.getElementById(treeId).focus(); + }, 0); + }, + // Make a given node the active descendant of a given tree. + setActiveDesc: function(newActiveDesc, tree) { + this.unmarkActiveDesc_(this.getActiveDescId(tree.id)); + this.markActiveDesc_(newActiveDesc.id); + this.activeDescendantIds_[tree.id] = newActiveDesc.id; }, onWorkspaceToolbarKeypress: function(e, treeId) { blocklyApp.debug && console.log( @@ -140,7 +155,7 @@ blocklyApp.TreeService = ng.core }, onKeypress: function(e, tree) { var treeId = tree.id; - var node = this.getActiveDesc(treeId); + var node = document.getElementById(this.getActiveDescId(treeId)); var keepFocus = this.previousKey_ == 13; if (!node) { blocklyApp.debug && console.log('KeyHandler: no active descendant'); @@ -179,7 +194,7 @@ blocklyApp.TreeService = ng.core if (!nextNode || nextNode.className == 'treeview') { return; } - this.setActiveDesc(nextNode, tree, keepFocus); + this.setActiveDesc(nextNode, tree); this.previousKey_ = e.keyCode; e.preventDefault(); e.stopPropagation(); @@ -189,7 +204,7 @@ blocklyApp.TreeService = ng.core blocklyApp.debug && console.log('node passed in: ' + node.id); var prevSibling = this.getPreviousSibling(node); if (prevSibling && prevSibling.tagName != 'H1') { - this.setActiveDesc(prevSibling, tree, keepFocus); + this.setActiveDesc(prevSibling, tree); } else { blocklyApp.debug && console.log('no previous sibling'); } @@ -201,7 +216,7 @@ blocklyApp.TreeService = ng.core blocklyApp.debug && console.log('in right arrow section'); var firstChild = this.getFirstChild(node); if (firstChild) { - this.setActiveDesc(firstChild, tree, keepFocus); + this.setActiveDesc(firstChild, tree); } else { blocklyApp.debug && console.log('no valid child'); } @@ -215,7 +230,7 @@ blocklyApp.TreeService = ng.core blocklyApp.debug && console.log('preventing propogation'); var nextSibling = this.getNextSibling(node); if (nextSibling) { - this.setActiveDesc(nextSibling, tree, keepFocus); + this.setActiveDesc(nextSibling, tree); } else { blocklyApp.debug && console.log('no next sibling'); } @@ -226,7 +241,7 @@ blocklyApp.TreeService = ng.core case 13: // If I've pressed enter, I want to interact with a child. blocklyApp.debug && console.log('enter is pressed'); - var activeDesc = this.getActiveDesc(treeId); + var activeDesc = node; if (activeDesc) { var children = activeDesc.children; var child = children[0]; diff --git a/accessible/workspace-tree.component.js b/accessible/workspace-tree.component.js index 36a98e872..2fe99002c 100644 --- a/accessible/workspace-tree.component.js +++ b/accessible/workspace-tree.component.js @@ -51,7 +51,7 @@ blocklyApp.WorkspaceTreeComponent = ng.core
  • - @@ -59,7 +59,7 @@ blocklyApp.WorkspaceTreeComponent = ng.core
  • - @@ -94,7 +94,8 @@ blocklyApp.WorkspaceTreeComponent = ng.core
    + [block]="inputBlock.connection.targetBlock()" [level]="level" + [tree]="tree">
  • + [level]="level" [tree]="tree"> `, directives: [blocklyApp.FieldComponent, ng.core.forwardRef(function() { return blocklyApp.WorkspaceTreeComponent; })], - // The 'tree' input is only passed down at the top level. - inputs: ['block', 'level', 'tree'], + inputs: ['block', 'level', 'tree', 'isTopLevel'], pipes: [blocklyApp.TranslatePipe] }) .Class({ @@ -142,13 +142,14 @@ blocklyApp.WorkspaceTreeComponent = ng.core this.treeService = _treeService; this.utilsService = _utilsService; }], - ngOnInit: function() { + getElementsNeedingIds_: function() { var elementsNeedingIds = ['blockSummary', 'listItem', 'label', 'cutListItem', 'cutButton', 'copyListItem', 'copyButton', 'pasteBelow', 'pasteBelowButton', 'pasteAbove', 'pasteAboveButton', 'markBelow', 'markBelowButton', 'markAbove', 'markAboveButton', 'sendToSelectedListItem', 'sendToSelectedButton', 'delete', 'deleteButton']; + for (var i = 0; i < this.block.inputList.length; i++) { var inputBlock = this.block.inputList[i]; if (inputBlock.connection && !inputBlock.connection.targetBlock()) { @@ -157,21 +158,39 @@ blocklyApp.WorkspaceTreeComponent = ng.core 'markSpotButton' + i, 'paste' + i, 'pasteButton' + i]); } } - this.idMap = this.utilsService.generateIds(elementsNeedingIds); + + return elementsNeedingIds; + }, + ngOnInit: function() { + var elementsNeedingIds = this.getElementsNeedingIds_(); + + this.idMap = {} this.idMap['parentList'] = this.utilsService.generateUniqueId(); + for (var i = 0; i < elementsNeedingIds.length; i++) { + this.idMap[elementsNeedingIds[i]] = + this.block.id + elementsNeedingIds[i]; + } }, ngAfterViewInit: function() { - // If this is a top-level tree in the workspace, set its active + // If this is a top-level tree in the workspace, set its id and active // descendant. - if (this.tree && - (!this.tree.id || - this.treeService.isTopLevelWorkspaceTree(this.tree.id))) { + if (this.tree && this.isTopLevel && !this.tree.id) { this.tree.id = this.utilsService.generateUniqueId(); + } + + if (this.tree && this.isTopLevel && + !this.treeService.getActiveDescId(this.tree.id)) { this.treeService.setActiveDesc( document.getElementById(this.idMap['parentList']), this.tree); } }, + hasPreviousConnection: function(block) { + return Boolean(block.previousConnection); + }, + hasNextConnection: function(block) { + return Boolean(block.nextConnection); + }, isCompatibleWithClipboard: function(connection) { return this.clipboardService.isClipboardCompatibleWithConnection( connection); @@ -181,6 +200,18 @@ blocklyApp.WorkspaceTreeComponent = ng.core return topBlock.id == block.id; }); }, + pasteAbove: function(block) { + var that = this; + this.treeService.runWhilePreservingFocus(function() { + that.clipboardService.pasteFromClipboard(block.previousConnection); + }, this.tree.id); + }, + pasteBelow: function(block) { + var that = this; + this.treeService.runWhilePreservingFocus(function() { + that.clipboardService.pasteFromClipboard(block.nextConnection); + }, this.tree.id); + }, cutToClipboard: function(block) { if (this.isTopLevelBlock(block)) { nextNodeToFocusOn = this.treeService.getNodeToFocusOnWhenTreeIsDeleted( @@ -192,7 +223,7 @@ blocklyApp.WorkspaceTreeComponent = ng.core this.clipboardService.cut(block); } }, - deleteBlock: function(block, cutToClipboard) { + deleteBlock: function(block) { if (this.isTopLevelBlock(block)) { nextNodeToFocusOn = this.treeService.getNodeToFocusOnWhenTreeIsDeleted( this.tree.id); @@ -207,12 +238,6 @@ blocklyApp.WorkspaceTreeComponent = ng.core return this.utilsService.generateAriaLabelledByAttr( mainLabel, secondLabel, isDisabled); }, - hasPreviousConnection: function(block) { - return Boolean(block.previousConnection); - }, - hasNextConnection: function(block) { - return Boolean(block.nextConnection); - }, sendToMarkedSpot: function(block) { this.clipboardService.pasteToMarkedConnection(block, false); diff --git a/accessible/workspace.component.js b/accessible/workspace.component.js index 81b3de5cc..38e50fae5 100644 --- a/accessible/workspace.component.js +++ b/accessible/workspace.component.js @@ -48,9 +48,10 @@ blocklyApp.WorkspaceComponent = ng.core
      - +
    @@ -74,6 +75,9 @@ blocklyApp.WorkspaceComponent = ng.core clearWorkspace: function() { this.workspace.clear(); }, + getActiveDescId: function(tree) { + return this.treeService.getActiveDescId(tree.id); + }, onWorkspaceToolbarKeypress: function(e) { this.treeService.onWorkspaceToolbarKeypress( e, document.activeElement.id);