Skip to content

Commit

Permalink
implemented new aria-labelledby api and a few tests, see #701
Browse files Browse the repository at this point in the history
  • Loading branch information
zepumph committed Jun 7, 2018
1 parent 817c126 commit a532690
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 37 deletions.
90 changes: 62 additions & 28 deletions js/accessibility/Accessibility.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,10 +351,17 @@ define( function( require ) {
// element. See ariaDescribessNodoe for more information
this._ariaDescriptionContent = AccessiblePeer.PRIMARY_SIBLING;

// @private {Array.<Object>} - See addAriaLabelledByAssociation for more info...
// TODO: Add more docs, see https://github.com/phetsims/scenery/issues/701
// @private {Array.<Object>} - Keep track of what this Node is aria-labelledby via "associationObjects"
// see addAriaLabelledbyAssociation for why we support more than one association.
this._ariaLabelledbyAssociations = [];

// Keep a reference to all nodes that are aria-labelledby this node, i.e. that have store one of this Node's
// peer HTMLElement's id in their peer HTMLElement's aria-labelledby attribute. This way we can tell other
// nodes to update their aria-labelledby associations when this Node rebuilds its accessible content.
// @public (scenery-internal) - only used by Accessibility.js and invalidateAccessibleContent.js
// {Array.<Node>}
this._nodesThatAreAriaLabelledByThisNode = [];

// @private {?boolean} - whether or not this node's DOM element has been explicitly set to receive focus from
// tab navigation. Sets the tabIndex attribute on the node's DOM element. Setting to false will not remove the
// node's DOM from the document, but will ensure that it cannot receive focus by pressing 'tab'. Several
Expand Down Expand Up @@ -1256,41 +1263,41 @@ define( function( require ) {


/**
* TODO: docs, see https://github.com/phetsims/scenery/issues/701
* Add an aria-labelledby association to this node. The data in the associationObject will be implemented like
* "a peer's HTMLElement of this Node (specified with the string constant stored in `thisElementName`) will have an
* aria-labelledby attribute with a value that includes the `otherNode`'s peer HTMLElement's id (specified with
* `otherElementName`)."
*
* There can be more than one association because an aria-labelledby attribute's value can be a space separated
* list of HTML ids, and not just a single id, see https://www.w3.org/WAI/GL/wiki/Using_aria-labelledby_to_concatenate_a_label_from_several_text_nodes
*
* @param {Object} association - with key value pairs like
* @param {Object} associationObject - with key value pairs like
* { otherNode: {Node}, otherElementName: {string}, thisElementName: {string } }
* see AccessiblePeer for valid element names.
*/
addAriaLabelledByAssociation: function( associationObject ) {
this._ariaLabelledbyAssociations.push( associationObject );
addAriaLabelledbyAssociation: function( associationObject ) {

// flag that this node is is being labelled by the other nodes, so that if the other node changes we can
// restore the associations
associationObject.otherNode._ariaLabellingNodes.push( this );
},
this._ariaLabelledbyAssociations.push( associationObject ); // Keep track of this association.

/**
* Update
*
* @param {Node} [node] TODO: Add this optional node, if exists, only update association objects that
* are related to that node
*
* @private
*/
updateAriaLabelledByAssociations: function( node ) {
for ( var i = 0; i < this._ariaLabelledbyAssociations.length; i++ ) {
var associationObject = this._ariaLabelledbyAssociations[ i ];
this.addAriaLabelledByAssociationImplementation( associationObject );
}
// Flag that this node is is being labelled by the other node, so that if the other node changes it can tell
// this node to restore the association appropriately, see invalidateAccessibleContent for implementation.
associationObject.otherNode._nodesThatAreAriaLabelledByThisNode.push( this );


// update the accessiblePeers with this aria-labelledby association
this.addAriaLabelledbyAssociationImplementation( associationObject );
},


/**
* Implementation for addAriaLabelledbyAssociation. Called in addAriaLabelledByAssociation, as well as
* invalidateAccessibleContent when we recreate the accessible content for a Node.
*
* @param {Object} assocation
* Update accessible peers with this aria-labelledby association.
*
* @param {Object} associationObject - see addAriaLabelledbyAssociation doc
*/
addAriaLabelledByAssociationImplementation: function( associationObject ) {
addAriaLabelledbyAssociationImplementation: function( associationObject ) {
this.updateAccessiblePeers( function( peer ) {

// We are just using the first AccessibleInstance for simplicity, but it is OK because the accessible
Expand All @@ -1300,8 +1307,35 @@ define( function( require ) {

var otherPeerElement = firstAccessibleInstance.peer.getElementByName( associationObject.otherElementName );
var thisPeerElement = peer.getElementByName( associationObject.thisElementName );
thisPeerElement.setAttribute( 'aria-labelledby', otherPeerElement.id );
var previousAriaLabelledbyValue = thisPeerElement.getAttribute( 'aria-labelledby' ) || '';
assert && assert( typeof previousAriaLabelledbyValue === 'string' );

// add the id from the new association to the value of the HTMLElement's attribute.
thisPeerElement.setAttribute( 'aria-labelledby', [ previousAriaLabelledbyValue, otherPeerElement.id ].join( ' ' ) );
} );
},
/**
* Update all of the aria-labelledby associations for this Node. This involves clearing out the current aria-labelledby
* values in the AccessiblePeer, to be restored to the value of the state stored by this Node
*
* @public (scenery-internal) only used by invalidateAccessibleContent.js
*/
updateAriaLabelledbyAssociations: function() {

// clear the current aria-labelledby attribute and recreate it from stored associations
// TODO: make this more efficient
this.updateAccessiblePeers( function( peer ) {
peer.primarySibling && peer.primarySibling.setAttribute( 'aria-labelledby', '' );
peer.labelSibling && peer.labelSibling.setAttribute( 'aria-labelledby', '' );
peer.descriptionSibling && peer.descriptionSibling.setAttribute( 'aria-labelledby', '' );
peer.containerParent && peer.containerParent.setAttribute( 'aria-labelledby', '' );
} );

for ( var i = 0; i < this._ariaLabelledbyAssociations.length; i++ ) {
var associationObject = this._ariaLabelledbyAssociations[ i ];

this.addAriaLabelledbyAssociationImplementation( associationObject );
}
},

/**
Expand Down Expand Up @@ -2193,12 +2227,12 @@ define( function( require ) {
Accessibility.beforeOp = function() {

// paranoia about initialization order (should be safe)
focusedNode = scenery.Display && scenery.Display.focusedNode;
focusedNode = scenery.Display && scenery.Display.focusedNode;
};

/**
* Restores state after an operation that might have caused cause state to be lost.
*
*
* @public
* @static
*/
Expand Down
57 changes: 54 additions & 3 deletions js/accessibility/AccessibilityTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,57 @@ define( function( require ) {

} );

// new aria-labelledby api
QUnit.test( 'aria-labelledby', function( assert ) {

var rootNode = new Node();
var display = new Display( rootNode ); // eslint-disable-line
document.body.appendChild( display.domElement );

// two new nodes that will be related with the aria-labelledby and aria-describedby associations
var a = new Node( { tagName: 'button', labelTagName: 'p', descriptionTagName: 'p' } );
var b = new Node( { tagName: 'p', innerContent: TEST_LABEL_2 } );
rootNode.children = [ a, b ];

a.addAriaLabelledbyAssociation( {
otherNode: b,
thisElementName: AccessiblePeer.PRIMARY_SIBLING,
otherElementName: AccessiblePeer.PRIMARY_SIBLING
} );

var aElement = getPrimarySiblingElementByNode( a );
var bElement = getPrimarySiblingElementByNode( b );
assert.ok( aElement.getAttribute( 'aria-labelledby' ).indexOf( bElement.id ) >= 0, 'aria-labelledby for one node.' );

var c = new Node( { tagName: 'div', innerContent: TEST_LABEL } );
rootNode.addChild( c );

a.addAriaLabelledbyAssociation( {
otherNode: c,
thisElementName: AccessiblePeer.PRIMARY_SIBLING,
otherElementName: AccessiblePeer.PRIMARY_SIBLING
} );

aElement = getPrimarySiblingElementByNode( a );
bElement = getPrimarySiblingElementByNode( b );
var cElement = getPrimarySiblingElementByNode( c );
var expectedValue = [ bElement.id, cElement.id ].join( ' ' );
assert.ok( aElement.getAttribute( 'aria-labelledby' ).trim() === expectedValue,
'aria-labelledby two nodes' );

// Make c invalidate
rootNode.removeChild( c );
rootNode.addChild( new Node( { children: [ c ] } ) );

var oldValue = expectedValue;

aElement = getPrimarySiblingElementByNode( a );
cElement = getPrimarySiblingElementByNode( c );
assert.ok( aElement.getAttribute( 'aria-labelledby' ).trim() !== oldValue, 'should have invalidated on tree change' );
assert.ok( aElement.getAttribute( 'aria-labelledby' ).trim() === [ bElement.id, cElement.id ].join( ' ' ),
'should have invalidated on tree change' );

} );
QUnit.test( 'aria-labelledby, aria-describedby', function( assert ) {
var rootNode = new Node();
var display = new Display( rootNode ); // eslint-disable-line
Expand Down Expand Up @@ -1030,7 +1081,7 @@ define( function( require ) {
assert.ok( containerElement.childNodes[ 1 ].tagName.toUpperCase() === DEFAULT_DESCRIPTION_TAG_NAME, 'description sibling second' );
assert.ok( containerElement.childNodes[ 2 ].tagName.toUpperCase() === 'LI', 'primary sibling last' );
} );

// Higher level setter/getter options
QUnit.test( 'accessibleName option', function( assert ) {

Expand All @@ -1053,7 +1104,7 @@ define( function( require ) {
// var b = new Node( { tagName: 'input', accessibleName: TEST_LABEL } );
// a.addChild( b );
// var bElement = getPrimarySiblingElementByNode( b );
// var bParent = getPrimarySiblingElementByNode( b ).parentElement;
// var bParent = getPrimarySiblingElementByNode( b ).parentElement;
// var bLabelSibling = bParent.children[ DEFAULT_LABEL_SIBLING_INDEX ];
// assert.ok( bLabelSibling.textContent === TEST_LABEL, 'accessibleName sets label sibling' );
// assert.ok( bLabelSibling.getAttribute( 'for' ).indexOf( bElement.id ) >= 0, 'accessibleName sets label\'s "for" attribute' );
Expand All @@ -1070,7 +1121,7 @@ define( function( require ) {

var a = new Node( { tagName: 'button', focusHighlight: TEST_HIGHLIGHT } );
var b = new Node( { tagName: 'button', focusHighlight: TEST_HIGHLIGHT } );
rootNode.children = [ a, b ];
rootNode.children = [ a, b ];
b.focus();

// after moving a to front, b should still have focus
Expand Down
12 changes: 6 additions & 6 deletions js/accessibility/invalidateAccessibleContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,13 @@ define( function( require ) {
self.setInputType( self._inputType );
}

// restore this nodes aria-labelledby associations
self.updateAriaLabelledByAssociations();
// restore this nodes aria-labelledby associations
self.updateAriaLabelledbyAssociations();

// if any other nodes are aria-labelledby this node, update those associations of this node's
// accessible content needs to be recreated
for ( i = 0; i < self._ariaLabellingNodes.length; i++ ) {
self._ariaLabellingNodes[ i ].updateAriaLabelledByAssociations();
// if any other nodes are aria-labelledby this Node, update those associations too. Since this node's
// accessible content needs to be recreated, they need to update their aria-labelledby associations accordingly.
for ( i = 0; i < self._nodesThatAreAriaLabelledByThisNode.length; i++ ) {
self._nodesThatAreAriaLabelledByThisNode[ i ].updateAriaLabelledbyAssociations();
}

// restore aria-labelledby associations
Expand Down

0 comments on commit a532690

Please sign in to comment.