Refactor how activeDescendant is set. Introduce helper functions to ensure that calls like pasteAbove() preserve the focus.

This commit is contained in:
Sean Lip
2016-06-17 17:42:51 -07:00
parent 41f6f50b77
commit 3ca593273a
5 changed files with 106 additions and 74 deletions

View File

@@ -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);

View File

@@ -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">
<div *ngIf="category && category.attributes">
<label [id]="idMap['Label' + i]" #name>
{{category.attributes.name.value}}
</label>
{{labelCategory(name, i, tree)}}
<ol role="group" *ngIf="getToolboxWorkspace(category).topBlocks_.length > 0">
<blockly-toolbox-tree *ngFor="#block of getToolboxWorkspace(category).topBlocks_"
[level]=2 [block]="block"
[displayBlockMenu]="true"
[clipboardService]="clipboardService">
[tree]="tree">
</blockly-toolbox-tree>
</ol>
</div>
@@ -59,9 +59,9 @@ blocklyApp.ToolboxComponent = ng.core
<blockly-toolbox-tree *ngFor="#block of getToolboxWorkspace(toolboxCategories[0]).topBlocks_; #i=index"
[level]=1 [block]="block"
[displayBlockMenu]="true"
[clipboardService]="clipboardService"
[index]="i" [tree]="tree"
[noCategories]="true">
[noCategories]="true"
[isTopLevel]="true">
</blockly-toolbox-tree>
</div>
</ol>
@@ -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) {

View File

@@ -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];

View File

@@ -51,7 +51,7 @@ blocklyApp.WorkspaceTreeComponent = ng.core
<li [id]="idMap['pasteBelow']" role="treeitem"
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['pasteBelowButton'], 'blockly-button', !hasNextConnection(block) || !isCompatibleWithClipboard(block.nextConnection))"
[attr.aria-level]="level+2" aria-selected=false>
<button [id]="idMap['pasteBelowButton']" (click)="clipboardService.pasteFromClipboard(block.nextConnection)"
<button [id]="idMap['pasteBelowButton']" (click)="pasteBelow(block)"
[disabled]="!hasNextConnection(block) || !isCompatibleWithClipboard(block.nextConnection)">
{{'PASTE_BELOW'|translate}}
</button>
@@ -59,7 +59,7 @@ blocklyApp.WorkspaceTreeComponent = ng.core
<li [id]="idMap['pasteAbove']" role="treeitem"
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['pasteAboveButton'], 'blockly-button', !hasPreviousConnection(block) || !isCompatibleWithClipboard(block.previousConnection))"
[attr.aria-level]="level+2" aria-selected=false>
<button [id]="idMap['pasteAboveButton']" (click)="clipboardService.pasteFromClipboard(block.previousConnection)"
<button [id]="idMap['pasteAboveButton']" (click)="pasteAbove(block)"
[disabled]="!hasPreviousConnection(block) || !isCompatibleWithClipboard(block.previousConnection)">
{{'PASTE_ABOVE'|translate}}
</button>
@@ -94,7 +94,8 @@ blocklyApp.WorkspaceTreeComponent = ng.core
<div *ngFor="#inputBlock of block.inputList; #i = index">
<blockly-field *ngFor="#field of inputBlock.fieldRow" [field]="field"></blockly-field>
<blockly-workspace-tree *ngIf="inputBlock.connection && inputBlock.connection.targetBlock()"
[block]="inputBlock.connection.targetBlock()" [level]="level">
[block]="inputBlock.connection.targetBlock()" [level]="level"
[tree]="tree">
</blockly-workspace-tree>
<li #inputList [attr.aria-level]="level + 1" [id]="idMap['inputList' + i]"
[attr.aria-labelledBy]="generateAriaLabelledByAttr('blockly-menu', idMap['inputMenuLabel' + i])"
@@ -123,14 +124,13 @@ blocklyApp.WorkspaceTreeComponent = ng.core
<blockly-workspace-tree *ngIf= "block.nextConnection && block.nextConnection.targetBlock()"
[block]="block.nextConnection.targetBlock()"
[level]="level">
[level]="level" [tree]="tree">
</blockly-workspace-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);

View File

@@ -48,9 +48,10 @@ blocklyApp.WorkspaceComponent = ng.core
<div *ngIf="workspace">
<ol #tree *ngFor="#block of workspace.topBlocks_; #i = index"
tabIndex="0" role="group" class="blocklyTree blocklyWorkspaceTree"
[attr.aria-activedescendant]="getActiveDescId(tree.id)"
[attr.aria-labelledby]="workspaceTitle.id"
(keydown)="onKeypress($event, tree)">
<blockly-workspace-tree [level]=1 [block]="block" [tree]="tree">
<blockly-workspace-tree [level]=1 [block]="block" [tree]="tree" [isTopLevel]="true">
</blockly-workspace-tree>
</ol>
</div>
@@ -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);