HTML5 provides the canvas tag which is an area that can be drawn to via Javascript. Createjs (https://www.createjs.com/) is a library that help manage the low level canvas draw calls with higher level DisplayObjects. This module allows for annotating those DisplayObjects with additional information needed for accessibility support, which the module uses to create and update HTML elements that are placed under the canvas. In order to help with keyboard support and other Assistive Technologies (ATs) that cause input events, this module also sends up events to the corresponding DisplayObject that the consuming app can respond to accordingly.
The translation of DisplayObjects to HTML is necessary for accessibility support since Javascript draw calls don't provide information to ATs for understanding the content in the canvas or interacting with it. However, other HTML tags do work with ATs through a combination of the tag choice, various attributes, and text values.
This module's accessibility support is based around WCAG 2.1, though there are considerations outside the scope of this module that are needed for a particular webpage to meet the standard (e.g. contrast ratio, supported orientations). For what the module can provide to meet that standard, it does so by using HTML5 semantic markup as much as possible and following WAI-ARIA 1.1. The module does have more roles than WAI-ARIA specifies due to trying to use semantic markup which can sometimes lead to multiple tag options. For example instead of having just a "heading" role, the module has roles "heading1" through "heading6" which it translates to the "h1" through "h6" tags. It also provides additional roles that translate to the different format text tags (e.g. bold, emphasize, code).
This can be added to your project via npm: npm install CurriculumAssociates/createjs-accessibility
Then in ES6, the module can be imported by doing: import AccessibilityModule from 'CurriculumAssociates/createjs-accessibility';
DisplayObjects that are to be translated to the DOM need to be registered with the module so that the additional annotation needed for accessibility support can be supplied. Registering adds an AccessibilityObject
instance (or one of its subtypes) to the .accessible
field to the DisplayObject, which can be used to get or set the various pieces of accessibility information that are relevant to that role. If the role used for the DisplayObject needs to be changed, then it needs to be re-registered with the module using the new role, which will result in a new AccessibilityObject
instance (or one of its subtypes) being attached to the DisplayObject overwriting the .accessible
field's value.
AccessibilityModule.register
allows for registering 1 or multiple DisplayObjects with the module. It takes either an object containing accessibility configuration data for a DisplayObject or an array of those as an argument. The configuration data needed is the DisplayObject and the desired role. Optionally the configuration data can include the parent DisplayObject in the accessibility tree (that has already been registered with the module), the index into the parent's accessibility child order to add the DisplayObject, focus and keyboardClick event handlers, and an object containing initial values to use for the various fields that the AccessibilityObject (or subclass) has. For example:
const container = new createjs.Container();
AccessibilityModule.register({
displayObject: container,
role: AccessibilityModule.ROLES.NONE,
});
const text = new createjs.Text('Test string', '16px Arial');
AccessibilityModule.register({
displayObject: text,
role: AccessibilityModule.ROLES.HEADING3,
parent: container,
accessibleOptions: {
text: 'Test string',
},
});
container.addChild(text);
Registered DisplayObjects need to be added to the tree of DisplayObjects to be translated in order to be included in the DOM. The first way to do this is to set the parent
field in the configuration object sent to AccessibilityModule.register
for that DisplayObject. The second way is to set the DisplayObject as the root of the accessibility tree, which is discussed in a later section. The third way is to call addChild
or addChildAt
on the AccessibilityObject
contained in the desired parent DisplayObject's .accessible
field, which does require that the parent has already been registered with the module as well. For example:
const container = new createjs.Container();
AccessibilityModule.register({
displayObject: container,
role: AccessibilityModule.ROLES.NONE,
});
const shape = new createjs.Shape();
AccessibilityModule.register({
displayObject: shape,
role: AccessibilityModule.ROLES.NONE,
});
container.addChild(shape);
container.accessible.addChild(shape);
That example has the shape as a child of the container in both the Createjs container/child tree and in the accessibility tree. However, that isn't required, which allows for flexibility of having the DOM order and Createjs's draw order be independent of eachother. One example usage of this is when implementing a drag and drop interaction; the DisplayObject picked up might be re-added to its parent in Createjs's container/child tree to get it drawn on top of other elements, but there's no reason to move it in the accessibility tree.
Each stage that needs accessibility support needs to be registered with the module in order to create an accessibility translator instance for it, such as with the following lines:
const stage = new createjs.Stage('stage');
const onReady = () => {
// discussed in next step
};
AccessibilityModule.setupStage(stage, document.getElementById('cam-test'), onReady);
When the supplied onReady function is called, then the new accessibilityTranslator
field on the supplied stage is ready for use. The next step is attaching an already registered DisplayObject as the root DisplayObject for the translator to use for creating/updating the DOM. Such as by adding the following lines to the onReady function:
const container = new createjs.Container();
AccessibilityModule.register({
displayObject: container,
role: AccessibilityModule.ROLES.NONE,
});
stage.accessibilityTranslator.root = container;
In order to actually create and update the DOM, the accessibility translator's update method needs to be called. So that the canvas and DOM are updated in sync with eachother, it is recommended to call the stage's update then the translator's update.
stage.update();
stage.accessibilityTranslator.update();
Since the DOM translation is to be rendered behind the canvas, whenever the canvas is resized the DOM translation should be as well. One of the main motivations for this is to ensure that DisplayObjects are positioned and sized the same in the canvas as their DOM translation in order to work with screen magnifiers. Resizing the DOM translation is simply:
AccessibilityModule.resize(stage);
All pixels of the canvas must be set to avoid the translated DOM bleeding through Element passed to setupStage being in the right position to allow for the translated DOM to be under the stage
Keyboard events on the translated DOM are communicated to the associated DisplayObject by way of Createjs events. While some events can be emitted by any role, others are role specific. It is also worth noting that while HTML elements can receive a "click" event from keyboard interaction, the module emits a "keyboardClick" event instead for the applicable roles. This is due to the event being dispatched to the DisplayObject, and "click" events are already caused by mouse interaction. So by using a different event name, a consuming app can customize its handling of keyboard clicks from mouse clicks if needed. If a consuming app doesn't want custom handling, then the same listener can be used for both events.
Role | Event | Additional Data to the Event instance | Description |
---|---|---|---|
Any | focus | Element has received focus | |
blur | Element has lost focus | ||
Any when enabled via displayObject.accessible.enableKeyEvents = true |
keydown | keyCode : code for which key was pressed |
A key was pressed on the keyboard |
keyup | keyCode : code for which key was released |
A key was released on the keyboard | |
button | keyboardClick | clicking event from keyboard interaction | |
checkbox | keyboardClick | clicking event from keyboard interaction | |
menu item | closeMenu | the menu should close | |
menu item | openMenu | the menu should open | |
menu item | keyboardClick | the menu item was clicked | |
multi-line textbox | valueChanged | value : the new string in the textbox |
the string in the textbox has changed |
selectionChanged | selectionStart : index into the value where the section startsselectionEnd index into the value where the section endsselectionDirection : "forward" for the selection increasing towards the end of the string, "backward" for the section increasing towards the beginning of the string |
there has been a change in which part of the text is selected | |
multi-select listbox | valueChanged | selectedValues : array where each entry is the value of selected options in the listboxselectedDisplayObjects : array containing the DisplayObjects that are selected in the listbox |
which items are selected in the listbox have changed |
radio | keyboardClick | clicking event from keyboard interaction | |
radio | change | value of the radio button has changed | |
scrollbar | scroll | For horizontal scrollbars, scrollLeft : value in pixels, for how far the content is scrolled horizontally.Otherwise, scrollTop : value, in pixels, for how far the content is scrolled vertically |
scrolling the scrollbar |
single line textbox | valueChanged | value : the new string in the textbox |
the string in the textbox has changed |
selectionChanged | selectionStart : index into the value where the section startsselectionEnd index into the value where the section endsselectionDirection : "forward" for the selection increasing towards the end of the string, "backward" for the section increasing towards the beginning of the string |
there has been a change in which part of the text is selected | |
single-select listbox | valueChanged | selectedValue : value of selected options in the listboxselectedDisplayObject : the DisplayObjects that is selected in the listbox |
which item is selected in the listbox has changed |
slider | valueChanged | newValue : new value of the slider |
value of the slider has changed |
spin button | increment | the value of the spin button should increase | |
decrement | the value of the spin button should decrease | ||
change | value : number representing the new value |
the value of the spin button has changed | |
tab | click | clicking event from keyboard interaction | |
tab list | click | clicking event from keyboard interaction | |
tree grid | collapseRow | rowDisplayObject : DisplayObject for the row to collapse |
The specified expandable row of the grid should be collapsed |
tree grid | expandRow | rowDisplayObject : DisplayObject for the row to collapse |
The specified expandable row of the grid should be collapsed |
tree item | click | clicking event from keyboard interaction |
The test-app directory contains a test app for both testing changes to this module and providing a reference implementation for the various roles. Some roles (e.g. menubar) require a particular structure to work correctly, and this test app shows them in that structure and functional.
Since the canvas is on top of the DOM translation performed by CAM, the app can be interacted with the mouse or keyboard where the canvas and DOM remain in sync due to the communication provided by CAM. Since the purpose of CAM is to provide the markup needed to work with ATs (e.g. screen readers or refreshable braille displays), the app can also be interacted with through those as well.
The area below the menu bar and above the footer is where most test cases display. The test case displayed in this area can be changed by using the menu bar to select another test case. The only test cases that don't use that area are the ones that are on screen at all times, which includes the banner, menu bar, footer, as well as the content of those areas.
The test cases are intended to test a role or multiple roles in a reasonably realistic way, such as mocking up a signup interface to test multiple input roles. Each test case should be reasonably realistic in itself, but the collection of test cases at the moment does not create a clear cohesive app. While not every permutation of attribute and value is tested for each role in a test case (which can be done via the console in the browser's dev tools during development), they do test/demonstrate correct nesting structure of roles, relationship of elements, handling of input, and passing of relevant information to CAM.
Every role currently implemented in CAM should be included in at least 1 test case in this app. This of course means that adding roles to CAM means updating this app to demonstrate and test its usage. The process for doing so is to evaluate if the role can be added to an existing test case while keeping it reasonably realistic. If there isn't a good existing test case, then a new one reasonably realistic one can be created. Test cases should also follow WAI-ARIA authoring practices (https://www.w3.org/TR/wai-aria-practices-1.1/).
Once a new role has been added to CAM, a widget should be created in the test app to allow for testing and demoing it. The widgets are not intended to be part of a fully featured and customizable UI library, but enough to test and demo the role while meeting WCAG 2.1 AA. Each widget has a file in src/widgets
which exports the class for the widget. Since widgets typically need to contain multiple DisplayObjects to render in canvas properly, these classes typically extend createjs.Container
. The constructor should register the widget with CAM without specifying a parent, such as:
AccessibilityModule.register({
displayObject: this,
accessibleOptions: options,
role: AccessibilityModule.ROLES.BUTTON,
});
Instances of the widget will be added to the accessibility tree when they are created in AppWindow.js (discussed later). If the widget has descendant DisplayObjects that should be registered with CAM, that registration should occur while the instance is being created and those should be added as descendants of this
on both the Createjs side and in the widget's accessibility tree. Also, binding of event listeners for both Createjs emitted events and events emitted from CAM should be done as part of creating the instance.
Using the newly created widget is done by adding at least one instance of it to a test case in src/widgets/AppWindow.js
or creating a new test case in that file. Test cases that use the area between the menu and the footer are written as functions where their function bodies start with this._clearScreen();
in order to clear the previously displayed test case in that area. Then they can create and configure whatever widgets the test case requires. To display the widgets for a test case, they should be added as children of this._contentArea
, both on the Createjs side and with CAM. New test cases that use this area also need to be added to the menu bar, generally in the "Test Cases" menu before the "Show Grid" test case.
If a non-code file (e.g. image) needs to be added, then it should be placed in src/widgets/media
.
The test app for the latest version of CAM is hosted at: https://curriculumassociates.github.io/createjs-accessibility
The test app uses its parent folder as its CAM dependency, and can be used to test local changes. First, ensure that the parent CAM project has dependencies installed and is built:
npm install && npm run build
Then start the test app, which will run on http://localhost:8007/.
npm run start:test-app
Since the canvas tag was introduced in HTML5, an HTML5 compatible browser is required. While the module is intended to work across HTML5 browsers, the ones currently tested against are:
- Chrome 88+
- ChromeOS 88+ (with and without ChromeVox)
- Edge 88
- Firefox 79+ (with and without NVDA 2020.4)
- IE11 (with and without Jaws 2019)
- Safari 13.1.3 (with and without VoiceOver)
The Createjs Accessibility Module is released under the ISC license. https://opensource.org/licenses/ISC