Files
blockly/accessible/tree.service.js
Neil Fraser 5dc6fc4657 Cleanup of style and simplifications. (#378)
* Cleanup of style and simplifications.

* Removing unused addClass function.
2016-05-25 00:02:28 -07:00

326 lines
11 KiB
JavaScript

/**
* 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 that handles all tree keyboard navigation.
* @author madeeha@google.com (Madeeha Ghori)
*/
blocklyApp.TreeService = ng.core
.Class({
constructor: function() {
blocklyApp.debug && console.log('making a new tree service');
// Keeping track of the active descendants in each tree.
this.activeDesc_ = Object.create(null);
this.trees = document.getElementsByClassName('blocklyTree');
// Keeping track of the last key pressed. If the user presses
// enter (to edit a text input or press a button), the keyboard
// focus shifts to that element. In the next keystroke, if the user
// navigates away from the element using the arrow keys, we want
// to shift focus back to the tree as a whole.
this.previousKey_ = null;
},
createId: function(obj) {
if (obj && obj.id) {
return obj.id;
}
return 'blockly-' + Blockly.genUid();
},
setActiveDesc: function(node, id) {
blocklyApp.debug && console.log('setting active descendant for tree ' + id);
this.activeDesc_[id] = node;
},
getActiveDesc: function(id) {
return this.activeDesc_[id] ||
document.getElementById((document.getElementById(id)).getAttribute('aria-activedescendant'));
},
// Makes a given node the active descendant of a given tree.
updateSelectedNode: function(node, tree, keepFocus) {
blocklyApp.debug && console.log('updating node: ' + node.id);
var treeId = tree.id;
var activeDesc = this.getActiveDesc(treeId);
if (activeDesc) {
activeDesc.classList.remove('blocklyActiveDescendant');
activeDesc.setAttribute('aria-selected', 'false');
} else {
blocklyApp.debug && console.log('updateSelectedNode: there is no active descendant');
}
node.classList.add('blocklyActiveDescendant');
tree.setAttribute('aria-activedescendant', node.id);
this.setActiveDesc(node, treeId);
node.setAttribute('aria-selected', 'true');
// Make sure keyboard focus is on tree as a whole
// in case focus was previously on a button or input
// element.
if (keepFocus) {
tree.focus();
}
},
onWorkspaceToolbarKeypress: function(e, treeId) {
blocklyApp.debug && console.log(e.keyCode + 'inside TreeService onWorkspaceToolbarKeypress');
switch (e.keyCode) {
case 9:
// 16,9: shift, tab
if (e.shiftKey) {
blocklyApp.debug && console.log('shifttabbing');
// If the previous key is shift, we're shift-tabbing mode.
this.goToPreviousTree(treeId);
} else {
// If previous key isn't shift, we're tabbing.
this.goToNextTree(treeId);
}
e.preventDefault();
e.stopPropagation();
break;
}
},
goToNextTree: function(treeId, e) {
for (var i = 0; i < this.trees.length; i++) {
if (this.trees[i].id == treeId) {
if (i + 1 < this.trees.length) {
this.trees[i + 1].focus();
}
break;
}
}
},
goToPreviousTree: function(treeId, e) {
if (treeId == this.trees[0].id) {
return;
}
for (var i = (this.trees.length - 1); i >= 0; i--) {
if (this.trees[i].id == treeId) {
if (i - 1 < this.trees.length) {
this.trees[i - 1].focus();
}
break;
}
}
},
onKeypress: function(e, tree) {
var treeId = tree.id;
var node = this.getActiveDesc(treeId);
var keepFocus = this.previousKey_ == 13;
if (!node) {
blocklyApp.debug && console.log('KeyHandler: no active descendant');
}
blocklyApp.debug && console.log(e.keyCode + ': inside TreeService');
switch (e.keyCode) {
case 9:
// 16,9: shift, tab
if (e.shiftKey) {
blocklyApp.debug && console.log('shifttabbing');
// If the previous key is shift, we're shift-tabbing.
this.goToPreviousTree(treeId);
} else {
// If previous key isn't shift, we're tabbing
// we want to go to the run code button.
this.goToNextTree(treeId);
}
// Setting the previous key variable in each case because
// we only want to save the previous navigation keystroke,
// not any typing.
this.previousKey_ = e.keyCode;
e.preventDefault();
e.stopPropagation();
break;
case 37:
// Left-facing arrow: go out a level, if possible. If not, do nothing.
blocklyApp.debug && console.log('in left arrow section');
var nextNode = node.parentNode;
if (node.tagName == 'BUTTON' || node.tagName == 'INPUT') {
nextNode = nextNode.parentNode;
}
while (nextNode && nextNode.className != 'treeview' &&
nextNode.tagName != 'LI') {
nextNode = nextNode.parentNode;
}
if (!nextNode || nextNode.className == 'treeview') {
return;
}
this.updateSelectedNode(nextNode, tree, keepFocus);
this.previousKey_ = e.keyCode;
e.preventDefault();
e.stopPropagation();
break;
case 38:
// Up-facing arrow: go up a level, if possible. If not, do nothing.
blocklyApp.debug && console.log('node passed in: ' + node.id);
var prevSibling = this.getPreviousSibling(node);
if (prevSibling && prevSibling.tagName != 'H1') {
this.updateSelectedNode(prevSibling, tree, keepFocus);
} else {
blocklyApp.debug && console.log('no previous sibling');
}
this.previousKey_ = e.keyCode;
e.preventDefault();
e.stopPropagation();
break;
case 39:
blocklyApp.debug && console.log('in right arrow section');
var firstChild = this.getFirstChild(node);
if (firstChild) {
this.updateSelectedNode(firstChild, tree, keepFocus);
} else {
blocklyApp.debug && console.log('no valid child');
}
this.previousKey_ = e.keyCode;
e.preventDefault();
e.stopPropagation();
break;
case 40:
// Down-facing arrow: go down a level, if possible.
// If not, do nothing.
blocklyApp.debug && console.log('preventing propogation');
var nextSibling = this.getNextSibling(node);
if (nextSibling) {
this.updateSelectedNode(nextSibling, tree, keepFocus);
} else {
blocklyApp.debug && console.log('no next sibling');
}
this.previousKey_ = e.keyCode;
e.preventDefault();
e.stopPropagation();
break;
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);
if (activeDesc) {
var children = activeDesc.children;
var child = children[0];
if (children.length == 1 && (child.tagName == 'INPUT' ||
child.tagName == 'BUTTON')) {
if (child.tagName == 'BUTTON') {
child.click();
}
else if (child.tagName == 'INPUT') {
child.focus();
}
}
} else {
blocklyApp.debug && console.log('no activeDesc');
}
this.previousKey_ = e.keyCode;
break;
}
},
getFirstChild: function(element) {
if (!element) {
return element;
} else {
var childList = element.children;
for (var i = 0; i < childList.length; i++) {
if (childList[i].tagName == 'LI') {
return childList[i];
} else {
var potentialElement = this.getFirstChild(childList[i]);
if (potentialElement) {
return potentialElement;
}
}
}
return null;
}
},
getNextSibling: function(element) {
if (element.nextElementSibling) {
// If there is a sibling, find the list element child of the sibling.
var node = element.nextElementSibling;
if (node.tagName != 'LI') {
var listElems = node.getElementsByTagName('li');
// getElementsByTagName returns in DFS order
// therefore the first element is the first relevant list child.
return listElems[0];
} else {
return element.nextElementSibling;
}
} else {
var parent = element.parentNode;
while (parent && parent.tagName != 'OL') {
if (parent.nextElementSibling) {
var node = parent.nextElementSibling;
if (node.tagName == 'LI') {
return node;
} else {
return this.getFirstChild(node);
}
} else {
parent = parent.parentNode;
}
}
return null;
}
},
getPreviousSibling: function(element) {
if (element.previousElementSibling) {
var sibling = element.previousElementSibling;
if (sibling.tagName == 'LI') {
return sibling;
} else {
return this.getLastChild(sibling);
}
} else {
var parent = element.parentNode;
while (parent) {
blocklyApp.debug && console.log('looping');
if (parent.tagName == 'OL') {
break;
}
if (parent.previousElementSibling) {
blocklyApp.debug && console.log('parent has a sibling!');
var node = parent.previousElementSibling;
if (node.tagName == 'LI') {
blocklyApp.debug && console.log('return the sibling of the parent!');
return node;
} else {
// Find the last list element child of the sibling of the parent.
return this.getLastChild(node);
}
} else {
parent = parent.parentNode;
}
}
return null;
}
},
getLastChild: function(element) {
if (!element) {
blocklyApp.debug && console.log('no element');
return element;
} else {
var childList = element.children;
for (var i = childList.length - 1; i >= 0; i--) {
// Find the last child that is a list element.
if (childList[i].tagName == 'LI') {
return childList[i];
} else {
var potentialElement = this.getLastChild(childList[i]);
if (potentialElement) {
return potentialElement;
}
}
}
blocklyApp.debug && console.log('no last child');
return null;
}
}
});