Skip to content
Permalink
Browse files

Add accessible directory

  • Loading branch information...
rachel-fenichel committed Jul 2, 2019
1 parent 2371e28 commit cdf5b79f799ba865913b0055839a892ba34ad854
Showing with 4,023 additions and 0 deletions.
  1. +54 −0 accessible/README.md
  2. +109 −0 accessible/app.component.js
  3. +96 −0 accessible/audio.service.js
  4. +135 −0 accessible/block-connection.service.js
  5. +146 −0 accessible/block-options-modal.component.js
  6. +62 −0 accessible/block-options-modal.service.js
  7. +95 −0 accessible/commonModal.js
  8. +206 −0 accessible/field-segment.component.js
  9. +58 −0 accessible/keyboard-input.service.js
  10. +15 −0 accessible/libs/README
  11. +748 −0 accessible/libs/Rx.umd.min.js
  12. +44 −0 accessible/libs/angular2-all.umd.min.js
  13. +24 −0 accessible/libs/angular2-polyfills.min.js
  14. +12 −0 accessible/libs/es6-shim.min.js
  15. +80 −0 accessible/media/accessible.css
  16. BIN accessible/media/click.mp3
  17. BIN accessible/media/click.ogg
  18. BIN accessible/media/click.wav
  19. BIN accessible/media/delete.mp3
  20. BIN accessible/media/delete.ogg
  21. BIN accessible/media/delete.wav
  22. BIN accessible/media/oops.mp3
  23. BIN accessible/media/oops.ogg
  24. BIN accessible/media/oops.wav
  25. +62 −0 accessible/messages.js
  26. +61 −0 accessible/notifications.service.js
  27. +132 −0 accessible/sidebar.component.js
  28. +188 −0 accessible/toolbox-modal.component.js
  29. +230 −0 accessible/toolbox-modal.service.js
  30. +36 −0 accessible/translate.pipe.js
  31. +609 −0 accessible/tree.service.js
  32. +44 −0 accessible/utils.service.js
  33. +118 −0 accessible/variable-add-modal.component.js
  34. +91 −0 accessible/variable-modal.service.js
  35. +125 −0 accessible/variable-remove-modal.component.js
  36. +121 −0 accessible/variable-rename-modal.component.js
  37. +216 −0 accessible/workspace-block.component.js
  38. +106 −0 accessible/workspace.component.js
@@ -0,0 +1,54 @@
Accessible Blockly
==================

Google's Blockly is a web-based, visual programming editor that is accessible
to blind users.

The code in this directory renders a version of the Blockly toolbox and
workspace that is fully keyboard-navigable, and compatible with most screen
readers. It is optimized for NVDA on Firefox.

In the future, Accessible Blockly may be modified to suit accessibility needs
other than visual impairments. Note that deaf users are expected to continue
using Blockly over Accessible Blockly.


Using Accessible Blockly in Your Web App
----------------------------------------
The demo at blockly/demos/accessible covers the absolute minimum required to
import Accessible Blockly into your web app. You will need to import the files
in the same order as in the demo: utils.service.js will need to be the first
Angular file imported.

When the DOMContentLoaded event fires, call ng.platform.browser.bootstrap() on
the main component to be loaded. This will usually be blocklyApp.AppComponent,
but if you have another component that wraps it, use that one instead.


Customizing the Sidebar and Audio
---------------------------------
The Accessible Blockly workspace comes with a customizable sidebar.

To customize the sidebar, you will need to declare an ACCESSIBLE_GLOBALS object
in the global scope that looks like this:

var ACCESSIBLE_GLOBALS = {
mediaPathPrefix: null,
customSidebarButtons: []
};

The value of mediaPathPrefix should be the location of the accessible/media
folder.

The value of 'customSidebarButtons' should be a list of objects, each
representing buttons on the sidebar. Each of these objects should have the
following keys:
- 'text' (the text to display on the button)
- 'action' (the function that gets run when the button is clicked)
- 'id' (optional; the id of the button)


Limitations
-----------
- We do not support having multiple Accessible Blockly apps in a single webpage.
- Accessible Blockly does not support the use of shadow blocks.
@@ -0,0 +1,109 @@
/**
* 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 Top-level component for the Accessible Blockly application.
* @author madeeha@google.com (Madeeha Ghori)
*/

goog.provide('blocklyApp.AppComponent');

goog.require('Blockly');

