mirror of
https://github.com/google/blockly.git
synced 2026-01-08 09:30:06 +01:00
Refactor how activeDescendant is set. Introduce helper functions to ensure that calls like pasteAbove() preserve the focus.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user