Background Information

Prior to reading through the following documentation, please familiarize yourself with this background information about accessible HTML, the accessibility tree, accessible names, and ARIA. The rest of this document will assume you have knowledge of these concepts.

Please note that some of the information in this document is specific to PhET Interactive Simulations.

In-Depth Information

For more information on the implementation of accessibility in Scenery, see the Accessibility Implementation Notes.

The Parallel DOM

Scenery uses HTML5 technologies (svg, canvas, webgl) to draw the display. These have very little semantic data as to what is inside the rendered graphic. The PDOM (parallel DOM (document object model)) pulls semantic data from the scene graph and adds it to a separate HTML structure that is available for assistive technologies (AT). When we say PDOM, think the HTML manifestation of the graphical Node content in the display.

The parallel DOM is an invisible structure that runs alongside the graphics. It is a basic HTML document that represents active scenery elements. It provides an interface to assistive technologies so that they have a representation of the display at a given state. The PDOM is dynamic and its DOM tree will update with changes to the scene. Any Node that has accessible content specified will be represented in the parallel DOM. HTML is used so that scenery can rely on semantic HTML and accessibility conventions of the web. This way, scenery can push some of the accessibility work load to the browser and AT for providing keyboard navigation and auditory descriptions.

Any Node in scenery, even those that are not visible or pickable, can have accessible content. The accessible content will represent the node in the parallel DOM tree.

Scenery's Accessibility API

Most of Scenery's accessibility features are defined and implemented in Accessibility.js. Accessibility.js is a trait that is mixed in to Node.js. It adds getters and setters for accessibility properties, so all we have to do is pass in a11y specific options like normal into the Parent.call() or mutate() function calls.

The following explains how to use the accessibility functionality of Scenery. For more information and up-to-date api documentation, see the source code. On the side bar, options are categorized by where they are introduced and explained. In this file there is little "traditional" documentation, rather example based explanation. The source code is the best place for specifics and implementation.

A Basic Example

The primary way that developers will implement a11y is through options passed through to Node.js. First off, each Node that wants content in the PDOM will need an HTML element in the PDOM to represent it. To do this, use the tagName option:

Above is a simple scenery Rectangle, that is represented as a paragraph tag in the PDOM. I also gave the <p> text content inside the html element.

Multiple DOM Elements per Node

Up to this point there has been a one to one correlation of Nodes to HTML elements in the PDOM, but the a11y api supports a richer architecture to represent a Node to AT: each Node can have multiple DOM Elements. A Node's HTML Elements, or the Node's peer, can be thought of as collectively equating to the Node. (Note: a "peer" is of type AccessiblePeer). Although a Node has a primary HTML element (the HTMLElement that is created with the tagName option), this is only a single element of the peer. Other HTML Elements can provide supplemental information to be conveyed to assistive technology. Label and description elements can be added, as well as a parent element to contain all HTML Element "siblings" for a Node's peer. In total a peer can have up to four HTML Elements: the primary sibling, label sibling, description sibling, and the container parent of these siblings.

Terminology

Terminology is key in understanding the specifics of creating the PDOM. From here on, when speaking about "siblings," we are speaking about the relationship between HTML elements in the PDOM. These Elements are not "siblings to the Node," but instead only siblings to each other, with an HTML Element parent called the "containerParent".

Summary: Each Node has an AccessiblePeer (aka “peer”) that manages the HTMLElements (aka “elements”) that are related to that Node in the PDOM. A Node has one or more associated elements, one of which is the “primary element”, whose tag is specified by option tagName. There are two other optional “supplementary elements”, whose tags are specified via options labelTagName and descriptionTagName. If more than the primary element is specified, they are all referred to as “sibling elements” (including the "primary sibling") and are automatically grouped together under a “container element”. The container element is given a default tag, which can be overridden with option containerTagName.

Example

Here is an example of a Node that uses all of its elements to provide the fullest semantic picture of the sim component to the PDOM.

In this example, the rectangle's primary sibling is a button with an Accessible Name of "Grab Magnet". It has a label sibling with an h3 tag with inner content "Grab Magnet", and a description sibling with a tagName of "p" with the specified sentence.