goog.require('blocklyApp.AudioService');
goog.require('blocklyApp.BlockConnectionService');
goog.require('blocklyApp.BlockOptionsModalComponent');
goog.require('blocklyApp.BlockOptionsModalService');
goog.require('blocklyApp.KeyboardInputService');
goog.require('blocklyApp.NotificationsService');
goog.require('blocklyApp.SidebarComponent');
goog.require('blocklyApp.ToolboxModalComponent');
goog.require('blocklyApp.ToolboxModalService');
goog.require('blocklyApp.TranslatePipe');
goog.require('blocklyApp.TreeService');
goog.require('blocklyApp.UtilsService');
goog.require('blocklyApp.VariableAddModalComponent');
goog.require('blocklyApp.VariableModalService');
goog.require('blocklyApp.VariableRenameModalComponent');
goog.require('blocklyApp.VariableRemoveModalComponent');
goog.require('blocklyApp.WorkspaceComponent');


blocklyApp.workspace = new Blockly.Workspace();

blocklyApp.AppComponent = ng.core.Component({
selector: 'blockly-app',
template: `
<blockly-workspace></blockly-workspace>
<blockly-sidebar></blockly-sidebar>
<!-- Warning: Hiding this when there is no content looks visually nicer,
but it can have unexpected side effects. In particular, it sometimes stops
screenreaders from reading anything in this div. -->
<div class="blocklyAriaLiveStatus">
<span aria-live="polite" role="status">{{getAriaLiveReadout()}}</span>
</div>
<blockly-add-variable-modal></blockly-add-variable-modal>
<blockly-rename-variable-modal></blockly-rename-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>
{{'BUTTON'|translate}}
</label>
<label id="blockly-translate-workspace-block" aria-hidden="true" hidden>
{{'WORKSPACE_BLOCK'|translate}}
</label>
`,
directives: [
blocklyApp.BlockOptionsModalComponent,
blocklyApp.SidebarComponent,
blocklyApp.ToolboxModalComponent,
blocklyApp.VariableAddModalComponent,
blocklyApp.VariableRenameModalComponent,
blocklyApp.VariableRemoveModalComponent,
blocklyApp.WorkspaceComponent
],
pipes: [blocklyApp.TranslatePipe],
// All services are declared here, so that all components in the application
// use the same instance of the service.
// https://www.sitepoint.com/angular-2-components-providers-classes-factories-values/
providers: [
blocklyApp.AudioService,
blocklyApp.BlockConnectionService,
blocklyApp.BlockOptionsModalService,
blocklyApp.KeyboardInputService,
blocklyApp.NotificationsService,
blocklyApp.ToolboxModalService,
blocklyApp.TreeService,
blocklyApp.UtilsService,
blocklyApp.VariableModalService
]
})
.Class({
constructor: [
blocklyApp.NotificationsService, function(notificationsService) {
this.notificationsService = notificationsService;
}
],
getAriaLiveReadout: function() {
return this.notificationsService.getDisplayedMessage();
}
});
@@ -0,0 +1,96 @@
/**
* 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 for playing audio files.
* @author sll@google.com (Sean Lip)
*/

goog.provide('blocklyApp.AudioService');

goog.require('blocklyApp.NotificationsService');


blocklyApp.AudioService = ng.core.Class({
constructor: [
blocklyApp.NotificationsService, function(notificationsService) {
this.notificationsService = notificationsService;

// We do not play any audio unless a media path prefix is specified.
this.canPlayAudio = false;

if (ACCESSIBLE_GLOBALS.hasOwnProperty('mediaPathPrefix')) {
this.canPlayAudio = true;
var mediaPathPrefix = ACCESSIBLE_GLOBALS['mediaPathPrefix'];
this.AUDIO_PATHS_ = {
'connect': mediaPathPrefix + 'click.mp3',
'delete': mediaPathPrefix + 'delete.mp3',
'oops': mediaPathPrefix + 'oops.mp3'
};
}

this.cachedAudioFiles_ = {};
// Store callback references here so that they can be removed if a new
// call to this.play_() comes in.
this.onEndedCallbacks_ = {
'connect': [],
'delete': [],
'oops': []
};
}
],
play_: function(audioId, onEndedCallback) {
if (this.canPlayAudio) {
if (!this.cachedAudioFiles_.hasOwnProperty(audioId)) {
this.cachedAudioFiles_[audioId] = new Audio(this.AUDIO_PATHS_[audioId]);
}

if (onEndedCallback) {
this.onEndedCallbacks_[audioId].push(onEndedCallback);
this.cachedAudioFiles_[audioId].addEventListener(
'ended', onEndedCallback);
} else {
var that = this;
this.onEndedCallbacks_[audioId].forEach(function(callback) {
that.cachedAudioFiles_[audioId].removeEventListener(
'ended', callback);
});
this.onEndedCallbacks_[audioId].length = 0;
}

this.cachedAudioFiles_[audioId].play();
}
},
playConnectSound: function() {
this.play_('connect');
},
playDeleteSound: function() {
this.play_('delete');
},
playOopsSound: function(optionalStatusMessage) {
if (optionalStatusMessage) {
var that = this;
this.play_('oops', function() {
that.notificationsService.speak(optionalStatusMessage);
});
} else {
this.play_('oops');
}
}
});
@@ -0,0 +1,135 @@
/**
* 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 for handling the mechanics of how blocks
* get connected to each other.
* @author sll@google.com (Sean Lip)
*/

