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.
For more information on the implementation of accessibility in Scenery, see the Accessibility Implementation Notes.
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.
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.
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.
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 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 HTMLElement
s (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
.
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:
AccessibilityUtil
for the default tag names.
appendLabel
and appendDescription
options.
Text
node "North South Magnet" has no accessible content and so it does not appear anywhere
in 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
.
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.
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.
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.
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()
.
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.
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:
tagName
: set/get the tag name of the primary DOM sibling of a nodelabelTagName
: set/get the tag name of the label DOM sibling for a nodedescriptionTagName
: set/get the tag name for the description DOM sibling of a nodeinnerContent
: set/get the text content of primary sibling of a nodelabelContent
: set/get the text content of label sibling for a nodedescriptionContent
: set/get the text content of description sibling for a nodeariaRole
: set/get the ARIA role for the primary DOM sibling for a node
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.
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.
<button>My Button</button>
. To accomplish this with the
a11y api use innerContent
.
label
Element: a label element can be associated with an interactive input
type
that does not have inner content in order to provide the input with an
accessible name. A label is the preferred naming
method when the display's interaction has visible text-based identifying it on screen. A label element can only
be associated with "labelable" elements like
typical interactive HTML elements.
To add an Accessible Name via a label
, set the labelTagName
to "label" and the "for"
attribute will automatically be filled in to point to the primary sibling.
aria-label
Attribute: an ARIA attribute that can provide an accessible name. For the a11y
api use the ariaLabel
option to set the value of said attribute on the primary DOM Element.
aria-labelledby
Attribute: this can be used to associate an HTML
element other than the label element to another element. The elements do not have to be right beside each
other. TODO: the a11y api docs to come. Also
add describedby? see phetsims/scenery#701
Node.accessibleOrder = []
. Scenery supports a fully
independent
tree of AccessibleInstances to order the PDOM versus the ordering based on the Node
s 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 Node
s, even if the they aren't children to that Node
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.
The following information is specific to PhET's work with accessibility and this api.
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
.
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.
{{SIM}}A11yStrings.js
.string!
plugin. This means:
OhmsLawA11yStrings.js
for an model example.
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' 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.
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.
setAccessibleOrder
on local children, if not. . .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.
//
a11y
comment as a header.
aria-labelledby
attribute.
With this association the H2's content, "Play Area", provides the region with an accessible name in the
Accessibility Tree which is accessed by assistive technology.
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.
For up-to-date documentation and the latest API for accessibility in Scenery, please visit the source code.
Good luck and happy coding!