A few notes here:

The Structure of the PDOM

By default, the PDOM hierarchy will match the hierarchy of the scene graph. This is an important feature to consider. If a parent Node and child Node both have accessible content, then, in the PDOM, the accessible HTML of the child node will be added as a child of the parent's primary sibling. In scenery code, this is managed by AccessiblePeer, a type that stores and controls all HTML Elements for a given Node.

Leveraging the Scene Graph

Consider the following example where we have a box filled with circles and the desired a11y representation is an unordered list filled with list items.

In this example, scenery automatically structured the PDOM such that the list items are children of the unordered list to match the hierarchy of the scene graph.

Flexibility

The a11y api can provide lots of flexibility in how to display content in the PDOM. Each sibling of the peer has a name (like label or description), but at its core it is still just an HTML element, and it can be any tag name specified. Below is an example of a Node that is used just to add text content to the PDOM. In looking at the example, remember that there are default tag names for supplementary peer Elements. (Note: as of writing this, sibling tag names default to "p").

In this sense, the naming of the options to control each sibling in a bit "arbitrary," because you can use the api for what will work best for the situation. Every Node does not necessarily require all four HTML Elements of its peer in the PDOM, use your judgement.

Keyboard Navigation

The a11y api supports keyboard navigation only on the Node's primary sibling. A general philosophy to follow is to have the DOM Element hold as much semantic information as possible. For example, if there is a button in the sim, it is an obvious choice to use a "button" element as the Node's primary sibling tag. Another solution that works, although it is much worse, would be to choose a div, and then add listeners manually to control that div like a button. As a "div", an AT will not be able to tell the user what the element is. In general try to pick semantic html elements that will assist in conveying as much meaning as possible to the user. Although it is possible to use the ARIA spec to improve accessible experience, it should be used as little as possible because it has minimal support. Addressing semantics any further goes beyond the scope of this document.

Input types

If you specify a tagName: 'input', then use the inputType option to fill in the "type" attribute of the element. There are also inputValue and accessibleChecked options to manipulate specific and common (that we found) attributes of input tags. If you need more control of the primary DOM element's attributes, see Node.setAccessibleAttribute().

The above example is a Node whose PDOM representation is that of a basic checkbox. In order to give it interactive functionality, use Node.addInputListener(). The function takes in type Object.<string, function> where the key is the name of the DOM Event you want to listen to. This event is more often than not different than the listener needed for a mouse. Don't forget to remove the listener when the Node is disposed with Node.removeInputListener().

Focus

All interactive elements in the PDOM receive keyboard focus, but not all objects in the display are interactive. For example, using PhET Interactive Simulations, the Sweater in Balloons and Static Electricity is a dynamic content object because its electrons can be transferred to a balloon. Even so it is not directly interacted with by the user, thus the sweater never receives focus.

When an element in the PDOM is focused, a focus highlight is automatically rendered in the display to support keyboard navigation. For more complex interactions, type input, or other native and focusable elements, may not work. Other tag names can be focused with the focusable option. The ARIA attribute role can help inform the user to the custom interaction (use the ariaRole option). For example using the ARIA "application" role has worked well for freely moving, draggable objects. This will add a tab index of 0 to the element. Focusable elements can be manually focussed and blurred using the Node.focus() and Node.blur() functions. If a specific focus highlight is desired, a Node or Shape can be passed into the focusHighlight option.

Visibility in the PDOM and the focus order is directly effected by Node.visible, but can also be toggled independently with the option Node.accessibleVisible. When set to true this will hide content from screen readers and remove the element from focus order.

Manipulating the Parallel DOM

Most properties of the Accessibility.js trait are mutable so that the Parallel DOM can update with the graphical scene. Here are a few examples:

Up to this point these have only been offered as options, but each of these can be dynamically set also. Setting any of the .*[tT]agName setters to null will clear that element from the PDOM. If you set the Node.tagName = null, this will clear all accessible content of the node.

Please see the Accessibility trait for a complete and up-to-date list of getters/setters.