goog.provide('blocklyApp.BlockConnectionService');

goog.require('blocklyApp.AudioService');
goog.require('blocklyApp.NotificationsService');


blocklyApp.BlockConnectionService = ng.core.Class({
constructor: [
blocklyApp.NotificationsService, blocklyApp.AudioService,
function(_notificationsService, _audioService) {
this.notificationsService = _notificationsService;
this.audioService = _audioService;

// When a user "adds a link" to a block, the connection representing this
// link is stored here.
this.markedConnection_ = null;
}],
findCompatibleConnection_: function(block, targetConnection) {
// Locates and returns a connection on the given block that is compatible
// with the target connection, if one exists. Returns null if no such
// connection exists.
// Note: the targetConnection is assumed to be the markedConnection_, or
// possibly its counterpart (in the case where the marked connection is
// currently attached to another connection). This method therefore ignores
// input connections on the given block, since one doesn't usually mark an
// output connection and attach a block to it.
if (!targetConnection || !targetConnection.getSourceBlock().workspace) {
return null;
}

var desiredType = Blockly.OPPOSITE_TYPE[targetConnection.type];
var potentialConnection = (
desiredType == Blockly.OUTPUT_VALUE ? block.outputConnection :
desiredType == Blockly.PREVIOUS_STATEMENT ? block.previousConnection :
desiredType == Blockly.NEXT_STATEMENT ? block.nextConnection :
null);

if (potentialConnection &&
potentialConnection.checkType_(targetConnection)) {
return potentialConnection;
} else {
return null;
}
},
isAnyConnectionMarked: function() {
return Boolean(this.markedConnection_);
},
getMarkedConnectionSourceBlock: function() {
return this.markedConnection_ ?
this.markedConnection_.getSourceBlock() : null;
},
canBeAttachedToMarkedConnection: function(block) {
return Boolean(
this.findCompatibleConnection_(block, this.markedConnection_));
},
canBeMovedToMarkedConnection: function(block) {
if (!this.markedConnection_) {
return false;
}

// It should not be possible to move any ancestor of the block containing
// the marked connection to the marked connection.
var ancestorBlock = this.getMarkedConnectionSourceBlock();
while (ancestorBlock) {
if (ancestorBlock.id == block.id) {
return false;
}
ancestorBlock = ancestorBlock.getParent();
}

return this.canBeAttachedToMarkedConnection(block);
},
markConnection: function(connection) {
this.markedConnection_ = connection;
this.notificationsService.speak(Blockly.Msg.ADDED_LINK_MSG);
},
attachToMarkedConnection: function(block) {
var xml = Blockly.Xml.blockToDom(block);
var reconstitutedBlock = Blockly.Xml.domToBlock(xml, blocklyApp.workspace);

var targetConnection = null;
if (this.markedConnection_.targetBlock() &&
this.markedConnection_.type == Blockly.PREVIOUS_STATEMENT) {
// Is the marked connection a 'previous' connection that is already
// connected? If so, find the block that's currently connected to it, and
// use that block's 'next' connection as the new marked connection.
// Otherwise, splicing does not happen correctly, and inserting a block
// in the middle of a group of two linked blocks will split the group.
targetConnection = this.markedConnection_.targetConnection;
} else {
targetConnection = this.markedConnection_;
}

var connection = this.findCompatibleConnection_(
reconstitutedBlock, targetConnection);
if (connection) {
targetConnection.connect(connection);

this.markedConnection_ = null;
this.audioService.playConnectSound();
return reconstitutedBlock.id;
} else {
// We throw an error here, because we expect any UI controls that would
// result in a non-connection to be disabled or hidden.
throw Error(
'Unable to connect block to marked connection. This should not ' +
'happen.');
}
}
});

0 comments on commit cdf5b79

Please sign in to comment.
You can’t perform that action at this time.