Adding the common modal class. (#1017)

Centralizes accessible modal behavior.
This commit is contained in:
CoryDCode
2017-04-05 16:06:45 -07:00
committed by GitHub
parent 4b44635867
commit 33355415df
9 changed files with 130 additions and 303 deletions

View File

@@ -36,11 +36,11 @@ blocklyApp.AppComponent = ng.core.Component({
<span aria-live="polite" role="status">{{getAriaLiveReadout()}}</span> <span aria-live="polite" role="status">{{getAriaLiveReadout()}}</span>
</div> </div>
<blockly-block-options-modal></blockly-block-options-modal>
<blockly-toolbox-modal></blockly-toolbox-modal>
<blockly-add-variable-modal></blockly-add-variable-modal> <blockly-add-variable-modal></blockly-add-variable-modal>
<blockly-rename-variable-modal></blockly-rename-variable-modal> <blockly-rename-variable-modal></blockly-rename-variable-modal>
<blockly-remove-variable-modal></blockly-remove-variable-modal> <blockly-remove-variable-modal></blockly-remove-variable-modal>
<blockly-toolbox-modal></blockly-toolbox-modal>
<blockly-block-options-modal></blockly-block-options-modal>
<label id="blockly-translate-button" aria-hidden="true" hidden> <label id="blockly-translate-button" aria-hidden="true" hidden>
{{'BUTTON'|translate}} {{'BUTTON'|translate}}

View File

@@ -39,7 +39,7 @@ blocklyApp.BlockOptionsModalComponent = ng.core.Component({
*ngFor="#buttonInfo of actionButtonsInfo; #buttonIndex=index"> *ngFor="#buttonInfo of actionButtonsInfo; #buttonIndex=index">
<button [id]="getOptionId(buttonIndex)" <button [id]="getOptionId(buttonIndex)"
(click)="buttonInfo.action(); hideModal();" (click)="buttonInfo.action(); hideModal();"
[ngClass]="{activeButton: activeActionButtonIndex == buttonIndex}"> [ngClass]="{activeButton: activeButtonIndex == buttonIndex}">
{{buttonInfo.translationIdForText|translate}} {{buttonInfo.translationIdForText|translate}}
</button> </button>
</div> </div>
@@ -48,7 +48,7 @@ blocklyApp.BlockOptionsModalComponent = ng.core.Component({
<div class="blocklyModalButtonContainer"> <div class="blocklyModalButtonContainer">
<button [id]="getCancelOptionId()" <button [id]="getCancelOptionId()"
(click)="dismissModal()" (click)="dismissModal()"
[ngClass]="{activeButton: activeActionButtonIndex == actionButtonsInfo.length}"> [ngClass]="{activeButton: activeButtonIndex == actionButtonsInfo.length}">
{{'CANCEL'|translate}} {{'CANCEL'|translate}}
</button> </button>
</div> </div>
@@ -68,7 +68,7 @@ blocklyApp.BlockOptionsModalComponent = ng.core.Component({
this.modalIsVisible = false; this.modalIsVisible = false;
this.actionButtonsInfo = []; this.actionButtonsInfo = [];
this.activeActionButtonIndex = -1; this.activeButtonIndex = -1;
this.onDismissCallback = null; this.onDismissCallback = null;
var that = this; var that = this;
@@ -79,67 +79,26 @@ blocklyApp.BlockOptionsModalComponent = ng.core.Component({
that.activeActionButtonIndex = -1; that.activeActionButtonIndex = -1;
that.onDismissCallback = onDismissCallback; that.onDismissCallback = onDismissCallback;
that.keyboardInputService.setOverride({ Blockly.CommonModal.setupKeyboardOverrides(that);
// Tab key: navigates to the previous or next item in the list. that.keyboardInputService.addOverride('13', function(evt) {
'9': function(evt) {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
if (evt.shiftKey) { if (that.activeButtonIndex == -1) {
// Move to the previous item in the list.
if (that.activeActionButtonIndex <= 0) {
that.activeActionButtonIndex = 0;
that.audioService.playOopsSound();
} else {
that.activeActionButtonIndex--;
}
} else {
// Move to the next item in the list.
if (that.activeActionButtonIndex ==
that.actionButtonsInfo.length) {
that.audioService.playOopsSound();
} else {
that.activeActionButtonIndex++;
}
}
that.focusOnOption(that.activeActionButtonIndex);
},
// Enter key: selects an action, performs it, and closes the modal.
'13': function(evt) {
evt.preventDefault();
evt.stopPropagation();
if (that.activeActionButtonIndex == -1) {
return; return;
} }
var button = document.getElementById( var button = document.getElementById(
that.getOptionId(that.activeActionButtonIndex)); that.getOptionId(that.activeButtonIndex));
if (that.activeActionButtonIndex < if (that.activeButtonIndex <
that.actionButtonsInfo.length) { that.actionButtonsInfo.length) {
that.actionButtonsInfo[that.activeActionButtonIndex].action(); that.actionButtonsInfo[that.activeButtonIndex].action();
} else { } else {
that.dismissModal(); that.dismissModal();
} }
that.hideModal(); that.hideModal();
}, });
// Escape key: closes the modal.
'27': function() {
that.dismissModal();
},
// Up key: no-op.
'38': function(evt) {
// Prevent the page from scrolling.
evt.preventDefault();
},
// Down key: no-op.
'40': function(evt) {
// Prevent the page from scrolling.
evt.preventDefault();
}
});
setTimeout(function() { setTimeout(function() {
document.getElementById('blockOptionsModal').focus(); document.getElementById('blockOptionsModal').focus();
@@ -152,6 +111,10 @@ blocklyApp.BlockOptionsModalComponent = ng.core.Component({
var button = document.getElementById(this.getOptionId(index)); var button = document.getElementById(this.getOptionId(index));
button.focus(); button.focus();
}, },
// Counts the number of interactive elements for the modal.
numInteractiveElements: function() {
return this.actionButtonsInfo.length + 1;
},
// Returns the ID for the corresponding option button. // Returns the ID for the corresponding option button.
getOptionId: function(index) { getOptionId: function(index) {
return 'block-options-modal-option-' + index; return 'block-options-modal-option-' + index;

74
accessible/commonModal.js Normal file
View File

@@ -0,0 +1,74 @@
Blockly.CommonModal = function() {};
Blockly.CommonModal.setupKeyboardOverrides = function(component) {
component.keyboardInputService.setOverride({
// Tab key: navigates to the previous or next item in the list.
'9': function(evt) {
evt.preventDefault();
evt.stopPropagation();
if (evt.shiftKey) {
// Move to the previous item in the list.
if (component.activeButtonIndex <= 0) {
component.activeActionButtonIndex = 0;
component.audioService.playOopsSound();
} else {
component.activeButtonIndex--;
}
} else {
// Move to the next item in the list.
if (component.activeButtonIndex == component.numInteractiveElements(component) - 1) {
component.audioService.playOopsSound();
} else {
component.activeButtonIndex++;
}
}
component.focusOnOption(component.activeButtonIndex, component);
},
// Escape key: closes the modal.
'27': function() {
component.dismissModal();
},
// Up key: no-op.
'38': function(evt) {
evt.preventDefault();
},
// Down key: no-op.
'40': function(evt) {
evt.preventDefault();
}
});
}
Blockly.CommonModal.getInteractiveElements = function(component) {
return Array.prototype.filter.call(
component.getInteractiveContainer().elements, function(element) {
if (element.type === 'hidden') {
return false;
}
if (element.disabled) {
return false;
}
if (element.tabIndex < 0) {
return false;
}
return true;
});
};
Blockly.CommonModal.numInteractiveElements = function(component) {
var elements = this.getInteractiveElements(component);
return elements.length;
};
Blockly.CommonModal.focusOnOption = function(index, component) {
var elements = this.getInteractiveElements(component);
var button = elements[index];
button.focus();
};
Blockly.CommonModal.hideModal = function() {
this.modalIsVisible = false;
this.keyboardInputService.clearOverride();
};

View File

@@ -46,6 +46,9 @@ blocklyApp.KeyboardInputService = ng.core.Class({
setOverride: function(newKeysToActions) { setOverride: function(newKeysToActions) {
this.keysToActionsOverride = newKeysToActions; this.keysToActionsOverride = newKeysToActions;
}, },
addOverride: function(keyCode, action) {
this.keysToActionsOverride[keyCode] = action;
},
clearOverride: function() { clearOverride: function() {
this.keysToActionsOverride = null; this.keysToActionsOverride = null;
} }

View File

@@ -103,34 +103,8 @@ blocklyApp.ToolboxModalComponent = ng.core.Component({
that.firstBlockIndexes.push(cumulativeIndex); that.firstBlockIndexes.push(cumulativeIndex);
that.totalNumBlocks = cumulativeIndex; that.totalNumBlocks = cumulativeIndex;
that.keyboardInputService.setOverride({ Blockly.CommonModal.setupKeyboardOverrides(that);
// Tab key: navigates to the previous or next item in the list. that.keyboardInputService.addOverride('13', function(evt) {
'9': function(evt) {
evt.preventDefault();
evt.stopPropagation();
if (evt.shiftKey) {
// Move to the previous item in the list.
if (that.activeButtonIndex <= 0) {
that.activeActionButtonIndex = 0;
that.audioService.playOopsSound();
} else {
that.activeButtonIndex--;
}
} else {
// Move to the next item in the list.
if (that.activeButtonIndex == that.totalNumBlocks) {
that.audioService.playOopsSound();
} else {
that.activeButtonIndex++;
}
}
that.focusOnOption(that.activeButtonIndex);
},
// Enter key: selects a block (or the 'Cancel' button), and closes
// the modal.
'13': function(evt) {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
@@ -154,20 +128,7 @@ blocklyApp.ToolboxModalComponent = ng.core.Component({
// The 'Cancel' button has been pressed. // The 'Cancel' button has been pressed.
that.dismissModal(); that.dismissModal();
}, });
// Escape key: closes the modal.
'27': function() {
that.dismissModal();
},
// Up key: no-op.
'38': function(evt) {
evt.preventDefault();
},
// Down key: no-op.
'40': function(evt) {
evt.preventDefault();
}
});
setTimeout(function() { setTimeout(function() {
document.getElementById('toolboxModal').focus(); document.getElementById('toolboxModal').focus();
@@ -177,10 +138,15 @@ blocklyApp.ToolboxModalComponent = ng.core.Component({
} }
], ],
// Closes the modal (on both success and failure). // Closes the modal (on both success and failure).
hideModal_: function() { hideModal_: Blockly.CommonModal.hideModal,
this.modalIsVisible = false; // Focuses on the button represented by the given index.
this.keyboardInputService.clearOverride(); focusOnOption: function(index) {
this.toolboxModalService.hideModal(); var button = document.getElementById(this.getOptionId(index));
button.focus();
},
// Counts the number of interactive elements for the modal.
numInteractiveElements: function() {
return this.totalNumBlocks + 1;
}, },
getOverallIndex: function(categoryIndex, blockIndex) { getOverallIndex: function(categoryIndex, blockIndex) {
return this.firstBlockIndexes[categoryIndex] + blockIndex; return this.firstBlockIndexes[categoryIndex] + blockIndex;
@@ -191,11 +157,6 @@ blocklyApp.ToolboxModalComponent = ng.core.Component({
getBlockDescription: function(block) { getBlockDescription: function(block) {
return this.utilsService.getBlockDescription(block); return this.utilsService.getBlockDescription(block);
}, },
// Focuses on the button represented by the given index.
focusOnOption: function(index) {
var button = document.getElementById(this.getOptionId(index));
button.focus();
},
// Returns the ID for the corresponding option button. // Returns the ID for the corresponding option button.
getOptionId: function(index) { getOptionId: function(index) {
return 'toolbox-modal-option-' + index; return 'toolbox-modal-option-' + index;

View File

@@ -68,44 +68,7 @@ blocklyApp.VariableAddModalComponent = ng.core.Component({
function() { function() {
that.modalIsVisible = true; that.modalIsVisible = true;
that.keyboardInputService.setOverride({ Blockly.CommonModal.setupKeyboardOverrides(that);
// Tab key: navigates to the previous or next item in the list.
'9': function(evt) {
evt.preventDefault();
evt.stopPropagation();
if (evt.shiftKey) {
// Move to the previous item in the list.
if (that.activeButtonIndex <= 0) {
that.activeActionButtonIndex = 0;
that.audioService.playOopsSound();
} else {
that.activeButtonIndex--;
}
} else {
// Move to the next item in the list.
if (that.activeButtonIndex == that.numInteractiveElements() - 1) {
that.audioService.playOopsSound();
} else {
that.activeButtonIndex++;
}
}
that.focusOnOption(that.activeButtonIndex);
},
// Escape key: closes the modal.
'27': function() {
that.dismissModal();
},
// Up key: no-op.
'38': function(evt) {
evt.preventDefault();
},
// Down key: no-op.
'40': function(evt) {
evt.preventDefault();
}
});
setTimeout(function() { setTimeout(function() {
document.getElementById('mainFieldId').focus(); document.getElementById('mainFieldId').focus();
@@ -119,36 +82,16 @@ blocklyApp.VariableAddModalComponent = ng.core.Component({
this.variableName = newValue; this.variableName = newValue;
}, },
// Closes the modal (on both success and failure). // Closes the modal (on both success and failure).
hideModal_: function() { hideModal_: Blockly.CommonModal.hideModal,
this.modalIsVisible = false;
this.keyboardInputService.clearOverride();
},
// Focuses on the button represented by the given index. // Focuses on the button represented by the given index.
focusOnOption: function(index) { focusOnOption: Blockly.CommonModal.focusOnOption,
var elements = this.getInteractiveElements();
var button = elements[index];
button.focus();
},
// Counts the number of interactive elements for the modal. // Counts the number of interactive elements for the modal.
numInteractiveElements: function() { numInteractiveElements: Blockly.CommonModal.numInteractiveElements,
var elements = this.getInteractiveElements();
return elements.length;
},
// Gets all the interactive elements for the modal. // Gets all the interactive elements for the modal.
getInteractiveElements: function() { getInteractiveElements: Blockly.CommonModal.getInteractiveElements,
return Array.prototype.filter.call( // Gets the container with interactive elements.
document.getElementById("varForm").elements, function(element) { getInteractiveContainer: function() {
if (element.type === 'hidden') { return document.getElementById("varForm");
return false;
}
if (element.disabled) {
return false;
}
if (element.tabIndex < 0) {
return false;
}
return true;
});
}, },
// Submits the name change for the variable. // Submits the name change for the variable.
submit: function() { submit: function() {

View File

@@ -70,44 +70,7 @@ blocklyApp.VariableRemoveModalComponent = ng.core.Component({
that.count = count that.count = count
that.modalIsVisible = true; that.modalIsVisible = true;
that.keyboardInputService.setOverride({ Blockly.CommonModal.setupKeyboardOverrides(that);
// Tab key: navigates to the previous or next item in the list.
'9': function(evt) {
evt.preventDefault();
evt.stopPropagation();
if (evt.shiftKey) {
// Move to the previous item in the list.
if (that.activeButtonIndex <= 0) {
that.activeActionButtonIndex = 0;
that.audioService.playOopsSound();
} else {
that.activeButtonIndex--;
}
} else {
// Move to the next item in the list.
if (that.activeButtonIndex == that.numInteractiveElements() - 1) {
that.audioService.playOopsSound();
} else {
that.activeButtonIndex++;
}
}
that.focusOnOption(that.activeButtonIndex);
},
// Escape key: closes the modal.
'27': function() {
that.dismissModal();
},
// Up key: no-op.
'38': function(evt) {
evt.preventDefault();
},
// Down key: no-op.
'40': function(evt) {
evt.preventDefault();
}
});
setTimeout(function() { setTimeout(function() {
document.getElementById('label').focus(); document.getElementById('label').focus();
@@ -116,41 +79,17 @@ blocklyApp.VariableRemoveModalComponent = ng.core.Component({
); );
} }
], ],
// Caches the current text variable as the user types.
setTextValue: function(newValue) {
this.variableName = newValue;
},
// Closes the modal (on both success and failure). // Closes the modal (on both success and failure).
hideModal_: function() { hideModal_: Blockly.CommonModal.hideModal,
this.modalIsVisible = false;
this.keyboardInputService.clearOverride();
},
// Focuses on the button represented by the given index. // Focuses on the button represented by the given index.
focusOnOption: function(index) { focusOnOption: Blockly.CommonModal.focusOnOption,
var elements = this.getInteractiveElements();
var button = elements[index];
button.focus();
},
// Counts the number of interactive elements for the modal. // Counts the number of interactive elements for the modal.
numInteractiveElements: function() { numInteractiveElements: Blockly.CommonModal.numInteractiveElements,
var elements = this.getInteractiveElements();
return elements.length;
},
// Gets all the interactive elements for the modal. // Gets all the interactive elements for the modal.
getInteractiveElements: function() { getInteractiveElements: Blockly.CommonModal.getInteractiveElements,
return Array.prototype.filter.call( // Gets the container with interactive elements.
document.getElementById("varForm").elements, function(element) { getInteractiveContainer: function() {
if (element.type === 'hidden') { return document.getElementById("varForm");
return false;
}
if (element.disabled) {
return false;
}
if (element.tabIndex < 0) {
return false;
}
return true;
});
}, },
// Submits the name change for the variable. // Submits the name change for the variable.
submit: function() { submit: function() {

View File

@@ -70,44 +70,7 @@ blocklyApp.VariableRenameModalComponent = ng.core.Component({
that.currentVariableName = oldName; that.currentVariableName = oldName;
that.modalIsVisible = true; that.modalIsVisible = true;
that.keyboardInputService.setOverride({ Blockly.CommonModal.setupKeyboardOverrides(that);
// Tab key: navigates to the previous or next item in the list.
'9': function(evt) {
evt.preventDefault();
evt.stopPropagation();
if (evt.shiftKey) {
// Move to the previous item in the list.
if (that.activeButtonIndex <= 0) {
that.activeActionButtonIndex = 0;
that.audioService.playOopsSound();
} else {
that.activeButtonIndex--;
}
} else {
// Move to the next item in the list.
if (that.activeButtonIndex == that.numInteractiveElements() - 1) {
that.audioService.playOopsSound();
} else {
that.activeButtonIndex++;
}
}
that.focusOnOption(that.activeButtonIndex);
},
// Escape key: closes the modal.
'27': function() {
that.dismissModal();
},
// Up key: no-op.
'38': function(evt) {
evt.preventDefault();
},
// Down key: no-op.
'40': function(evt) {
evt.preventDefault();
}
});
setTimeout(function() { setTimeout(function() {
document.getElementById('mainFieldId').focus(); document.getElementById('mainFieldId').focus();
@@ -121,36 +84,16 @@ blocklyApp.VariableRenameModalComponent = ng.core.Component({
this.variableName = newValue; this.variableName = newValue;
}, },
// Closes the modal (on both success and failure). // Closes the modal (on both success and failure).
hideModal_: function() { hideModal_: Blockly.CommonModal.hideModal,
this.modalIsVisible = false;
this.keyboardInputService.clearOverride();
},
// Focuses on the button represented by the given index. // Focuses on the button represented by the given index.
focusOnOption: function(index) { focusOnOption: Blockly.CommonModal.focusOnOption,
var elements = this.getInteractiveElements();
var button = elements[index];
button.focus();
},
// Counts the number of interactive elements for the modal. // Counts the number of interactive elements for the modal.
numInteractiveElements: function() { numInteractiveElements: Blockly.CommonModal.numInteractiveElements,
var elements = this.getInteractiveElements();
return elements.length;
},
// Gets all the interactive elements for the modal. // Gets all the interactive elements for the modal.
getInteractiveElements: function() { getInteractiveElements: Blockly.CommonModal.getInteractiveElements,
return Array.prototype.filter.call( // Gets the container with interactive elements.
document.getElementById("varForm").elements, function(element) { getInteractiveContainer: function() {
if (element.type === 'hidden') { return document.getElementById("varForm");
return false;
}
if (element.disabled) {
return false;
}
if (element.tabIndex < 0) {
return false;
}
return true;
});
}, },
// Submits the name change for the variable. // Submits the name change for the variable.
submit: function() { submit: function() {

View File

@@ -29,6 +29,7 @@
<script src="../../accessible/translate.pipe.js"></script> <script src="../../accessible/translate.pipe.js"></script>
<script src="../../accessible/variable-modal.service.js"></script> <script src="../../accessible/variable-modal.service.js"></script>
<script src="../../accessible/commonModal.js"></script>
<script src="../../accessible/field-segment.component.js"></script> <script src="../../accessible/field-segment.component.js"></script>
<script src="../../accessible/block-options-modal.component.js"></script> <script src="../../accessible/block-options-modal.component.js"></script>
<script src="../../accessible/toolbox-modal.component.js"></script> <script src="../../accessible/toolbox-modal.component.js"></script>