A note about Accessible Name

The Accessible Name of an element is how assistive technology identifies an element in the browser's accessibility tree. Diving into the nuance of this idea goes beyond the scope of this document, but understanding this is imperative to successfully creating an accessible PDOM. For more info see background reading about the topic.

Here is an overview about the various ways to set the Accessible Name via the Scenery a11y api.

Ordering

To manipulate the order in the PDOM, use Node.accessibleOrder = []. Scenery supports a fully independent tree of AccessibleInstances to order the PDOM versus the ordering based on the Nodes into the Instance tree. Because of this, you can use Node.accessibleOrder to largely remap the scene graph (for rendering into the PDOM) without affecting the visually rendered output. Node.accessibleOrder takes any array of Nodes, even if the they aren't children to that Node

Interactive Alerts

All interactive alerts are powered with the aria-live attribute. PhET manages alerts in a custom queue, see utteranceQueue.js (note this is in repo scenery-phet) for more information. All PhET alerts should go through utteranceQueue, aria-live should not be added to elements in the parallel DOM.

PhET Specific Information

The following information is specific to PhET's work with accessibility and this api.

Enabling A11y

Accessibility features are hidden behind an ?accessibility query parameter. This hides a11y features until they are ready for production. When ready, accessibility can be enabled by default by adding Sim option accessibility: true to {{sim}}-main.js.

A11y Specific Strings

Accessibility specific strings are not yet translatable. That being said PhET has every intention of getting to the point, we just don't have the infrastructure set up yet. Keeping that in mind, please follow the following conventions. This will help us greatly when it comes time to move these a11y strings into the translatable strings json file.

Beginning a11y work on a sim

When beginning a11y work in a simulation, add "accessible": true to the sim's package.json. Then regenerate the lists to add the simulation to perennial/data/accessibility list, and generate an a11y-view HTML document to assist with development (grunt generate-a11y-view-html).

The a11y-view

The 'A11y view' is an HTML page that runs the simulation in an iframe and shows an up-to-date copy of the PDOM next to the sim. It can be used to assist in development of accessibility features by allowing you to see the accessible labels, descriptions, and alerts without requiring screen reader testing. This should be generated by Bayes, but it can be generated manually with grunt generate-a11y-view-html in the sim repo.

Accessible Order for PhET Sims

The PDOM in PhET sims are specific and designed to give the most semantic and pedagogical information possible. These priorities can differ from those displayed in the visual simulation. We use Node.accessibleOrder to help manage this discrepancy. As a PhET Developer, please use the following guide to develop the a11y ordering in the PDOM versus the traditional rendering order of the scene graph. Each item in the list is ranked, such that you should start with item 1, and then if that doesn't work for your situation, try the next item down.

NOTE: This list was created with a mindset of instrumenting a simulation with accessibility. If a new sim is being created, then likely this list is irrelevant because the design process from the beginning will be focused on accessibility and regular sim development together.

  1. Add accessibility to the simulation, see if order is correct. If not. . .
  2. use setAccessibleOrder on local children, if not. . .
  3. Change z-order in the scene graph structure to get the order correct, if there is not an overriding constraint from the visible rendering order, if not. . .
  4. Discuss with the design team to inform them the order is unnatural OR we may decide another order based on simplifying implementation--revise desired order. if not. . .
  5. use setAccessibleOrder on descendants (can be local vars like controlPanel.flashlight.button.label)

If setAccessibleOrder is needed on Nodes that are not descendants, then likely there is a structural issue that needs to be addressed fully, rather than hacked at by using Node.etAccessibleOrder, although the setter will accept any Node in the scene graph.

Other misc notes for PhET Devs

Next Steps for Understanding

Please discuss developer related questions or problems with @jessegreenberg, @zepumph, or @mbarlow12 and update this document accordingly to help those who follow in your footsteps. Also @terracoda is a great resource on questions about ARIA and accessibility in general.

PhET Published Resources

Source Code

For up-to-date documentation and the latest API for accessibility in Scenery, please visit the source code.

Good luck and happy coding!