Use Tab keys instead of arrow keys for dialog boxes. Set role=alertdialog and read out the header/text automatically. Ensure that Esc key actually closes dialogs and that all keystrokes are captured.

This commit is contained in:
Sean Lip
2016-12-08 18:27:17 -08:00
parent da70ce4cdb
commit 5960aa8461
2 changed files with 88 additions and 73 deletions

View File

@@ -27,14 +27,14 @@ blocklyApp.BlockOptionsModalComponent = ng.core.Component({
selector: 'blockly-block-options-modal', selector: 'blockly-block-options-modal',
template: ` template: `
<div *ngIf="modalIsVisible" class="blocklyModalCurtain" <div *ngIf="modalIsVisible" class="blocklyModalCurtain"
(click)="hideModal()"> (click)="dismissModal()">
<!-- $event.stopPropagation() prevents the modal from closing when its <!-- $event.stopPropagation() prevents the modal from closing when its
interior is clicked. --> interior is clicked. -->
<div id="blockOptionsModal" class="blocklyModal" role="dialog" <div id="blockOptionsModal" class="blocklyModal" role="alertdialog"
(click)="$event.stopPropagation()" tabindex="-1"> (click)="$event.stopPropagation()" tabindex="-1"
aria-labelledby="blockOptionsModalHeading">
<h3 id="blockOptionsModalHeading">{{'BLOCK_OPTIONS'|translate}}</h3>
<div role="document"> <div role="document">
<h3>{{'BLOCK_OPTIONS'|translate}}</h3>
<div class="blocklyModalButtonContainer" <div class="blocklyModalButtonContainer"
*ngFor="#buttonInfo of actionButtonsInfo; #i=index"> *ngFor="#buttonInfo of actionButtonsInfo; #i=index">
<button [id]="getOptionId(i)" <button [id]="getOptionId(i)"
@@ -43,13 +43,14 @@ blocklyApp.BlockOptionsModalComponent = ng.core.Component({
{{buttonInfo.translationIdForText|translate}} {{buttonInfo.translationIdForText|translate}}
</button> </button>
</div> </div>
<div class="blocklyModalButtonContainer"> </div>
<button [id]="getCancelOptionId()"
(click)="hideModal()" <div class="blocklyModalButtonContainer">
[ngClass]="{activeButton: activeActionButtonIndex == actionButtonsInfo.length}"> <button [id]="getCancelOptionId()"
{{'CANCEL'|translate}} (click)="dismissModal()"
</button> [ngClass]="{activeButton: activeActionButtonIndex == actionButtonsInfo.length}">
</div> {{'CANCEL'|translate}}
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -67,7 +68,7 @@ blocklyApp.BlockOptionsModalComponent = ng.core.Component({
this.modalIsVisible = false; this.modalIsVisible = false;
this.actionButtonsInfo = []; this.actionButtonsInfo = [];
this.activeActionButtonIndex = 0; this.activeActionButtonIndex = -1;
this.onDismissCallback = null; this.onDismissCallback = null;
var that = this; var that = this;
@@ -75,17 +76,36 @@ blocklyApp.BlockOptionsModalComponent = ng.core.Component({
function(newActionButtonsInfo, onDismissCallback) { function(newActionButtonsInfo, onDismissCallback) {
that.modalIsVisible = true; that.modalIsVisible = true;
that.actionButtonsInfo = newActionButtonsInfo; that.actionButtonsInfo = newActionButtonsInfo;
that.activeActionButtonIndex = 0; that.activeActionButtonIndex = -1;
that.onDismissCallback = onDismissCallback; that.onDismissCallback = onDismissCallback;
that.keyboardInputService.setOverride({ that.keyboardInputService.setOverride({
// Tab key: no-op. // Tab key: navigates to the previous or next item in the list.
'9': function(evt) { '9': function(evt) {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
if (evt.shiftKey) {
// 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 // Enter key: selects an action, performs it, and closes the modal.
// modal.
'13': function(evt) { '13': function(evt) {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
@@ -96,38 +116,24 @@ blocklyApp.BlockOptionsModalComponent = ng.core.Component({
that.actionButtonsInfo.length) { that.actionButtonsInfo.length) {
that.actionButtonsInfo[that.activeActionButtonIndex].action(); that.actionButtonsInfo[that.activeActionButtonIndex].action();
} else { } else {
that.onDismissCallback(); that.dismissModal();
} }
that.hideModal(); that.hideModal();
}, },
// Escape key: closes the modal. // Escape key: closes the modal.
'27': function() { '27': function() {
that.onDismissCallback(); that.dismissModal();
that.hideModal();
}, },
// Up key: navigates to the previous item in the list. // Up key: no-op.
'38': function(evt) { '38': function(evt) {
// Prevent the page from scrolling. // Prevent the page from scrolling.
evt.preventDefault(); evt.preventDefault();
if (that.activeActionButtonIndex == 0) {
that.audioService.playOopsSound();
} else {
that.activeActionButtonIndex--;
that.focusOnOption(that.activeActionButtonIndex);
}
}, },
// Down key: navigates to the next item in the list. // Down key: no-op.
'40': function(evt) { '40': function(evt) {
// Prevent the page from scrolling. // Prevent the page from scrolling.
evt.preventDefault(); evt.preventDefault();
if (that.activeActionButtonIndex ==
that.actionButtonsInfo.length) {
that.audioService.playOopsSound();
} else {
that.activeActionButtonIndex++;
that.focusOnOption(that.activeActionButtonIndex);
}
} }
}); });
@@ -150,6 +156,10 @@ blocklyApp.BlockOptionsModalComponent = ng.core.Component({
getCancelOptionId: function() { getCancelOptionId: function() {
return this.getOptionId(this.actionButtonsInfo.length); return this.getOptionId(this.actionButtonsInfo.length);
}, },
dismissModal: function() {
this.onDismissCallback();
this.hideModal();
},
// Closes the modal. // Closes the modal.
hideModal: function() { hideModal: function() {
this.modalIsVisible = false; this.modalIsVisible = false;

View File

@@ -30,30 +30,29 @@ blocklyApp.ToolboxModalComponent = ng.core.Component({
(click)="dismissModal()"> (click)="dismissModal()">
<!-- $event.stopPropagation() prevents the modal from closing when its <!-- $event.stopPropagation() prevents the modal from closing when its
interior is clicked. --> interior is clicked. -->
<div id="toolboxModal" class="blocklyModal" role="dialog" <div id="toolboxModal" class="blocklyModal" role="alertdialog"
(click)="$event.stopPropagation()" tabindex="-1"> (click)="$event.stopPropagation()" tabindex="-1"
<div role="document"> aria-labelledby="toolboxModalHeading">
<h3>{{'SELECT_A_BLOCK'|translate}}</h3> <h3 id="toolboxModalHeading">{{'SELECT_A_BLOCK'|translate}}</h3>
<div *ngFor="#toolboxCategory of toolboxCategories; #categoryIndex=index"> <div *ngFor="#toolboxCategory of toolboxCategories; #categoryIndex=index">
<h4 *ngIf="toolboxCategory.categoryName">{{toolboxCategory.categoryName}}</h4> <h4 *ngIf="toolboxCategory.categoryName">{{toolboxCategory.categoryName}}</h4>
<div class="blocklyModalButtonContainer" <div class="blocklyModalButtonContainer"
*ngFor="#block of toolboxCategory.blocks; #blockIndex=index"> *ngFor="#block of toolboxCategory.blocks; #blockIndex=index">
<button [id]="getOptionId(getOverallIndex(categoryIndex, blockIndex))" <button [id]="getOptionId(getOverallIndex(categoryIndex, blockIndex))"
(click)="selectBlock(getBlock(categoryIndex, blockIndex))" (click)="selectBlock(getBlock(categoryIndex, blockIndex))"
[ngClass]="{activeButton: activeButtonIndex == getOverallIndex(categoryIndex, blockIndex)}"> [ngClass]="{activeButton: activeButtonIndex == getOverallIndex(categoryIndex, blockIndex)}">
{{getBlockDescription(block)}} {{getBlockDescription(block)}}
</button>
</div>
</div>
<hr>
<div class="blocklyModalButtonContainer">
<button [id]="getCancelOptionId()" (click)="dismissModal()"
[ngClass]="{activeButton: activeButtonIndex == totalNumBlocks}">
{{'CANCEL'|translate}}
</button> </button>
</div> </div>
</div> </div>
<hr>
<div class="blocklyModalButtonContainer">
<button [id]="getCancelOptionId()" (click)="dismissModal()"
[ngClass]="{activeButton: activeButtonIndex == totalNumBlocks}">
{{'CANCEL'|translate}}
</button>
</div>
</div> </div>
</div> </div>
`, `,
@@ -78,7 +77,7 @@ blocklyApp.ToolboxModalComponent = ng.core.Component({
this.onDismissCallback = null; this.onDismissCallback = null;
this.firstBlockIndexes = []; this.firstBlockIndexes = [];
this.activeButtonIndex = 0; this.activeButtonIndex = -1;
this.totalNumBlocks = 0; this.totalNumBlocks = 0;
var that = this; var that = this;
@@ -91,7 +90,7 @@ blocklyApp.ToolboxModalComponent = ng.core.Component({
that.onDismissCallback = onDismissCallback; that.onDismissCallback = onDismissCallback;
that.firstBlockIndexes = []; that.firstBlockIndexes = [];
that.activeButtonIndex = 0; that.activeButtonIndex = -1;
that.totalNumBlocks = 0; that.totalNumBlocks = 0;
var cumulativeIndex = 0; var cumulativeIndex = 0;
@@ -102,12 +101,30 @@ blocklyApp.ToolboxModalComponent = ng.core.Component({
that.firstBlockIndexes.push(cumulativeIndex); that.firstBlockIndexes.push(cumulativeIndex);
that.totalNumBlocks = cumulativeIndex; that.totalNumBlocks = cumulativeIndex;
that.activeButtonIndex = 0;
that.keyboardInputService.setOverride({ that.keyboardInputService.setOverride({
// Tab key: no-op. // Tab key: navigates to the previous or next item in the list.
'9': function(evt) { '9': function(evt) {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); 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 an action, performs it, and closes the modal. // Enter key: selects an action, performs it, and closes the modal.
'13': function(evt) { '13': function(evt) {
@@ -135,25 +152,13 @@ blocklyApp.ToolboxModalComponent = ng.core.Component({
'27': function() { '27': function() {
that.dismissModal(); that.dismissModal();
}, },
// Up key: navigates to the previous item in the list. // Up key: no-op.
'38': function(evt) { '38': function(evt) {
evt.preventDefault(); evt.preventDefault();
if (that.activeButtonIndex == 0) {
that.audioService.playOopsSound();
} else {
that.activeButtonIndex--;
}
that.focusOnOption(that.activeButtonIndex);
}, },
// Down key: navigates to the next item in the list. // Down key: no-op.
'40': function(evt) { '40': function(evt) {
evt.preventDefault(); evt.preventDefault();
if (that.activeButtonIndex == that.totalNumBlocks) {
that.audioService.playOopsSound();
} else {
that.activeButtonIndex++;
}
that.focusOnOption(that.activeButtonIndex);
} }
}); });